@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,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Print mode (single-shot): Send prompts, output result, exit.
|
|
3
|
+
*
|
|
4
|
+
* Used for:
|
|
5
|
+
* - `pi -p "prompt"` - text output
|
|
6
|
+
* - `pi --mode json "prompt"` - JSON event stream
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
|
|
10
|
+
import type { AgentSession } from "../core/agent-session.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Run in print (single-shot) mode.
|
|
14
|
+
* Sends prompts to the agent and outputs the result.
|
|
15
|
+
*
|
|
16
|
+
* @param session The agent session
|
|
17
|
+
* @param mode Output mode: "text" for final response only, "json" for all events
|
|
18
|
+
* @param messages Array of prompts to send
|
|
19
|
+
* @param initialMessage Optional first message (may contain @file content)
|
|
20
|
+
* @param initialImages Optional images for the initial message
|
|
21
|
+
*/
|
|
22
|
+
export async function runPrintMode(
|
|
23
|
+
session: AgentSession,
|
|
24
|
+
mode: "text" | "json",
|
|
25
|
+
messages: string[],
|
|
26
|
+
initialMessage?: string,
|
|
27
|
+
initialImages?: ImageContent[],
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
// Hook runner already has no-op UI context by default (set in main.ts)
|
|
30
|
+
// Set up hooks for print mode (no UI)
|
|
31
|
+
const hookRunner = session.hookRunner;
|
|
32
|
+
if (hookRunner) {
|
|
33
|
+
hookRunner.initialize({
|
|
34
|
+
getModel: () => session.model,
|
|
35
|
+
sendMessageHandler: (message, triggerTurn) => {
|
|
36
|
+
session.sendHookMessage(message, triggerTurn).catch((e) => {
|
|
37
|
+
console.error(`Hook sendMessage failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
appendEntryHandler: (customType, data) => {
|
|
41
|
+
session.sessionManager.appendCustomEntry(customType, data);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
hookRunner.onError((err) => {
|
|
45
|
+
console.error(`Hook error (${err.hookPath}): ${err.error}`);
|
|
46
|
+
});
|
|
47
|
+
// Emit session_start event
|
|
48
|
+
await hookRunner.emit({
|
|
49
|
+
type: "session_start",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Emit session start event to custom tools (no UI in print mode)
|
|
54
|
+
for (const { tool } of session.customTools) {
|
|
55
|
+
if (tool.onSession) {
|
|
56
|
+
try {
|
|
57
|
+
await tool.onSession(
|
|
58
|
+
{
|
|
59
|
+
reason: "start",
|
|
60
|
+
previousSessionFile: undefined,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
sessionManager: session.sessionManager,
|
|
64
|
+
modelRegistry: session.modelRegistry,
|
|
65
|
+
model: session.model,
|
|
66
|
+
isIdle: () => !session.isStreaming,
|
|
67
|
+
hasQueuedMessages: () => session.queuedMessageCount > 0,
|
|
68
|
+
abort: () => {
|
|
69
|
+
session.abort();
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
} catch (_err) {
|
|
74
|
+
// Silently ignore tool errors
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Always subscribe to enable session persistence via _handleAgentEvent
|
|
80
|
+
session.subscribe((event) => {
|
|
81
|
+
// In JSON mode, output all events
|
|
82
|
+
if (mode === "json") {
|
|
83
|
+
console.log(JSON.stringify(event));
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Send initial message with attachments
|
|
88
|
+
if (initialMessage) {
|
|
89
|
+
await session.prompt(initialMessage, { images: initialImages });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Send remaining messages
|
|
93
|
+
for (const message of messages) {
|
|
94
|
+
await session.prompt(message);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// In text mode, output final response
|
|
98
|
+
if (mode === "text") {
|
|
99
|
+
const state = session.state;
|
|
100
|
+
const lastMessage = state.messages[state.messages.length - 1];
|
|
101
|
+
|
|
102
|
+
if (lastMessage?.role === "assistant") {
|
|
103
|
+
const assistantMsg = lastMessage as AssistantMessage;
|
|
104
|
+
|
|
105
|
+
// Check for error/aborted
|
|
106
|
+
if (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") {
|
|
107
|
+
console.error(assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Output text content
|
|
112
|
+
for (const content of assistantMsg.content) {
|
|
113
|
+
if (content.type === "text") {
|
|
114
|
+
console.log(content.text);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Ensure stdout is fully flushed before returning
|
|
121
|
+
// This prevents race conditions where the process exits before all output is written
|
|
122
|
+
await new Promise<void>((resolve, reject) => {
|
|
123
|
+
process.stdout.write("", (err) => {
|
|
124
|
+
if (err) reject(err);
|
|
125
|
+
else resolve();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC Client for programmatic access to the coding agent.
|
|
3
|
+
*
|
|
4
|
+
* Spawns the agent in RPC mode and provides a typed API for all operations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AgentEvent, AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
8
|
+
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
9
|
+
import type { Subprocess } from "bun";
|
|
10
|
+
import type { SessionStats } from "../../core/agent-session.js";
|
|
11
|
+
import type { BashResult } from "../../core/bash-executor.js";
|
|
12
|
+
import type { CompactionResult } from "../../core/compaction/index.js";
|
|
13
|
+
import type { RpcCommand, RpcResponse, RpcSessionState } from "./rpc-types.js";
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Types
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
/** Distributive Omit that works with union types */
|
|
20
|
+
type DistributiveOmit<T, K extends keyof T> = T extends unknown ? Omit<T, K> : never;
|
|
21
|
+
|
|
22
|
+
/** RpcCommand without the id field (for internal send) */
|
|
23
|
+
type RpcCommandBody = DistributiveOmit<RpcCommand, "id">;
|
|
24
|
+
|
|
25
|
+
export interface RpcClientOptions {
|
|
26
|
+
/** Path to the CLI entry point (default: searches for dist/cli.js) */
|
|
27
|
+
cliPath?: string;
|
|
28
|
+
/** Working directory for the agent */
|
|
29
|
+
cwd?: string;
|
|
30
|
+
/** Environment variables */
|
|
31
|
+
env?: Record<string, string>;
|
|
32
|
+
/** Provider to use */
|
|
33
|
+
provider?: string;
|
|
34
|
+
/** Model ID to use */
|
|
35
|
+
model?: string;
|
|
36
|
+
/** Additional CLI arguments */
|
|
37
|
+
args?: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ModelInfo {
|
|
41
|
+
provider: string;
|
|
42
|
+
id: string;
|
|
43
|
+
contextWindow: number;
|
|
44
|
+
reasoning: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type RpcEventListener = (event: AgentEvent) => void;
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// RPC Client
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
export class RpcClient {
|
|
54
|
+
private process: Subprocess | null = null;
|
|
55
|
+
private lineReader: ReadableStreamDefaultReader<string> | null = null;
|
|
56
|
+
private eventListeners: RpcEventListener[] = [];
|
|
57
|
+
private pendingRequests: Map<string, { resolve: (response: RpcResponse) => void; reject: (error: Error) => void }> =
|
|
58
|
+
new Map();
|
|
59
|
+
private requestId = 0;
|
|
60
|
+
private stderr = "";
|
|
61
|
+
|
|
62
|
+
constructor(private options: RpcClientOptions = {}) {}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Start the RPC agent process.
|
|
66
|
+
*/
|
|
67
|
+
async start(): Promise<void> {
|
|
68
|
+
if (this.process) {
|
|
69
|
+
throw new Error("Client already started");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const cliPath = this.options.cliPath ?? "dist/cli.js";
|
|
73
|
+
const args = ["--mode", "rpc"];
|
|
74
|
+
|
|
75
|
+
if (this.options.provider) {
|
|
76
|
+
args.push("--provider", this.options.provider);
|
|
77
|
+
}
|
|
78
|
+
if (this.options.model) {
|
|
79
|
+
args.push("--model", this.options.model);
|
|
80
|
+
}
|
|
81
|
+
if (this.options.args) {
|
|
82
|
+
args.push(...this.options.args);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.process = Bun.spawn(["bun", cliPath, ...args], {
|
|
86
|
+
cwd: this.options.cwd,
|
|
87
|
+
env: { ...process.env, ...this.options.env },
|
|
88
|
+
stdin: "pipe",
|
|
89
|
+
stdout: "pipe",
|
|
90
|
+
stderr: "pipe",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Collect stderr for debugging
|
|
94
|
+
(async () => {
|
|
95
|
+
const reader = (this.process!.stderr as ReadableStream<Uint8Array>).getReader();
|
|
96
|
+
const decoder = new TextDecoder();
|
|
97
|
+
while (true) {
|
|
98
|
+
const { done, value } = await reader.read();
|
|
99
|
+
if (done) break;
|
|
100
|
+
this.stderr += decoder.decode(value);
|
|
101
|
+
}
|
|
102
|
+
})();
|
|
103
|
+
|
|
104
|
+
// Set up line reader for stdout
|
|
105
|
+
const textStream = (this.process.stdout as ReadableStream<Uint8Array>).pipeThrough(new TextDecoderStream());
|
|
106
|
+
this.lineReader = textStream
|
|
107
|
+
.pipeThrough(
|
|
108
|
+
new TransformStream<string, string>({
|
|
109
|
+
transform(chunk, controller) {
|
|
110
|
+
const lines = chunk.split("\n");
|
|
111
|
+
for (const line of lines) {
|
|
112
|
+
if (line.trim()) {
|
|
113
|
+
controller.enqueue(line);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
)
|
|
119
|
+
.getReader() as ReadableStreamDefaultReader<string>;
|
|
120
|
+
|
|
121
|
+
// Process lines in background
|
|
122
|
+
(async () => {
|
|
123
|
+
try {
|
|
124
|
+
while (true) {
|
|
125
|
+
const { done, value } = await this.lineReader!.read();
|
|
126
|
+
if (done) break;
|
|
127
|
+
this.handleLine(value);
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
// Stream closed
|
|
131
|
+
}
|
|
132
|
+
})();
|
|
133
|
+
|
|
134
|
+
// Wait a moment for process to initialize
|
|
135
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const exitCode = await Promise.race([
|
|
139
|
+
this.process.exited,
|
|
140
|
+
new Promise<null>((resolve) => setTimeout(() => resolve(null), 50)),
|
|
141
|
+
]);
|
|
142
|
+
if (exitCode !== null) {
|
|
143
|
+
throw new Error(`Agent process exited immediately with code ${exitCode}. Stderr: ${this.stderr}`);
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
// Process still running, which is what we want
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Stop the RPC agent process.
|
|
152
|
+
*/
|
|
153
|
+
async stop(): Promise<void> {
|
|
154
|
+
if (!this.process) return;
|
|
155
|
+
|
|
156
|
+
this.lineReader?.cancel();
|
|
157
|
+
this.process.kill();
|
|
158
|
+
|
|
159
|
+
// Wait for process to exit
|
|
160
|
+
await Promise.race([
|
|
161
|
+
this.process.exited,
|
|
162
|
+
new Promise<void>((resolve) => {
|
|
163
|
+
setTimeout(() => {
|
|
164
|
+
try {
|
|
165
|
+
this.process?.kill(9);
|
|
166
|
+
} catch {
|
|
167
|
+
// Already dead
|
|
168
|
+
}
|
|
169
|
+
resolve();
|
|
170
|
+
}, 1000);
|
|
171
|
+
}),
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
this.process = null;
|
|
175
|
+
this.lineReader = null;
|
|
176
|
+
this.pendingRequests.clear();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Subscribe to agent events.
|
|
181
|
+
*/
|
|
182
|
+
onEvent(listener: RpcEventListener): () => void {
|
|
183
|
+
this.eventListeners.push(listener);
|
|
184
|
+
return () => {
|
|
185
|
+
const index = this.eventListeners.indexOf(listener);
|
|
186
|
+
if (index !== -1) {
|
|
187
|
+
this.eventListeners.splice(index, 1);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get collected stderr output (useful for debugging).
|
|
194
|
+
*/
|
|
195
|
+
getStderr(): string {
|
|
196
|
+
return this.stderr;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// =========================================================================
|
|
200
|
+
// Command Methods
|
|
201
|
+
// =========================================================================
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Send a prompt to the agent.
|
|
205
|
+
* Returns immediately after sending; use onEvent() to receive streaming events.
|
|
206
|
+
* Use waitForIdle() to wait for completion.
|
|
207
|
+
*/
|
|
208
|
+
async prompt(message: string, images?: ImageContent[]): Promise<void> {
|
|
209
|
+
await this.send({ type: "prompt", message, images });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Queue a message while agent is streaming.
|
|
214
|
+
*/
|
|
215
|
+
async queueMessage(message: string): Promise<void> {
|
|
216
|
+
await this.send({ type: "queue_message", message });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Abort current operation.
|
|
221
|
+
*/
|
|
222
|
+
async abort(): Promise<void> {
|
|
223
|
+
await this.send({ type: "abort" });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Start a new session, optionally with parent tracking.
|
|
228
|
+
* @param parentSession - Optional parent session path for lineage tracking
|
|
229
|
+
* @returns Object with `cancelled: true` if a hook cancelled the new session
|
|
230
|
+
*/
|
|
231
|
+
async newSession(parentSession?: string): Promise<{ cancelled: boolean }> {
|
|
232
|
+
const response = await this.send({ type: "new_session", parentSession });
|
|
233
|
+
return this.getData(response);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Get current session state.
|
|
238
|
+
*/
|
|
239
|
+
async getState(): Promise<RpcSessionState> {
|
|
240
|
+
const response = await this.send({ type: "get_state" });
|
|
241
|
+
return this.getData(response);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Set model by provider and ID.
|
|
246
|
+
*/
|
|
247
|
+
async setModel(provider: string, modelId: string): Promise<{ provider: string; id: string }> {
|
|
248
|
+
const response = await this.send({ type: "set_model", provider, modelId });
|
|
249
|
+
return this.getData(response);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Cycle to next model.
|
|
254
|
+
*/
|
|
255
|
+
async cycleModel(): Promise<{
|
|
256
|
+
model: { provider: string; id: string };
|
|
257
|
+
thinkingLevel: ThinkingLevel;
|
|
258
|
+
isScoped: boolean;
|
|
259
|
+
} | null> {
|
|
260
|
+
const response = await this.send({ type: "cycle_model" });
|
|
261
|
+
return this.getData(response);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get list of available models.
|
|
266
|
+
*/
|
|
267
|
+
async getAvailableModels(): Promise<ModelInfo[]> {
|
|
268
|
+
const response = await this.send({ type: "get_available_models" });
|
|
269
|
+
return this.getData<{ models: ModelInfo[] }>(response).models;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Set thinking level.
|
|
274
|
+
*/
|
|
275
|
+
async setThinkingLevel(level: ThinkingLevel): Promise<void> {
|
|
276
|
+
await this.send({ type: "set_thinking_level", level });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Cycle thinking level.
|
|
281
|
+
*/
|
|
282
|
+
async cycleThinkingLevel(): Promise<{ level: ThinkingLevel } | null> {
|
|
283
|
+
const response = await this.send({ type: "cycle_thinking_level" });
|
|
284
|
+
return this.getData(response);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Set queue mode.
|
|
289
|
+
*/
|
|
290
|
+
async setQueueMode(mode: "all" | "one-at-a-time"): Promise<void> {
|
|
291
|
+
await this.send({ type: "set_queue_mode", mode });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Compact session context.
|
|
296
|
+
*/
|
|
297
|
+
async compact(customInstructions?: string): Promise<CompactionResult> {
|
|
298
|
+
const response = await this.send({ type: "compact", customInstructions });
|
|
299
|
+
return this.getData(response);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Set auto-compaction enabled/disabled.
|
|
304
|
+
*/
|
|
305
|
+
async setAutoCompaction(enabled: boolean): Promise<void> {
|
|
306
|
+
await this.send({ type: "set_auto_compaction", enabled });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Set auto-retry enabled/disabled.
|
|
311
|
+
*/
|
|
312
|
+
async setAutoRetry(enabled: boolean): Promise<void> {
|
|
313
|
+
await this.send({ type: "set_auto_retry", enabled });
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Abort in-progress retry.
|
|
318
|
+
*/
|
|
319
|
+
async abortRetry(): Promise<void> {
|
|
320
|
+
await this.send({ type: "abort_retry" });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Execute a bash command.
|
|
325
|
+
*/
|
|
326
|
+
async bash(command: string): Promise<BashResult> {
|
|
327
|
+
const response = await this.send({ type: "bash", command });
|
|
328
|
+
return this.getData(response);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Abort running bash command.
|
|
333
|
+
*/
|
|
334
|
+
async abortBash(): Promise<void> {
|
|
335
|
+
await this.send({ type: "abort_bash" });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get session statistics.
|
|
340
|
+
*/
|
|
341
|
+
async getSessionStats(): Promise<SessionStats> {
|
|
342
|
+
const response = await this.send({ type: "get_session_stats" });
|
|
343
|
+
return this.getData(response);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Export session to HTML.
|
|
348
|
+
*/
|
|
349
|
+
async exportHtml(outputPath?: string): Promise<{ path: string }> {
|
|
350
|
+
const response = await this.send({ type: "export_html", outputPath });
|
|
351
|
+
return this.getData(response);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Switch to a different session file.
|
|
356
|
+
* @returns Object with `cancelled: true` if a hook cancelled the switch
|
|
357
|
+
*/
|
|
358
|
+
async switchSession(sessionPath: string): Promise<{ cancelled: boolean }> {
|
|
359
|
+
const response = await this.send({ type: "switch_session", sessionPath });
|
|
360
|
+
return this.getData(response);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Branch from a specific message.
|
|
365
|
+
* @returns Object with `text` (the message text) and `cancelled` (if hook cancelled)
|
|
366
|
+
*/
|
|
367
|
+
async branch(entryId: string): Promise<{ text: string; cancelled: boolean }> {
|
|
368
|
+
const response = await this.send({ type: "branch", entryId });
|
|
369
|
+
return this.getData(response);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Get messages available for branching.
|
|
374
|
+
*/
|
|
375
|
+
async getBranchMessages(): Promise<Array<{ entryId: string; text: string }>> {
|
|
376
|
+
const response = await this.send({ type: "get_branch_messages" });
|
|
377
|
+
return this.getData<{ messages: Array<{ entryId: string; text: string }> }>(response).messages;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Get text of last assistant message.
|
|
382
|
+
*/
|
|
383
|
+
async getLastAssistantText(): Promise<string | null> {
|
|
384
|
+
const response = await this.send({ type: "get_last_assistant_text" });
|
|
385
|
+
return this.getData<{ text: string | null }>(response).text;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Get all messages in the session.
|
|
390
|
+
*/
|
|
391
|
+
async getMessages(): Promise<AgentMessage[]> {
|
|
392
|
+
const response = await this.send({ type: "get_messages" });
|
|
393
|
+
return this.getData<{ messages: AgentMessage[] }>(response).messages;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// =========================================================================
|
|
397
|
+
// Helpers
|
|
398
|
+
// =========================================================================
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Wait for agent to become idle (no streaming).
|
|
402
|
+
* Resolves when agent_end event is received.
|
|
403
|
+
*/
|
|
404
|
+
waitForIdle(timeout = 60000): Promise<void> {
|
|
405
|
+
return new Promise((resolve, reject) => {
|
|
406
|
+
const timer = setTimeout(() => {
|
|
407
|
+
unsubscribe();
|
|
408
|
+
reject(new Error(`Timeout waiting for agent to become idle. Stderr: ${this.stderr}`));
|
|
409
|
+
}, timeout);
|
|
410
|
+
|
|
411
|
+
const unsubscribe = this.onEvent((event) => {
|
|
412
|
+
if (event.type === "agent_end") {
|
|
413
|
+
clearTimeout(timer);
|
|
414
|
+
unsubscribe();
|
|
415
|
+
resolve();
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Collect events until agent becomes idle.
|
|
423
|
+
*/
|
|
424
|
+
collectEvents(timeout = 60000): Promise<AgentEvent[]> {
|
|
425
|
+
return new Promise((resolve, reject) => {
|
|
426
|
+
const events: AgentEvent[] = [];
|
|
427
|
+
const timer = setTimeout(() => {
|
|
428
|
+
unsubscribe();
|
|
429
|
+
reject(new Error(`Timeout collecting events. Stderr: ${this.stderr}`));
|
|
430
|
+
}, timeout);
|
|
431
|
+
|
|
432
|
+
const unsubscribe = this.onEvent((event) => {
|
|
433
|
+
events.push(event);
|
|
434
|
+
if (event.type === "agent_end") {
|
|
435
|
+
clearTimeout(timer);
|
|
436
|
+
unsubscribe();
|
|
437
|
+
resolve(events);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Send prompt and wait for completion, returning all events.
|
|
445
|
+
*/
|
|
446
|
+
async promptAndWait(message: string, images?: ImageContent[], timeout = 60000): Promise<AgentEvent[]> {
|
|
447
|
+
const eventsPromise = this.collectEvents(timeout);
|
|
448
|
+
await this.prompt(message, images);
|
|
449
|
+
return eventsPromise;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// =========================================================================
|
|
453
|
+
// Internal
|
|
454
|
+
// =========================================================================
|
|
455
|
+
|
|
456
|
+
private handleLine(line: string): void {
|
|
457
|
+
try {
|
|
458
|
+
const data = JSON.parse(line);
|
|
459
|
+
|
|
460
|
+
// Check if it's a response to a pending request
|
|
461
|
+
if (data.type === "response" && data.id && this.pendingRequests.has(data.id)) {
|
|
462
|
+
const pending = this.pendingRequests.get(data.id)!;
|
|
463
|
+
this.pendingRequests.delete(data.id);
|
|
464
|
+
pending.resolve(data as RpcResponse);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Otherwise it's an event
|
|
469
|
+
for (const listener of this.eventListeners) {
|
|
470
|
+
listener(data as AgentEvent);
|
|
471
|
+
}
|
|
472
|
+
} catch {
|
|
473
|
+
// Ignore non-JSON lines
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private send(command: RpcCommandBody): Promise<RpcResponse> {
|
|
478
|
+
if (!this.process?.stdin) {
|
|
479
|
+
throw new Error("Client not started");
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const id = `req_${++this.requestId}`;
|
|
483
|
+
const fullCommand = { ...command, id } as RpcCommand;
|
|
484
|
+
|
|
485
|
+
return new Promise((resolve, reject) => {
|
|
486
|
+
const timeout = setTimeout(() => {
|
|
487
|
+
this.pendingRequests.delete(id);
|
|
488
|
+
reject(new Error(`Timeout waiting for response to ${command.type}. Stderr: ${this.stderr}`));
|
|
489
|
+
}, 30000);
|
|
490
|
+
|
|
491
|
+
this.pendingRequests.set(id, {
|
|
492
|
+
resolve: (response) => {
|
|
493
|
+
clearTimeout(timeout);
|
|
494
|
+
resolve(response);
|
|
495
|
+
},
|
|
496
|
+
reject: (error) => {
|
|
497
|
+
clearTimeout(timeout);
|
|
498
|
+
reject(error);
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// Write to stdin after registering the handler
|
|
503
|
+
const stdin = this.process!.stdin as import("bun").FileSink;
|
|
504
|
+
stdin.write(new TextEncoder().encode(`${JSON.stringify(fullCommand)}\n`));
|
|
505
|
+
// flush() returns number | Promise<number> - handle both cases
|
|
506
|
+
const flushResult = stdin.flush();
|
|
507
|
+
if (flushResult instanceof Promise) {
|
|
508
|
+
flushResult.catch((err: Error) => {
|
|
509
|
+
this.pendingRequests.delete(id);
|
|
510
|
+
clearTimeout(timeout);
|
|
511
|
+
reject(err);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private getData<T>(response: RpcResponse): T {
|
|
518
|
+
if (!response.success) {
|
|
519
|
+
const errorResponse = response as Extract<RpcResponse, { success: false }>;
|
|
520
|
+
throw new Error(errorResponse.error);
|
|
521
|
+
}
|
|
522
|
+
// Type assertion: we trust response.data matches T based on the command sent.
|
|
523
|
+
// This is safe because each public method specifies the correct T for its command.
|
|
524
|
+
const successResponse = response as Extract<RpcResponse, { success: true; data: unknown }>;
|
|
525
|
+
return successResponse.data as T;
|
|
526
|
+
}
|
|
527
|
+
}
|