Skip to content

Latest commit

 

History

History
1591 lines (1250 loc) · 36.8 KB

File metadata and controls

1591 lines (1250 loc) · 36.8 KB

Snow CLI Usage Documentation - SSE Service Mode

Welcome to Snow CLI! Agentic coding in your terminal.

Quick Start

Want to quickly experience the SSE client? We provide a complete browser test client:

Location: source/test/sse-client/index.html

Simply open this file in your browser and connect to the SSE server to start testing.

What is SSE Service Mode

SSE (Server-Sent Events) service mode allows you to run Snow CLI as a backend service, providing AI capabilities to external applications. It is perfect for:

  • Web application integration
  • Mobile app backend
  • Third-party tool integration
  • Microservice architecture
  • Custom chat interfaces

Basic Usage

Starting SSE Server

Basic Startup

# Use default port 3000 (foreground)
snow --sse

# Specify port
snow --sse --sse-port 8080

# Specify working directory
snow --sse --work-dir /path/to/project

# Custom interaction timeout (default 300000ms, i.e., 5 minutes)
snow --sse --sse-timeout 600000  # Set to 10 minutes

# Combined usage
snow --sse --sse-port 8080 --work-dir /path/to/project --sse-timeout 600000

Background Daemon Mode

If you don't want to occupy the terminal, you can run the server as a background daemon:

# Start background daemon (default port 3000)
snow --sse-daemon

# Specify different ports (supports multiple instances)
snow --sse-daemon --sse-port 3000
snow --sse-daemon --sse-port 8080
snow --sse-daemon --sse-port 9000

# Specify full parameters
snow --sse-daemon --sse-port 8080 --work-dir /path/to/project --sse-timeout 600000

# Check all daemon status
snow --sse-status

# Stop daemon (three methods)
snow --sse-stop                    # Stop default port 3000 daemon
snow --sse-stop --sse-port 8080    # Stop by port number
snow --sse-stop 12345              # Stop by PID

Daemon features:

  • Supports multiple instances (different ports)
  • Runs in background, doesn't occupy terminal
  • Individual log file per port: ~/.snow/sse-logs/port-<port>.log
  • Individual PID file per port: ~/.snow/sse-daemons/port-<port>.pid
  • Supports stop by port or PID
  • Status check shows all running daemons

Enabling YOLO Mode on Startup

While the SSE server itself doesn't use the --yolo parameter, you can achieve similar functionality through the following methods:

Method 1: Client message with yoloMode

This is the recommended approach, allowing flexible control over whether each request uses YOLO mode:

// Specify YOLO mode when sending message
await fetch('http://localhost:3000/message', {
	method: 'POST',
	headers: {'Content-Type': 'application/json'},
	body: JSON.stringify({
		type: 'chat',
		content: 'Your question',
		yoloMode: true, // Enable YOLO mode
	}),
});

Method 2: Configure auto-approval list

Add commonly used tools to the project's permission configuration file for default auto-approval:

# Edit project permission configuration
vi .snow/permissions.json
{
	"alwaysApprovedTools": [
		"filesystem-read",
		"filesystem-edit",
		"filesystem-create",
		"codebase-search",
		"ace-search",
		"notebook-add"
	]
}

This way, tools in the list will be automatically approved without requiring confirmation each time.

Notes:

  • SSE server startup doesn't support --yolo parameter
  • YOLO mode needs to be enabled via client message's yoloMode field
  • Or implement tool auto-approval via .snow/permissions.json configuration
  • Sensitive commands require confirmation even in YOLO mode

Server Information

After startup, the terminal will display a beautiful server status interface:

✓ SSE server started
Port: 3000 | Working Directory: /Users/xxx/project | ● Running

Available Endpoints:
  http://localhost:3000/events
  POST http://localhost:3000/message
  POST http://localhost:3000/session/create
  POST http://localhost:3000/session/load
  GET http://localhost:3000/session/list
  GET http://localhost:3000/session/rollback-points?sessionId={sessionId}
  DELETE http://localhost:3000/session/{sessionId}
  GET http://localhost:3000/health

Running Logs:
[14:30:45] SSE service started on port 3000
[14:30:50] Created new session: abc-123

Press Ctrl+C to stop server

API Endpoints

1. SSE Event Stream Connection

Endpoint: GET /events

