Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
193 lines
5.8 KiB
TypeScript
193 lines
5.8 KiB
TypeScript
import type { ToolCall, ToolResult, AIPermissionMode, WebSearchConfig } from '../types';
|
|
import type {
|
|
TerminalContextReader,
|
|
} from '../../../domain/terminalContextRead';
|
|
import {
|
|
executeTerminalExecute,
|
|
executeWorkspaceGetInfo,
|
|
executeWorkspaceGetSessionInfo,
|
|
executeWebSearch,
|
|
executeUrlFetch,
|
|
type ToolDeps,
|
|
type ToolExecResult,
|
|
} from '../shared/toolExecutors';
|
|
import { fitTerminalExecuteResultForModel } from '../harness/terminalCompression';
|
|
|
|
/**
|
|
* Bridge interface for Catty Agent to interact with the Electron main process.
|
|
* This mirrors the AI-related subset of window.netcatty from electron/preload.cjs.
|
|
*/
|
|
export interface NetcattyBridge {
|
|
aiExec(
|
|
sessionId: string,
|
|
command: string,
|
|
chatSessionId?: string,
|
|
): Promise<{
|
|
ok: boolean;
|
|
stdout?: string;
|
|
stderr?: string;
|
|
exitCode?: number;
|
|
error?: string;
|
|
}>;
|
|
/**
|
|
* Cancel any in-flight Catty Agent command execution scoped to the
|
|
* given chat session. Idempotent — safe to call when nothing is
|
|
* running. Used by tools to re-issue cancel during the IPC transit
|
|
* window if the user clicks Stop after we've already dispatched
|
|
* `aiExec` but before the main process has registered it.
|
|
*/
|
|
aiCattyCancelExec?(chatSessionId: string): Promise<unknown>;
|
|
aiSetChatSessionCancelled?(chatSessionId: string, cancelled?: boolean): Promise<{ ok: boolean; error?: string }>;
|
|
aiCapability?(
|
|
rpcMethod: string,
|
|
params: Record<string, unknown>,
|
|
chatSessionId?: string,
|
|
): Promise<unknown>;
|
|
}
|
|
|
|
// Workspace context provided to the executor
|
|
export interface ExecutorContext {
|
|
// Available sessions in scope
|
|
sessions: Array<{
|
|
sessionId: string;
|
|
hostId: string;
|
|
hostname: string;
|
|
label: string;
|
|
os?: string;
|
|
username?: string;
|
|
protocol?: string;
|
|
shellType?: string;
|
|
deviceType?: string;
|
|
connected: boolean;
|
|
}>;
|
|
// Workspace info
|
|
workspaceId?: string;
|
|
workspaceName?: string;
|
|
readTerminalContext?: TerminalContextReader;
|
|
}
|
|
|
|
/** Convert a shared ToolExecResult into the executor's ToolResult format. */
|
|
function toToolResult(toolCallId: string, r: ToolExecResult): ToolResult {
|
|
if (r.ok === false) {
|
|
if (
|
|
typeof r.data === 'object'
|
|
&& r.data !== null
|
|
&& 'stdout' in r.data
|
|
&& 'stderr' in r.data
|
|
&& 'exitCode' in r.data
|
|
) {
|
|
const fitted = fitTerminalExecuteResultForModel(r.data as {
|
|
stdout: string;
|
|
stderr: string;
|
|
exitCode: number | null;
|
|
});
|
|
const output = [
|
|
r.error,
|
|
fitted.stdout ? `Partial output:\n${fitted.stdout}` : '',
|
|
fitted.stderr ? `Stderr:\n${fitted.stderr}` : '',
|
|
].filter(Boolean).join('\n\n');
|
|
return { toolCallId, content: output, isError: true };
|
|
}
|
|
return { toolCallId, content: r.error, isError: true };
|
|
}
|
|
// For terminal_execute, format as the legacy STDOUT/STDERR/exitCode text block
|
|
if (
|
|
typeof r.data === 'object' &&
|
|
r.data !== null &&
|
|
'stdout' in r.data &&
|
|
'stderr' in r.data &&
|
|
'exitCode' in r.data
|
|
) {
|
|
const d = r.data as { stdout: string; stderr: string; exitCode: number };
|
|
const output = [
|
|
d.stdout ? `STDOUT:\n${d.stdout}` : '',
|
|
d.stderr ? `STDERR:\n${d.stderr}` : '',
|
|
`Exit code: ${d.exitCode === -1 ? 'unknown' : d.exitCode}`,
|
|
]
|
|
.filter(Boolean)
|
|
.join('\n\n');
|
|
return { toolCallId, content: output || 'Command completed (no output)' };
|
|
}
|
|
// Default: JSON-serialize the data
|
|
return { toolCallId, content: JSON.stringify(r.data, null, 2) };
|
|
}
|
|
|
|
/**
|
|
* Create a tool executor function for the Catty Agent.
|
|
* This bridges tool calls to the netcatty Electron IPC layer.
|
|
*/
|
|
export function createToolExecutor(
|
|
bridge: NetcattyBridge | undefined,
|
|
context: ExecutorContext,
|
|
commandBlocklist?: string[],
|
|
permissionMode: AIPermissionMode = 'confirm',
|
|
webSearchConfig?: WebSearchConfig,
|
|
chatSessionId?: string,
|
|
): (toolCall: ToolCall) => Promise<ToolResult> {
|
|
return async (toolCall: ToolCall): Promise<ToolResult> => {
|
|
if (!bridge) {
|
|
return {
|
|
toolCallId: toolCall.id,
|
|
content: 'Netcatty bridge is not available',
|
|
isError: true,
|
|
};
|
|
}
|
|
|
|
const deps: ToolDeps = { bridge, context, commandBlocklist, permissionMode, webSearchConfig, chatSessionId };
|
|
const args = toolCall.arguments;
|
|
|
|
try {
|
|
switch (toolCall.name) {
|
|
case 'terminal_execute': {
|
|
const r = await executeTerminalExecute(deps, {
|
|
sessionId: String(args.sessionId || ''),
|
|
command: String(args.command || ''),
|
|
});
|
|
return toToolResult(toolCall.id, r);
|
|
}
|
|
|
|
case 'workspace_get_info': {
|
|
const r = executeWorkspaceGetInfo(deps);
|
|
return toToolResult(toolCall.id, r);
|
|
}
|
|
|
|
case 'workspace_get_session_info': {
|
|
const r = executeWorkspaceGetSessionInfo(deps, {
|
|
sessionId: String(args.sessionId || ''),
|
|
});
|
|
return toToolResult(toolCall.id, r);
|
|
}
|
|
|
|
case 'web_search': {
|
|
const r = await executeWebSearch(deps, {
|
|
query: String(args.query || ''),
|
|
maxResults: Number(args.maxResults) || 5,
|
|
});
|
|
return toToolResult(toolCall.id, r);
|
|
}
|
|
|
|
case 'url_fetch': {
|
|
const r = await executeUrlFetch(deps, {
|
|
url: String(args.url || ''),
|
|
maxLength: Number(args.maxLength) || 50000,
|
|
});
|
|
return toToolResult(toolCall.id, r);
|
|
}
|
|
|
|
default:
|
|
return {
|
|
toolCallId: toolCall.id,
|
|
content: `Unknown tool: ${toolCall.name}`,
|
|
isError: true,
|
|
};
|
|
}
|
|
} catch (err) {
|
|
return {
|
|
toolCallId: toolCall.id,
|
|
content: `Tool execution error: ${err instanceof Error ? err.message : String(err)}`,
|
|
isError: true,
|
|
};
|
|
}
|
|
};
|
|
}
|