code-puppy 0.0.348__py3-none-any.whl → 0.0.361__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_puppy/agents/__init__.py +2 -0
- code_puppy/agents/agent_manager.py +49 -0
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_qa_kitten.py +12 -7
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/base_agent.py +17 -4
- code_puppy/agents/event_stream_handler.py +101 -8
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +446 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +73 -0
- code_puppy/claude_cache_client.py +249 -34
- code_puppy/command_line/core_commands.py +85 -0
- code_puppy/config.py +66 -62
- code_puppy/messaging/__init__.py +15 -0
- code_puppy/messaging/messages.py +27 -0
- code_puppy/messaging/queue_console.py +1 -1
- code_puppy/messaging/rich_renderer.py +36 -1
- code_puppy/messaging/spinner/__init__.py +20 -2
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_utils.py +54 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +90 -19
- code_puppy/plugins/antigravity_oauth/transport.py +1 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/status_display.py +6 -2
- code_puppy/tools/__init__.py +37 -1
- code_puppy/tools/agent_tools.py +83 -33
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +6 -6
- code_puppy/tools/browser/browser_interactions.py +21 -20
- code_puppy/tools/browser/browser_locators.py +9 -9
- code_puppy/tools/browser/browser_navigation.py +7 -7
- code_puppy/tools/browser/browser_screenshot.py +78 -140
- code_puppy/tools/browser/browser_scripts.py +15 -13
- code_puppy/tools/browser/camoufox_manager.py +226 -64
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +292 -101
- code_puppy/tools/common.py +176 -1
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/subagent_context.py +158 -0
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.361.dist-info}/METADATA +13 -11
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.361.dist-info}/RECORD +69 -38
- code_puppy/tools/browser/vqa_agent.py +0 -90
- {code_puppy-0.0.348.data → code_puppy-0.0.361.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.348.data → code_puppy-0.0.361.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.361.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.361.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.361.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" class="h-full">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>🐶 Code Puppy Terminal</title>
|
|
7
|
+
|
|
8
|
+
<!-- Tailwind CSS -->
|
|
9
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
10
|
+
|
|
11
|
+
<!-- xterm.js CSS -->
|
|
12
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" />
|
|
13
|
+
|
|
14
|
+
<!-- xterm.js -->
|
|
15
|
+
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
|
|
16
|
+
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
|
|
17
|
+
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.min.js"></script>
|
|
18
|
+
|
|
19
|
+
<style>
|
|
20
|
+
/* Custom terminal styling */
|
|
21
|
+
.xterm {
|
|
22
|
+
padding: 8px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.xterm-viewport::-webkit-scrollbar {
|
|
26
|
+
width: 8px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.xterm-viewport::-webkit-scrollbar-track {
|
|
30
|
+
background: #1e1e1e;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.xterm-viewport::-webkit-scrollbar-thumb {
|
|
34
|
+
background: #4a4a4a;
|
|
35
|
+
border-radius: 4px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.xterm-viewport::-webkit-scrollbar-thumb:hover {
|
|
39
|
+
background: #5a5a5a;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.status-indicator {
|
|
43
|
+
width: 10px;
|
|
44
|
+
height: 10px;
|
|
45
|
+
border-radius: 50%;
|
|
46
|
+
animation: pulse 2s infinite;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.status-connected {
|
|
50
|
+
background-color: #22c55e;
|
|
51
|
+
box-shadow: 0 0 8px #22c55e;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.status-disconnected {
|
|
55
|
+
background-color: #ef4444;
|
|
56
|
+
box-shadow: 0 0 8px #ef4444;
|
|
57
|
+
animation: none;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.status-connecting {
|
|
61
|
+
background-color: #f59e0b;
|
|
62
|
+
box-shadow: 0 0 8px #f59e0b;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@keyframes pulse {
|
|
66
|
+
0%, 100% { opacity: 1; }
|
|
67
|
+
50% { opacity: 0.5; }
|
|
68
|
+
}
|
|
69
|
+
</style>
|
|
70
|
+
</head>
|
|
71
|
+
<body class="h-full bg-gray-900 text-white overflow-hidden">
|
|
72
|
+
<div class="h-full flex flex-col">
|
|
73
|
+
<!-- Header -->
|
|
74
|
+
<header class="bg-gray-800 border-b border-gray-700 px-4 py-2 flex items-center justify-between">
|
|
75
|
+
<div class="flex items-center space-x-3">
|
|
76
|
+
<span class="text-2xl">🐶</span>
|
|
77
|
+
<h1 class="text-lg font-semibold text-gray-100">Code Puppy Terminal</h1>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div class="flex items-center space-x-4">
|
|
81
|
+
<!-- Connection Status -->
|
|
82
|
+
<div class="flex items-center space-x-2">
|
|
83
|
+
<div id="status-indicator" class="status-indicator status-disconnected"></div>
|
|
84
|
+
<span id="status-text" class="text-sm text-gray-400">Disconnected</span>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<!-- Session ID -->
|
|
88
|
+
<div class="text-sm text-gray-500">
|
|
89
|
+
Session: <span id="session-id" class="font-mono text-gray-400">-</span>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<!-- Controls -->
|
|
93
|
+
<div class="flex items-center space-x-2">
|
|
94
|
+
<button id="btn-reconnect"
|
|
95
|
+
class="px-3 py-1 text-sm bg-blue-600 hover:bg-blue-700 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
96
|
+
disabled>
|
|
97
|
+
Reconnect
|
|
98
|
+
</button>
|
|
99
|
+
<button id="btn-clear"
|
|
100
|
+
class="px-3 py-1 text-sm bg-gray-600 hover:bg-gray-700 rounded transition-colors">
|
|
101
|
+
Clear
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</header>
|
|
106
|
+
|
|
107
|
+
<!-- Terminal Container -->
|
|
108
|
+
<main class="flex-1 bg-black p-2">
|
|
109
|
+
<div id="terminal" class="h-full w-full"></div>
|
|
110
|
+
</main>
|
|
111
|
+
|
|
112
|
+
<!-- Footer -->
|
|
113
|
+
<footer class="bg-gray-800 border-t border-gray-700 px-4 py-1 flex items-center justify-between text-xs text-gray-500">
|
|
114
|
+
<div>
|
|
115
|
+
<span id="terminal-size">80x24</span>
|
|
116
|
+
</div>
|
|
117
|
+
<div class="flex items-center space-x-4">
|
|
118
|
+
<span>Ctrl+C to interrupt</span>
|
|
119
|
+
<span>•</span>
|
|
120
|
+
<span>Ctrl+D to exit</span>
|
|
121
|
+
</div>
|
|
122
|
+
</footer>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<script>
|
|
126
|
+
// Terminal configuration
|
|
127
|
+
const CONFIG = {
|
|
128
|
+
wsUrl: `ws://${window.location.host}/ws/terminal`,
|
|
129
|
+
sessionId: new URLSearchParams(window.location.search).get('session') || 'default',
|
|
130
|
+
theme: {
|
|
131
|
+
background: '#1e1e1e',
|
|
132
|
+
foreground: '#d4d4d4',
|
|
133
|
+
cursor: '#aeafad',
|
|
134
|
+
cursorAccent: '#1e1e1e',
|
|
135
|
+
selection: 'rgba(255, 255, 255, 0.3)',
|
|
136
|
+
black: '#000000',
|
|
137
|
+
red: '#cd3131',
|
|
138
|
+
green: '#0dbc79',
|
|
139
|
+
yellow: '#e5e510',
|
|
140
|
+
blue: '#2472c8',
|
|
141
|
+
magenta: '#bc3fbc',
|
|
142
|
+
cyan: '#11a8cd',
|
|
143
|
+
white: '#e5e5e5',
|
|
144
|
+
brightBlack: '#666666',
|
|
145
|
+
brightRed: '#f14c4c',
|
|
146
|
+
brightGreen: '#23d18b',
|
|
147
|
+
brightYellow: '#f5f543',
|
|
148
|
+
brightBlue: '#3b8eea',
|
|
149
|
+
brightMagenta: '#d670d6',
|
|
150
|
+
brightCyan: '#29b8db',
|
|
151
|
+
brightWhite: '#ffffff'
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// State
|
|
156
|
+
let terminal = null;
|
|
157
|
+
let fitAddon = null;
|
|
158
|
+
let socket = null;
|
|
159
|
+
let reconnectAttempts = 0;
|
|
160
|
+
const MAX_RECONNECT_ATTEMPTS = 5;
|
|
161
|
+
|
|
162
|
+
// DOM Elements
|
|
163
|
+
const statusIndicator = document.getElementById('status-indicator');
|
|
164
|
+
const statusText = document.getElementById('status-text');
|
|
165
|
+
const sessionIdEl = document.getElementById('session-id');
|
|
166
|
+
const terminalSizeEl = document.getElementById('terminal-size');
|
|
167
|
+
const btnReconnect = document.getElementById('btn-reconnect');
|
|
168
|
+
const btnClear = document.getElementById('btn-clear');
|
|
169
|
+
|
|
170
|
+
// Initialize terminal
|
|
171
|
+
function initTerminal() {
|
|
172
|
+
terminal = new Terminal({
|
|
173
|
+
theme: CONFIG.theme,
|
|
174
|
+
fontFamily: '"Cascadia Code", "Fira Code", "JetBrains Mono", Menlo, Monaco, monospace',
|
|
175
|
+
fontSize: 14,
|
|
176
|
+
lineHeight: 1.2,
|
|
177
|
+
cursorBlink: true,
|
|
178
|
+
cursorStyle: 'block',
|
|
179
|
+
scrollback: 10000,
|
|
180
|
+
convertEol: true,
|
|
181
|
+
allowTransparency: true
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Load addons
|
|
185
|
+
fitAddon = new FitAddon.FitAddon();
|
|
186
|
+
const webLinksAddon = new WebLinksAddon.WebLinksAddon();
|
|
187
|
+
|
|
188
|
+
terminal.loadAddon(fitAddon);
|
|
189
|
+
terminal.loadAddon(webLinksAddon);
|
|
190
|
+
|
|
191
|
+
// Open terminal in container
|
|
192
|
+
const container = document.getElementById('terminal');
|
|
193
|
+
terminal.open(container);
|
|
194
|
+
|
|
195
|
+
// Fit to container
|
|
196
|
+
fitAddon.fit();
|
|
197
|
+
updateTerminalSize();
|
|
198
|
+
|
|
199
|
+
// Handle user input
|
|
200
|
+
terminal.onData(data => {
|
|
201
|
+
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
202
|
+
socket.send(JSON.stringify({
|
|
203
|
+
type: 'input',
|
|
204
|
+
data: data
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Handle resize
|
|
210
|
+
terminal.onResize(({ cols, rows }) => {
|
|
211
|
+
updateTerminalSize();
|
|
212
|
+
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
213
|
+
socket.send(JSON.stringify({
|
|
214
|
+
type: 'resize',
|
|
215
|
+
cols: cols,
|
|
216
|
+
rows: rows
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Welcome message
|
|
222
|
+
terminal.writeln('\x1b[1;36m🐶 Welcome to Code Puppy Terminal!\x1b[0m');
|
|
223
|
+
terminal.writeln('\x1b[90mConnecting to server...\x1b[0m');
|
|
224
|
+
terminal.writeln('');
|
|
225
|
+
|
|
226
|
+
// Connect to WebSocket
|
|
227
|
+
connect();
|
|
228
|
+
|
|
229
|
+
// Update session ID display
|
|
230
|
+
sessionIdEl.textContent = CONFIG.sessionId;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Update terminal size display
|
|
234
|
+
function updateTerminalSize() {
|
|
235
|
+
if (terminal) {
|
|
236
|
+
terminalSizeEl.textContent = `${terminal.cols}x${terminal.rows}`;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Update connection status
|
|
241
|
+
function setStatus(status) {
|
|
242
|
+
statusIndicator.className = 'status-indicator';
|
|
243
|
+
|
|
244
|
+
switch (status) {
|
|
245
|
+
case 'connected':
|
|
246
|
+
statusIndicator.classList.add('status-connected');
|
|
247
|
+
statusText.textContent = 'Connected';
|
|
248
|
+
btnReconnect.disabled = true;
|
|
249
|
+
break;
|
|
250
|
+
case 'disconnected':
|
|
251
|
+
statusIndicator.classList.add('status-disconnected');
|
|
252
|
+
statusText.textContent = 'Disconnected';
|
|
253
|
+
btnReconnect.disabled = false;
|
|
254
|
+
break;
|
|
255
|
+
case 'connecting':
|
|
256
|
+
statusIndicator.classList.add('status-connecting');
|
|
257
|
+
statusText.textContent = 'Connecting...';
|
|
258
|
+
btnReconnect.disabled = true;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Connect to WebSocket
|
|
264
|
+
function connect() {
|
|
265
|
+
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
setStatus('connecting');
|
|
270
|
+
|
|
271
|
+
const url = CONFIG.wsUrl;
|
|
272
|
+
socket = new WebSocket(url);
|
|
273
|
+
|
|
274
|
+
socket.onopen = () => {
|
|
275
|
+
setStatus('connected');
|
|
276
|
+
reconnectAttempts = 0;
|
|
277
|
+
terminal.writeln('\x1b[1;32m✓ Connected to server\x1b[0m');
|
|
278
|
+
terminal.writeln('');
|
|
279
|
+
|
|
280
|
+
// Send initial resize
|
|
281
|
+
socket.send(JSON.stringify({
|
|
282
|
+
type: 'resize',
|
|
283
|
+
cols: terminal.cols,
|
|
284
|
+
rows: terminal.rows
|
|
285
|
+
}));
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
socket.onmessage = (event) => {
|
|
289
|
+
try {
|
|
290
|
+
const message = JSON.parse(event.data);
|
|
291
|
+
|
|
292
|
+
switch (message.type) {
|
|
293
|
+
case 'output':
|
|
294
|
+
terminal.write(new TextDecoder().decode(Uint8Array.from(atob(message.data), c => c.charCodeAt(0))));
|
|
295
|
+
break;
|
|
296
|
+
case 'error':
|
|
297
|
+
terminal.writeln(`\x1b[1;31mError: ${message.message}\x1b[0m`);
|
|
298
|
+
break;
|
|
299
|
+
case 'session_started':
|
|
300
|
+
terminal.writeln(`\x1b[90mSession started: ${message.session_id}\x1b[0m`);
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
} catch (e) {
|
|
304
|
+
// Plain text output
|
|
305
|
+
terminal.write(event.data);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
socket.onclose = (event) => {
|
|
310
|
+
setStatus('disconnected');
|
|
311
|
+
|
|
312
|
+
if (!event.wasClean) {
|
|
313
|
+
terminal.writeln('');
|
|
314
|
+
terminal.writeln('\x1b[1;31m✗ Connection lost\x1b[0m');
|
|
315
|
+
|
|
316
|
+
// Auto-reconnect with backoff
|
|
317
|
+
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
|
318
|
+
reconnectAttempts++;
|
|
319
|
+
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
|
|
320
|
+
terminal.writeln(`\x1b[90mReconnecting in ${delay/1000}s (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...\x1b[0m`);
|
|
321
|
+
setTimeout(connect, delay);
|
|
322
|
+
} else {
|
|
323
|
+
terminal.writeln('\x1b[90mMax reconnection attempts reached. Click "Reconnect" to try again.\x1b[0m');
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
socket.onerror = (error) => {
|
|
329
|
+
console.error('WebSocket error:', error);
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Event listeners
|
|
334
|
+
btnReconnect.addEventListener('click', () => {
|
|
335
|
+
reconnectAttempts = 0;
|
|
336
|
+
connect();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
btnClear.addEventListener('click', () => {
|
|
340
|
+
terminal.clear();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// Handle window resize
|
|
344
|
+
window.addEventListener('resize', () => {
|
|
345
|
+
if (fitAddon) {
|
|
346
|
+
fitAddon.fit();
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Handle page unload
|
|
351
|
+
window.addEventListener('beforeunload', () => {
|
|
352
|
+
if (socket) {
|
|
353
|
+
socket.close();
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Initialize on DOM ready
|
|
358
|
+
document.addEventListener('DOMContentLoaded', initTerminal);
|
|
359
|
+
</script>
|
|
360
|
+
</body>
|
|
361
|
+
</html>
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""WebSocket endpoints for Code Puppy API.
|
|
2
|
+
|
|
3
|
+
Provides real-time communication channels:
|
|
4
|
+
- /ws/events - Server-sent events stream
|
|
5
|
+
- /ws/terminal - Interactive PTY terminal sessions
|
|
6
|
+
- /ws/health - Simple health check endpoint
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import base64
|
|
11
|
+
import logging
|
|
12
|
+
import uuid
|
|
13
|
+
|
|
14
|
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def setup_websocket(app: FastAPI) -> None:
|
|
20
|
+
"""Setup WebSocket endpoints for the application."""
|
|
21
|
+
|
|
22
|
+
@app.websocket("/ws/events")
|
|
23
|
+
async def websocket_events(websocket: WebSocket) -> None:
|
|
24
|
+
"""Stream real-time events to connected clients."""
|
|
25
|
+
await websocket.accept()
|
|
26
|
+
logger.info("Events WebSocket client connected")
|
|
27
|
+
|
|
28
|
+
from code_puppy.plugins.frontend_emitter.emitter import (
|
|
29
|
+
get_recent_events,
|
|
30
|
+
subscribe,
|
|
31
|
+
unsubscribe,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
event_queue = subscribe()
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
recent_events = get_recent_events()
|
|
38
|
+
for event in recent_events:
|
|
39
|
+
await websocket.send_json(event)
|
|
40
|
+
|
|
41
|
+
while True:
|
|
42
|
+
try:
|
|
43
|
+
event = await asyncio.wait_for(event_queue.get(), timeout=30.0)
|
|
44
|
+
await websocket.send_json(event)
|
|
45
|
+
except asyncio.TimeoutError:
|
|
46
|
+
try:
|
|
47
|
+
await websocket.send_json({"type": "ping"})
|
|
48
|
+
except Exception:
|
|
49
|
+
break
|
|
50
|
+
except WebSocketDisconnect:
|
|
51
|
+
logger.info("Events WebSocket client disconnected")
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger.error(f"Events WebSocket error: {e}")
|
|
54
|
+
finally:
|
|
55
|
+
unsubscribe(event_queue)
|
|
56
|
+
|
|
57
|
+
@app.websocket("/ws/terminal")
|
|
58
|
+
async def websocket_terminal(websocket: WebSocket) -> None:
|
|
59
|
+
"""Interactive terminal WebSocket endpoint."""
|
|
60
|
+
await websocket.accept()
|
|
61
|
+
logger.info("Terminal WebSocket client connected")
|
|
62
|
+
|
|
63
|
+
from code_puppy.api.pty_manager import get_pty_manager
|
|
64
|
+
|
|
65
|
+
manager = get_pty_manager()
|
|
66
|
+
session_id = str(uuid.uuid4())[:8]
|
|
67
|
+
session = None
|
|
68
|
+
|
|
69
|
+
# Get the current event loop for thread-safe scheduling
|
|
70
|
+
loop = asyncio.get_running_loop()
|
|
71
|
+
|
|
72
|
+
# Queue to receive PTY output in a thread-safe way
|
|
73
|
+
output_queue: asyncio.Queue[bytes] = asyncio.Queue()
|
|
74
|
+
|
|
75
|
+
# Output callback - called from thread pool, puts data in queue
|
|
76
|
+
def on_output(data: bytes) -> None:
|
|
77
|
+
try:
|
|
78
|
+
loop.call_soon_threadsafe(output_queue.put_nowait, data)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.error(f"on_output error: {e}")
|
|
81
|
+
|
|
82
|
+
async def output_sender() -> None:
|
|
83
|
+
"""Coroutine that sends queued output to WebSocket."""
|
|
84
|
+
try:
|
|
85
|
+
while True:
|
|
86
|
+
data = await output_queue.get()
|
|
87
|
+
await websocket.send_json(
|
|
88
|
+
{
|
|
89
|
+
"type": "output",
|
|
90
|
+
"data": base64.b64encode(data).decode("ascii"),
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
except asyncio.CancelledError:
|
|
94
|
+
pass
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.error(f"output_sender error: {e}")
|
|
97
|
+
|
|
98
|
+
sender_task = None
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
# Create PTY session
|
|
102
|
+
session = await manager.create_session(
|
|
103
|
+
session_id=session_id,
|
|
104
|
+
on_output=on_output,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Send session info
|
|
108
|
+
await websocket.send_json({"type": "session", "id": session_id})
|
|
109
|
+
|
|
110
|
+
# Start output sender task
|
|
111
|
+
sender_task = asyncio.create_task(output_sender())
|
|
112
|
+
|
|
113
|
+
# Handle incoming messages
|
|
114
|
+
while True:
|
|
115
|
+
try:
|
|
116
|
+
msg = await websocket.receive_json()
|
|
117
|
+
|
|
118
|
+
if msg.get("type") == "input":
|
|
119
|
+
data = msg.get("data", "")
|
|
120
|
+
if isinstance(data, str):
|
|
121
|
+
data = data.encode("utf-8")
|
|
122
|
+
await manager.write(session_id, data)
|
|
123
|
+
elif msg.get("type") == "resize":
|
|
124
|
+
cols = msg.get("cols", 80)
|
|
125
|
+
rows = msg.get("rows", 24)
|
|
126
|
+
await manager.resize(session_id, cols, rows)
|
|
127
|
+
except WebSocketDisconnect:
|
|
128
|
+
break
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.error(f"Terminal WebSocket error: {e}")
|
|
131
|
+
break
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logger.error(f"Terminal session error: {e}")
|
|
134
|
+
finally:
|
|
135
|
+
if sender_task:
|
|
136
|
+
sender_task.cancel()
|
|
137
|
+
try:
|
|
138
|
+
await sender_task
|
|
139
|
+
except asyncio.CancelledError:
|
|
140
|
+
pass
|
|
141
|
+
if session:
|
|
142
|
+
await manager.close_session(session_id)
|
|
143
|
+
logger.info("Terminal WebSocket disconnected")
|
|
144
|
+
|
|
145
|
+
@app.websocket("/ws/health")
|
|
146
|
+
async def websocket_health(websocket: WebSocket) -> None:
|
|
147
|
+
"""Simple WebSocket health check - echoes messages back."""
|
|
148
|
+
await websocket.accept()
|
|
149
|
+
try:
|
|
150
|
+
while True:
|
|
151
|
+
data = await websocket.receive_text()
|
|
152
|
+
await websocket.send_text(f"echo: {data}")
|
|
153
|
+
except WebSocketDisconnect:
|
|
154
|
+
pass
|
code_puppy/callbacks.py
CHANGED
|
@@ -18,6 +18,9 @@ PhaseType = Literal[
|
|
|
18
18
|
"custom_command",
|
|
19
19
|
"custom_command_help",
|
|
20
20
|
"file_permission",
|
|
21
|
+
"pre_tool_call",
|
|
22
|
+
"post_tool_call",
|
|
23
|
+
"stream_event",
|
|
21
24
|
]
|
|
22
25
|
CallbackFunc = Callable[..., Any]
|
|
23
26
|
|
|
@@ -36,6 +39,9 @@ _callbacks: Dict[PhaseType, List[CallbackFunc]] = {
|
|
|
36
39
|
"custom_command": [],
|
|
37
40
|
"custom_command_help": [],
|
|
38
41
|
"file_permission": [],
|
|
42
|
+
"pre_tool_call": [],
|
|
43
|
+
"post_tool_call": [],
|
|
44
|
+
"stream_event": [],
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
logger = logging.getLogger(__name__)
|
|
@@ -271,3 +277,70 @@ def on_file_permission(
|
|
|
271
277
|
message_group,
|
|
272
278
|
operation_data,
|
|
273
279
|
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
async def on_pre_tool_call(
|
|
283
|
+
tool_name: str, tool_args: dict, context: Any = None
|
|
284
|
+
) -> List[Any]:
|
|
285
|
+
"""Trigger callbacks before a tool is called.
|
|
286
|
+
|
|
287
|
+
This allows plugins to inspect, modify, or log tool calls before
|
|
288
|
+
they are executed.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
tool_name: Name of the tool being called
|
|
292
|
+
tool_args: Arguments being passed to the tool
|
|
293
|
+
context: Optional context data for the tool call
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
List of results from registered callbacks.
|
|
297
|
+
"""
|
|
298
|
+
return await _trigger_callbacks("pre_tool_call", tool_name, tool_args, context)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
async def on_post_tool_call(
|
|
302
|
+
tool_name: str,
|
|
303
|
+
tool_args: dict,
|
|
304
|
+
result: Any,
|
|
305
|
+
duration_ms: float,
|
|
306
|
+
context: Any = None,
|
|
307
|
+
) -> List[Any]:
|
|
308
|
+
"""Trigger callbacks after a tool completes.
|
|
309
|
+
|
|
310
|
+
This allows plugins to inspect tool results, log execution times,
|
|
311
|
+
or perform post-processing.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
tool_name: Name of the tool that was called
|
|
315
|
+
tool_args: Arguments that were passed to the tool
|
|
316
|
+
result: The result returned by the tool
|
|
317
|
+
duration_ms: Execution time in milliseconds
|
|
318
|
+
context: Optional context data for the tool call
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
List of results from registered callbacks.
|
|
322
|
+
"""
|
|
323
|
+
return await _trigger_callbacks(
|
|
324
|
+
"post_tool_call", tool_name, tool_args, result, duration_ms, context
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
async def on_stream_event(
|
|
329
|
+
event_type: str, event_data: Any, agent_session_id: str | None = None
|
|
330
|
+
) -> List[Any]:
|
|
331
|
+
"""Trigger callbacks for streaming events.
|
|
332
|
+
|
|
333
|
+
This allows plugins to react to streaming events in real-time,
|
|
334
|
+
such as tokens being generated, tool calls starting, etc.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
event_type: Type of the streaming event
|
|
338
|
+
event_data: Data associated with the event
|
|
339
|
+
agent_session_id: Optional session ID of the agent emitting the event
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
List of results from registered callbacks.
|
|
343
|
+
"""
|
|
344
|
+
return await _trigger_callbacks(
|
|
345
|
+
"stream_event", event_type, event_data, agent_session_id
|
|
346
|
+
)
|