Establish SSE connection to receive real-time event stream.

JavaScript Example

const eventSource = new EventSource('http://localhost:3000/events');

eventSource.onmessage = event => {
	const data = JSON.parse(event.data);
	console.log('Received event:', data);

	switch (data.type) {
		case 'connected':
			console.log(
				'Connection successful, connection ID:',
				data.data.connectionId,
			);
			break;

		case 'message':
			if (data.data.streaming) {
				console.log('AI is responding:', data.data.content);
			} else if (data.data.role === 'user') {
				console.log('User message:', data.data.content);
			}
			break;

		case 'tool_confirmation_request':
			// User confirmation needed for tool execution
			handleToolConfirmation(data);
			break;

		case 'complete':
			console.log('Conversation complete');
			break;
	}
};

2. Send Message

Endpoint: POST /message

Content-Type: application/json

Send Plain Text Message

async function sendMessage(content) {
	const response = await fetch('http://localhost:3000/message', {
		method: 'POST',
		headers: {
			'Content-Type': 'application/json',
		},
		body: JSON.stringify({
			type: 'chat',
			content: content,
		}),
	});

	return await response.json();
}

// Usage example
await sendMessage('Help me create a React component');

Continuous Conversation with Session

async function continueConversation(content, sessionId) {
	const response = await fetch('http://localhost:3000/message', {
		method: 'POST',
		headers: {
			'Content-Type': 'application/json',
		},
		body: JSON.stringify({
			type: 'chat',
			content: content,
			sessionId: sessionId, // Use session ID to continue conversation
		}),
	});

	return await response.json();
}

// Session ID will be returned in complete event
eventSource.onmessage = event => {
	const data = JSON.parse(event.data);
	if (data.type === 'complete') {
		const sessionId = data.data.sessionId;
		console.log('Session ID:', sessionId);
	}
};

Send Image Message

async function sendImageMessage(content, imageFile) {
	// Convert image to Base64 Data URI
	const reader = new FileReader();
	const imageData = await new Promise((resolve, reject) => {
		reader.onload = e => resolve(e.target.result);
		reader.onerror = reject;
		reader.readAsDataURL(imageFile);
	});

	const response = await fetch('http://localhost:3000/message', {
		method: 'POST',
		headers: {
			'Content-Type': 'application/json',
		},
		body: JSON.stringify({
			type: 'chat',
			content: content || 'Please analyze this image',
			images: [
				{
					data: imageData, // Complete data URI, e.g., data:image/png;base64,iVBORw0KG...
					mimeType: imageFile.type, // e.g., image/png, image/jpeg
				},
			],
		}),
	});

	return await response.json();
}

// Usage example
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', async e => {
	const file = e.target.files[0];
	if (file && file.type.startsWith('image/')) {
		await sendImageMessage('What is this?', file);
	}
});

Abort Running Task

async function abortTask(sessionId) {
	const response = await fetch('http://localhost:3000/message', {
		method: 'POST',
		headers: {
			'Content-Type': 'application/json',
		},
		body: JSON.stringify({
			type: 'abort',
			sessionId: sessionId,
		}),
	});

	return await response.json();
}

// Listen for abort confirmation
eventSource.onmessage = event => {
	const data = JSON.parse(event.data);
	if (data.type === 'complete' && data.data.cancelled) {
		console.log('Task has been aborted by user');
	}
};

Enable YOLO Mode

async function sendWithYolo(content) {
	const response = await fetch('http://localhost:3000/message', {
		method: 'POST',
		headers: {
			'Content-Type': 'application/json',
		},
		body: JSON.stringify({
			type: 'chat',
			content: content,
			yoloMode: true, // Auto-approve all non-sensitive tools
		}),
	});

	return await response.json();
}

3. Session Management

Create New Session

Endpoint: POST /session/create

Content-Type: application/json

Create a new conversation session, returns session information and automatically binds to current connection.

async function createSession() {
	const response = await fetch('http://localhost:3000/session/create', {
		method: 'POST',
		headers: {
			'Content-Type': 'application/json',
		},
		body: JSON.stringify({
			connectionId: 'conn_xxx', // Optional, specify connection ID
		}),
	});

	const data = await response.json();
	console.log('Session ID:', data.session.id);
	console.log('Created at:', data.session.createdAt);
	return data.session;
}

