Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions examples/hook-execution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Hook execution streaming example.
*
* Demonstrates how to handle hook execution messages in a session stream.
*
* Usage:
* npx tsx examples/hook-execution.ts
*/

import { DroidMessageType, createSession } from '../src/index.js';

async function main(): Promise<void> {
const prompt = 'Run a simple shell command using Execute tool.';

console.log(`Sending prompt: "${prompt}"\n`);

// Note: To actually see hooks, you need to have hooks configured in your droid settings.
const session = await createSession({ cwd: process.cwd() });

try {
for await (const msg of session.stream(prompt)) {
switch (msg.type) {
case DroidMessageType.Assistant:
process.stdout.write(msg.text);
break;

case DroidMessageType.ToolCall:
console.log(`\n[Tool Call] ${msg.toolUse.name} (${msg.toolUse.id})`);
break;

case DroidMessageType.Hook:
if (msg.status === 'started') {
console.log(
` [Hook Started] ID: ${msg.hookId}, Event: ${msg.eventName}, Command: ${msg.command}`
);
} else {
console.log(
` [Hook ${msg.status}] ID: ${msg.hookId}, Exit Code: ${msg.exitCode}`
);
if (msg.stdout) console.log(` stdout: ${msg.stdout.trim()}`);
if (msg.stderr) console.log(` stderr: ${msg.stderr.trim()}`);
}
break;

case DroidMessageType.ToolResult:
console.log(`[Tool Result] ${msg.isError ? 'Error' : 'OK'}`);
break;

case DroidMessageType.Result:
console.log('\n\n--- Turn complete ---');
break;
}
}
} finally {
await session.close();
}
}

