@donkeylabs/server 0.4.8 → 0.5.1

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.
@@ -0,0 +1,366 @@
1
+ """
2
+ Donkeylabs External Job Python Wrapper
3
+
4
+ This module provides a simple interface for Python scripts to communicate
5
+ with the Donkeylabs job system via Unix sockets or TCP.
6
+
7
+ Usage:
8
+ from donkeylabs_job import DonkeylabsJob, run_job
9
+
10
+ def my_job(job: DonkeylabsJob):
11
+ job.progress(0, "Starting...")
12
+ # Do work...
13
+ job.progress(50, "Halfway done")
14
+ # More work...
15
+ return {"result": "success"}
16
+
17
+ if __name__ == "__main__":
18
+ run_job(my_job)
19
+ """
20
+
21
+ import json
22
+ import os
23
+ import socket
24
+ import sys
25
+ import threading
26
+ import time
27
+ from typing import Any, Callable, Dict, Optional
28
+
29
+
30
+ class DonkeylabsJob:
31
+ """Interface for communicating with the Donkeylabs job system."""
32
+
33
+ def __init__(
34
+ self,
35
+ job_id: str,
36
+ name: str,
37
+ data: Any,
38
+ socket_path: str,
39
+ heartbeat_interval: float = 5.0,
40
+ reconnect_interval: float = 2.0,
41
+ max_reconnect_attempts: int = 30,
42
+ ):
43
+ self.job_id = job_id
44
+ self.name = name
45
+ self.data = data
46
+ self._socket_path = socket_path
47
+ self._heartbeat_interval = heartbeat_interval
48
+ self._reconnect_interval = reconnect_interval
49
+ self._max_reconnect_attempts = max_reconnect_attempts
50
+ self._socket: Optional[socket.socket] = None
51
+ self._heartbeat_thread: Optional[threading.Thread] = None
52
+ self._reconnect_thread: Optional[threading.Thread] = None
53
+ self._running = False
54
+ self._connected = False
55
+ self._lock = threading.Lock()
56
+ self._reconnect_lock = threading.Lock()
57
+
58
+ def connect(self) -> None:
59
+ """Connect to the job server socket."""
60
+ self._do_connect()
61
+ self._running = True
62
+ self._connected = True
63
+ self._start_heartbeat()
64
+ self._send_started()
65
+
66
+ def _do_connect(self) -> None:
67
+ """Internal connection logic."""
68
+ if self._socket_path.startswith("tcp://"):
69
+ # TCP connection (Windows fallback)
70
+ addr = self._socket_path[6:] # Remove "tcp://"
71
+ host, port = addr.rsplit(":", 1)
72
+ self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
73
+ self._socket.connect((host, int(port)))
74
+ else:
75
+ # Unix socket
76
+ self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
77
+ self._socket.connect(self._socket_path)
78
+
79
+ def _try_reconnect(self) -> bool:
80
+ """Attempt to reconnect to the server (for server restart resilience)."""
81
+ with self._reconnect_lock:
82
+ if self._connected:
83
+ return True
84
+
85
+ print(f"[DonkeylabsJob] Attempting to reconnect...", file=sys.stderr)
86
+
87
+ for attempt in range(self._max_reconnect_attempts):
88
+ try:
89
+ # Close old socket
90
+ if self._socket:
91
+ try:
92
+ self._socket.close()
93
+ except Exception:
94
+ pass
95
+
96
+ # Try to reconnect
97
+ self._do_connect()
98
+ self._connected = True
99
+ print(f"[DonkeylabsJob] Reconnected after {attempt + 1} attempts", file=sys.stderr)
100
+
101
+ # Send started message to let server know we're back
102
+ self._send_started()
103
+ return True
104
+ except Exception as e:
105
+ print(f"[DonkeylabsJob] Reconnect attempt {attempt + 1}/{self._max_reconnect_attempts} failed: {e}", file=sys.stderr)
106
+ time.sleep(self._reconnect_interval)
107
+
108
+ print(f"[DonkeylabsJob] Failed to reconnect after {self._max_reconnect_attempts} attempts", file=sys.stderr)
109
+ return False
110
+
111
+ def disconnect(self) -> None:
112
+ """Disconnect from the job server."""
113
+ self._running = False
114
+ if self._heartbeat_thread:
115
+ self._heartbeat_thread.join(timeout=2.0)
116
+ if self._socket:
117
+ try:
118
+ self._socket.close()
119
+ except Exception:
120
+ pass
121
+
122
+ def _send_message(self, message: Dict[str, Any]) -> bool:
123
+ """Send a JSON message to the server. Returns True if sent successfully."""
124
+ if not self._socket:
125
+ return False
126
+
127
+ message["jobId"] = self.job_id
128
+ message["timestamp"] = int(time.time() * 1000)
129
+
130
+ with self._lock:
131
+ try:
132
+ data = json.dumps(message) + "\n"
133
+ self._socket.sendall(data.encode("utf-8"))
134
+ return True
135
+ except (BrokenPipeError, ConnectionResetError, OSError) as e:
136
+ print(f"[DonkeylabsJob] Connection lost: {e}", file=sys.stderr)
137
+ self._connected = False
138
+
139
+ # Try to reconnect in background (don't block the caller)
140
+ if self._running and not self._reconnect_thread:
141
+ self._reconnect_thread = threading.Thread(
142
+ target=self._reconnect_loop,
143
+ daemon=True
144
+ )
145
+ self._reconnect_thread.start()
146
+ return False
147
+ except Exception as e:
148
+ print(f"[DonkeylabsJob] Failed to send message: {e}", file=sys.stderr)
149
+ return False
150
+
151
+ def _reconnect_loop(self) -> None:
152
+ """Background thread that attempts to reconnect."""
153
+ if self._try_reconnect():
154
+ print(f"[DonkeylabsJob] Reconnection successful, resuming operation", file=sys.stderr)
155
+ else:
156
+ print(f"[DonkeylabsJob] Reconnection failed, job may be lost", file=sys.stderr)
157
+ self._reconnect_thread = None
158
+
159
+ def _send_started(self) -> None:
160
+ """Send a started message to the server."""
161
+ self._send_message({"type": "started"})
162
+
163
+ def _start_heartbeat(self) -> None:
164
+ """Start the background heartbeat thread."""
165
+
166
+ def heartbeat_loop():
167
+ while self._running:
168
+ self._send_message({"type": "heartbeat"})
169
+ time.sleep(self._heartbeat_interval)
170
+
171
+ self._heartbeat_thread = threading.Thread(target=heartbeat_loop, daemon=True)
172
+ self._heartbeat_thread.start()
173
+
174
+ def progress(
175
+ self,
176
+ percent: float,
177
+ message: Optional[str] = None,
178
+ **data: Any,
179
+ ) -> None:
180
+ """
181
+ Report progress to the job server.
182
+
183
+ Args:
184
+ percent: Progress percentage (0-100)
185
+ message: Optional status message
186
+ **data: Additional data to include
187
+ """
188
+ msg: Dict[str, Any] = {
189
+ "type": "progress",
190
+ "percent": percent,
191
+ }
192
+ if message:
193
+ msg["message"] = message
194
+ if data:
195
+ msg["data"] = data
196
+
197
+ self._send_message(msg)
198
+
199
+ def log(
200
+ self,
201
+ level: str,
202
+ message: str,
203
+ **data: Any,
204
+ ) -> None:
205
+ """
206
+ Send a log message to the job server.
207
+
208
+ Args:
209
+ level: Log level (debug, info, warn, error)
210
+ message: Log message
211
+ **data: Additional data to include
212
+ """
213
+ msg: Dict[str, Any] = {
214
+ "type": "log",
215
+ "level": level,
216
+ "message": message,
217
+ }
218
+ if data:
219
+ msg["data"] = data
220
+
221
+ self._send_message(msg)
222
+
223
+ def debug(self, message: str, **data: Any) -> None:
224
+ """Send a debug log message."""
225
+ self.log("debug", message, **data)
226
+
227
+ def info(self, message: str, **data: Any) -> None:
228
+ """Send an info log message."""
229
+ self.log("info", message, **data)
230
+
231
+ def warn(self, message: str, **data: Any) -> None:
232
+ """Send a warning log message."""
233
+ self.log("warn", message, **data)
234
+
235
+ def error(self, message: str, **data: Any) -> None:
236
+ """Send an error log message."""
237
+ self.log("error", message, **data)
238
+
239
+ def complete(self, result: Any = None) -> None:
240
+ """
241
+ Mark the job as completed.
242
+
243
+ Args:
244
+ result: Optional result data to return
245
+ """
246
+ msg: Dict[str, Any] = {"type": "completed"}
247
+ if result is not None:
248
+ msg["result"] = result
249
+
250
+ self._send_message(msg)
251
+
252
+ def fail(self, error: str, stack: Optional[str] = None) -> None:
253
+ """
254
+ Mark the job as failed.
255
+
256
+ Args:
257
+ error: Error message
258
+ stack: Optional stack trace
259
+ """
260
+ msg: Dict[str, Any] = {
261
+ "type": "failed",
262
+ "error": error,
263
+ }
264
+ if stack:
265
+ msg["stack"] = stack
266
+
267
+ self._send_message(msg)
268
+
269
+
270
+ def run_job(
271
+ handler: Callable[[DonkeylabsJob], Any],
272
+ heartbeat_interval: float = 5.0,
273
+ ) -> None:
274
+ """
275
+ Run a job handler function.
276
+
277
+ This function reads the job payload from stdin, connects to the job server,
278
+ runs the handler, and reports the result.
279
+
280
+ Args:
281
+ handler: A function that takes a DonkeylabsJob and returns the result
282
+ heartbeat_interval: How often to send heartbeats (seconds)
283
+
284
+ Example:
285
+ def my_job(job: DonkeylabsJob):
286
+ job.progress(0, "Starting...")
287
+ result = do_work(job.data)
288
+ return result
289
+
290
+ if __name__ == "__main__":
291
+ run_job(my_job)
292
+ """
293
+ # Read payload from stdin
294
+ payload_line = sys.stdin.readline()
295
+ if not payload_line:
296
+ print("No payload received on stdin", file=sys.stderr)
297
+ sys.exit(1)
298
+
299
+ try:
300
+ payload = json.loads(payload_line)
301
+ except json.JSONDecodeError as e:
302
+ print(f"Failed to parse payload: {e}", file=sys.stderr)
303
+ sys.exit(1)
304
+
305
+ job_id = payload.get("jobId")
306
+ name = payload.get("name")
307
+ data = payload.get("data")
308
+ socket_path = payload.get("socketPath")
309
+
310
+ # Fall back to environment variables if not in payload
311
+ if not job_id:
312
+ job_id = os.environ.get("DONKEYLABS_JOB_ID")
313
+ if not socket_path:
314
+ socket_path = os.environ.get("DONKEYLABS_SOCKET_PATH")
315
+ tcp_port = os.environ.get("DONKEYLABS_TCP_PORT")
316
+ if tcp_port and not socket_path:
317
+ socket_path = f"tcp://127.0.0.1:{tcp_port}"
318
+
319
+ if not job_id or not socket_path:
320
+ print("Missing jobId or socketPath", file=sys.stderr)
321
+ sys.exit(1)
322
+
323
+ job = DonkeylabsJob(
324
+ job_id=job_id,
325
+ name=name or "unknown",
326
+ data=data,
327
+ socket_path=socket_path,
328
+ heartbeat_interval=heartbeat_interval,
329
+ )
330
+
331
+ try:
332
+ job.connect()
333
+
334
+ # Run the handler
335
+ result = handler(job)
336
+
337
+ # Send completion
338
+ job.complete(result)
339
+ except Exception as e:
340
+ import traceback
341
+
342
+ job.fail(str(e), traceback.format_exc())
343
+ sys.exit(1)
344
+ finally:
345
+ job.disconnect()
346
+
347
+
348
+ # Example job handler
349
+ def example_handler(job: DonkeylabsJob) -> Dict[str, Any]:
350
+ """Example job handler that processes data in steps."""
351
+ job.info(f"Starting job with data: {job.data}")
352
+
353
+ total_steps = job.data.get("steps", 5)
354
+
355
+ for i in range(total_steps):
356
+ progress = (i / total_steps) * 100
357
+ job.progress(progress, f"Processing step {i + 1} of {total_steps}")
358
+ time.sleep(0.5) # Simulate work
359
+
360
+ job.progress(100, "Complete!")
361
+ return {"processed": True, "steps": total_steps}
362
+
363
+
364
+ if __name__ == "__main__":
365
+ # If run directly, use the example handler
366
+ run_job(example_handler)
@@ -0,0 +1,264 @@
1
+ #!/bin/bash
2
+ #
3
+ # Donkeylabs External Job Shell Wrapper
4
+ #
5
+ # This script provides functions for shell scripts to communicate
6
+ # with the Donkeylabs job system via Unix sockets or TCP.
7
+ #
8
+ # Usage:
9
+ # #!/bin/bash
10
+ # source /path/to/donkeylabs-job.sh
11
+ #
12
+ # # Initialize the job (reads from stdin)
13
+ # job_init
14
+ #
15
+ # # Report progress
16
+ # job_progress 50 "Halfway done"
17
+ #
18
+ # # Log messages
19
+ # job_log info "Processing data..."
20
+ #
21
+ # # Complete the job
22
+ # job_complete '{"result": "success"}'
23
+ #
24
+ # # Or fail the job
25
+ # job_fail "Something went wrong"
26
+ #
27
+
28
+ # Global variables (set by job_init)
29
+ DONKEYLABS_JOB_ID=""
30
+ DONKEYLABS_JOB_NAME=""
31
+ DONKEYLABS_JOB_DATA=""
32
+ DONKEYLABS_SOCKET_PATH=""
33
+ DONKEYLABS_HEARTBEAT_PID=""
34
+
35
+ # Get current timestamp in milliseconds
36
+ _job_timestamp() {
37
+ # Try to use date with milliseconds, fall back to seconds * 1000
38
+ if date '+%s%3N' >/dev/null 2>&1; then
39
+ date '+%s%3N'
40
+ else
41
+ echo "$(($(date '+%s') * 1000))"
42
+ fi
43
+ }
44
+
45
+ # Send a message to the socket
46
+ _job_send() {
47
+ local message="$1"
48
+
49
+ if [[ "$DONKEYLABS_SOCKET_PATH" == tcp://* ]]; then
50
+ # TCP connection
51
+ local addr="${DONKEYLABS_SOCKET_PATH#tcp://}"
52
+ local host="${addr%:*}"
53
+ local port="${addr##*:}"
54
+
55
+ # Use bash's /dev/tcp or nc
56
+ if [[ -e /dev/tcp ]]; then
57
+ echo "$message" > /dev/tcp/"$host"/"$port" 2>/dev/null
58
+ else
59
+ echo "$message" | nc -q0 "$host" "$port" 2>/dev/null || \
60
+ echo "$message" | nc -w0 "$host" "$port" 2>/dev/null
61
+ fi
62
+ else
63
+ # Unix socket
64
+ if command -v socat >/dev/null 2>&1; then
65
+ echo "$message" | socat - UNIX-CONNECT:"$DONKEYLABS_SOCKET_PATH" 2>/dev/null
66
+ elif command -v nc >/dev/null 2>&1; then
67
+ echo "$message" | nc -U "$DONKEYLABS_SOCKET_PATH" 2>/dev/null
68
+ else
69
+ echo "Error: Neither socat nor nc (netcat) found. Cannot send messages." >&2
70
+ return 1
71
+ fi
72
+ fi
73
+ }
74
+
75
+ # Build a JSON message
76
+ _job_build_message() {
77
+ local type="$1"
78
+ local extra="$2"
79
+
80
+ local timestamp
81
+ timestamp=$(_job_timestamp)
82
+
83
+ local message="{\"type\":\"$type\",\"jobId\":\"$DONKEYLABS_JOB_ID\",\"timestamp\":$timestamp"
84
+
85
+ if [[ -n "$extra" ]]; then
86
+ message="$message,$extra"
87
+ fi
88
+
89
+ message="$message}"
90
+
91
+ echo "$message"
92
+ }
93
+
94
+ # Start the heartbeat background process
95
+ _job_start_heartbeat() {
96
+ local interval="${1:-5}"
97
+
98
+ (
99
+ while true; do
100
+ sleep "$interval"
101
+ _job_send "$(_job_build_message "heartbeat")"
102
+ done
103
+ ) &
104
+
105
+ DONKEYLABS_HEARTBEAT_PID=$!
106
+ }
107
+
108
+ # Stop the heartbeat background process
109
+ _job_stop_heartbeat() {
110
+ if [[ -n "$DONKEYLABS_HEARTBEAT_PID" ]]; then
111
+ kill "$DONKEYLABS_HEARTBEAT_PID" 2>/dev/null
112
+ wait "$DONKEYLABS_HEARTBEAT_PID" 2>/dev/null
113
+ DONKEYLABS_HEARTBEAT_PID=""
114
+ fi
115
+ }
116
+
117
+ # Initialize the job by reading payload from stdin
118
+ job_init() {
119
+ local heartbeat_interval="${1:-5}"
120
+
121
+ # Read payload from stdin
122
+ local payload
123
+ read -r payload
124
+
125
+ if [[ -z "$payload" ]]; then
126
+ echo "Error: No payload received on stdin" >&2
127
+ exit 1
128
+ fi
129
+
130
+ # Parse JSON payload using jq if available, otherwise use basic grep/sed
131
+ if command -v jq >/dev/null 2>&1; then
132
+ DONKEYLABS_JOB_ID=$(echo "$payload" | jq -r '.jobId // empty')
133
+ DONKEYLABS_JOB_NAME=$(echo "$payload" | jq -r '.name // empty')
134
+ DONKEYLABS_JOB_DATA=$(echo "$payload" | jq -c '.data // {}')
135
+ DONKEYLABS_SOCKET_PATH=$(echo "$payload" | jq -r '.socketPath // empty')
136
+ else
137
+ # Basic parsing (less robust)
138
+ DONKEYLABS_JOB_ID=$(echo "$payload" | grep -o '"jobId":"[^"]*"' | cut -d'"' -f4)
139
+ DONKEYLABS_JOB_NAME=$(echo "$payload" | grep -o '"name":"[^"]*"' | cut -d'"' -f4)
140
+ DONKEYLABS_SOCKET_PATH=$(echo "$payload" | grep -o '"socketPath":"[^"]*"' | cut -d'"' -f4)
141
+ DONKEYLABS_JOB_DATA="{}"
142
+ fi
143
+
144
+ # Fall back to environment variables
145
+ DONKEYLABS_JOB_ID="${DONKEYLABS_JOB_ID:-$DONKEYLABS_JOB_ID}"
146
+ DONKEYLABS_SOCKET_PATH="${DONKEYLABS_SOCKET_PATH:-$DONKEYLABS_SOCKET_PATH}"
147
+
148
+ # If TCP port is set but not socket path, construct TCP URL
149
+ if [[ -z "$DONKEYLABS_SOCKET_PATH" && -n "$DONKEYLABS_TCP_PORT" ]]; then
150
+ DONKEYLABS_SOCKET_PATH="tcp://127.0.0.1:$DONKEYLABS_TCP_PORT"
151
+ fi
152
+
153
+ if [[ -z "$DONKEYLABS_JOB_ID" || -z "$DONKEYLABS_SOCKET_PATH" ]]; then
154
+ echo "Error: Missing jobId or socketPath" >&2
155
+ exit 1
156
+ fi
157
+
158
+ # Start heartbeat in background
159
+ _job_start_heartbeat "$heartbeat_interval"
160
+
161
+ # Send started message
162
+ _job_send "$(_job_build_message "started")"
163
+
164
+ # Set up cleanup trap
165
+ trap '_job_stop_heartbeat' EXIT
166
+ }
167
+
168
+ # Report progress
169
+ # Usage: job_progress <percent> [message]
170
+ job_progress() {
171
+ local percent="$1"
172
+ local message="${2:-}"
173
+
174
+ local extra="\"percent\":$percent"
175
+
176
+ if [[ -n "$message" ]]; then
177
+ # Escape message for JSON
178
+ message="${message//\\/\\\\}"
179
+ message="${message//\"/\\\"}"
180
+ message="${message//$'\n'/\\n}"
181
+ extra="$extra,\"message\":\"$message\""
182
+ fi
183
+
184
+ _job_send "$(_job_build_message "progress" "$extra")"
185
+ }
186
+
187
+ # Send a log message
188
+ # Usage: job_log <level> <message>
189
+ job_log() {
190
+ local level="$1"
191
+ local message="$2"
192
+
193
+ # Escape message for JSON
194
+ message="${message//\\/\\\\}"
195
+ message="${message//\"/\\\"}"
196
+ message="${message//$'\n'/\\n}"
197
+
198
+ _job_send "$(_job_build_message "log" "\"level\":\"$level\",\"message\":\"$message\"")"
199
+ }
200
+
201
+ # Convenience log functions
202
+ job_debug() { job_log "debug" "$1"; }
203
+ job_info() { job_log "info" "$1"; }
204
+ job_warn() { job_log "warn" "$1"; }
205
+ job_error() { job_log "error" "$1"; }
206
+
207
+ # Complete the job
208
+ # Usage: job_complete [result_json]
209
+ job_complete() {
210
+ local result="${1:-null}"
211
+
212
+ _job_stop_heartbeat
213
+
214
+ if [[ "$result" == "null" || -z "$result" ]]; then
215
+ _job_send "$(_job_build_message "completed")"
216
+ else
217
+ _job_send "$(_job_build_message "completed" "\"result\":$result")"
218
+ fi
219
+ }
220
+
221
+ # Fail the job
222
+ # Usage: job_fail <error_message>
223
+ job_fail() {
224
+ local error="$1"
225
+
226
+ _job_stop_heartbeat
227
+
228
+ # Escape error for JSON
229
+ error="${error//\\/\\\\}"
230
+ error="${error//\"/\\\"}"
231
+ error="${error//$'\n'/\\n}"
232
+
233
+ _job_send "$(_job_build_message "failed" "\"error\":\"$error\"")"
234
+ }
235
+
236
+ # Get a value from job data (requires jq)
237
+ # Usage: job_data_get <path>
238
+ job_data_get() {
239
+ local path="$1"
240
+
241
+ if command -v jq >/dev/null 2>&1; then
242
+ echo "$DONKEYLABS_JOB_DATA" | jq -r "$path"
243
+ else
244
+ echo "Error: jq is required to parse job data" >&2
245
+ return 1
246
+ fi
247
+ }
248
+
249
+ # Example usage (only runs if script is executed directly)
250
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
251
+ echo "Donkeylabs Job Shell Wrapper"
252
+ echo ""
253
+ echo "Usage: source this file in your shell script"
254
+ echo ""
255
+ echo "Example:"
256
+ echo " #!/bin/bash"
257
+ echo " source donkeylabs-job.sh"
258
+ echo ""
259
+ echo " job_init"
260
+ echo " job_progress 0 \"Starting...\""
261
+ echo " # Do work..."
262
+ echo " job_progress 100 \"Done!\""
263
+ echo " job_complete '{\"result\": \"success\"}'"
264
+ fi
@@ -0,0 +1,47 @@
1
+ #!/bin/bash
2
+ #
3
+ # Example External Job Script
4
+ #
5
+ # This script demonstrates how to use the donkeylabs-job.sh wrapper
6
+ # to create an external job that can be executed by the Donkeylabs server.
7
+ #
8
+
9
+ # Get the directory of this script
10
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+
12
+ # Source the job wrapper
13
+ source "$SCRIPT_DIR/donkeylabs-job.sh"
14
+
15
+ # Initialize the job (reads payload from stdin, starts heartbeat)
16
+ job_init 5 # 5 second heartbeat interval
17
+
18
+ # Log that we're starting
19
+ job_info "Starting example job"
20
+ job_info "Job ID: $DONKEYLABS_JOB_ID"
21
+ job_info "Job Name: $DONKEYLABS_JOB_NAME"
22
+
23
+ # Get configuration from job data
24
+ STEPS=$(job_data_get '.steps // 5')
25
+ DELAY=$(job_data_get '.delay // 1')
26
+
27
+ job_info "Processing $STEPS steps with ${DELAY}s delay"
28
+
29
+ # Process each step
30
+ for i in $(seq 1 "$STEPS"); do
31
+ # Calculate progress
32
+ PROGRESS=$(( (i - 1) * 100 / STEPS ))
33
+
34
+ # Report progress
35
+ job_progress "$PROGRESS" "Processing step $i of $STEPS"
36
+
37
+ # Simulate work
38
+ sleep "$DELAY"
39
+
40
+ job_debug "Completed step $i"
41
+ done
42
+
43
+ # Final progress
44
+ job_progress 100 "All steps completed"
45
+
46
+ # Complete the job with result
47
+ job_complete "{\"processed\": true, \"steps\": $STEPS}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkeylabs/server",
3
- "version": "0.4.8",
3
+ "version": "0.5.1",
4
4
  "type": "module",
5
5
  "description": "Type-safe plugin system for building RPC-style APIs with Bun",
6
6
  "main": "./src/index.ts",
@@ -30,6 +30,7 @@
30
30
  "files": [
31
31
  "src",
32
32
  "docs",
33
+ "examples",
33
34
  "CLAUDE.md",
34
35
  "context.d.ts",
35
36
  "registry.d.ts",