Load Existing Session

Endpoint: POST /session/load

Content-Type: application/json

Load a saved session to restore conversation context.

async function loadSession(sessionId) {
	const response = await fetch('http://localhost:3000/session/load', {
		method: 'POST',
		headers: {
			'Content-Type': 'application/json',
		},
		body: JSON.stringify({
			sessionId: sessionId,
			connectionId: 'conn_xxx', // Optional, specify connection ID
		}),
	});

	const data = await response.json();
	if (data.success) {
		console.log('Session loaded:', data.session.id);
		console.log('Message count:', data.session.messages.length);
		return data.session;
	} else {
		console.error('Load failed:', data.error);
	}
}

Get Session List

Endpoint: GET /session/list

Query Parameters:

  • page: Page number, starting from 0 (optional, default 0)
  • pageSize: Items per page (optional, default 20, max 200)
  • q: Search keyword (optional, searches message content in sessions)

Get all saved sessions with pagination and search support.

async function listSessions(page = 0, pageSize = 20, searchQuery = '') {
	const params = new URLSearchParams({
		page: page.toString(),
		pageSize: pageSize.toString(),
	});

	if (searchQuery) {
		params.append('q', searchQuery);
	}

	const response = await fetch(`http://localhost:3000/session/list?${params}`);
	const data = await response.json();

	console.log('Total sessions:', data.total);
	console.log('Current page:', data.page);
	console.log('Page size:', data.pageSize);
	console.log('Session list:', data.sessions);

	// Session list example
	// data.sessions = [
	//   {
	//     id: 'abc-123',
	//     createdAt: '2025-12-30T10:00:00.000Z',
	//     updatedAt: '2025-12-30T10:30:00.000Z',
	//     messageCount: 10,
	//     firstMessage: 'Help me create a function'
	//   },
	//   ...
	// ]

	return data;
}

Get Rollback Points

Endpoint: GET /session/rollback-points

Query Parameters:

  • sessionId: Session ID (required)

Returns a list of user messages in the specified session that can be used as rollback points (demo use).

async function getRollbackPoints(sessionId) {
	const params = new URLSearchParams({sessionId});
	const response = await fetch(
		`http://localhost:3000/session/rollback-points?${params.toString()}`,
	);
	const data = await response.json();

	// Example (key fields):
	// {
	//   success: true,
	//   sessionId: 'abc-123',
	//   points: [
	//     {
	//       messageIndex: 0,
	//       role: 'user',
	//       timestamp: 1730000000000,
	//       summary: '...',
	//       hasSnapshot: true,
	//       snapshot: {timestamp: 1730000000000, fileCount: 12},
	//       filesToRollbackCount: 5
	//     }
	//   ]
	// }
	return data;
}

Delete Session

Endpoint: DELETE /session/{sessionId}

Delete the specified session and all its data.

async function deleteSession(sessionId) {
	const response = await fetch(`http://localhost:3000/session/${sessionId}`, {
		method: 'DELETE',
	});

	const data = await response.json();
	if (data.success) {
		console.log('Session deleted:', data.deleted);
	}
	return data;
}

4. Health Check

Endpoint: GET /health

Check server status and current connection count.

async function checkHealth() {
	const response = await fetch('http://localhost:3000/health');
	const data = await response.json();
	console.log('Status:', data.status);
	console.log('Connections:', data.connections);
}

Event Type Descriptions

connected

Connection successful event.

{
  type: 'connected',
  data: {
    connectionId: 'conn_1234567890'
  },
  timestamp: '2025-12-30T15:30:00.000Z'
}

message

Message event (user or AI).

// User message
{
  type: 'message',
  data: {
    role: 'user',
    content: 'Help me create a function'
  }
}

// AI streaming response
{
  type: 'message',
  data: {
    role: 'assistant',
    content: 'Sure, let me help you...',
    streaming: true
  }
}

// AI final response
{
  type: 'message',
  data: {
    role: 'assistant',
    content: 'Complete response content',
    streaming: false
  }
}

tool_call

Tool invocation event.

{
  type: 'tool_call',
  data: {
    name: 'filesystem-create',
    arguments: {
      filePath: 'example.js',
      content: '...'
    }
  }
}

tool_confirmation_request

Request confirmation for tool execution.