main().catch((err: unknown) => {
console.error('Error:', err);
process.exit(1);
});
13 changes: 13 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import type {
CancelMcpAuthResult,
ClearMcpAuthRequestParams,
ClearMcpAuthResult,
CloseSessionRequestParams,
CloseSessionResult,
CompactSessionRequestParams,
CompactSessionResult,
GetContextStatsResult,
Expand Down Expand Up @@ -61,6 +63,7 @@ import {
AuthenticateMcpServerResultSchema,
CancelMcpAuthResultSchema,
ClearMcpAuthResultSchema,
CloseSessionResultSchema,
CompactSessionResultSchema,
GetContextStatsResultSchema,
ExecuteRewindResultSchema,
Expand Down Expand Up @@ -241,6 +244,16 @@ export class DroidClient {
);
}

async closeSession(
params: CloseSessionRequestParams = {}
): Promise<CloseSessionResult> {
return this._sessionRpc(
DroidServerMethod.CLOSE_SESSION,
params,
CloseSessionResultSchema
);
}

async killWorkerSession(
params: KillWorkerSessionRequestParams
): Promise<KillWorkerSessionResult> {
Expand Down
91 changes: 91 additions & 0 deletions src/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import type { DroidHookEvent } from './schemas/hooks.js';

export type { DroidHookEvent, DroidHookOutput } from './schemas/hooks.js';

export type DroidPermissionMode =
| 'off'
| 'spec'
| 'auto-low'
| 'auto-medium'
| 'auto-high';

export interface BaseDroidHookInput {
session_id: string;
transcript_path: string;
cwd: string;
permission_mode: DroidPermissionMode;
hook_event_name: DroidHookEvent;
message_id?: string;
[key: string]: unknown;
}

export interface ToolHookInput extends BaseDroidHookInput {
hook_event_name: 'PreToolUse' | 'PostToolUse';
tool_name: string;
tool_input: Record<string, unknown>;
tool_response?: unknown;
}

export interface UserPromptSubmitHookInput extends BaseDroidHookInput {
hook_event_name: 'UserPromptSubmit';
prompt: string;
has_images?: boolean;
}

export interface NotificationHookInput extends BaseDroidHookInput {
hook_event_name: 'Notification';
message: string;
notification_type:
| 'permission_prompt'
| 'idle_prompt'
| 'auth_success'
| 'elicitation_dialog';
}

export interface StopHookInput extends BaseDroidHookInput {
hook_event_name: 'Stop';
stop_hook_active: boolean;
tool_execution_count?: number;
elapsed_time?: number;
}

export interface SubagentStopHookInput extends BaseDroidHookInput {
hook_event_name: 'SubagentStop';
task_name: string;
task_result?: string;
task_error?: string;
stop_hook_active: boolean;
}

export interface PreCompactHookInput extends BaseDroidHookInput {
hook_event_name: 'PreCompact';
trigger: 'manual' | 'auto';
custom_instructions?: string;
message_count: number;
estimated_tokens: number;
}

export interface SessionStartHookInput extends BaseDroidHookInput {
hook_event_name: 'SessionStart';
source: 'startup' | 'resume' | 'clear' | 'compact';
previous_session_id?: string;
calling_session_id?: string;
}

export interface SessionEndHookInput extends BaseDroidHookInput {
hook_event_name: 'SessionEnd';
reason: 'clear' | 'logout' | 'prompt_input_exit' | 'other';
session_duration_ms: number;
message_count: number;
}

export type DroidHookInput =
| ToolHookInput
| UserPromptSubmitHookInput
| NotificationHookInput
| StopHookInput
| SubagentStopHookInput
| PreCompactHookInput
| SessionStartHookInput
| SessionEndHookInput
| BaseDroidHookInput;
17 changes: 17 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ export type {
DroidClientOptions,
} from './client.js';

export type {
BaseDroidHookInput,
DroidHookEvent,
DroidHookInput,
DroidHookOutput,
DroidPermissionMode,
NotificationHookInput,
PreCompactHookInput,
SessionEndHookInput,
SessionStartHookInput,
StopHookInput,
SubagentStopHookInput,
ToolHookInput,
UserPromptSubmitHookInput,
} from './hooks.js';

export {
convertNotificationToStreamMessage,
DroidMessageType,
Expand Down Expand Up @@ -59,6 +75,7 @@ export type {
MissionWorkerCompleted,
McpAuthRequired,
McpAuthCompleted,
HookExecution,
StructuredOutput,
StructuredOutputFields,
ErrorEvent,
Expand Down
37 changes: 36 additions & 1 deletion src/schemas/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {
DroidInteractionMode,
DroidServerMethod,
McpServerType,
MissionState,
ModelProvider,
MissionState,
ReasoningEffort,
SettingsLevel,
SkillLocation,
Expand Down Expand Up @@ -311,6 +311,19 @@ export type InterruptSessionRequestParams = z.infer<
typeof InterruptSessionRequestParamsSchema
>;

/** Parameters for droid.close_session request. */
export const CloseSessionRequestParamsSchema = z
.object({
reason: z
.enum(['clear', 'logout', 'prompt_input_exit', 'other'])
.optional(),
})
.strict();

export type CloseSessionRequestParams = z.infer<
typeof CloseSessionRequestParamsSchema
>;

/** Parameters for droid.kill_worker_session request. */
export const KillWorkerSessionRequestParamsSchema = z
.object({
Expand Down Expand Up @@ -634,6 +647,13 @@ export type InterruptSessionRequest = z.infer<
typeof InterruptSessionRequestSchema
>;

export const CloseSessionRequestSchema = JsonRpcRequestSchema.extend({
method: z.literal(DroidServerMethod.CLOSE_SESSION),
params: CloseSessionRequestParamsSchema,
});

export type CloseSessionRequest = z.infer<typeof CloseSessionRequestSchema>;

export const KillWorkerSessionRequestSchema = JsonRpcRequestSchema.extend({
method: z.literal(DroidServerMethod.KILL_WORKER_SESSION),
params: KillWorkerSessionRequestParamsSchema,
Expand Down Expand Up @@ -811,6 +831,7 @@ export const ClientRequestSchema = z.discriminatedUnion('method', [
InitializeSessionRequestSchema,
LoadSessionRequestSchema,
AddUserMessageRequestSchema,
CloseSessionRequestSchema,
InterruptSessionRequestSchema,
KillWorkerSessionRequestSchema,
UpdateSessionSettingsRequestSchema,
Expand Down Expand Up @@ -904,6 +925,11 @@ export type InterruptSessionResult = z.infer<
typeof InterruptSessionResultSchema
>;

/** Result for droid.close_session response (empty). */
export const CloseSessionResultSchema = EmptyResultSchema;

export type CloseSessionResult = z.infer<typeof CloseSessionResultSchema>;

/** Result for droid.kill_worker_session response (empty). */
export const KillWorkerSessionResultSchema = EmptyResultSchema;

Expand Down Expand Up @@ -1114,6 +1140,15 @@ export type InterruptSessionResponse = z.infer<
typeof InterruptSessionResponseSchema
>;

export const CloseSessionResponseSchema = z.union([
JsonRpcResponseSuccessSchema.extend({
result: CloseSessionResultSchema,
}),
JsonRpcResponseFailureSchema,
]);

export type CloseSessionResponse = z.infer<typeof CloseSessionResponseSchema>;

export const KillWorkerSessionResponseSchema = z.union([
JsonRpcResponseSuccessSchema.extend({
result: KillWorkerSessionResultSchema,
Expand Down
3 changes: 3 additions & 0 deletions src/schemas/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export enum DroidServerMethod {
INITIALIZE_SESSION = 'droid.initialize_session',
LOAD_SESSION = 'droid.load_session',
ADD_USER_MESSAGE = 'droid.add_user_message',
CLOSE_SESSION = 'droid.close_session',
INTERRUPT_SESSION = 'droid.interrupt_session',
KILL_WORKER_SESSION = 'droid.kill_worker_session',
UPDATE_SESSION_SETTINGS = 'droid.update_session_settings',
Expand Down Expand Up @@ -69,6 +70,8 @@ export enum SessionNotificationType {
MISSION_WORKER_COMPLETED = 'mission_worker_completed',
MCP_AUTH_REQUIRED = 'mcp_auth_required',
MCP_AUTH_COMPLETED = 'mcp_auth_completed',
HOOK_EXECUTION_STARTED = 'hook_execution_started',
HOOK_EXECUTION_COMPLETED = 'hook_execution_completed',
STRUCTURED_OUTPUT = 'structured_output',
}

Expand Down
49 changes: 49 additions & 0 deletions src/schemas/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { z } from 'zod';

export const DroidHookEventSchema = z.enum([
'PreToolUse',
'PostToolUse',
'Notification',
'UserPromptSubmit',
'Stop',
'SubagentStop',
'PreCompact',
'SessionStart',
'SessionEnd',
]);

export type DroidHookEvent = z.infer<typeof DroidHookEventSchema>;

export const DroidHookSpecificOutputSchema = z
.object({
hookEventName: DroidHookEventSchema.optional(),
permissionDecision: z.enum(['allow', 'deny', 'ask']).optional(),
permissionDecisionReason: z.string().optional(),
additionalContext: z.string().optional(),
updatedInput: z.record(z.unknown()).optional(),
})
.passthrough();

export const DroidHookOutputSchema = z
.object({
continue: z.boolean().optional(),
stopReason: z.string().optional(),
suppressOutput: z.boolean().optional(),
systemMessage: z.string().optional(),
decision: z.enum(['block', 'approve']).optional(),
reason: z.string().optional(),
hookSpecificOutput: DroidHookSpecificOutputSchema.optional(),
})
.passthrough();

export type DroidHookOutput = z.infer<typeof DroidHookOutputSchema>;

export const DroidHookExecutionResultSchema = DroidHookOutputSchema.extend({
exitCode: z.number(),
stdout: z.string(),
stderr: z.string(),
});

export type DroidHookExecutionResult = z.infer<
typeof DroidHookExecutionResultSchema
>;
Loading
Loading