@oh-my-pi/pi-coding-agent 1.337.0
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 +1228 -0
- package/README.md +1041 -0
- package/docs/compaction.md +403 -0
- package/docs/custom-tools.md +541 -0
- package/docs/extension-loading.md +1004 -0
- package/docs/hooks.md +867 -0
- package/docs/rpc.md +1040 -0
- package/docs/sdk.md +994 -0
- package/docs/session-tree-plan.md +441 -0
- package/docs/session.md +240 -0
- package/docs/skills.md +290 -0
- package/docs/theme.md +637 -0
- package/docs/tree.md +197 -0
- package/docs/tui.md +341 -0
- package/examples/README.md +21 -0
- package/examples/custom-tools/README.md +124 -0
- package/examples/custom-tools/hello/index.ts +20 -0
- package/examples/custom-tools/question/index.ts +84 -0
- package/examples/custom-tools/subagent/README.md +172 -0
- package/examples/custom-tools/subagent/agents/planner.md +37 -0
- package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
- package/examples/custom-tools/subagent/agents/scout.md +50 -0
- package/examples/custom-tools/subagent/agents/worker.md +24 -0
- package/examples/custom-tools/subagent/agents.ts +156 -0
- package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
- package/examples/custom-tools/subagent/commands/implement.md +10 -0
- package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
- package/examples/custom-tools/subagent/index.ts +1002 -0
- package/examples/custom-tools/todo/index.ts +212 -0
- package/examples/hooks/README.md +56 -0
- package/examples/hooks/auto-commit-on-exit.ts +49 -0
- package/examples/hooks/confirm-destructive.ts +59 -0
- package/examples/hooks/custom-compaction.ts +116 -0
- package/examples/hooks/dirty-repo-guard.ts +52 -0
- package/examples/hooks/file-trigger.ts +41 -0
- package/examples/hooks/git-checkpoint.ts +53 -0
- package/examples/hooks/handoff.ts +150 -0
- package/examples/hooks/permission-gate.ts +34 -0
- package/examples/hooks/protected-paths.ts +30 -0
- package/examples/hooks/qna.ts +119 -0
- package/examples/hooks/snake.ts +343 -0
- package/examples/hooks/status-line.ts +40 -0
- package/examples/sdk/01-minimal.ts +22 -0
- package/examples/sdk/02-custom-model.ts +49 -0
- package/examples/sdk/03-custom-prompt.ts +44 -0
- package/examples/sdk/04-skills.ts +44 -0
- package/examples/sdk/05-tools.ts +90 -0
- package/examples/sdk/06-hooks.ts +61 -0
- package/examples/sdk/07-context-files.ts +36 -0
- package/examples/sdk/08-slash-commands.ts +42 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
- package/examples/sdk/10-settings.ts +38 -0
- package/examples/sdk/11-sessions.ts +48 -0
- package/examples/sdk/12-full-control.ts +95 -0
- package/examples/sdk/README.md +154 -0
- package/package.json +81 -0
- package/src/cli/args.ts +246 -0
- package/src/cli/file-processor.ts +72 -0
- package/src/cli/list-models.ts +104 -0
- package/src/cli/plugin-cli.ts +650 -0
- package/src/cli/session-picker.ts +41 -0
- package/src/cli.ts +10 -0
- package/src/commands/init.md +20 -0
- package/src/config.ts +159 -0
- package/src/core/agent-session.ts +1900 -0
- package/src/core/auth-storage.ts +236 -0
- package/src/core/bash-executor.ts +196 -0
- package/src/core/compaction/branch-summarization.ts +343 -0
- package/src/core/compaction/compaction.ts +742 -0
- package/src/core/compaction/index.ts +7 -0
- package/src/core/compaction/utils.ts +154 -0
- package/src/core/custom-tools/index.ts +21 -0
- package/src/core/custom-tools/loader.ts +248 -0
- package/src/core/custom-tools/types.ts +169 -0
- package/src/core/custom-tools/wrapper.ts +28 -0
- package/src/core/exec.ts +129 -0
- package/src/core/export-html/index.ts +211 -0
- package/src/core/export-html/template.css +781 -0
- package/src/core/export-html/template.html +54 -0
- package/src/core/export-html/template.js +1185 -0
- package/src/core/export-html/vendor/highlight.min.js +1213 -0
- package/src/core/export-html/vendor/marked.min.js +6 -0
- package/src/core/hooks/index.ts +16 -0
- package/src/core/hooks/loader.ts +312 -0
- package/src/core/hooks/runner.ts +434 -0
- package/src/core/hooks/tool-wrapper.ts +99 -0
- package/src/core/hooks/types.ts +773 -0
- package/src/core/index.ts +52 -0
- package/src/core/mcp/client.ts +158 -0
- package/src/core/mcp/config.ts +154 -0
- package/src/core/mcp/index.ts +45 -0
- package/src/core/mcp/loader.ts +68 -0
- package/src/core/mcp/manager.ts +181 -0
- package/src/core/mcp/tool-bridge.ts +148 -0
- package/src/core/mcp/transports/http.ts +316 -0
- package/src/core/mcp/transports/index.ts +6 -0
- package/src/core/mcp/transports/stdio.ts +252 -0
- package/src/core/mcp/types.ts +220 -0
- package/src/core/messages.ts +189 -0
- package/src/core/model-registry.ts +317 -0
- package/src/core/model-resolver.ts +393 -0
- package/src/core/plugins/doctor.ts +59 -0
- package/src/core/plugins/index.ts +38 -0
- package/src/core/plugins/installer.ts +189 -0
- package/src/core/plugins/loader.ts +338 -0
- package/src/core/plugins/manager.ts +672 -0
- package/src/core/plugins/parser.ts +105 -0
- package/src/core/plugins/paths.ts +32 -0
- package/src/core/plugins/types.ts +190 -0
- package/src/core/sdk.ts +760 -0
- package/src/core/session-manager.ts +1128 -0
- package/src/core/settings-manager.ts +443 -0
- package/src/core/skills.ts +437 -0
- package/src/core/slash-commands.ts +248 -0
- package/src/core/system-prompt.ts +439 -0
- package/src/core/timings.ts +25 -0
- package/src/core/tools/ask.ts +211 -0
- package/src/core/tools/bash-interceptor.ts +120 -0
- package/src/core/tools/bash.ts +250 -0
- package/src/core/tools/context.ts +32 -0
- package/src/core/tools/edit-diff.ts +475 -0
- package/src/core/tools/edit.ts +208 -0
- package/src/core/tools/exa/company.ts +59 -0
- package/src/core/tools/exa/index.ts +64 -0
- package/src/core/tools/exa/linkedin.ts +59 -0
- package/src/core/tools/exa/logger.ts +56 -0
- package/src/core/tools/exa/mcp-client.ts +368 -0
- package/src/core/tools/exa/render.ts +196 -0
- package/src/core/tools/exa/researcher.ts +90 -0
- package/src/core/tools/exa/search.ts +337 -0
- package/src/core/tools/exa/types.ts +168 -0
- package/src/core/tools/exa/websets.ts +248 -0
- package/src/core/tools/find.ts +261 -0
- package/src/core/tools/grep.ts +555 -0
- package/src/core/tools/index.ts +202 -0
- package/src/core/tools/ls.ts +140 -0
- package/src/core/tools/lsp/client.ts +605 -0
- package/src/core/tools/lsp/config.ts +147 -0
- package/src/core/tools/lsp/edits.ts +101 -0
- package/src/core/tools/lsp/index.ts +804 -0
- package/src/core/tools/lsp/render.ts +447 -0
- package/src/core/tools/lsp/rust-analyzer.ts +145 -0
- package/src/core/tools/lsp/types.ts +463 -0
- package/src/core/tools/lsp/utils.ts +486 -0
- package/src/core/tools/notebook.ts +229 -0
- package/src/core/tools/path-utils.ts +61 -0
- package/src/core/tools/read.ts +240 -0
- package/src/core/tools/renderers.ts +540 -0
- package/src/core/tools/task/agents.ts +153 -0
- package/src/core/tools/task/artifacts.ts +114 -0
- package/src/core/tools/task/bundled-agents/browser.md +71 -0
- package/src/core/tools/task/bundled-agents/explore.md +82 -0
- package/src/core/tools/task/bundled-agents/plan.md +54 -0
- package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
- package/src/core/tools/task/bundled-agents/task.md +53 -0
- package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
- package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
- package/src/core/tools/task/bundled-commands/implement.md +11 -0
- package/src/core/tools/task/commands.ts +213 -0
- package/src/core/tools/task/discovery.ts +208 -0
- package/src/core/tools/task/executor.ts +367 -0
- package/src/core/tools/task/index.ts +388 -0
- package/src/core/tools/task/model-resolver.ts +115 -0
- package/src/core/tools/task/parallel.ts +38 -0
- package/src/core/tools/task/render.ts +232 -0
- package/src/core/tools/task/types.ts +99 -0
- package/src/core/tools/truncate.ts +265 -0
- package/src/core/tools/web-fetch.ts +2370 -0
- package/src/core/tools/web-search/auth.ts +193 -0
- package/src/core/tools/web-search/index.ts +537 -0
- package/src/core/tools/web-search/providers/anthropic.ts +198 -0
- package/src/core/tools/web-search/providers/exa.ts +302 -0
- package/src/core/tools/web-search/providers/perplexity.ts +195 -0
- package/src/core/tools/web-search/render.ts +182 -0
- package/src/core/tools/web-search/types.ts +180 -0
- package/src/core/tools/write.ts +99 -0
- package/src/index.ts +176 -0
- package/src/main.ts +464 -0
- package/src/migrations.ts +135 -0
- package/src/modes/index.ts +43 -0
- package/src/modes/interactive/components/armin.ts +382 -0
- package/src/modes/interactive/components/assistant-message.ts +86 -0
- package/src/modes/interactive/components/bash-execution.ts +196 -0
- package/src/modes/interactive/components/bordered-loader.ts +41 -0
- package/src/modes/interactive/components/branch-summary-message.ts +42 -0
- package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
- package/src/modes/interactive/components/custom-editor.ts +122 -0
- package/src/modes/interactive/components/diff.ts +147 -0
- package/src/modes/interactive/components/dynamic-border.ts +25 -0
- package/src/modes/interactive/components/footer.ts +381 -0
- package/src/modes/interactive/components/hook-editor.ts +117 -0
- package/src/modes/interactive/components/hook-input.ts +64 -0
- package/src/modes/interactive/components/hook-message.ts +96 -0
- package/src/modes/interactive/components/hook-selector.ts +91 -0
- package/src/modes/interactive/components/model-selector.ts +247 -0
- package/src/modes/interactive/components/oauth-selector.ts +120 -0
- package/src/modes/interactive/components/plugin-settings.ts +479 -0
- package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
- package/src/modes/interactive/components/session-selector.ts +204 -0
- package/src/modes/interactive/components/settings-selector.ts +453 -0
- package/src/modes/interactive/components/show-images-selector.ts +45 -0
- package/src/modes/interactive/components/theme-selector.ts +62 -0
- package/src/modes/interactive/components/thinking-selector.ts +64 -0
- package/src/modes/interactive/components/tool-execution.ts +675 -0
- package/src/modes/interactive/components/tree-selector.ts +866 -0
- package/src/modes/interactive/components/user-message-selector.ts +159 -0
- package/src/modes/interactive/components/user-message.ts +18 -0
- package/src/modes/interactive/components/visual-truncate.ts +50 -0
- package/src/modes/interactive/components/welcome.ts +183 -0
- package/src/modes/interactive/interactive-mode.ts +2516 -0
- package/src/modes/interactive/theme/dark.json +101 -0
- package/src/modes/interactive/theme/light.json +98 -0
- package/src/modes/interactive/theme/theme-schema.json +308 -0
- package/src/modes/interactive/theme/theme.ts +998 -0
- package/src/modes/print-mode.ts +128 -0
- package/src/modes/rpc/rpc-client.ts +527 -0
- package/src/modes/rpc/rpc-mode.ts +483 -0
- package/src/modes/rpc/rpc-types.ts +203 -0
- package/src/utils/changelog.ts +99 -0
- package/src/utils/clipboard.ts +265 -0
- package/src/utils/fuzzy.ts +108 -0
- package/src/utils/mime.ts +30 -0
- package/src/utils/shell.ts +276 -0
- package/src/utils/tools-manager.ts +274 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook runner - executes hooks and manages their lifecycle.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
6
|
+
import type { Model } from "@oh-my-pi/pi-ai";
|
|
7
|
+
import { theme } from "../../modes/interactive/theme/theme.js";
|
|
8
|
+
import type { ModelRegistry } from "../model-registry.js";
|
|
9
|
+
import type { SessionManager } from "../session-manager.js";
|
|
10
|
+
import type {
|
|
11
|
+
AppendEntryHandler,
|
|
12
|
+
BranchHandler,
|
|
13
|
+
LoadedHook,
|
|
14
|
+
NavigateTreeHandler,
|
|
15
|
+
NewSessionHandler,
|
|
16
|
+
SendMessageHandler,
|
|
17
|
+
} from "./loader.js";
|
|
18
|
+
import type {
|
|
19
|
+
BeforeAgentStartEvent,
|
|
20
|
+
BeforeAgentStartEventResult,
|
|
21
|
+
ContextEvent,
|
|
22
|
+
ContextEventResult,
|
|
23
|
+
HookCommandContext,
|
|
24
|
+
HookContext,
|
|
25
|
+
HookError,
|
|
26
|
+
HookEvent,
|
|
27
|
+
HookMessageRenderer,
|
|
28
|
+
HookUIContext,
|
|
29
|
+
RegisteredCommand,
|
|
30
|
+
SessionBeforeCompactResult,
|
|
31
|
+
SessionBeforeTreeResult,
|
|
32
|
+
ToolCallEvent,
|
|
33
|
+
ToolCallEventResult,
|
|
34
|
+
ToolResultEventResult,
|
|
35
|
+
} from "./types.js";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Listener for hook errors.
|
|
39
|
+
*/
|
|
40
|
+
export type HookErrorListener = (error: HookError) => void;
|
|
41
|
+
|
|
42
|
+
// Re-export execCommand for backward compatibility
|
|
43
|
+
export { execCommand } from "../exec.js";
|
|
44
|
+
|
|
45
|
+
/** No-op UI context used when no UI is available */
|
|
46
|
+
const noOpUIContext: HookUIContext = {
|
|
47
|
+
select: async () => undefined,
|
|
48
|
+
confirm: async () => false,
|
|
49
|
+
input: async () => undefined,
|
|
50
|
+
notify: () => {},
|
|
51
|
+
setStatus: () => {},
|
|
52
|
+
custom: async () => undefined as never,
|
|
53
|
+
setEditorText: () => {},
|
|
54
|
+
getEditorText: () => "",
|
|
55
|
+
editor: async () => undefined,
|
|
56
|
+
get theme() {
|
|
57
|
+
return theme;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* HookRunner executes hooks and manages event emission.
|
|
63
|
+
*/
|
|
64
|
+
export class HookRunner {
|
|
65
|
+
private hooks: LoadedHook[];
|
|
66
|
+
private uiContext: HookUIContext;
|
|
67
|
+
private hasUI: boolean;
|
|
68
|
+
private cwd: string;
|
|
69
|
+
private sessionManager: SessionManager;
|
|
70
|
+
private modelRegistry: ModelRegistry;
|
|
71
|
+
private errorListeners: Set<HookErrorListener> = new Set();
|
|
72
|
+
private getModel: () => Model<any> | undefined = () => undefined;
|
|
73
|
+
private isIdleFn: () => boolean = () => true;
|
|
74
|
+
private waitForIdleFn: () => Promise<void> = async () => {};
|
|
75
|
+
private abortFn: () => void = () => {};
|
|
76
|
+
private hasQueuedMessagesFn: () => boolean = () => false;
|
|
77
|
+
private newSessionHandler: NewSessionHandler = async () => ({ cancelled: false });
|
|
78
|
+
private branchHandler: BranchHandler = async () => ({ cancelled: false });
|
|
79
|
+
private navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false });
|
|
80
|
+
|
|
81
|
+
constructor(hooks: LoadedHook[], cwd: string, sessionManager: SessionManager, modelRegistry: ModelRegistry) {
|
|
82
|
+
this.hooks = hooks;
|
|
83
|
+
this.uiContext = noOpUIContext;
|
|
84
|
+
this.hasUI = false;
|
|
85
|
+
this.cwd = cwd;
|
|
86
|
+
this.sessionManager = sessionManager;
|
|
87
|
+
this.modelRegistry = modelRegistry;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Initialize HookRunner with all required context.
|
|
92
|
+
* Modes call this once the agent session is fully set up.
|
|
93
|
+
*/
|
|
94
|
+
initialize(options: {
|
|
95
|
+
/** Function to get the current model */
|
|
96
|
+
getModel: () => Model<any> | undefined;
|
|
97
|
+
/** Handler for hooks to send messages */
|
|
98
|
+
sendMessageHandler: SendMessageHandler;
|
|
99
|
+
/** Handler for hooks to append entries */
|
|
100
|
+
appendEntryHandler: AppendEntryHandler;
|
|
101
|
+
/** Handler for creating new sessions (for HookCommandContext) */
|
|
102
|
+
newSessionHandler?: NewSessionHandler;
|
|
103
|
+
/** Handler for branching sessions (for HookCommandContext) */
|
|
104
|
+
branchHandler?: BranchHandler;
|
|
105
|
+
/** Handler for navigating session tree (for HookCommandContext) */
|
|
106
|
+
navigateTreeHandler?: NavigateTreeHandler;
|
|
107
|
+
/** Function to check if agent is idle */
|
|
108
|
+
isIdle?: () => boolean;
|
|
109
|
+
/** Function to wait for agent to be idle */
|
|
110
|
+
waitForIdle?: () => Promise<void>;
|
|
111
|
+
/** Function to abort current operation (fire-and-forget) */
|
|
112
|
+
abort?: () => void;
|
|
113
|
+
/** Function to check if there are queued messages */
|
|
114
|
+
hasQueuedMessages?: () => boolean;
|
|
115
|
+
/** UI context for interactive prompts */
|
|
116
|
+
uiContext?: HookUIContext;
|
|
117
|
+
/** Whether UI is available */
|
|
118
|
+
hasUI?: boolean;
|
|
119
|
+
}): void {
|
|
120
|
+
this.getModel = options.getModel;
|
|
121
|
+
this.isIdleFn = options.isIdle ?? (() => true);
|
|
122
|
+
this.waitForIdleFn = options.waitForIdle ?? (async () => {});
|
|
123
|
+
this.abortFn = options.abort ?? (() => {});
|
|
124
|
+
this.hasQueuedMessagesFn = options.hasQueuedMessages ?? (() => false);
|
|
125
|
+
// Store session handlers for HookCommandContext
|
|
126
|
+
if (options.newSessionHandler) {
|
|
127
|
+
this.newSessionHandler = options.newSessionHandler;
|
|
128
|
+
}
|
|
129
|
+
if (options.branchHandler) {
|
|
130
|
+
this.branchHandler = options.branchHandler;
|
|
131
|
+
}
|
|
132
|
+
if (options.navigateTreeHandler) {
|
|
133
|
+
this.navigateTreeHandler = options.navigateTreeHandler;
|
|
134
|
+
}
|
|
135
|
+
// Set per-hook handlers for pi.sendMessage() and pi.appendEntry()
|
|
136
|
+
for (const hook of this.hooks) {
|
|
137
|
+
hook.setSendMessageHandler(options.sendMessageHandler);
|
|
138
|
+
hook.setAppendEntryHandler(options.appendEntryHandler);
|
|
139
|
+
}
|
|
140
|
+
this.uiContext = options.uiContext ?? noOpUIContext;
|
|
141
|
+
this.hasUI = options.hasUI ?? false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get the UI context (set by mode).
|
|
146
|
+
*/
|
|
147
|
+
getUIContext(): HookUIContext | null {
|
|
148
|
+
return this.uiContext;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get whether UI is available.
|
|
153
|
+
*/
|
|
154
|
+
getHasUI(): boolean {
|
|
155
|
+
return this.hasUI;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get the paths of all loaded hooks.
|
|
160
|
+
*/
|
|
161
|
+
getHookPaths(): string[] {
|
|
162
|
+
return this.hooks.map((h) => h.path);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Subscribe to hook errors.
|
|
167
|
+
* @returns Unsubscribe function
|
|
168
|
+
*/
|
|
169
|
+
onError(listener: HookErrorListener): () => void {
|
|
170
|
+
this.errorListeners.add(listener);
|
|
171
|
+
return () => this.errorListeners.delete(listener);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Emit an error to all listeners.
|
|
176
|
+
*/
|
|
177
|
+
/**
|
|
178
|
+
* Emit an error to all error listeners.
|
|
179
|
+
*/
|
|
180
|
+
emitError(error: HookError): void {
|
|
181
|
+
for (const listener of this.errorListeners) {
|
|
182
|
+
listener(error);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Check if any hooks have handlers for the given event type.
|
|
188
|
+
*/
|
|
189
|
+
hasHandlers(eventType: string): boolean {
|
|
190
|
+
for (const hook of this.hooks) {
|
|
191
|
+
const handlers = hook.handlers.get(eventType);
|
|
192
|
+
if (handlers && handlers.length > 0) {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get a message renderer for the given customType.
|
|
201
|
+
* Returns the first renderer found across all hooks, or undefined if none.
|
|
202
|
+
*/
|
|
203
|
+
getMessageRenderer(customType: string): HookMessageRenderer | undefined {
|
|
204
|
+
for (const hook of this.hooks) {
|
|
205
|
+
const renderer = hook.messageRenderers.get(customType);
|
|
206
|
+
if (renderer) {
|
|
207
|
+
return renderer;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get all registered commands from all hooks.
|
|
215
|
+
*/
|
|
216
|
+
getRegisteredCommands(): RegisteredCommand[] {
|
|
217
|
+
const commands: RegisteredCommand[] = [];
|
|
218
|
+
for (const hook of this.hooks) {
|
|
219
|
+
for (const command of hook.commands.values()) {
|
|
220
|
+
commands.push(command);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return commands;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get a registered command by name.
|
|
228
|
+
* Returns the first command found across all hooks, or undefined if none.
|
|
229
|
+
*/
|
|
230
|
+
getCommand(name: string): RegisteredCommand | undefined {
|
|
231
|
+
for (const hook of this.hooks) {
|
|
232
|
+
const command = hook.commands.get(name);
|
|
233
|
+
if (command) {
|
|
234
|
+
return command;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Create the event context for handlers.
|
|
242
|
+
*/
|
|
243
|
+
private createContext(): HookContext {
|
|
244
|
+
return {
|
|
245
|
+
ui: this.uiContext,
|
|
246
|
+
hasUI: this.hasUI,
|
|
247
|
+
cwd: this.cwd,
|
|
248
|
+
sessionManager: this.sessionManager,
|
|
249
|
+
modelRegistry: this.modelRegistry,
|
|
250
|
+
model: this.getModel(),
|
|
251
|
+
isIdle: () => this.isIdleFn(),
|
|
252
|
+
abort: () => this.abortFn(),
|
|
253
|
+
hasQueuedMessages: () => this.hasQueuedMessagesFn(),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Create the command context for slash command handlers.
|
|
259
|
+
* Extends HookContext with session control methods that are only safe in commands.
|
|
260
|
+
*/
|
|
261
|
+
createCommandContext(): HookCommandContext {
|
|
262
|
+
return {
|
|
263
|
+
...this.createContext(),
|
|
264
|
+
waitForIdle: () => this.waitForIdleFn(),
|
|
265
|
+
newSession: (options) => this.newSessionHandler(options),
|
|
266
|
+
branch: (entryId) => this.branchHandler(entryId),
|
|
267
|
+
navigateTree: (targetId, options) => this.navigateTreeHandler(targetId, options),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Check if event type is a session "before_*" event that can be cancelled.
|
|
273
|
+
*/
|
|
274
|
+
private isSessionBeforeEvent(
|
|
275
|
+
type: string,
|
|
276
|
+
): type is "session_before_switch" | "session_before_branch" | "session_before_compact" | "session_before_tree" {
|
|
277
|
+
return (
|
|
278
|
+
type === "session_before_switch" ||
|
|
279
|
+
type === "session_before_branch" ||
|
|
280
|
+
type === "session_before_compact" ||
|
|
281
|
+
type === "session_before_tree"
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Emit an event to all hooks.
|
|
287
|
+
* Returns the result from session before_* / tool_result events (if any handler returns one).
|
|
288
|
+
*/
|
|
289
|
+
async emit(
|
|
290
|
+
event: HookEvent,
|
|
291
|
+
): Promise<SessionBeforeCompactResult | SessionBeforeTreeResult | ToolResultEventResult | undefined> {
|
|
292
|
+
const ctx = this.createContext();
|
|
293
|
+
let result: SessionBeforeCompactResult | SessionBeforeTreeResult | ToolResultEventResult | undefined;
|
|
294
|
+
|
|
295
|
+
for (const hook of this.hooks) {
|
|
296
|
+
const handlers = hook.handlers.get(event.type);
|
|
297
|
+
if (!handlers || handlers.length === 0) continue;
|
|
298
|
+
|
|
299
|
+
for (const handler of handlers) {
|
|
300
|
+
try {
|
|
301
|
+
const handlerResult = await handler(event, ctx);
|
|
302
|
+
|
|
303
|
+
// For session before_* events, capture the result (for cancellation)
|
|
304
|
+
if (this.isSessionBeforeEvent(event.type) && handlerResult) {
|
|
305
|
+
result = handlerResult as SessionBeforeCompactResult | SessionBeforeTreeResult;
|
|
306
|
+
// If cancelled, stop processing further hooks
|
|
307
|
+
if (result.cancel) {
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// For tool_result events, capture the result
|
|
313
|
+
if (event.type === "tool_result" && handlerResult) {
|
|
314
|
+
result = handlerResult as ToolResultEventResult;
|
|
315
|
+
}
|
|
316
|
+
} catch (err) {
|
|
317
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
318
|
+
this.emitError({
|
|
319
|
+
hookPath: hook.path,
|
|
320
|
+
event: event.type,
|
|
321
|
+
error: message,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Emit a tool_call event to all hooks.
|
|
332
|
+
* No timeout - user prompts can take as long as needed.
|
|
333
|
+
* Errors are thrown (not swallowed) so caller can block on failure.
|
|
334
|
+
*/
|
|
335
|
+
async emitToolCall(event: ToolCallEvent): Promise<ToolCallEventResult | undefined> {
|
|
336
|
+
const ctx = this.createContext();
|
|
337
|
+
let result: ToolCallEventResult | undefined;
|
|
338
|
+
|
|
339
|
+
for (const hook of this.hooks) {
|
|
340
|
+
const handlers = hook.handlers.get("tool_call");
|
|
341
|
+
if (!handlers || handlers.length === 0) continue;
|
|
342
|
+
|
|
343
|
+
for (const handler of handlers) {
|
|
344
|
+
// No timeout - let user take their time
|
|
345
|
+
const handlerResult = await handler(event, ctx);
|
|
346
|
+
|
|
347
|
+
if (handlerResult) {
|
|
348
|
+
result = handlerResult as ToolCallEventResult;
|
|
349
|
+
// If blocked, stop processing further hooks
|
|
350
|
+
if (result.block) {
|
|
351
|
+
return result;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Emit a context event to all hooks.
|
|
362
|
+
* Handlers are chained - each gets the previous handler's output (if any).
|
|
363
|
+
* Returns the final modified messages, or the original if no modifications.
|
|
364
|
+
*
|
|
365
|
+
* Note: Messages are already deep-copied by the caller (pi-ai preprocessor).
|
|
366
|
+
*/
|
|
367
|
+
async emitContext(messages: AgentMessage[]): Promise<AgentMessage[]> {
|
|
368
|
+
const ctx = this.createContext();
|
|
369
|
+
let currentMessages = messages;
|
|
370
|
+
|
|
371
|
+
for (const hook of this.hooks) {
|
|
372
|
+
const handlers = hook.handlers.get("context");
|
|
373
|
+
if (!handlers || handlers.length === 0) continue;
|
|
374
|
+
|
|
375
|
+
for (const handler of handlers) {
|
|
376
|
+
try {
|
|
377
|
+
const event: ContextEvent = { type: "context", messages: currentMessages };
|
|
378
|
+
const handlerResult = await handler(event, ctx);
|
|
379
|
+
|
|
380
|
+
if (handlerResult && (handlerResult as ContextEventResult).messages) {
|
|
381
|
+
currentMessages = (handlerResult as ContextEventResult).messages!;
|
|
382
|
+
}
|
|
383
|
+
} catch (err) {
|
|
384
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
385
|
+
this.emitError({
|
|
386
|
+
hookPath: hook.path,
|
|
387
|
+
event: "context",
|
|
388
|
+
error: message,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return currentMessages;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Emit before_agent_start event to all hooks.
|
|
399
|
+
* Returns the first message to inject (if any handler returns one).
|
|
400
|
+
*/
|
|
401
|
+
async emitBeforeAgentStart(
|
|
402
|
+
prompt: string,
|
|
403
|
+
images?: import("@oh-my-pi/pi-ai").ImageContent[],
|
|
404
|
+
): Promise<BeforeAgentStartEventResult | undefined> {
|
|
405
|
+
const ctx = this.createContext();
|
|
406
|
+
let result: BeforeAgentStartEventResult | undefined;
|
|
407
|
+
|
|
408
|
+
for (const hook of this.hooks) {
|
|
409
|
+
const handlers = hook.handlers.get("before_agent_start");
|
|
410
|
+
if (!handlers || handlers.length === 0) continue;
|
|
411
|
+
|
|
412
|
+
for (const handler of handlers) {
|
|
413
|
+
try {
|
|
414
|
+
const event: BeforeAgentStartEvent = { type: "before_agent_start", prompt, images };
|
|
415
|
+
const handlerResult = await handler(event, ctx);
|
|
416
|
+
|
|
417
|
+
// Take the first message returned
|
|
418
|
+
if (handlerResult && (handlerResult as BeforeAgentStartEventResult).message && !result) {
|
|
419
|
+
result = handlerResult as BeforeAgentStartEventResult;
|
|
420
|
+
}
|
|
421
|
+
} catch (err) {
|
|
422
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
423
|
+
this.emitError({
|
|
424
|
+
hookPath: hook.path,
|
|
425
|
+
event: "before_agent_start",
|
|
426
|
+
error: message,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return result;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool wrapper - wraps tools with hook callbacks for interception.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AgentTool, AgentToolContext, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
6
|
+
import type { HookRunner } from "./runner.js";
|
|
7
|
+
import type { ToolCallEventResult, ToolResultEventResult } from "./types.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Wrap a tool with hook callbacks.
|
|
11
|
+
* - Emits tool_call event before execution (can block)
|
|
12
|
+
* - Emits tool_result event after execution (can modify result)
|
|
13
|
+
* - Forwards onUpdate callback to wrapped tool for progress streaming
|
|
14
|
+
*/
|
|
15
|
+
export function wrapToolWithHooks<T>(tool: AgentTool<any, T>, hookRunner: HookRunner): AgentTool<any, T> {
|
|
16
|
+
return {
|
|
17
|
+
...tool,
|
|
18
|
+
execute: async (
|
|
19
|
+
toolCallId: string,
|
|
20
|
+
params: Record<string, unknown>,
|
|
21
|
+
signal?: AbortSignal,
|
|
22
|
+
onUpdate?: AgentToolUpdateCallback<T>,
|
|
23
|
+
context?: AgentToolContext,
|
|
24
|
+
) => {
|
|
25
|
+
// Emit tool_call event - hooks can block execution
|
|
26
|
+
// If hook errors/times out, block by default (fail-safe)
|
|
27
|
+
if (hookRunner.hasHandlers("tool_call")) {
|
|
28
|
+
try {
|
|
29
|
+
const callResult = (await hookRunner.emitToolCall({
|
|
30
|
+
type: "tool_call",
|
|
31
|
+
toolName: tool.name,
|
|
32
|
+
toolCallId,
|
|
33
|
+
input: params,
|
|
34
|
+
})) as ToolCallEventResult | undefined;
|
|
35
|
+
|
|
36
|
+
if (callResult?.block) {
|
|
37
|
+
const reason = callResult.reason || "Tool execution was blocked by a hook";
|
|
38
|
+
throw new Error(reason);
|
|
39
|
+
}
|
|
40
|
+
} catch (err) {
|
|
41
|
+
// Hook error or block - throw to mark as error
|
|
42
|
+
if (err instanceof Error) {
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
throw new Error(`Hook failed, blocking execution: ${String(err)}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Execute the actual tool, forwarding onUpdate for progress streaming
|
|
50
|
+
try {
|
|
51
|
+
const result = await tool.execute(toolCallId, params, signal, onUpdate, context);
|
|
52
|
+
|
|
53
|
+
// Emit tool_result event - hooks can modify the result
|
|
54
|
+
if (hookRunner.hasHandlers("tool_result")) {
|
|
55
|
+
const resultResult = (await hookRunner.emit({
|
|
56
|
+
type: "tool_result",
|
|
57
|
+
toolName: tool.name,
|
|
58
|
+
toolCallId,
|
|
59
|
+
input: params,
|
|
60
|
+
content: result.content,
|
|
61
|
+
details: result.details,
|
|
62
|
+
isError: false,
|
|
63
|
+
})) as ToolResultEventResult | undefined;
|
|
64
|
+
|
|
65
|
+
// Apply modifications if any
|
|
66
|
+
if (resultResult) {
|
|
67
|
+
return {
|
|
68
|
+
content: resultResult.content ?? result.content,
|
|
69
|
+
details: (resultResult.details ?? result.details) as T,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return result;
|
|
75
|
+
} catch (err) {
|
|
76
|
+
// Emit tool_result event for errors so hooks can observe failures
|
|
77
|
+
if (hookRunner.hasHandlers("tool_result")) {
|
|
78
|
+
await hookRunner.emit({
|
|
79
|
+
type: "tool_result",
|
|
80
|
+
toolName: tool.name,
|
|
81
|
+
toolCallId,
|
|
82
|
+
input: params,
|
|
83
|
+
content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
|
|
84
|
+
details: undefined,
|
|
85
|
+
isError: true,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
throw err; // Re-throw original error for agent-loop
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Wrap all tools with hook callbacks.
|
|
96
|
+
*/
|
|
97
|
+
export function wrapToolsWithHooks<T>(tools: AgentTool<any, T>[], hookRunner: HookRunner): AgentTool<any, T>[] {
|
|
98
|
+
return tools.map((tool) => wrapToolWithHooks(tool, hookRunner));
|
|
99
|
+
}
|