{
  type: 'tool_confirmation_request',
  data: {
    toolCall: {
      function: {
        name: 'terminal-execute',
        arguments: '{"command":"rm -rf node_modules"}'
      }
    },
    isSensitive: true,  // Whether it's a sensitive command
    sensitiveInfo: {
      pattern: 'rm -rf',
      description: 'Delete files or directories'
    },
    availableOptions: [
      {value: 'approve', label: 'Approve once'},
      {value: 'approve_always', label: 'Always approve'},  // Only for non-sensitive commands
      {value: 'reject_with_reply', label: 'Reject with reply'},
      {value: 'reject', label: 'Reject and end session'}
    ]
  },
  requestId: 'req_1234567890'
}

tool_result

Tool execution result.

{
  type: 'tool_result',
  data: {
    content: 'Execution successful',
    status: 'success'
  }
}

user_question_request

AI asking user a question.

{
  type: 'user_question_request',
  data: {
    question: 'Please select an option',
    options: ['Option 1', 'Option 2', 'Option 3'],
    multiSelect: false
  },
  requestId: 'req_1234567890'
}

usage

Token usage information.

{
  type: 'usage',
  data: {
    input_tokens: 150,
    output_tokens: 200
  }
}

error

Error message.

{
  type: 'error',
  data: {
    message: 'Error description',
    stack: 'Error stack (optional)'
  }
}

complete

Conversation completed.

{
  type: 'complete',
  data: {
    usage: {
      input_tokens: 150,
      output_tokens: 200
    },
    tokenCount: 350,
    sessionId: 'abc-123-def-456',  // Session ID
    cancelled: false  // Whether cancelled by user (optional)
  }
}

abort

Task abort request (sent by client).

// Client sends abort request
await fetch('http://localhost:3000/message', {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({
    type: 'abort',
    sessionId: 'abc-123-def-456'
  })
});

// Server responds with abort confirmation
{
  type: 'message',
  data: {
    role: 'assistant',
    content: 'Task has been aborted'
  },
  timestamp: '2025-12-30T15:30:00.000Z'
}

// Followed by complete event
{
  type: 'complete',
  data: {
    usage: {input_tokens: 0, output_tokens: 0},
    tokenCount: 0,
    sessionId: 'abc-123-def-456',
    cancelled: true
  }
}

Tool Confirmation Flow

Confirmation Request Response

When receiving a tool_confirmation_request event, send a confirmation response:

async function handleToolConfirmation(event) {
	const toolCall = event.data.toolCall;
	const options = event.data.availableOptions;

	// Display tool information to user
	console.log('Tool:', toolCall.function.name);
	console.log('Arguments:', toolCall.function.arguments);
	console.log('Available options:', options);

	// Send response after user selection
	const response = await fetch('http://localhost:3000/message', {
		method: 'POST',
		headers: {
			'Content-Type': 'application/json',
		},
		body: JSON.stringify({
			type: 'tool_confirmation_response',
			requestId: event.requestId,
			response: 'approve', // or 'approve_always', 'reject', {type: 'reject_with_reply', reason: '...'}
		}),
	});

	return await response.json();
}

Confirmation Options Explained

Option Value Description Applicable Scenario
Approve once 'approve' Approve this execution only All tools
Always approve 'approve_always' Approve and add to auto-approval list Non-sensitive only
Reject with reply {type: 'reject_with_reply', reason: '...'} Reject and tell AI the reason All tools
Reject and end 'reject' Reject and end session All tools

Sensitive Command Detection

The system automatically detects sensitive commands (like rm -rf, sudo, etc.). Sensitive commands:

  • Will not show "Always approve" option
  • Require confirmation even in YOLO mode
  • Display warning information and matched command pattern

For sensitive command configuration, refer to: Sensitive Commands Configuration

User Question Response

When receiving a user_question_request event:

async function handleUserQuestion(event) {
	const question = event.data.question;
	const options = event.data.options;
	const multiSelect = event.data.multiSelect;

	// Display question and options to user
	console.log('Question:', question);
	console.log('Options:', options);

	// Send response after user selection
	const response = await fetch('http://localhost:3000/message', {
		method: 'POST',
		headers: {
			'Content-Type': 'application/json',
		},
		body: JSON.stringify({
			type: 'user_question_response',
			requestId: event.requestId,
			response: {
				selected: multiSelect ? ['Option 1', 'Option 2'] : 'Option 1',
				customInput: '', // Optional custom input
			},
		}),
	});

	return await response.json();
}

