mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 13:55:50 +00:00
feat: 远程群控 (#243)
* feat: restore pipe IPC, LAN pipes, monitor tool, and PR-package features Core IPC system (UDS_INBOX): - PipeServer/PipeClient with UDS + TCP dual transport, NDJSON protocol - PipeRegistry: machineId-based role assignment, file locking - Master/slave attach, prompt relay, permission forwarding - Heartbeat lifecycle with parallel isPipeAlive probes - Commands: /pipes, /attach, /detach, /send, /claim-main, /pipe-status LAN Pipes (LAN_PIPES): - UDP multicast beacon (224.0.71.67:7101) for zero-config LAN discovery - PipeServer TCP listener, PipeClient TCP connect mode - Heartbeat auto-attaches LAN peers via TCP - Cross-machine attach allowed regardless of role - /pipes shows [LAN] peers with role + hostname/IP - SendMessageTool supports tcp: scheme with user consent Architecture — extracted hooks from REPL.tsx (~830 lines → ~20 lines): - usePipeIpc: lifecycle (bootstrap, handlers, heartbeat, cleanup) - usePipeRelay: slave→master message relay via module singleton - usePipePermissionForward: permission request/cancel forwarding - usePipeRouter: selected pipe input routing with role+IP labels - Shared ndjsonFramer.ts replaces 3 duplicate NDJSON parsers Key fixes applied during development: - Multicast binds to correct LAN interface (not WSL/Docker) - Beacon ref stored as module singleton (not Zustand state mutation) - Heartbeat preserves LAN peers in discoveredPipes and selectedPipes - Disconnect handler calls removeSlaveClient (fixes listener leak) - cleanupStaleEntries probes without lock, writes briefly under lock - getMachineId uses async execFile (not blocking execSync) - globalThis.__pipeSendToMaster replaced with setPipeRelay singleton - M key only toggles route mode when selector panel is expanded - User prompt displayed in message list on pipe broadcast - Broadcast notifications show [role] + hostname/IP for LAN peers Other restored features: - Monitor tool: /monitor command, MonitorTool, MonitorMcpTask lifecycle - Daemon supervisor and remoteControlServer command - Tools: SnipTool, SleepTool, ListPeersTool, SendUserFileTool, WebBrowserTool, WorkflowTool, and 10+ stub→implementation rewrites - Feature flags: UDS_INBOX, LAN_PIPES, MONITOR_TOOL, FORK_SUBAGENT, KAIROS, COORDINATOR_MODE, WORKFLOW_SCRIPTS, HISTORY_SNIP Tests: 2190 pass / 0 fail (15 new: lanBeacon 7, peerAddress 8) * fix: resolve merge conflicts and fix all tsc/test errors after main merge - Export ToolResultBlockParam from Tool.ts (14 tool files fixed) - Migrate ink imports from ../../ink.js to @anthropic/ink (7 files) - Fix toolUseID → toolUseId typo in monitor.ts and MonitorTool.tsx - Add fallback values for string|undefined type errors (8 locations) - Fix AppState type in assistant.ts, add NewInstallWizard stubs - Fix ParsedRepository.repo → .name in subscribe-pr.ts - Fix AgentId/string type mismatch in BackgroundTasksDialog.tsx - Fix PipeRelayFn return type in pipePermissionRelay.ts - Use PipeMessage type in usePipeRelay.ts - Fix lanBeacon.test.ts mock type assertions - Create missing MouseActionEvent class for ink package - Use ansi: color format instead of bare "green"/"red" - Resolve theme.permission access via getTheme() Result: 0 tsc errors, 2496 tests pass, 0 fail Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: 恢复 /poor 的说明 --------- Co-authored-by: unraid <local@unraid.local> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
80
src/tools/CtxInspectTool/CtxInspectTool.ts
Normal file
80
src/tools/CtxInspectTool/CtxInspectTool.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { z } from 'zod/v4'
|
||||
import type { ToolResultBlockParam } from '../../Tool.js'
|
||||
import { buildTool } from '../../Tool.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
|
||||
const CTX_INSPECT_TOOL_NAME = 'CtxInspect'
|
||||
|
||||
const inputSchema = lazySchema(() =>
|
||||
z.strictObject({
|
||||
query: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Optional query to filter context entries. If omitted, returns a summary of all context.'),
|
||||
}),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
type CtxInput = z.infer<InputSchema>
|
||||
|
||||
type CtxOutput = {
|
||||
total_tokens: number
|
||||
message_count: number
|
||||
summary: string
|
||||
}
|
||||
|
||||
export const CtxInspectTool = buildTool({
|
||||
name: CTX_INSPECT_TOOL_NAME,
|
||||
searchHint: 'context inspect tokens usage messages window collapse',
|
||||
maxResultSizeChars: 50_000,
|
||||
strict: true,
|
||||
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
|
||||
async description() {
|
||||
return 'Inspect the current context window contents and token usage'
|
||||
},
|
||||
async prompt() {
|
||||
return `Inspect the current conversation context. Shows token usage, message count, and a breakdown of what's consuming context space.
|
||||
|
||||
Use this to understand your context budget before deciding whether to snip old messages or adjust your approach.`
|
||||
},
|
||||
|
||||
isConcurrencySafe() {
|
||||
return true
|
||||
},
|
||||
isReadOnly() {
|
||||
return true
|
||||
},
|
||||
|
||||
userFacingName() {
|
||||
return 'CtxInspect'
|
||||
},
|
||||
|
||||
renderToolUseMessage() {
|
||||
return 'Context Inspect'
|
||||
},
|
||||
|
||||
mapToolResultToToolResultBlockParam(
|
||||
content: CtxOutput,
|
||||
toolUseID: string,
|
||||
): ToolResultBlockParam {
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result',
|
||||
content: `Context: ${content.total_tokens} tokens, ${content.message_count} messages\n${content.summary}`,
|
||||
}
|
||||
},
|
||||
|
||||
async call() {
|
||||
// Context inspection is wired into the context collapse system.
|
||||
return {
|
||||
data: {
|
||||
total_tokens: 0,
|
||||
message_count: 0,
|
||||
summary: 'Context inspection requires the CONTEXT_COLLAPSE runtime.',
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
107
src/tools/ListPeersTool/ListPeersTool.ts
Normal file
107
src/tools/ListPeersTool/ListPeersTool.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { z } from 'zod/v4'
|
||||
import type { ToolResultBlockParam } from '../../Tool.js'
|
||||
import { buildTool } from '../../Tool.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
|
||||
const LIST_PEERS_TOOL_NAME = 'ListPeers'
|
||||
|
||||
const inputSchema = lazySchema(() =>
|
||||
z.strictObject({
|
||||
include_self: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Whether to include the current session in the list. Defaults to false.'),
|
||||
}),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
type ListPeersInput = z.infer<InputSchema>
|
||||
|
||||
type PeerInfo = {
|
||||
address: string
|
||||
name?: string
|
||||
cwd?: string
|
||||
pid?: number
|
||||
}
|
||||
type ListPeersOutput = { peers: PeerInfo[] }
|
||||
|
||||
export const ListPeersTool = buildTool({
|
||||
name: LIST_PEERS_TOOL_NAME,
|
||||
searchHint: 'list peers sessions discover uds socket messaging',
|
||||
maxResultSizeChars: 50_000,
|
||||
strict: true,
|
||||
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
|
||||
async description() {
|
||||
return 'Discover other Claude Code sessions for cross-session messaging'
|
||||
},
|
||||
async prompt() {
|
||||
return `List active Claude Code sessions that can receive messages via SendMessage.
|
||||
|
||||
Returns an array of peers with their addresses. Use these addresses as the \`to\` field in SendMessage:
|
||||
- \`"uds:/path/to.sock"\` — local sessions on the same machine (Unix Domain Socket)
|
||||
- \`"bridge:session_..."\` — remote sessions via Remote Control
|
||||
|
||||
Use this tool to discover messaging targets before sending cross-session messages. Only running sessions with active messaging sockets are returned.`
|
||||
},
|
||||
|
||||
isConcurrencySafe() {
|
||||
return true
|
||||
},
|
||||
isReadOnly() {
|
||||
return true
|
||||
},
|
||||
|
||||
userFacingName() {
|
||||
return LIST_PEERS_TOOL_NAME
|
||||
},
|
||||
|
||||
renderToolUseMessage() {
|
||||
return 'ListPeers'
|
||||
},
|
||||
|
||||
mapToolResultToToolResultBlockParam(
|
||||
content: ListPeersOutput,
|
||||
toolUseID: string,
|
||||
): ToolResultBlockParam {
|
||||
const lines = content.peers.map(
|
||||
p => `${p.address}${p.name ? ` (${p.name})` : ''}${p.cwd ? ` @ ${p.cwd}` : ''}`,
|
||||
)
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result',
|
||||
content:
|
||||
lines.length > 0
|
||||
? `Found ${lines.length} peer(s):\n${lines.join('\n')}`
|
||||
: 'No peers found.',
|
||||
}
|
||||
},
|
||||
|
||||
async call(_input: ListPeersInput, context) {
|
||||
// Peer discovery uses the concurrent sessions PID registry and
|
||||
// UDS socket directory. The implementation scans for live sockets
|
||||
// and optionally includes Remote Control bridge peers.
|
||||
const peers: PeerInfo[] = []
|
||||
|
||||
// Discovery is handled by the UDS messaging subsystem initialized in setup.ts.
|
||||
// Return discovered peers from the app state.
|
||||
const appState = context.getAppState()
|
||||
const messagingSocketPath = (appState as Record<string, unknown>).messagingSocketPath as string | undefined
|
||||
if (messagingSocketPath) {
|
||||
// Self entry for reference
|
||||
if (_input.include_self) {
|
||||
peers.push({
|
||||
address: `uds:${messagingSocketPath}`,
|
||||
name: 'self',
|
||||
pid: process.pid,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data: { peers },
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,3 +0,0 @@
|
||||
// Auto-generated stub — replace with real implementation
|
||||
export {};
|
||||
export const MonitorTool: Record<string, unknown> = {};
|
||||
190
src/tools/MonitorTool/MonitorTool.tsx
Normal file
190
src/tools/MonitorTool/MonitorTool.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import React from 'react'
|
||||
import { Text } from '@anthropic/ink'
|
||||
import { z } from 'zod/v4'
|
||||
import { TOOL_SUMMARY_MAX_LENGTH } from '../../constants/toolLimits.js'
|
||||
import type { ToolResultBlockParam, ToolUseContext, ValidationResult } from '../../Tool.js'
|
||||
import { buildTool } from '../../Tool.js'
|
||||
import { spawnShellTask } from '../../tasks/LocalShellTask/LocalShellTask.js'
|
||||
import { bashToolHasPermission } from '../BashTool/bashPermissions.js'
|
||||
import type { PermissionResult } from '../../utils/permissions/PermissionResult.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
import { truncate } from '../../utils/format.js'
|
||||
import { exec } from '../../utils/Shell.js'
|
||||
import { getTaskOutputPath } from '../../utils/task/diskOutput.js'
|
||||
import { logEvent } from '../../services/analytics/index.js'
|
||||
|
||||
const MONITOR_TOOL_NAME = 'Monitor'
|
||||
|
||||
const inputSchema = lazySchema(() =>
|
||||
z.strictObject({
|
||||
command: z
|
||||
.string()
|
||||
.describe(
|
||||
'The shell command to run as a long-running monitor. Should produce streaming output (e.g., tail -f, watch, polling loops).',
|
||||
),
|
||||
description: z
|
||||
.string()
|
||||
.describe(
|
||||
'Clear, concise description of what this monitor watches. Used as the label in the background tasks UI.',
|
||||
),
|
||||
}),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
export type MonitorInput = z.infer<InputSchema>
|
||||
|
||||
const outputSchema = lazySchema(() =>
|
||||
z.object({
|
||||
taskId: z.string(),
|
||||
outputFile: z.string(),
|
||||
}),
|
||||
)
|
||||
type OutputSchema = ReturnType<typeof outputSchema>
|
||||
export type MonitorOutput = z.infer<OutputSchema>
|
||||
|
||||
export const MonitorTool = buildTool({
|
||||
name: MONITOR_TOOL_NAME,
|
||||
searchHint: 'start long-running background monitor for streaming events',
|
||||
maxResultSizeChars: 10_000,
|
||||
strict: true,
|
||||
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
get outputSchema(): OutputSchema {
|
||||
return outputSchema()
|
||||
},
|
||||
|
||||
async description() {
|
||||
return 'Start a long-running background monitor'
|
||||
},
|
||||
async prompt() {
|
||||
return `Use Monitor to start a long-running background process that streams output (watching logs, polling APIs, tailing files, etc.). The command runs in the background and you receive a notification when it exits. Use the Read tool with the output file path to check its output at any time.
|
||||
|
||||
Guidelines:
|
||||
- Use Monitor for commands that produce ongoing streaming output: \`tail -f\`, log watchers, file watchers, API polling loops, \`watch\` commands
|
||||
- Do NOT use Monitor for one-shot commands that finish quickly — use Bash for those
|
||||
- Do NOT use Monitor for commands that need interactive input — they will hang
|
||||
- The description should clearly explain what is being monitored
|
||||
- You'll get a task notification when the monitor process exits (stream ends, script fails, or killed)
|
||||
- To check output at any time, use Read on the output file path returned by this tool
|
||||
|
||||
Examples:
|
||||
- Watching a log file: command="tail -f /var/log/app.log", description="Watch app log for errors"
|
||||
- Polling an API: command="while true; do curl -s http://localhost:3000/health; sleep 5; done", description="Poll health endpoint every 5s"
|
||||
- Watching for file changes: command="inotifywait -m -r ./src", description="Watch src directory for file changes"`
|
||||
},
|
||||
|
||||
isConcurrencySafe() {
|
||||
return true
|
||||
},
|
||||
|
||||
isReadOnly() {
|
||||
// Monitor executes shell commands which may have side effects
|
||||
return false
|
||||
},
|
||||
|
||||
toAutoClassifierInput(input: MonitorInput) {
|
||||
return `Monitor: ${input.command}`
|
||||
},
|
||||
|
||||
async checkPermissions(
|
||||
input: MonitorInput,
|
||||
context: ToolUseContext,
|
||||
): Promise<PermissionResult> {
|
||||
// Reuse bash permission checking for the underlying command
|
||||
return bashToolHasPermission(
|
||||
{ command: input.command },
|
||||
context,
|
||||
)
|
||||
},
|
||||
|
||||
userFacingName() {
|
||||
return MONITOR_TOOL_NAME
|
||||
},
|
||||
|
||||
getActivityDescription(input: MonitorInput) {
|
||||
if (!input?.description) {
|
||||
return 'Starting monitor'
|
||||
}
|
||||
return `Monitoring: ${truncate(input.description, TOOL_SUMMARY_MAX_LENGTH)}`
|
||||
},
|
||||
|
||||
async validateInput(input: MonitorInput): Promise<ValidationResult> {
|
||||
if (!input.command || input.command.trim() === '') {
|
||||
return {
|
||||
result: false,
|
||||
message: 'Monitor command cannot be empty.',
|
||||
errorCode: 1,
|
||||
}
|
||||
}
|
||||
if (!input.description || input.description.trim() === '') {
|
||||
return {
|
||||
result: false,
|
||||
message: 'Monitor description cannot be empty.',
|
||||
errorCode: 2,
|
||||
}
|
||||
}
|
||||
return { result: true }
|
||||
},
|
||||
|
||||
async call(input: MonitorInput, context: ToolUseContext) {
|
||||
const { command, description } = input
|
||||
const {
|
||||
abortController,
|
||||
setAppState,
|
||||
toolUseId,
|
||||
agentId,
|
||||
} = context
|
||||
|
||||
logEvent('tengu_monitor_tool_used', {})
|
||||
|
||||
// Create the shell command via exec
|
||||
const shellCommand = await exec(command, abortController.signal, 'bash')
|
||||
|
||||
// Spawn as a background task with kind: 'monitor'
|
||||
const handle = await spawnShellTask(
|
||||
{
|
||||
command,
|
||||
description,
|
||||
shellCommand,
|
||||
toolUseId: toolUseId,
|
||||
agentId,
|
||||
kind: 'monitor',
|
||||
},
|
||||
{
|
||||
abortController,
|
||||
getAppState: context.getAppState,
|
||||
setAppState,
|
||||
},
|
||||
)
|
||||
|
||||
const outputFile = getTaskOutputPath(handle.taskId)
|
||||
|
||||
return {
|
||||
data: {
|
||||
taskId: handle.taskId,
|
||||
outputFile,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
renderToolUseMessage(input: MonitorInput, { verbose }) {
|
||||
const desc = truncate(input.description || input.command, 80)
|
||||
return `Monitor: ${desc}`
|
||||
},
|
||||
|
||||
mapToolResultToToolResultBlockParam(
|
||||
content: MonitorOutput,
|
||||
toolUseId: string,
|
||||
): ToolResultBlockParam {
|
||||
return {
|
||||
tool_use_id: toolUseId,
|
||||
type: 'tool_result',
|
||||
content: `Monitor started (task ${content.taskId}). Output file: ${content.outputFile}`,
|
||||
}
|
||||
},
|
||||
|
||||
renderToolResultMessage(output: MonitorOutput) {
|
||||
return <Text>Monitor started (task {output.taskId}). Output: {output.outputFile}</Text>
|
||||
},
|
||||
})
|
||||
@@ -7,10 +7,6 @@ mock.module("src/utils/cwd.js", () => ({
|
||||
getCwd: () => mockCwd,
|
||||
}));
|
||||
|
||||
mock.module("src/utils/powershell/parser.js", () => ({
|
||||
PS_TOKENIZER_DASH_CHARS: new Set(["-", "\u2013", "\u2014", "\u2015"]),
|
||||
}));
|
||||
|
||||
const { isGitInternalPathPS, isDotGitPathPS } = await import("../gitSafety");
|
||||
|
||||
describe("isGitInternalPathPS", () => {
|
||||
|
||||
87
src/tools/PushNotificationTool/PushNotificationTool.ts
Normal file
87
src/tools/PushNotificationTool/PushNotificationTool.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { z } from 'zod/v4'
|
||||
import type { ToolResultBlockParam } from '../../Tool.js'
|
||||
import { buildTool } from '../../Tool.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
|
||||
const PUSH_NOTIFICATION_TOOL_NAME = 'PushNotification'
|
||||
|
||||
const inputSchema = lazySchema(() =>
|
||||
z.strictObject({
|
||||
title: z
|
||||
.string()
|
||||
.describe('Title of the push notification.'),
|
||||
body: z
|
||||
.string()
|
||||
.describe('Body text of the push notification.'),
|
||||
priority: z
|
||||
.enum(['normal', 'high'])
|
||||
.optional()
|
||||
.describe('Notification priority. Use "high" for blockers or permission prompts.'),
|
||||
}),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
type PushInput = z.infer<InputSchema>
|
||||
|
||||
type PushOutput = { sent: boolean }
|
||||
|
||||
export const PushNotificationTool = buildTool({
|
||||
name: PUSH_NOTIFICATION_TOOL_NAME,
|
||||
searchHint: 'push notification mobile alert notify user',
|
||||
maxResultSizeChars: 1_000,
|
||||
strict: true,
|
||||
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
|
||||
async description() {
|
||||
return 'Send a push notification to the user\'s mobile device'
|
||||
},
|
||||
async prompt() {
|
||||
return `Send a push notification to the user's mobile device via Remote Control.
|
||||
|
||||
Use this when:
|
||||
- A long-running task completes and the user may not be watching
|
||||
- A permission prompt is waiting and you need user input
|
||||
- Something urgent requires the user's attention
|
||||
|
||||
Requires Remote Control to be configured. Respects user notification settings (taskCompleteNotifEnabled, inputNeededNotifEnabled, agentPushNotifEnabled).`
|
||||
},
|
||||
|
||||
isConcurrencySafe() {
|
||||
return true
|
||||
},
|
||||
isReadOnly() {
|
||||
return true
|
||||
},
|
||||
|
||||
userFacingName() {
|
||||
return 'Notify'
|
||||
},
|
||||
|
||||
renderToolUseMessage(input: Partial<PushInput>) {
|
||||
return `Push: ${input.title ?? '...'}`
|
||||
},
|
||||
|
||||
mapToolResultToToolResultBlockParam(
|
||||
content: PushOutput,
|
||||
toolUseID: string,
|
||||
): ToolResultBlockParam {
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result',
|
||||
content: content.sent ? 'Notification sent.' : 'Failed to send notification.',
|
||||
}
|
||||
},
|
||||
|
||||
async call(_input: PushInput) {
|
||||
// Push delivery is handled by the Remote Control / KAIROS transport layer.
|
||||
// Without the KAIROS runtime, this tool is not available.
|
||||
return {
|
||||
data: {
|
||||
sent: false,
|
||||
error: 'PushNotification requires the KAIROS transport layer.',
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1 +0,0 @@
|
||||
export const REPLTool = { name: 'REPLTool', isEnabled: () => false }
|
||||
89
src/tools/REPLTool/REPLTool.ts
Normal file
89
src/tools/REPLTool/REPLTool.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { z } from 'zod/v4'
|
||||
import type { ToolResultBlockParam } from '../../Tool.js'
|
||||
import { buildTool } from '../../Tool.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
import { REPL_TOOL_NAME } from './constants.js'
|
||||
|
||||
const inputSchema = lazySchema(() =>
|
||||
z.strictObject({
|
||||
code: z
|
||||
.string()
|
||||
.describe(
|
||||
'The code to execute in the REPL. Can call any primitive tool (Read, Write, Edit, Glob, Grep, Bash, NotebookEdit, Agent) via their APIs.',
|
||||
),
|
||||
}),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
type REPLInput = z.infer<InputSchema>
|
||||
|
||||
type REPLOutput = { result: string; tool_calls: number }
|
||||
|
||||
export const REPLTool = buildTool({
|
||||
name: REPL_TOOL_NAME,
|
||||
searchHint: 'repl execute batch code read write edit glob grep bash',
|
||||
maxResultSizeChars: 100_000,
|
||||
strict: true,
|
||||
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
|
||||
async description() {
|
||||
return 'Execute code in the REPL environment with access to all primitive tools'
|
||||
},
|
||||
async prompt() {
|
||||
return `Execute code in the REPL — a sandboxed environment with direct access to primitive tools (Read, Write, Edit, Glob, Grep, Bash, NotebookEdit, Agent).
|
||||
|
||||
When REPL mode is active, primitive tools are only accessible through this tool. Use REPL for:
|
||||
- Batch operations across many files
|
||||
- Complex multi-step file transformations
|
||||
- Operations that benefit from programmatic control flow
|
||||
- Combining search results with edits in a single turn
|
||||
|
||||
The REPL runs in a VM context with tool APIs available as functions. Results from each tool call are collected and returned together.`
|
||||
},
|
||||
|
||||
isConcurrencySafe() {
|
||||
return false
|
||||
},
|
||||
isReadOnly() {
|
||||
return false
|
||||
},
|
||||
isTransparentWrapper() {
|
||||
return true
|
||||
},
|
||||
|
||||
userFacingName() {
|
||||
return REPL_TOOL_NAME
|
||||
},
|
||||
|
||||
renderToolUseMessage(input: Partial<REPLInput>) {
|
||||
const code = input.code ?? ''
|
||||
const preview = code.length > 80 ? code.slice(0, 77) + '...' : code
|
||||
return `REPL: ${preview}`
|
||||
},
|
||||
|
||||
mapToolResultToToolResultBlockParam(
|
||||
content: REPLOutput,
|
||||
toolUseID: string,
|
||||
): ToolResultBlockParam {
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result',
|
||||
content: content.result,
|
||||
}
|
||||
},
|
||||
|
||||
async call(_input: REPLInput) {
|
||||
// REPL execution engine is provided by the ant-native runtime.
|
||||
// This stub satisfies the tool interface; the actual VM dispatch
|
||||
// is wired in the ant build. Without the ant runtime, REPL is
|
||||
// not available and callers should be informed.
|
||||
return {
|
||||
data: {
|
||||
result: 'Error: REPL tool is not available in this build. The REPL execution engine requires the ant-native runtime.',
|
||||
tool_calls: 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,3 +1,142 @@
|
||||
// Auto-generated stub — replace with real implementation
|
||||
export {};
|
||||
export const ReviewArtifactTool: Record<string, unknown> = {};
|
||||
import { z } from 'zod/v4'
|
||||
import { buildTool, type ToolDef } from '../../Tool.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
import React from 'react'
|
||||
import { Box, Text } from '@anthropic/ink'
|
||||
|
||||
const REVIEW_ARTIFACT_TOOL_NAME = 'ReviewArtifact'
|
||||
|
||||
const DESCRIPTION =
|
||||
'Review an artifact (code snippet, document, or other content) with inline annotations and feedback.'
|
||||
|
||||
const inputSchema = lazySchema(() =>
|
||||
z.strictObject({
|
||||
artifact: z
|
||||
.string()
|
||||
.describe(
|
||||
'The content of the artifact to review (code snippet, document text, etc.).',
|
||||
),
|
||||
title: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Optional title or file path for the artifact being reviewed.',
|
||||
),
|
||||
annotations: z
|
||||
.array(
|
||||
z.object({
|
||||
line: z.number().optional().describe('Line number for the annotation (1-based).'),
|
||||
message: z.string().describe('The annotation or feedback message.'),
|
||||
severity: z
|
||||
.enum(['info', 'warning', 'error', 'suggestion'])
|
||||
.optional()
|
||||
.describe('Severity level of the annotation.'),
|
||||
}),
|
||||
)
|
||||
.describe('List of annotations/comments on the artifact.'),
|
||||
summary: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('An overall summary of the review.'),
|
||||
}),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
|
||||
const outputSchema = lazySchema(() =>
|
||||
z.object({
|
||||
artifact: z.string().describe('The reviewed artifact content.'),
|
||||
title: z.string().optional().describe('Title of the reviewed artifact.'),
|
||||
annotationCount: z
|
||||
.number()
|
||||
.describe('Number of annotations applied.'),
|
||||
summary: z.string().optional().describe('Summary of the review.'),
|
||||
}),
|
||||
)
|
||||
type OutputSchema = ReturnType<typeof outputSchema>
|
||||
|
||||
export type Output = z.infer<OutputSchema>
|
||||
|
||||
export const ReviewArtifactTool = buildTool({
|
||||
name: REVIEW_ARTIFACT_TOOL_NAME,
|
||||
searchHint: 'review code or documents with inline annotations',
|
||||
maxResultSizeChars: 100_000,
|
||||
async description(input) {
|
||||
const { title } = input as { title?: string }
|
||||
return title
|
||||
? `Claude wants to review: ${title}`
|
||||
: 'Claude wants to review an artifact'
|
||||
},
|
||||
userFacingName() {
|
||||
return 'ReviewArtifact'
|
||||
},
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
get outputSchema(): OutputSchema {
|
||||
return outputSchema()
|
||||
},
|
||||
isConcurrencySafe() {
|
||||
return true
|
||||
},
|
||||
isReadOnly() {
|
||||
return true
|
||||
},
|
||||
toAutoClassifierInput(input) {
|
||||
return input.title ?? input.artifact.slice(0, 200)
|
||||
},
|
||||
async prompt() {
|
||||
return `Use this tool to present a review of a code snippet, document, or other artifact with inline annotations and feedback. Each annotation can target a specific line and include a severity level. ${DESCRIPTION}`
|
||||
},
|
||||
mapToolResultToToolResultBlockParam(output, toolUseID) {
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result',
|
||||
content: `Review delivered with ${output.annotationCount} annotation(s).${output.summary ? ` Summary: ${output.summary}` : ''}`,
|
||||
}
|
||||
},
|
||||
renderToolUseMessage(
|
||||
input: Partial<z.infer<InputSchema>>,
|
||||
{ verbose }: { theme?: string; verbose: boolean },
|
||||
): React.ReactNode {
|
||||
const title = input.title ?? 'Untitled artifact'
|
||||
const count = input.annotations?.length ?? 0
|
||||
if (verbose) {
|
||||
return `Review: "${title}" (${count} annotation(s))`
|
||||
}
|
||||
return title
|
||||
},
|
||||
renderToolResultMessage(
|
||||
output: Output,
|
||||
_progressMessages: unknown[],
|
||||
{ verbose }: { verbose: boolean },
|
||||
): React.ReactNode {
|
||||
if (verbose) {
|
||||
return React.createElement(
|
||||
Box,
|
||||
{ flexDirection: 'column' },
|
||||
React.createElement(
|
||||
Text,
|
||||
null,
|
||||
`Reviewed artifact: ${output.title ?? 'Untitled'} (${output.annotationCount} annotations)`,
|
||||
),
|
||||
output.summary
|
||||
? React.createElement(Text, { dimColor: true }, output.summary)
|
||||
: null,
|
||||
)
|
||||
}
|
||||
return React.createElement(
|
||||
Text,
|
||||
null,
|
||||
`Review complete: ${output.annotationCount} annotation(s)`,
|
||||
)
|
||||
},
|
||||
async call({ artifact, title, annotations, summary }, _context) {
|
||||
const output: Output = {
|
||||
artifact,
|
||||
title,
|
||||
annotationCount: annotations.length,
|
||||
summary,
|
||||
}
|
||||
return { data: output }
|
||||
},
|
||||
} satisfies ToolDef<InputSchema, Output>)
|
||||
|
||||
@@ -70,7 +70,7 @@ const inputSchema = lazySchema(() =>
|
||||
.string()
|
||||
.describe(
|
||||
feature('UDS_INBOX')
|
||||
? 'Recipient: teammate name, "*" for broadcast, "uds:<socket-path>" for a local peer, or "bridge:<session-id>" for a Remote Control peer (use ListPeers to discover)'
|
||||
? `Recipient: teammate name, "*" for broadcast, "uds:<socket-path>" for a local peer, "bridge:<session-id>" for a Remote Control peer${feature('LAN_PIPES') ? ', or "tcp:<host>:<port>" for a LAN peer' : ''} (use ListPeers to discover)`
|
||||
: 'Recipient: teammate name, or "*" for broadcast to all teammates',
|
||||
),
|
||||
summary: z
|
||||
@@ -587,9 +587,6 @@ export const SendMessageTool: Tool<InputSchema, SendMessageToolOutput> =
|
||||
return {
|
||||
behavior: 'ask' as const,
|
||||
message: `Send a message to Remote Control session ${input.to}? It arrives as a user prompt on the receiving Claude (possibly another machine) via Anthropic's servers.`,
|
||||
// safetyCheck (not mode) — permissions.ts guards this before both
|
||||
// bypassPermissions (step 1g) and auto-mode's allowlist/classifier.
|
||||
// Cross-machine prompt injection must stay bypass-immune.
|
||||
decisionReason: {
|
||||
type: 'safetyCheck',
|
||||
reason:
|
||||
@@ -598,6 +595,17 @@ export const SendMessageTool: Tool<InputSchema, SendMessageToolOutput> =
|
||||
},
|
||||
}
|
||||
}
|
||||
if (feature('LAN_PIPES') && parseAddress(input.to).scheme === 'tcp') {
|
||||
return {
|
||||
behavior: 'ask' as const,
|
||||
message: `Send a message to LAN peer ${input.to}? This connects directly over TCP to a machine on your local network.`,
|
||||
decisionReason: {
|
||||
type: 'safetyCheck',
|
||||
reason: 'Cross-machine LAN message requires explicit user consent',
|
||||
classifierApprovable: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
return { behavior: 'allow' as const, updatedInput: input }
|
||||
},
|
||||
|
||||
@@ -611,7 +619,9 @@ export const SendMessageTool: Tool<InputSchema, SendMessageToolOutput> =
|
||||
}
|
||||
const addr = parseAddress(input.to)
|
||||
if (
|
||||
(addr.scheme === 'bridge' || addr.scheme === 'uds') &&
|
||||
(addr.scheme === 'bridge' ||
|
||||
addr.scheme === 'uds' ||
|
||||
addr.scheme === 'tcp') &&
|
||||
addr.target.trim().length === 0
|
||||
) {
|
||||
return {
|
||||
@@ -659,9 +669,13 @@ export const SendMessageTool: Tool<InputSchema, SendMessageToolOutput> =
|
||||
parseAddress(input.to).scheme === 'uds' &&
|
||||
typeof input.message === 'string'
|
||||
) {
|
||||
// UDS cross-session send: summary isn't rendered (UI.tsx returns null
|
||||
// for string messages), so don't require it. Structured messages fall
|
||||
// through to the rejection below.
|
||||
return { result: true }
|
||||
}
|
||||
if (
|
||||
feature('LAN_PIPES') &&
|
||||
parseAddress(input.to).scheme === 'tcp' &&
|
||||
typeof input.message === 'string'
|
||||
) {
|
||||
return { result: true }
|
||||
}
|
||||
if (typeof input.message === 'string') {
|
||||
@@ -783,7 +797,7 @@ export const SendMessageTool: Tool<InputSchema, SendMessageToolOutput> =
|
||||
return {
|
||||
data: {
|
||||
success: true,
|
||||
message: `“${preview}” → ${input.to}`,
|
||||
message: `”${preview}” → ${input.to}`,
|
||||
},
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -795,6 +809,41 @@ export const SendMessageTool: Tool<InputSchema, SendMessageToolOutput> =
|
||||
}
|
||||
}
|
||||
}
|
||||
if (addr.scheme === 'tcp' && feature('LAN_PIPES')) {
|
||||
const { parseTcpTarget } =
|
||||
require('../../utils/peerAddress.js') as typeof import('../../utils/peerAddress.js')
|
||||
const { PipeClient } =
|
||||
require('../../utils/pipeTransport.js') as typeof import('../../utils/pipeTransport.js')
|
||||
const ep = parseTcpTarget(addr.target)
|
||||
if (!ep) {
|
||||
return {
|
||||
data: {
|
||||
success: false,
|
||||
message: `Invalid TCP target format: ${addr.target}. Expected host:port`,
|
||||
},
|
||||
}
|
||||
}
|
||||
try {
|
||||
const client = new PipeClient(input.to, `send-${process.pid}`, ep)
|
||||
await client.connect(5000)
|
||||
client.send({ type: 'chat', data: input.message })
|
||||
client.disconnect()
|
||||
const preview = input.summary || truncate(input.message, 50)
|
||||
return {
|
||||
data: {
|
||||
success: true,
|
||||
message: `”${preview}” → ${input.to} (TCP ${ep.host}:${ep.port})`,
|
||||
},
|
||||
}
|
||||
} catch (e) {
|
||||
return {
|
||||
data: {
|
||||
success: false,
|
||||
message: `Failed to send via TCP to ${input.to}: ${errorMessage(e)}`,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Route to in-process subagent by name or raw agentId before falling
|
||||
@@ -826,7 +875,9 @@ export const SendMessageTool: Tool<InputSchema, SendMessageToolOutput> =
|
||||
prompt: input.message,
|
||||
toolUseContext: context,
|
||||
canUseTool,
|
||||
invokingRequestId: assistantMessage?.requestId as string | undefined,
|
||||
invokingRequestId: assistantMessage?.requestId as
|
||||
| string
|
||||
| undefined,
|
||||
})
|
||||
return {
|
||||
data: {
|
||||
@@ -853,7 +904,9 @@ export const SendMessageTool: Tool<InputSchema, SendMessageToolOutput> =
|
||||
prompt: input.message,
|
||||
toolUseContext: context,
|
||||
canUseTool,
|
||||
invokingRequestId: assistantMessage?.requestId as string | undefined,
|
||||
invokingRequestId: assistantMessage?.requestId as
|
||||
| string
|
||||
| undefined,
|
||||
})
|
||||
return {
|
||||
data: {
|
||||
|
||||
84
src/tools/SendUserFileTool/SendUserFileTool.ts
Normal file
84
src/tools/SendUserFileTool/SendUserFileTool.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { z } from 'zod/v4'
|
||||
import type { ToolResultBlockParam } from '../../Tool.js'
|
||||
import { buildTool } from '../../Tool.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
import { SEND_USER_FILE_TOOL_NAME } from './prompt.js'
|
||||
|
||||
const inputSchema = lazySchema(() =>
|
||||
z.strictObject({
|
||||
file_path: z
|
||||
.string()
|
||||
.describe('Absolute path to the file to send to the user.'),
|
||||
description: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Optional description of the file being sent.'),
|
||||
}),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
type SendUserFileInput = z.infer<InputSchema>
|
||||
|
||||
type SendUserFileOutput = { sent: boolean; file_path: string }
|
||||
|
||||
export const SendUserFileTool = buildTool({
|
||||
name: SEND_USER_FILE_TOOL_NAME,
|
||||
searchHint: 'send file to user mobile device upload share',
|
||||
maxResultSizeChars: 5_000,
|
||||
strict: true,
|
||||
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
|
||||
async description() {
|
||||
return 'Send a file to the user (KAIROS assistant mode)'
|
||||
},
|
||||
async prompt() {
|
||||
return `Send a file to the user's device. Use this in assistant mode when the user requests a file or when a file is relevant to the conversation.
|
||||
|
||||
Guidelines:
|
||||
- Use absolute paths
|
||||
- The file must exist and be readable
|
||||
- Large files may take time to transfer`
|
||||
},
|
||||
|
||||
isConcurrencySafe() {
|
||||
return true
|
||||
},
|
||||
isReadOnly() {
|
||||
return true
|
||||
},
|
||||
|
||||
userFacingName() {
|
||||
return 'SendFile'
|
||||
},
|
||||
|
||||
renderToolUseMessage(input: Partial<SendUserFileInput>) {
|
||||
return `Send file: ${input.file_path ?? '...'}`
|
||||
},
|
||||
|
||||
mapToolResultToToolResultBlockParam(
|
||||
content: SendUserFileOutput,
|
||||
toolUseID: string,
|
||||
): ToolResultBlockParam {
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result',
|
||||
content: content.sent
|
||||
? `File sent: ${content.file_path}`
|
||||
: `Failed to send file: ${content.file_path}`,
|
||||
}
|
||||
},
|
||||
|
||||
async call(_input: SendUserFileInput) {
|
||||
// File transfer is handled by the KAIROS assistant transport layer.
|
||||
// Without the KAIROS runtime, this tool is not available.
|
||||
return {
|
||||
data: {
|
||||
sent: false,
|
||||
file_path: _input.file_path,
|
||||
error: 'SendUserFile requires the KAIROS assistant transport layer.',
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,3 +1 @@
|
||||
// Auto-generated stub — replace with real implementation
|
||||
export {};
|
||||
export const SEND_USER_FILE_TOOL_NAME: string = '';
|
||||
export const SEND_USER_FILE_TOOL_NAME = 'SendUserFile'
|
||||
|
||||
134
src/tools/SleepTool/SleepTool.ts
Normal file
134
src/tools/SleepTool/SleepTool.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { feature } from 'bun:bundle'
|
||||
import { z } from 'zod/v4'
|
||||
import type { ToolResultBlockParam } from '../../Tool.js'
|
||||
import { buildTool } from '../../Tool.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
import { SLEEP_TOOL_NAME, DESCRIPTION, SLEEP_TOOL_PROMPT } from './prompt.js'
|
||||
|
||||
const inputSchema = lazySchema(() =>
|
||||
z.strictObject({
|
||||
duration_seconds: z
|
||||
.number()
|
||||
.describe(
|
||||
'How long to sleep in seconds. Can be interrupted by the user at any time.',
|
||||
),
|
||||
}),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
type SleepInput = z.infer<InputSchema>
|
||||
|
||||
type SleepOutput = { slept_seconds: number; interrupted: boolean }
|
||||
|
||||
export const SleepTool = buildTool({
|
||||
name: SLEEP_TOOL_NAME,
|
||||
searchHint: 'wait pause sleep rest idle duration timer',
|
||||
maxResultSizeChars: 1_000,
|
||||
strict: true,
|
||||
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
|
||||
async description() {
|
||||
return DESCRIPTION
|
||||
},
|
||||
async prompt() {
|
||||
return SLEEP_TOOL_PROMPT
|
||||
},
|
||||
|
||||
isConcurrencySafe() {
|
||||
return true
|
||||
},
|
||||
isReadOnly() {
|
||||
return true
|
||||
},
|
||||
|
||||
userFacingName() {
|
||||
return SLEEP_TOOL_NAME
|
||||
},
|
||||
|
||||
renderToolUseMessage(input: Partial<SleepInput>) {
|
||||
const secs = input.duration_seconds ?? '?'
|
||||
return `Sleep: ${secs}s`
|
||||
},
|
||||
|
||||
mapToolResultToToolResultBlockParam(
|
||||
content: SleepOutput,
|
||||
toolUseID: string,
|
||||
): ToolResultBlockParam {
|
||||
const msg = content.interrupted
|
||||
? `Sleep interrupted after ${content.slept_seconds}s`
|
||||
: `Slept for ${content.slept_seconds}s`
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result',
|
||||
content: msg,
|
||||
}
|
||||
},
|
||||
|
||||
async call(input: SleepInput, context) {
|
||||
// Refuse to sleep when proactive mode is off — prevents the model from
|
||||
// re-issuing Sleep after an interruption caused by /proactive disable.
|
||||
if (feature('PROACTIVE') || feature('KAIROS')) {
|
||||
const mod =
|
||||
require('../../proactive/index.js') as typeof import('../../proactive/index.js')
|
||||
if (!mod.isProactiveActive()) {
|
||||
return {
|
||||
data: {
|
||||
slept_seconds: 0,
|
||||
interrupted: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { duration_seconds } = input
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timer = setTimeout(resolve, duration_seconds * 1000)
|
||||
|
||||
// Abort via user interrupt
|
||||
context.abortController.signal.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
clearTimeout(timer)
|
||||
clearInterval(proactiveCheck)
|
||||
reject(new Error('interrupted'))
|
||||
},
|
||||
{ once: true },
|
||||
)
|
||||
|
||||
// Poll proactive state — if deactivated mid-sleep, interrupt early
|
||||
// so the user doesn't have to wait for the full duration.
|
||||
const proactiveCheck =
|
||||
feature('PROACTIVE') || feature('KAIROS')
|
||||
? setInterval(() => {
|
||||
const mod =
|
||||
require('../../proactive/index.js') as typeof import('../../proactive/index.js')
|
||||
if (!mod.isProactiveActive()) {
|
||||
clearTimeout(timer)
|
||||
clearInterval(proactiveCheck)
|
||||
reject(new Error('interrupted'))
|
||||
}
|
||||
}, 500)
|
||||
: (null as unknown as ReturnType<typeof setInterval>)
|
||||
})
|
||||
return {
|
||||
data: {
|
||||
slept_seconds: duration_seconds,
|
||||
interrupted: false,
|
||||
},
|
||||
}
|
||||
} catch {
|
||||
const elapsed = Math.round((Date.now() - startTime) / 1000)
|
||||
return {
|
||||
data: {
|
||||
slept_seconds: elapsed,
|
||||
interrupted: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
92
src/tools/SnipTool/SnipTool.ts
Normal file
92
src/tools/SnipTool/SnipTool.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { z } from 'zod/v4'
|
||||
import type { ToolResultBlockParam } from '../../Tool.js'
|
||||
import { buildTool } from '../../Tool.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
import { SNIP_TOOL_NAME } from './prompt.js'
|
||||
|
||||
const inputSchema = lazySchema(() =>
|
||||
z.strictObject({
|
||||
message_ids: z
|
||||
.array(z.string())
|
||||
.describe(
|
||||
'IDs of the messages to snip from history. Snipped messages are replaced with a short summary.',
|
||||
),
|
||||
reason: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Why these messages are being snipped. Used in the summary replacement.',
|
||||
),
|
||||
}),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
type SnipInput = z.infer<InputSchema>
|
||||
|
||||
type SnipOutput = { snipped_count: number; summary: string }
|
||||
|
||||
export const SnipTool = buildTool({
|
||||
name: SNIP_TOOL_NAME,
|
||||
searchHint: 'snip trim history remove old messages compact context',
|
||||
maxResultSizeChars: 5_000,
|
||||
strict: true,
|
||||
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
|
||||
async description() {
|
||||
return 'Snip messages from conversation history to free up context'
|
||||
},
|
||||
async prompt() {
|
||||
return `Snip messages from your conversation history to free up context window space. Snipped messages are replaced with a compact summary so you retain awareness of what happened without the full content.
|
||||
|
||||
Use this when:
|
||||
- Your context is getting full and you need to make room
|
||||
- Earlier messages contain large tool outputs you no longer need in full
|
||||
- You want to compact a long exploration sequence into a summary
|
||||
|
||||
Guidelines:
|
||||
- Only snip messages you're confident you won't need verbatim again
|
||||
- The summary replacement preserves key facts (file paths, decisions, errors found)
|
||||
- You cannot un-snip — the original content is gone from context`
|
||||
},
|
||||
|
||||
isConcurrencySafe() {
|
||||
return false
|
||||
},
|
||||
isReadOnly() {
|
||||
return false
|
||||
},
|
||||
|
||||
userFacingName() {
|
||||
return 'Snip'
|
||||
},
|
||||
|
||||
renderToolUseMessage(input: Partial<SnipInput>) {
|
||||
const count = input.message_ids?.length ?? 0
|
||||
return `Snip: ${count} message${count !== 1 ? 's' : ''}`
|
||||
},
|
||||
|
||||
mapToolResultToToolResultBlockParam(
|
||||
content: SnipOutput,
|
||||
toolUseID: string,
|
||||
): ToolResultBlockParam {
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result',
|
||||
content: `Snipped ${content.snipped_count} messages. Summary: ${content.summary}`,
|
||||
}
|
||||
},
|
||||
|
||||
async call(input: SnipInput) {
|
||||
// Snip implementation is handled by the query engine's projection system.
|
||||
// The tool call itself records the intent; the query engine intercepts
|
||||
// snip tool results and adjusts its message projection accordingly.
|
||||
return {
|
||||
data: {
|
||||
snipped_count: input.message_ids.length,
|
||||
summary: input.reason ?? `Snipped ${input.message_ids.length} messages`,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,3 +1 @@
|
||||
// Auto-generated stub — replace with real implementation
|
||||
export {};
|
||||
export const SNIP_TOOL_NAME: string = '';
|
||||
export const SNIP_TOOL_NAME = 'Snip'
|
||||
|
||||
88
src/tools/SubscribePRTool/SubscribePRTool.ts
Normal file
88
src/tools/SubscribePRTool/SubscribePRTool.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { z } from 'zod/v4'
|
||||
import type { ToolResultBlockParam } from '../../Tool.js'
|
||||
import { buildTool } from '../../Tool.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
|
||||
const SUBSCRIBE_PR_TOOL_NAME = 'SubscribePR'
|
||||
|
||||
const inputSchema = lazySchema(() =>
|
||||
z.strictObject({
|
||||
repo: z
|
||||
.string()
|
||||
.describe('Repository in owner/repo format.'),
|
||||
pr_number: z
|
||||
.number()
|
||||
.describe('Pull request number to subscribe to.'),
|
||||
events: z
|
||||
.array(z.enum(['comment', 'review', 'ci', 'merge', 'close']))
|
||||
.optional()
|
||||
.describe('Event types to subscribe to. Defaults to all events.'),
|
||||
}),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
type SubscribeInput = z.infer<InputSchema>
|
||||
|
||||
type SubscribeOutput = { subscribed: boolean; subscription_id: string }
|
||||
|
||||
export const SubscribePRTool = buildTool({
|
||||
name: SUBSCRIBE_PR_TOOL_NAME,
|
||||
searchHint: 'subscribe pull request github webhook events watch',
|
||||
maxResultSizeChars: 5_000,
|
||||
strict: true,
|
||||
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
|
||||
async description() {
|
||||
return 'Subscribe to pull request events via GitHub webhooks'
|
||||
},
|
||||
async prompt() {
|
||||
return `Subscribe to events on a GitHub pull request. You'll receive notifications when selected events occur (comments, reviews, CI status changes, merge, close).
|
||||
|
||||
Use this to monitor PRs you've created or are reviewing. Events are delivered as messages you can act on.`
|
||||
},
|
||||
|
||||
isConcurrencySafe() {
|
||||
return true
|
||||
},
|
||||
isReadOnly() {
|
||||
return true
|
||||
},
|
||||
|
||||
userFacingName() {
|
||||
return 'SubscribePR'
|
||||
},
|
||||
|
||||
renderToolUseMessage(input: Partial<SubscribeInput>) {
|
||||
const pr = input.repo && input.pr_number
|
||||
? `${input.repo}#${input.pr_number}`
|
||||
: '...'
|
||||
return `Subscribe PR: ${pr}`
|
||||
},
|
||||
|
||||
mapToolResultToToolResultBlockParam(
|
||||
content: SubscribeOutput,
|
||||
toolUseID: string,
|
||||
): ToolResultBlockParam {
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result',
|
||||
content: content.subscribed
|
||||
? `Subscribed to PR events (id: ${content.subscription_id})`
|
||||
: 'Failed to subscribe to PR events.',
|
||||
}
|
||||
},
|
||||
|
||||
async call(_input: SubscribeInput) {
|
||||
// Webhook subscription is managed by the KAIROS GitHub webhook subsystem.
|
||||
// Without the KAIROS runtime, this tool is not available.
|
||||
return {
|
||||
data: {
|
||||
subscribed: false,
|
||||
subscription_id: '',
|
||||
error: 'SubscribePR requires the KAIROS GitHub webhook subsystem.',
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1 +0,0 @@
|
||||
export const SuggestBackgroundPRTool = { name: 'SuggestBackgroundPRTool', isEnabled: () => false }
|
||||
84
src/tools/SuggestBackgroundPRTool/SuggestBackgroundPRTool.ts
Normal file
84
src/tools/SuggestBackgroundPRTool/SuggestBackgroundPRTool.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { z } from 'zod/v4'
|
||||
import type { ToolResultBlockParam } from '../../Tool.js'
|
||||
import { buildTool } from '../../Tool.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
|
||||
const SUGGEST_BACKGROUND_PR_TOOL_NAME = 'SuggestBackgroundPR'
|
||||
|
||||
const inputSchema = lazySchema(() =>
|
||||
z.strictObject({
|
||||
title: z
|
||||
.string()
|
||||
.describe('Suggested title for the background PR.'),
|
||||
description: z
|
||||
.string()
|
||||
.describe('Description of the changes to make in the background PR.'),
|
||||
branch: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Branch name for the PR. Auto-generated if omitted.'),
|
||||
}),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
type SuggestInput = z.infer<InputSchema>
|
||||
|
||||
type SuggestOutput = { suggested: boolean; suggestion_id: string }
|
||||
|
||||
export const SuggestBackgroundPRTool = buildTool({
|
||||
name: SUGGEST_BACKGROUND_PR_TOOL_NAME,
|
||||
searchHint: 'suggest background pr pull request create',
|
||||
maxResultSizeChars: 5_000,
|
||||
strict: true,
|
||||
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
|
||||
async description() {
|
||||
return 'Suggest creating a background PR for follow-up changes'
|
||||
},
|
||||
async prompt() {
|
||||
return `Suggest creating a pull request in the background for follow-up work. Use this when you identify improvements or cleanup that should be done but aren't part of the current task.
|
||||
|
||||
The suggestion is presented to the user who can approve or dismiss it. If approved, a background agent creates the PR.`
|
||||
},
|
||||
|
||||
isConcurrencySafe() {
|
||||
return true
|
||||
},
|
||||
isReadOnly() {
|
||||
return true
|
||||
},
|
||||
|
||||
userFacingName() {
|
||||
return 'SuggestPR'
|
||||
},
|
||||
|
||||
renderToolUseMessage(input: Partial<SuggestInput>) {
|
||||
return `Suggest PR: ${input.title ?? '...'}`
|
||||
},
|
||||
|
||||
mapToolResultToToolResultBlockParam(
|
||||
content: SuggestOutput,
|
||||
toolUseID: string,
|
||||
): ToolResultBlockParam {
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result',
|
||||
content: content.suggested
|
||||
? `PR suggestion recorded (id: ${content.suggestion_id})`
|
||||
: 'Failed to record PR suggestion.',
|
||||
}
|
||||
},
|
||||
|
||||
async call(_input: SuggestInput) {
|
||||
// Background PR suggestion requires the KAIROS runtime.
|
||||
return {
|
||||
data: {
|
||||
suggested: false,
|
||||
suggestion_id: '',
|
||||
error: 'SuggestBackgroundPR requires the KAIROS runtime.',
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
82
src/tools/TerminalCaptureTool/TerminalCaptureTool.ts
Normal file
82
src/tools/TerminalCaptureTool/TerminalCaptureTool.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { z } from 'zod/v4'
|
||||
import type { ToolResultBlockParam } from '../../Tool.js'
|
||||
import { buildTool } from '../../Tool.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
import { TERMINAL_CAPTURE_TOOL_NAME } from './prompt.js'
|
||||
|
||||
const inputSchema = lazySchema(() =>
|
||||
z.strictObject({
|
||||
lines: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe('Number of lines to capture from the terminal. Defaults to 50.'),
|
||||
panel_id: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('ID of the terminal panel to capture from. Defaults to the active panel.'),
|
||||
}),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
type CaptureInput = z.infer<InputSchema>
|
||||
|
||||
type CaptureOutput = { content: string; line_count: number }
|
||||
|
||||
export const TerminalCaptureTool = buildTool({
|
||||
name: TERMINAL_CAPTURE_TOOL_NAME,
|
||||
searchHint: 'terminal capture screen output panel read',
|
||||
maxResultSizeChars: 100_000,
|
||||
strict: true,
|
||||
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
|
||||
async description() {
|
||||
return 'Capture output from a terminal panel'
|
||||
},
|
||||
async prompt() {
|
||||
return `Capture the current content of a terminal panel. Use this to read output from terminal sessions running in the terminal panel UI.
|
||||
|
||||
Guidelines:
|
||||
- Specify the number of lines to capture (default 50)
|
||||
- Optionally target a specific panel by ID
|
||||
- Content is returned as plain text`
|
||||
},
|
||||
|
||||
isConcurrencySafe() {
|
||||
return true
|
||||
},
|
||||
isReadOnly() {
|
||||
return true
|
||||
},
|
||||
|
||||
userFacingName() {
|
||||
return 'TerminalCapture'
|
||||
},
|
||||
|
||||
renderToolUseMessage(input: Partial<CaptureInput>) {
|
||||
const lines = input.lines ?? 50
|
||||
return `Terminal Capture: ${lines} lines`
|
||||
},
|
||||
|
||||
mapToolResultToToolResultBlockParam(
|
||||
content: CaptureOutput,
|
||||
toolUseID: string,
|
||||
): ToolResultBlockParam {
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result',
|
||||
content: content.content || '(empty terminal)',
|
||||
}
|
||||
},
|
||||
|
||||
async call(input: CaptureInput) {
|
||||
// Terminal panel capture is provided by the TERMINAL_PANEL runtime.
|
||||
return {
|
||||
data: {
|
||||
content: '',
|
||||
line_count: 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,3 +1 @@
|
||||
// Auto-generated stub — replace with real implementation
|
||||
export {};
|
||||
export const TERMINAL_CAPTURE_TOOL_NAME: string = '';
|
||||
export const TERMINAL_CAPTURE_TOOL_NAME = 'TerminalCapture'
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const VerifyPlanExecutionTool = { name: 'VerifyPlanExecutionTool', isEnabled: () => false }
|
||||
93
src/tools/VerifyPlanExecutionTool/VerifyPlanExecutionTool.ts
Normal file
93
src/tools/VerifyPlanExecutionTool/VerifyPlanExecutionTool.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { z } from 'zod/v4'
|
||||
import type { ToolResultBlockParam } from '../../Tool.js'
|
||||
import { buildTool } from '../../Tool.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
import { VERIFY_PLAN_EXECUTION_TOOL_NAME } from './constants.js'
|
||||
|
||||
const inputSchema = lazySchema(() =>
|
||||
z.strictObject({
|
||||
plan_summary: z
|
||||
.string()
|
||||
.describe('A summary of the plan that was executed.'),
|
||||
verification_notes: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Notes on what was verified and any issues found during verification.',
|
||||
),
|
||||
all_steps_completed: z
|
||||
.boolean()
|
||||
.describe('Whether all planned steps were completed successfully.'),
|
||||
}),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
type VerifyInput = z.infer<InputSchema>
|
||||
|
||||
type VerifyOutput = { verified: boolean; summary: string }
|
||||
|
||||
export const VerifyPlanExecutionTool = buildTool({
|
||||
name: VERIFY_PLAN_EXECUTION_TOOL_NAME,
|
||||
searchHint: 'verify plan execution check completion',
|
||||
maxResultSizeChars: 10_000,
|
||||
strict: true,
|
||||
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
|
||||
async description() {
|
||||
return 'Verify that a plan was executed correctly before exiting plan mode'
|
||||
},
|
||||
async prompt() {
|
||||
return `Verify that a plan has been executed correctly. Call this tool before exiting plan mode to confirm all steps were completed.
|
||||
|
||||
Guidelines:
|
||||
- Summarize the plan that was executed
|
||||
- Note whether all steps completed successfully
|
||||
- Include any verification notes (tests passed, files created, etc.)
|
||||
- If steps were skipped or failed, explain why in verification_notes`
|
||||
},
|
||||
|
||||
isConcurrencySafe() {
|
||||
return true
|
||||
},
|
||||
isReadOnly() {
|
||||
return true
|
||||
},
|
||||
|
||||
userFacingName() {
|
||||
return 'VerifyPlan'
|
||||
},
|
||||
|
||||
renderToolUseMessage(input: Partial<VerifyInput>) {
|
||||
if (input.all_steps_completed === true) {
|
||||
return 'Verify Plan: all steps completed'
|
||||
}
|
||||
if (input.all_steps_completed === false) {
|
||||
return 'Verify Plan: incomplete'
|
||||
}
|
||||
return 'Verify Plan'
|
||||
},
|
||||
|
||||
mapToolResultToToolResultBlockParam(
|
||||
content: VerifyOutput,
|
||||
toolUseID: string,
|
||||
): ToolResultBlockParam {
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result',
|
||||
content: content.verified
|
||||
? `Plan verified: ${content.summary}`
|
||||
: `Plan verification failed: ${content.summary}`,
|
||||
}
|
||||
},
|
||||
|
||||
async call(input: VerifyInput) {
|
||||
return {
|
||||
data: {
|
||||
verified: input.all_steps_completed,
|
||||
summary: input.plan_summary,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,3 +1 @@
|
||||
// Auto-generated stub — replace with real implementation
|
||||
export {};
|
||||
export const VERIFY_PLAN_EXECUTION_TOOL_NAME: string = '';
|
||||
export const VERIFY_PLAN_EXECUTION_TOOL_NAME = 'VerifyPlanExecution'
|
||||
|
||||
97
src/tools/WebBrowserTool/WebBrowserTool.ts
Normal file
97
src/tools/WebBrowserTool/WebBrowserTool.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { z } from 'zod/v4'
|
||||
import type { ToolResultBlockParam } from '../../Tool.js'
|
||||
import { buildTool } from '../../Tool.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
|
||||
const WEB_BROWSER_TOOL_NAME = 'WebBrowser'
|
||||
|
||||
const inputSchema = lazySchema(() =>
|
||||
z.strictObject({
|
||||
url: z
|
||||
.string()
|
||||
.describe('URL to navigate to in the browser.'),
|
||||
action: z
|
||||
.enum(['navigate', 'screenshot', 'click', 'type', 'scroll'])
|
||||
.optional()
|
||||
.describe('Browser action to perform. Defaults to "navigate".'),
|
||||
selector: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('CSS selector for click/type actions.'),
|
||||
text: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Text to type when action is "type".'),
|
||||
}),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
type BrowserInput = z.infer<InputSchema>
|
||||
|
||||
type BrowserOutput = {
|
||||
title: string
|
||||
url: string
|
||||
content?: string
|
||||
screenshot?: string
|
||||
}
|
||||
|
||||
export const WebBrowserTool = buildTool({
|
||||
name: WEB_BROWSER_TOOL_NAME,
|
||||
searchHint: 'web browser navigate url page screenshot click',
|
||||
maxResultSizeChars: 100_000,
|
||||
strict: true,
|
||||
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
|
||||
async description() {
|
||||
return 'Browse the web using an embedded browser'
|
||||
},
|
||||
async prompt() {
|
||||
return `Open and interact with web pages in an embedded browser. Supports navigation, screenshots, clicking, typing, and scrolling.
|
||||
|
||||
Use this for:
|
||||
- Viewing web pages and their content
|
||||
- Taking screenshots of UI
|
||||
- Interacting with web applications
|
||||
- Testing web endpoints with full browser rendering`
|
||||
},
|
||||
|
||||
isConcurrencySafe() {
|
||||
return false
|
||||
},
|
||||
isReadOnly() {
|
||||
return true
|
||||
},
|
||||
|
||||
userFacingName() {
|
||||
return 'Browser'
|
||||
},
|
||||
|
||||
renderToolUseMessage(input: Partial<BrowserInput>) {
|
||||
const action = input.action ?? 'navigate'
|
||||
return `Browser ${action}: ${input.url ?? '...'}`
|
||||
},
|
||||
|
||||
mapToolResultToToolResultBlockParam(
|
||||
content: BrowserOutput,
|
||||
toolUseID: string,
|
||||
): ToolResultBlockParam {
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result',
|
||||
content: `${content.title} (${content.url})\n${content.content ?? ''}`,
|
||||
}
|
||||
},
|
||||
|
||||
async call(input: BrowserInput) {
|
||||
// Browser integration requires the WEB_BROWSER_TOOL runtime (Bun WebView).
|
||||
return {
|
||||
data: {
|
||||
title: '',
|
||||
url: input.url,
|
||||
content: 'Web browser requires the WEB_BROWSER_TOOL runtime.',
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,3 +0,0 @@
|
||||
// Auto-generated stub — replace with real implementation
|
||||
export {};
|
||||
export const WorkflowPermissionRequest: (props: Record<string, unknown>) => null = () => null;
|
||||
166
src/tools/WorkflowTool/WorkflowPermissionRequest.tsx
Normal file
166
src/tools/WorkflowTool/WorkflowPermissionRequest.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { Box, Text, useTheme } from '@anthropic/ink'
|
||||
import { getTheme } from '../../utils/theme.js'
|
||||
import { env } from '../../utils/env.js'
|
||||
import { shouldShowAlwaysAllowOptions } from '../../utils/permissions/permissionsLoader.js'
|
||||
import { logUnaryEvent } from '../../utils/unaryLogging.js'
|
||||
import { PermissionDialog } from '../../components/permissions/PermissionDialog.js'
|
||||
import {
|
||||
PermissionPrompt,
|
||||
type PermissionPromptOption,
|
||||
} from '../../components/permissions/PermissionPrompt.js'
|
||||
import type { PermissionRequestProps } from '../../components/permissions/PermissionRequest.js'
|
||||
import { PermissionRuleExplanation } from '../../components/permissions/PermissionRuleExplanation.js'
|
||||
|
||||
type OptionValue = 'yes' | 'yes-dont-ask-again' | 'no'
|
||||
|
||||
/**
|
||||
* Permission request UI for the WorkflowTool. Asks the user to confirm
|
||||
* executing a workflow script.
|
||||
* Follows the MonitorPermissionRequest / FallbackPermissionRequest pattern.
|
||||
*/
|
||||
export function WorkflowPermissionRequest({
|
||||
toolUseConfirm,
|
||||
onDone,
|
||||
onReject,
|
||||
workerBadge,
|
||||
}: PermissionRequestProps): React.ReactNode {
|
||||
const [themeName] = useTheme()
|
||||
const theme = getTheme(themeName)
|
||||
|
||||
const input = toolUseConfirm.input as {
|
||||
workflow: string
|
||||
args?: string
|
||||
}
|
||||
|
||||
const showAlwaysAllowOptions = useMemo(
|
||||
() => shouldShowAlwaysAllowOptions(),
|
||||
[],
|
||||
)
|
||||
|
||||
const options: PermissionPromptOption<OptionValue>[] = useMemo(() => {
|
||||
const opts: PermissionPromptOption<OptionValue>[] = [
|
||||
{
|
||||
label: 'Yes',
|
||||
value: 'yes',
|
||||
feedbackConfig: { type: 'accept' as const },
|
||||
},
|
||||
]
|
||||
if (showAlwaysAllowOptions) {
|
||||
opts.push({
|
||||
label: (
|
||||
<Text>
|
||||
Yes, and don{'\u2019'}t ask again for{' '}
|
||||
<Text bold>{toolUseConfirm.tool.name}</Text> commands
|
||||
</Text>
|
||||
),
|
||||
value: 'yes-dont-ask-again',
|
||||
})
|
||||
}
|
||||
opts.push({
|
||||
label: 'No',
|
||||
value: 'no',
|
||||
feedbackConfig: { type: 'reject' as const },
|
||||
})
|
||||
return opts
|
||||
}, [showAlwaysAllowOptions, toolUseConfirm.tool.name])
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(value: OptionValue, feedback?: string) => {
|
||||
switch (value) {
|
||||
case 'yes':
|
||||
logUnaryEvent({
|
||||
completion_type: 'tool_use_single',
|
||||
event: 'accept',
|
||||
metadata: {
|
||||
language_name: 'none',
|
||||
message_id: toolUseConfirm.assistantMessage.message.id ?? '',
|
||||
platform: env.platform,
|
||||
},
|
||||
})
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, [], feedback)
|
||||
onDone()
|
||||
break
|
||||
case 'yes-dont-ask-again':
|
||||
logUnaryEvent({
|
||||
completion_type: 'tool_use_single',
|
||||
event: 'accept',
|
||||
metadata: {
|
||||
language_name: 'none',
|
||||
message_id: toolUseConfirm.assistantMessage.message.id ?? '',
|
||||
platform: env.platform,
|
||||
},
|
||||
})
|
||||
toolUseConfirm.onAllow(toolUseConfirm.input, [
|
||||
{
|
||||
type: 'addRules',
|
||||
rules: [{ toolName: toolUseConfirm.tool.name }],
|
||||
behavior: 'allow',
|
||||
destination: 'localSettings',
|
||||
},
|
||||
])
|
||||
onDone()
|
||||
break
|
||||
case 'no':
|
||||
logUnaryEvent({
|
||||
completion_type: 'tool_use_single',
|
||||
event: 'reject',
|
||||
metadata: {
|
||||
language_name: 'none',
|
||||
message_id: toolUseConfirm.assistantMessage.message.id ?? '',
|
||||
platform: env.platform,
|
||||
},
|
||||
})
|
||||
toolUseConfirm.onReject(feedback)
|
||||
onReject()
|
||||
onDone()
|
||||
break
|
||||
}
|
||||
},
|
||||
[toolUseConfirm, onDone, onReject],
|
||||
)
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
logUnaryEvent({
|
||||
completion_type: 'tool_use_single',
|
||||
event: 'reject',
|
||||
metadata: {
|
||||
language_name: 'none',
|
||||
message_id: toolUseConfirm.assistantMessage.message.id ?? '',
|
||||
platform: env.platform,
|
||||
},
|
||||
})
|
||||
toolUseConfirm.onReject()
|
||||
onReject()
|
||||
onDone()
|
||||
}, [toolUseConfirm, onDone, onReject])
|
||||
|
||||
return (
|
||||
<PermissionDialog
|
||||
title="Workflow"
|
||||
workerBadge={workerBadge}
|
||||
>
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Box flexDirection="column">
|
||||
<Text bold color={theme.permission as any}>
|
||||
Execute workflow: {input.workflow}
|
||||
</Text>
|
||||
{input.args && (
|
||||
<Text dimColor>
|
||||
Arguments: {input.args}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<PermissionRuleExplanation
|
||||
permissionResult={toolUseConfirm.permissionResult}
|
||||
toolType="command"
|
||||
/>
|
||||
<PermissionPrompt<OptionValue>
|
||||
options={options}
|
||||
onSelect={handleSelect}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
</Box>
|
||||
</PermissionDialog>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,74 @@
|
||||
// Auto-generated stub — replace with real implementation
|
||||
export {};
|
||||
export const WorkflowTool: Record<string, unknown> = {};
|
||||
import { z } from 'zod/v4'
|
||||
import type { ToolResultBlockParam } from '../../Tool.js'
|
||||
import { buildTool } from '../../Tool.js'
|
||||
import { truncate } from '../../utils/format.js'
|
||||
import { WORKFLOW_TOOL_NAME } from './constants.js'
|
||||
|
||||
const inputSchema = z.object({
|
||||
workflow: z.string().describe('Name of the workflow to execute'),
|
||||
args: z.string().optional().describe('Arguments to pass to the workflow'),
|
||||
})
|
||||
type Input = typeof inputSchema
|
||||
type WorkflowInput = z.infer<Input>
|
||||
|
||||
type WorkflowOutput = { output: string }
|
||||
|
||||
export const WorkflowTool = buildTool({
|
||||
name: WORKFLOW_TOOL_NAME,
|
||||
searchHint: 'execute user-defined workflow scripts',
|
||||
maxResultSizeChars: 50_000,
|
||||
strict: true,
|
||||
|
||||
inputSchema,
|
||||
|
||||
async description() {
|
||||
return 'Execute a user-defined workflow script from .claude/workflows/'
|
||||
},
|
||||
async prompt() {
|
||||
return `Use the Workflow tool to execute user-defined workflow scripts located in .claude/workflows/. Workflows are YAML or Markdown files that define a sequence of steps for common development tasks.
|
||||
|
||||
Guidelines:
|
||||
- Specify the workflow name to execute (must match a file in .claude/workflows/)
|
||||
- Optionally pass arguments that the workflow can use
|
||||
- Workflows run in the context of the current project`
|
||||
},
|
||||
userFacingName() {
|
||||
return 'Workflow'
|
||||
},
|
||||
isReadOnly() {
|
||||
return false
|
||||
},
|
||||
isEnabled() {
|
||||
return true
|
||||
},
|
||||
|
||||
renderToolUseMessage(input: Partial<WorkflowInput>) {
|
||||
const name = input.workflow ?? 'unknown'
|
||||
if (input.args) {
|
||||
return `Workflow: ${name} ${input.args}`
|
||||
}
|
||||
return `Workflow: ${name}`
|
||||
},
|
||||
|
||||
mapToolResultToToolResultBlockParam(
|
||||
content: WorkflowOutput,
|
||||
toolUseID: string,
|
||||
): ToolResultBlockParam {
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result',
|
||||
content: truncate(content.output, 50_000),
|
||||
}
|
||||
},
|
||||
|
||||
async call(_input: WorkflowInput, _context, _progress) {
|
||||
// Workflow execution is wired by the WORKFLOW_SCRIPTS feature bootstrap.
|
||||
// Without it, this tool is not functional.
|
||||
return {
|
||||
data: {
|
||||
output:
|
||||
'Error: Workflow execution requires the WORKFLOW_SCRIPTS runtime.',
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
15
src/tools/WorkflowTool/bundled/index.ts
Normal file
15
src/tools/WorkflowTool/bundled/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// Bundled workflow initialization.
|
||||
// Called by tools.ts when WORKFLOW_SCRIPTS feature flag is enabled.
|
||||
// Sets up any pre-bundled workflow scripts that ship with the CLI.
|
||||
|
||||
/**
|
||||
* Initialize bundled workflows. Called once at startup when the
|
||||
* WORKFLOW_SCRIPTS feature flag is active. This is the hook point
|
||||
* for registering any workflow scripts that are compiled into the
|
||||
* binary (as opposed to user-authored ones in .claude/workflows/).
|
||||
*/
|
||||
export function initBundledWorkflows(): void {
|
||||
// Bundled workflows are registered here at startup.
|
||||
// Currently a no-op — all workflows are user-authored in .claude/workflows/.
|
||||
// This function exists as the extension point for future built-in workflows.
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
// Auto-generated stub — replace with real implementation
|
||||
export const WORKFLOW_TOOL_NAME: string = '';
|
||||
export const WORKFLOW_TOOL_NAME = 'workflow'
|
||||
export const WORKFLOW_DIR_NAME = '.claude/workflows'
|
||||
export const WORKFLOW_FILE_EXTENSIONS = ['.yml', '.yaml', '.md']
|
||||
|
||||
@@ -1,3 +1,41 @@
|
||||
// Auto-generated stub — replace with real implementation
|
||||
export {};
|
||||
export const getWorkflowCommands: (...args: unknown[]) => unknown = () => {};
|
||||
import { readdir } from 'fs/promises'
|
||||
import { join, parse } from 'path'
|
||||
import type { Command } from '../../types/command.js'
|
||||
import { WORKFLOW_DIR_NAME, WORKFLOW_FILE_EXTENSIONS } from './constants.js'
|
||||
|
||||
/**
|
||||
* Scans .claude/workflows/ directory and creates Command objects for each workflow file.
|
||||
* Each workflow file becomes a slash command (e.g. /workflow-name).
|
||||
*/
|
||||
export async function getWorkflowCommands(cwd: string): Promise<Command[]> {
|
||||
const workflowDir = join(cwd, WORKFLOW_DIR_NAME)
|
||||
let files: string[]
|
||||
try {
|
||||
files = await readdir(workflowDir)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
|
||||
const workflowFiles = files.filter((f) => {
|
||||
const ext = parse(f).ext.toLowerCase()
|
||||
return WORKFLOW_FILE_EXTENSIONS.includes(ext)
|
||||
})
|
||||
|
||||
return workflowFiles.map((file) => {
|
||||
const name = parse(file).name
|
||||
return {
|
||||
type: 'prompt' as const,
|
||||
name,
|
||||
description: `Run workflow: ${name}`,
|
||||
kind: 'workflow' as const,
|
||||
source: 'builtin' as const,
|
||||
progressMessage: `Running workflow ${name}...`,
|
||||
contentLength: 0,
|
||||
async getPromptForCommand(args, _context) {
|
||||
const { readFile } = await import('fs/promises')
|
||||
const content = await readFile(join(workflowDir, file), 'utf-8')
|
||||
return [{ type: 'text' as const, text: `Execute this workflow:\n\n${content}${args ? `\n\nArguments: ${args}` : ''}` }]
|
||||
},
|
||||
} satisfies Command
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user