Permission Configuration

Auto-Approval List

SSE server automatically reads permission configuration file from project root directory:

Location: .snow/permissions.json

{
	"alwaysApprovedTools": [
		"filesystem-read",
		"codebase-search",
		"filesystem-edit",
		"notebook-add",
		"filesystem-create"
	]
}

Permission Inheritance Rules

  1. Project-level Configuration: Server reads .snow/permissions.json from working directory on startup
  2. Auto-approval: Tools in the list are executed automatically without user confirmation
  3. Sensitive Commands Priority: Sensitive commands require confirmation even if in auto-approval list
  4. Dynamic Updates: When user selects "Always approve", the tool is automatically added to configuration file

Configuration Example

{
	"alwaysApprovedTools": [
		"filesystem-read", // Read files
		"filesystem-edit", // Hashline edit
		"filesystem-create", // Create files
		"codebase-search", // Code search
		"ace-search", // Unified ACE code search (semantic_search / find_definition / find_references / file_outline / text_search via action)
		"notebook-add" // Add note
	]
}

YOLO Mode

Enable YOLO Mode

Carry yoloMode parameter when sending message:

const response = await fetch('http://localhost:3000/message', {
	method: 'POST',
	headers: {'Content-Type': 'application/json'},
	body: JSON.stringify({
		type: 'chat',
		content: 'Your question',
		yoloMode: true, // Enable YOLO mode
	}),
});

YOLO Mode Features

  • Auto-approval: Non-sensitive commands execute automatically
  • Sensitive Command Exception: Sensitive commands still require confirmation
  • Fast Response: Reduce interaction waiting time
  • Suitable for Automation: Script and automation scenarios

Security Considerations

Even with YOLO mode enabled:

  1. Sensitive commands still require confirmation
  2. Tools not in permission list require first-time confirmation
  3. Can abort execution at any time through rejection

Complete Examples

JavaScript Client

class SnowAIClient {
	constructor(baseUrl = 'http://localhost:3000') {
		this.baseUrl = baseUrl;
		this.eventSource = null;
		this.sessionId = null;
	}

	// Connect to SSE server
	connect() {
		return new Promise((resolve, reject) => {
			this.eventSource = new EventSource(`${this.baseUrl}/events`);

			this.eventSource.onopen = () => {
				console.log('Connected to Snow AI');
				resolve();
			};

			this.eventSource.onerror = error => {
				console.error('Connection error:', error);
				reject(error);
			};

			this.eventSource.onmessage = event => {
				this.handleEvent(JSON.parse(event.data));
			};
		});
	}

	// Handle events
	handleEvent(event) {
		console.log('[Event]', event.type);

		switch (event.type) {
			case 'tool_confirmation_request':
				this.handleToolConfirmation(event);
				break;

			case 'user_question_request':
				this.handleUserQuestion(event);
				break;

			case 'message':
				if (event.data.streaming) {
					process.stdout.write(event.data.content);
				}
				break;

			case 'complete':
				this.sessionId = event.data.sessionId;
				console.log('\nConversation complete, Session ID:', this.sessionId);
				break;
		}
	}

	// Handle tool confirmation
	async handleToolConfirmation(event) {
		const options = event.data.availableOptions;

		// Custom confirmation logic can be implemented here
		// Example: Auto-approve non-sensitive commands
		const decision = event.data.isSensitive ? 'reject' : 'approve';

		await this.sendToolConfirmation(event.requestId, decision);
	}

	// Handle user question
	async handleUserQuestion(event) {
		// Custom selection logic can be implemented here
		const selected = event.data.options[0];

		await this.sendUserQuestionResponse(event.requestId, {
			selected: selected,
		});
	}

	// Send message
	async sendMessage(content, yoloMode = false) {
		const payload = {
			type: 'chat',
			content: content,
		};

		if (this.sessionId) {
			payload.sessionId = this.sessionId;
		}

		if (yoloMode) {
			payload.yoloMode = true;
		}

		const response = await fetch(`${this.baseUrl}/message`, {
			method: 'POST',
			headers: {'Content-Type': 'application/json'},
			body: JSON.stringify(payload),
		});

		return await response.json();
	}

	// Send tool confirmation response
	async sendToolConfirmation(requestId, decision) {
		const response = await fetch(`${this.baseUrl}/message`, {
			method: 'POST',
			headers: {'Content-Type': 'application/json'},
			body: JSON.stringify({
				type: 'tool_confirmation_response',
				requestId: requestId,
				response: decision,
			}),
		});

		return await response.json();
	}

	// Send user question response
	async sendUserQuestionResponse(requestId, answer) {
		const response = await fetch(`${this.baseUrl}/message`, {
			method: 'POST',
			headers: {'Content-Type': 'application/json'},
			body: JSON.stringify({
				type: 'user_question_response',
				requestId: requestId,
				response: answer,
			}),
		});

		return await response.json();
	}

	// Disconnect
	disconnect() {
		if (this.eventSource) {
			this.eventSource.close();
			this.eventSource = null;
		}
	}
}

// Usage example
async function main() {
	const client = new SnowAIClient();

	// Connect
	await client.connect();

	// Send message (enable YOLO mode)
	await client.sendMessage('Help me create a TypeScript function', true);

	// Wait for response handling (via event listeners)
}

main();

Python Client

import requests
import json
import sseclient

class SnowAIClient:
    def __init__(self, base_url='http://localhost:3000'):
        self.base_url = base_url
        self.session = requests.Session()
        self.session_id = None

    def connect(self):
        """Connect to SSE server"""
        response = self.session.get(
            f'{self.base_url}/events',
            stream=True,
            headers={'Accept': 'text/event-stream'}
        )
        client = sseclient.SSEClient(response)

        for event in client.events():
            data = json.loads(event.data)
            self.handle_event(data)

    def handle_event(self, event):
        """Handle events"""
        print(f"[Event] {event['type']}")

        if event['type'] == 'tool_confirmation_request':
            self.handle_tool_confirmation(event)
        elif event['type'] == 'user_question_request':
            self.handle_user_question(event)
        elif event['type'] == 'complete':
            self.session_id = event['data']['sessionId']
            print(f"Session ID: {self.session_id}")

    def handle_tool_confirmation(self, event):
        """Handle tool confirmation"""
        # Auto-approve non-sensitive commands
        decision = 'reject' if event['data']['isSensitive'] else 'approve'
        self.send_tool_confirmation_response(event['requestId'], decision)

    def handle_user_question(self, event):
        """Handle user question"""
        selected = event['data']['options'][0]
        self.send_user_question_response(event['requestId'], {'selected': selected})

    def send_message(self, content, yolo_mode=False):
        """Send message"""
        payload = {
            'type': 'chat',
            'content': content,
        }

        if self.session_id:
            payload['sessionId'] = self.session_id

        if yolo_mode:
            payload['yoloMode'] = True

        response = self.session.post(
            f'{self.base_url}/message',
            json=payload
        )
        return response.json()

    def send_tool_confirmation_response(self, request_id, decision):
        """Send tool confirmation response"""
        response = self.session.post(
            f'{self.base_url}/message',
            json={
                'type': 'tool_confirmation_response',
                'requestId': request_id,
                'response': decision
            }
        )
        return response.json()

    def send_user_question_response(self, request_id, answer):
        """Send user question response"""
        response = self.session.post(
            f'{self.base_url}/message',
            json={
                'type': 'user_question_response',
                'requestId': request_id,
                'response': answer
            }
        )
        return response.json()

# Usage example
if __name__ == '__main__':
    client = SnowAIClient()

    # Send message (enable YOLO mode)
    client.send_message('Help me create a Python function', yolo_mode=True)

    # Listen for events
    client.connect()

Use Cases

Web Application Integration

Integrate Snow AI into your web application to provide intelligent programming assistant functionality:

// React component example
import {useState, useEffect, useRef} from 'react';

function AIAssistantChat() {
	const [connected, setConnected] = useState(false);
	const [messages, setMessages] = useState([]);
	const [sessionId, setSessionId] = useState(null);
	const eventSourceRef = useRef(null);

	// Connect to SSE server
	useEffect(() => {
		const eventSource = new EventSource('http://localhost:3000/events');
		eventSourceRef.current = eventSource;

		eventSource.onopen = () => {
			setConnected(true);
			console.log('Connected to Snow AI');
		};

		eventSource.onmessage = event => {
			const data = JSON.parse(event.data);
			handleSSEEvent(data);
		};

		eventSource.onerror = () => {
			setConnected(false);
			console.error('Connection lost');
		};

		return () => {
			eventSource.close();
		};
	}, []);

	// Handle SSE events
	const handleSSEEvent = data => {
		switch (data.type) {
			case 'message':
				if (data.data.role === 'assistant') {
					if (data.data.streaming) {
						// Stream update last message
						setMessages(prev => {
							const newMessages = [...prev];
							if (
								newMessages.length > 0 &&
								newMessages[newMessages.length - 1].role === 'assistant'
							) {
								newMessages[newMessages.length - 1].content = data.data.content;
							} else {
								newMessages.push({
									role: 'assistant',
									content: data.data.content,
								});
							}
							return newMessages;
						});
					}
				}
				break;

			case 'complete':
				setSessionId(data.data.sessionId);
				console.log('Conversation complete');
				break;

			case 'tool_confirmation_request':
				// Show tool confirmation dialog
				handleToolConfirmation(data);
				break;

			case 'error':
				console.error('Error:', data.data.message);
				break;
		}
	};

	// Send message
	const sendMessage = async text => {
		const newMessage = {role: 'user', content: text};
		setMessages(prev => [...prev, newMessage]);

		const payload = {
			type: 'chat',
			content: text,
			yoloMode: true, // Auto-approve safe tools
		};

		if (sessionId) {
			payload.sessionId = sessionId;
		}

		await fetch('http://localhost:3000/message', {
			method: 'POST',
			headers: {'Content-Type': 'application/json'},
			body: JSON.stringify(payload),
		});
	};

	// Handle tool confirmation
	const handleToolConfirmation = async event => {
		const confirmed = window.confirm(
			`AI wants to execute tool: ${event.data.toolCall.function.name}\nAllow?`,
		);

		await fetch('http://localhost:3000/message', {
			method: 'POST',
			headers: {'Content-Type': 'application/json'},
			body: JSON.stringify({
				type: 'tool_confirmation_response',
				requestId: event.requestId,
				response: confirmed ? 'approve' : 'reject',
			}),
		});
	};

	return (
		<div>
			<div>Status: {connected ? 'Connected' : 'Disconnected'}</div>
			<div>
				{messages.map((msg, i) => (
					<div key={i}>
						<strong>{msg.role}:</strong> {msg.content}
					</div>
				))}
			</div>
			<input
				type="text"
				onKeyPress={e => {
					if (e.key === 'Enter') {
						sendMessage(e.target.value);
						e.target.value = '';
					}
				}}
			/>
		</div>
	);
}

Mobile App Backend

Provide AI capabilities for mobile applications:

// Express middleware
app.post('/api/ai/chat', async (req, res) => {
	const {message, sessionId} = req.body;

	// Forward to Snow AI
	const response = await fetch('http://localhost:3000/message', {
		method: 'POST',
		headers: {'Content-Type': 'application/json'},
		body: JSON.stringify({
			type: 'chat',
			content: message,
			sessionId: sessionId,
			yoloMode: true,
		}),
	});

	res.json(await response.json());
});

Microservice Architecture

Use as AI microservice:

# Kubernetes deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: snow-ai-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: snow-ai
  template:
    metadata:
      labels:
        app: snow-ai
    spec:
      containers:
        - name: snow-ai
          image: snow-ai:latest
          command: ['snow', '--sse', '--sse-port', '3000']
          ports:
            - containerPort: 3000

Test Client

Snow CLI provides a complete HTML test client:

Location: sse-test-client.html

Features

  • Real-time SSE event monitoring
  • Beautiful chat interface
  • Event log viewer
  • YOLO mode toggle
  • Tool confirmation UI (with complete option display)
  • Session management
  • Connection status display

Usage

  1. Start SSE server:

    snow --sse
  2. Open sse-test-client.html in browser

  3. Click "Connect" button

  4. Start chatting and testing

Best Practices

1. Error Handling

// Comprehensive error handling
eventSource.onerror = error => {
	console.error('SSE connection error:', error);

	// Auto-reconnect
	setTimeout(() => {
		console.log('Attempting to reconnect...');
		connect();
	}, 5000);
};

2. Timeout Handling

// Set timeout for interaction requests
const TIMEOUT = 300000; // 5 minutes (default value, configurable via --sse-timeout parameter)

function waitForResponse(requestId) {
	return new Promise((resolve, reject) => {
		const timeout = setTimeout(() => {
			reject(new Error('Interaction timeout'));
		}, TIMEOUT);

		// Listen for response
		// clearTimeout(timeout) after receiving response
	});
}

3. Session Management

// Persist Session ID
localStorage.setItem('snow-session-id', sessionId);

// Restore Session
const savedSessionId = localStorage.getItem('snow-session-id');
if (savedSessionId) {
	await client.sendMessage(
		'Continue previous conversation',
		false,
		savedSessionId,
	);
}

4. Security Considerations

// Validate and sanitize user input
function sanitizeInput(input) {
	// Remove dangerous characters
	return input.replace(/[<>]/g, '');
}

// Add authentication in production
const response = await fetch('http://localhost:3000/message', {
	headers: {
		'Content-Type': 'application/json',
		Authorization: `Bearer ${apiToken}`,
	},
	// ...
});

Limitations and Notes

Unsupported Features

  1. Interactive UI:

    • Cannot use Ink terminal interface
    • Keyboard shortcuts not supported
  2. Plan Mode:

    • Interactive plan approval not supported
    • All operations execute immediately
  3. Local File Access Restrictions:

    • Can only access files under server working directory
    • Cannot access client-side local files

Performance Notes

  1. Connection Limit:

    • Recommended maximum 100 concurrent connections per server
    • Consider load balancing
  2. Session Size:

    • Long sessions increase memory usage
    • Regularly clean up old sessions
  3. Network Bandwidth:

    • Streaming output continuously occupies connection
    • Consider message size limits

Security Notes

  1. Authentication and Authorization:

    • Must add authentication in production environment
    • Implement access control
  2. API Key Protection:

    • Don't expose API keys on client side
    • Use server-side configuration
  3. Command Execution Risks:

    • Review all tool invocations
    • Restrict sensitive operations

FAQ

Q: What's the difference between SSE server and headless mode?

A: SSE server is a continuously running backend service supporting multiple client connections. Headless mode is single-execution mode that automatically exits after completion. SSE is suitable for web application integration, headless mode for script automation.

Q: How to use different API configurations in SSE mode?

A: SSE server reads configuration files from working directory. Use --work-dir parameter to specify different project directories, each with independent configuration.

Q: Can I run multiple SSE servers simultaneously?

A: Yes, but need to use different ports. For example:

snow --sse --sse-port 3000
snow --sse --sse-port 3001 --work-dir /another-project

Q: Do sessions expire?

A: Sessions don't expire and are permanently saved in ~/.snow/sessions/ directory. However, very long sessions increase token consumption.

Q: How to handle tool confirmation timeout?

A: Tool confirmation has a default 5-minute (300000ms) timeout. If timeout occurs, execution is automatically rejected and error returned. Recommend implementing auto-handling or user prompts in client.

You can customize the timeout duration via --sse-timeout parameter:

# Set to 10 minutes (600000ms)
snow --sse --sse-timeout 600000

# Set to 30 seconds (30000ms)
snow --sse --sse-timeout 30000

Q: Does YOLO mode execute all commands?

A: No. Sensitive commands require confirmation even in YOLO mode. YOLO mode only auto-approves safe tools in the permission list.

Q: How to debug SSE connection issues?

A:

  1. Check server logs (terminal display)
  2. Use browser developer tools to view network requests
  3. Use sse-test-client.html for testing
  4. Check firewall and port usage

Q: Can SSE server run in Docker?

A: Yes. Example Dockerfile:

FROM node:18
RUN npm install -g snow-ai
EXPOSE 3000
CMD ["snow", "--sse", "--sse-port", "3000"]

Configuration File Locations

Configuration files used by SSE server:

  • API Configuration: ~/.snow/profiles.json
  • Permission Configuration: <working-directory>/.snow/permissions.json
  • Sensitive Commands: ~/.snow/sensitive-commands.json
  • Session Storage: ~/.snow/sessions/<project-name>/<date>/

For configuration methods, refer to: First Time Configuration

Related Features