open-swarm 0.1.1745125933__py3-none-any.whl → 0.1.1745126277__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.
Files changed (52) hide show
  1. {open_swarm-0.1.1745125933.dist-info → open_swarm-0.1.1745126277.dist-info}/METADATA +12 -8
  2. {open_swarm-0.1.1745125933.dist-info → open_swarm-0.1.1745126277.dist-info}/RECORD +52 -25
  3. swarm/blueprints/README.md +19 -18
  4. swarm/blueprints/blueprint_audit_status.json +1 -1
  5. swarm/blueprints/chatbot/blueprint_chatbot.py +160 -72
  6. swarm/blueprints/codey/README.md +88 -8
  7. swarm/blueprints/codey/blueprint_codey.py +1116 -210
  8. swarm/blueprints/codey/codey_cli.py +10 -0
  9. swarm/blueprints/codey/session_logs/session_2025-04-19T01-15-31.md +17 -0
  10. swarm/blueprints/codey/session_logs/session_2025-04-19T01-16-03.md +17 -0
  11. swarm/blueprints/common/operation_box_utils.py +83 -0
  12. swarm/blueprints/digitalbutlers/blueprint_digitalbutlers.py +21 -298
  13. swarm/blueprints/divine_code/blueprint_divine_code.py +182 -9
  14. swarm/blueprints/django_chat/blueprint_django_chat.py +150 -24
  15. swarm/blueprints/echocraft/blueprint_echocraft.py +142 -13
  16. swarm/blueprints/geese/README.md +97 -0
  17. swarm/blueprints/geese/blueprint_geese.py +677 -93
  18. swarm/blueprints/geese/geese_cli.py +102 -0
  19. swarm/blueprints/jeeves/blueprint_jeeves.py +712 -0
  20. swarm/blueprints/jeeves/jeeves_cli.py +55 -0
  21. swarm/blueprints/mcp_demo/blueprint_mcp_demo.py +109 -22
  22. swarm/blueprints/mission_improbable/blueprint_mission_improbable.py +172 -40
  23. swarm/blueprints/monkai_magic/blueprint_monkai_magic.py +79 -41
  24. swarm/blueprints/nebula_shellz/blueprint_nebula_shellz.py +82 -35
  25. swarm/blueprints/omniplex/blueprint_omniplex.py +56 -24
  26. swarm/blueprints/poets/blueprint_poets.py +141 -100
  27. swarm/blueprints/poets/poets_cli.py +23 -0
  28. swarm/blueprints/rue_code/README.md +8 -0
  29. swarm/blueprints/rue_code/blueprint_rue_code.py +188 -20
  30. swarm/blueprints/rue_code/rue_code_cli.py +43 -0
  31. swarm/blueprints/stewie/apps.py +12 -0
  32. swarm/blueprints/stewie/blueprint_family_ties.py +349 -0
  33. swarm/blueprints/stewie/models.py +19 -0
  34. swarm/blueprints/stewie/serializers.py +10 -0
  35. swarm/blueprints/stewie/settings.py +17 -0
  36. swarm/blueprints/stewie/urls.py +11 -0
  37. swarm/blueprints/stewie/views.py +26 -0
  38. swarm/blueprints/suggestion/blueprint_suggestion.py +54 -39
  39. swarm/blueprints/whinge_surf/README.md +22 -0
  40. swarm/blueprints/whinge_surf/__init__.py +1 -0
  41. swarm/blueprints/whinge_surf/blueprint_whinge_surf.py +565 -0
  42. swarm/blueprints/whinge_surf/whinge_surf_cli.py +99 -0
  43. swarm/blueprints/whiskeytango_foxtrot/blueprint_whiskeytango_foxtrot.py +66 -37
  44. swarm/blueprints/zeus/__init__.py +2 -0
  45. swarm/blueprints/zeus/apps.py +4 -0
  46. swarm/blueprints/zeus/blueprint_zeus.py +270 -0
  47. swarm/blueprints/zeus/zeus_cli.py +13 -0
  48. swarm/cli/async_input.py +65 -0
  49. swarm/cli/async_input_demo.py +32 -0
  50. {open_swarm-0.1.1745125933.dist-info → open_swarm-0.1.1745126277.dist-info}/WHEEL +0 -0
  51. {open_swarm-0.1.1745125933.dist-info → open_swarm-0.1.1745126277.dist-info}/entry_points.txt +0 -0
  52. {open_swarm-0.1.1745125933.dist-info → open_swarm-0.1.1745126277.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,565 @@
1
+ import subprocess
2
+ import threading
3
+ import os
4
+ import signal
5
+ from typing import Optional, Dict
6
+ from swarm.core.blueprint_ux import BlueprintUXImproved
7
+ from swarm.core.blueprint_base import BlueprintBase
8
+ import json
9
+ import time
10
+ import psutil # For resource usage
11
+ from swarm.blueprints.common.operation_box_utils import display_operation_box
12
+
13
+ class WhingeSpinner:
14
+ FRAMES = ["Generating.", "Generating..", "Generating...", "Running..."]
15
+ LONG_WAIT_MSG = "Generating... Taking longer than expected"
16
+ INTERVAL = 0.12
17
+ SLOW_THRESHOLD = 10
18
+
19
+ def __init__(self):
20
+ self._idx = 0
21
+ self._start_time = None
22
+ self._last_frame = self.FRAMES[0]
23
+
24
+ def start(self):
25
+ self._start_time = time.time()
26
+ self._idx = 0
27
+ self._last_frame = self.FRAMES[0]
28
+
29
+ def _spin(self):
30
+ self._idx = (self._idx + 1) % len(self.FRAMES)
31
+ self._last_frame = self.FRAMES[self._idx]
32
+
33
+ def current_spinner_state(self):
34
+ if self._start_time and (time.time() - self._start_time) > self.SLOW_THRESHOLD:
35
+ return self.LONG_WAIT_MSG
36
+ return self._last_frame
37
+
38
+ class WhingeSurfBlueprint(BlueprintBase):
39
+ """
40
+ Blueprint to run subprocesses in the background and check on their status/output.
41
+ Now supports self-update via prompt (LLM/agent required for code generation).
42
+ """
43
+ NAME = "whinge_surf"
44
+ CLI_NAME = "whinge_surf"
45
+ DESCRIPTION = "Background subprocess manager: run, check, view output, cancel, and self-update."
46
+ VERSION = "0.3.0"
47
+ JOBS_FILE = os.path.expanduser("~/.whinge_surf_jobs.json")
48
+
49
+ def __init__(self, blueprint_id: str = "whinge_surf", config=None, config_path=None, **kwargs):
50
+ super().__init__(blueprint_id, config=config, config_path=config_path, **kwargs)
51
+ self.blueprint_id = blueprint_id
52
+ self.config_path = config_path
53
+ self._config = config if config is not None else None
54
+ self._llm_profile_name = None
55
+ self._llm_profile_data = None
56
+ self._markdown_output = None
57
+ self.spinner = WhingeSpinner()
58
+ self._procs: Dict[int, Dict] = {} # pid -> {proc, output, thread, status}
59
+ self.ux = BlueprintUXImproved(style="serious")
60
+ self._load_jobs()
61
+
62
+ def _load_jobs(self):
63
+ if os.path.exists(self.JOBS_FILE):
64
+ try:
65
+ with open(self.JOBS_FILE, "r") as f:
66
+ self._jobs = json.load(f)
67
+ except Exception:
68
+ self._jobs = {}
69
+ else:
70
+ self._jobs = {}
71
+
72
+ def _save_jobs(self):
73
+ with open(self.JOBS_FILE, "w") as f:
74
+ json.dump(self._jobs, f, indent=2)
75
+
76
+ def _display_job_status(self, job_id, status, output=None, progress=None, total=None):
77
+ self.spinner._spin()
78
+ display_operation_box(
79
+ title=f"WhingeSurf Job {job_id}",
80
+ content=f"Status: {status}\nOutput: {output if output else ''}",
81
+ spinner_state=self.spinner.current_spinner_state(),
82
+ progress_line=progress,
83
+ total_lines=total,
84
+ emoji="🌊"
85
+ )
86
+
87
+ def run_subprocess_in_background(self, cmd) -> int:
88
+ """Start a subprocess in the background. Returns the PID."""
89
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1)
90
+ output = []
91
+ status = {'finished': False, 'exit_code': None}
92
+ start_time = time.time()
93
+ # --- PATCH: Ensure instant jobs finalize output and status ---
94
+ def reader():
95
+ try:
96
+ for line in proc.stdout:
97
+ output.append(line)
98
+ proc.stdout.close()
99
+ proc.wait()
100
+ finally:
101
+ status['finished'] = True
102
+ status['exit_code'] = proc.returncode
103
+ self._jobs[str(proc.pid)]["end_time"] = time.time()
104
+ self._jobs[str(proc.pid)]["exit_code"] = proc.returncode
105
+ self._jobs[str(proc.pid)]["status"] = "finished"
106
+ self._jobs[str(proc.pid)]["output"] = ''.join(output)
107
+ self._save_jobs()
108
+ t = threading.Thread(target=reader, daemon=True)
109
+ t.start()
110
+ self._procs[proc.pid] = {'proc': proc, 'output': output, 'thread': t, 'status': status}
111
+ # Add to job table
112
+ self._jobs[str(proc.pid)] = {
113
+ "pid": proc.pid,
114
+ "cmd": cmd,
115
+ "start_time": start_time,
116
+ "status": "running",
117
+ "output": None,
118
+ "exit_code": None,
119
+ "end_time": None
120
+ }
121
+ self._save_jobs()
122
+ # --- If process already finished, finalize immediately ---
123
+ if proc.poll() is not None:
124
+ status['finished'] = True
125
+ status['exit_code'] = proc.returncode
126
+ self._jobs[str(proc.pid)]["end_time"] = time.time()
127
+ self._jobs[str(proc.pid)]["exit_code"] = proc.returncode
128
+ self._jobs[str(proc.pid)]["status"] = "finished"
129
+ try:
130
+ proc.stdout.close()
131
+ except Exception:
132
+ pass
133
+ self._jobs[str(proc.pid)]["output"] = ''.join(output)
134
+ self._save_jobs()
135
+ self._display_job_status(proc.pid, "Started")
136
+ return proc.pid
137
+
138
+ def list_jobs(self):
139
+ jobs = list(self._jobs.values())
140
+ jobs.sort(key=lambda j: j["start_time"] or 0)
141
+ lines = []
142
+ for job in jobs:
143
+ dur = (job["end_time"] or time.time()) - job["start_time"] if job["start_time"] else 0
144
+ lines.append(f"PID: {job['pid']} | Status: {job['status']} | Exit: {job['exit_code']} | Duration: {dur:.1f}s | Cmd: {' '.join(job['cmd'])}")
145
+ return self.ux.ansi_emoji_box(
146
+ "Job List",
147
+ '\n'.join(lines) or 'No jobs found.',
148
+ summary="All subprocess jobs.",
149
+ op_type="list_jobs",
150
+ params={},
151
+ result_count=len(jobs)
152
+ )
153
+
154
+ def show_output(self, pid: int) -> str:
155
+ job = self._jobs.get(str(pid))
156
+ if not job:
157
+ return self.ux.ansi_emoji_box("Show Output", f"No such job: {pid}", op_type="show_output", params={"pid": pid}, result_count=0)
158
+ out = job.get("output")
159
+ if out is None:
160
+ return self.ux.ansi_emoji_box("Show Output", f"Job {pid} still running.", op_type="show_output", params={"pid": pid}, result_count=0)
161
+ return self.ux.ansi_emoji_box("Show Output", out[-1000:], summary="Last 1000 chars of output.", op_type="show_output", params={"pid": pid}, result_count=len(out))
162
+
163
+ def tail_output(self, pid: int) -> str:
164
+ import time
165
+ import itertools
166
+ job = self._jobs.get(str(pid))
167
+ if not job:
168
+ return self.ux.ansi_emoji_box("Tail Output", f"No such job: {pid}", op_type="tail_output", params={"pid": pid}, result_count=0)
169
+ spinner_cycle = itertools.cycle([
170
+ "Generating.", "Generating..", "Generating...", "Running..."
171
+ ])
172
+ start = time.time()
173
+ last_len = 0
174
+ spinner_message = next(spinner_cycle)
175
+ while True:
176
+ job = self._jobs.get(str(pid))
177
+ out = job.get("output")
178
+ lines = out.splitlines()[-10:] if out else []
179
+ elapsed = int(time.time() - start)
180
+ # Spinner escalation if taking long
181
+ if elapsed > 10:
182
+ spinner_message = "Generating... Taking longer than expected"
183
+ else:
184
+ spinner_message = next(spinner_cycle)
185
+ print(self.ux.ansi_emoji_box(
186
+ f"Tail Output | {spinner_message}",
187
+ '\n'.join(f"{i+1}: {line}" for i, line in enumerate(lines)),
188
+ op_type="tail_output",
189
+ params={"pid": pid, "elapsed": elapsed},
190
+ result_count=len(lines)
191
+ ))
192
+ if job["status"] == "finished":
193
+ break
194
+ time.sleep(1)
195
+ return "[Tail finished]"
196
+
197
+ def check_subprocess_status(self, pid: int) -> Optional[Dict]:
198
+ entry = self._procs.get(pid)
199
+ if not entry:
200
+ # Check persistent job table
201
+ job = self._jobs.get(str(pid))
202
+ if job:
203
+ return {"finished": job["status"] == "finished", "exit_code": job["exit_code"]}
204
+ return None
205
+ return entry['status']
206
+
207
+ def get_subprocess_output(self, pid: int) -> Optional[str]:
208
+ entry = self._procs.get(pid)
209
+ if not entry:
210
+ # Check persistent job table
211
+ job = self._jobs.get(str(pid))
212
+ if job:
213
+ return job.get("output")
214
+ return None
215
+ return ''.join(entry['output'])
216
+
217
+ def kill_subprocess(self, pid: int) -> str:
218
+ entry = self._procs.get(pid)
219
+ if not entry:
220
+ # Try to kill by pid if not tracked
221
+ try:
222
+ os.kill(pid, signal.SIGTERM)
223
+ return f"Sent SIGTERM to {pid}."
224
+ except Exception as e:
225
+ return f"No such subprocess: {pid} ({e})"
226
+ proc = entry['proc']
227
+ if entry['status']['finished']:
228
+ return f"Process {pid} already finished."
229
+ try:
230
+ proc.terminate()
231
+ proc.wait(timeout=5)
232
+ entry['status']['finished'] = True
233
+ entry['status']['exit_code'] = proc.returncode
234
+ self._jobs[str(pid)]["status"] = "finished"
235
+ self._jobs[str(pid)]["exit_code"] = proc.returncode
236
+ self._jobs[str(pid)]["end_time"] = time.time()
237
+ self._save_jobs()
238
+ return f"Process {pid} killed."
239
+ except Exception as e:
240
+ return f"Error killing process {pid}: {e}"
241
+
242
+ def resource_usage(self, pid: int) -> str:
243
+ try:
244
+ p = psutil.Process(pid)
245
+ cpu = p.cpu_percent(interval=0.1)
246
+ mem = p.memory_info().rss // 1024
247
+ return self.ux.ansi_emoji_box("Resource Usage", f"CPU: {cpu}% | Mem: {mem} KB", op_type="resource_usage", params={"pid": pid}, result_count=1)
248
+ except Exception as e:
249
+ return self.ux.ansi_emoji_box("Resource Usage", f"Error: {e}", op_type="resource_usage", params={"pid": pid}, result_count=0)
250
+
251
+ def self_update_from_prompt(self, prompt: str, test: bool = True) -> str:
252
+ """
253
+ Update the blueprint's own code based on a user prompt. This version will append a comment with the prompt to prove self-modification.
254
+ """
255
+ import shutil, os, time
256
+ src_file = os.path.abspath(__file__)
257
+ backup_file = src_file + ".bak"
258
+ # Step 1: Backup current file
259
+ shutil.copy2(src_file, backup_file)
260
+ # Step 2: Read current code
261
+ with open(src_file, "r") as f:
262
+ code = f.read()
263
+ # Step 3: Apply improvement (append a comment with the prompt)
264
+ new_code = code + f"\n# SELF-IMPROVEMENT: {prompt} ({time.strftime('%Y-%m-%d %H:%M:%S')})\n"
265
+ with open(src_file, "w") as f:
266
+ f.write(new_code)
267
+ # Step 4: Optionally test (skip for proof)
268
+ return self.ux.ansi_emoji_box(
269
+ "Self-Update",
270
+ f"Appended self-improvement comment: {prompt}",
271
+ summary="Self-update completed.",
272
+ op_type="self_update",
273
+ params={"prompt": prompt},
274
+ result_count=1
275
+ )
276
+
277
+ def analyze_self(self, output_format: str = "ansi") -> str:
278
+ """
279
+ Ultra-enhanced: Analyze the whinge_surf blueprint's own code and return a concise, actionable summary.
280
+ - Classes/functions/lines, coverage, imports
281
+ - TODOs/FIXMEs with line numbers
282
+ - Longest/most complex function with code snippet
283
+ - Suggestions if code smells detected
284
+ - Output as ANSI box (default), plain text, or JSON
285
+ """
286
+ import inspect, ast, re, json
287
+ src_file = inspect.getfile(self.__class__)
288
+ with open(src_file, 'r') as f:
289
+ code = f.read()
290
+ tree = ast.parse(code, filename=src_file)
291
+ lines = code.splitlines()
292
+ num_lines = len(lines)
293
+ # Classes & functions
294
+ classes = [n for n in ast.walk(tree) if isinstance(n, ast.ClassDef)]
295
+ class_names = [c.name for c in classes]
296
+ functions = [n for n in ast.walk(tree) if isinstance(n, ast.FunctionDef)]
297
+ func_names = [f.name for f in functions]
298
+ # TODOs/FIXMEs with line numbers
299
+ todos = [(i+1, l.strip()) for i,l in enumerate(lines) if 'TODO' in l or 'FIXME' in l]
300
+ # Docstring/type hint coverage
301
+ docstring_count = sum(1 for f in functions if ast.get_docstring(f))
302
+ typehint_count = sum(1 for f in functions if f.returns or any(a.annotation for a in f.args.args))
303
+ doc_cov = f"{docstring_count}/{len(functions)} ({int(100*docstring_count/max(1,len(functions)))}%)"
304
+ hint_cov = f"{typehint_count}/{len(functions)} ({int(100*typehint_count/max(1,len(functions)))}%)"
305
+ # Function length stats
306
+ func_lens = []
307
+ for f in functions:
308
+ start = f.lineno-1
309
+ end = max([getattr(f, 'end_lineno', start+1), start+1])
310
+ func_lens.append(end-start)
311
+ avg_len = int(sum(func_lens)/max(1,len(func_lens))) if func_lens else 0
312
+ max_len = max(func_lens) if func_lens else 0
313
+ longest_func = func_names[func_lens.index(max_len)] if func_lens else 'N/A'
314
+ # Code snippet for longest function
315
+ if func_lens:
316
+ f = functions[func_lens.index(max_len)]
317
+ snippet = '\n'.join(lines[f.lineno-1:getattr(f, 'end_lineno', f.lineno)])
318
+ else:
319
+ snippet = ''
320
+ # Imports
321
+ stdlib = set()
322
+ third_party = set()
323
+ import_lines = [line for line in lines if line.strip().startswith('import') or line.strip().startswith('from')]
324
+ for line in import_lines:
325
+ match = re.match(r'(?:from|import)\s+([\w_\.]+)', line)
326
+ if match:
327
+ mod = match.group(1).split('.')[0]
328
+ if mod in ('os','sys','threading','subprocess','signal','inspect','ast','re','shutil','time','typing','logging'): stdlib.add(mod)
329
+ else: third_party.add(mod)
330
+ # Suggestions
331
+ suggestions = []
332
+ if docstring_count < len(functions)//2: suggestions.append('Add more docstrings for clarity.')
333
+ if max_len > 50: suggestions.append(f'Split function {longest_func} ({max_len} lines) into smaller parts.')
334
+ if todos: suggestions.append('Resolve TODOs/FIXMEs for production readiness.')
335
+ # Output construction
336
+ summary_table = (
337
+ f"File: {src_file}\n"
338
+ f"Classes: {class_names}\n"
339
+ f"Functions: {func_names}\n"
340
+ f"Lines: {num_lines}\n"
341
+ f"Docstring/typehint coverage: {doc_cov} / {hint_cov}\n"
342
+ f"Function avg/max length: {avg_len}/{max_len}\n"
343
+ f"Stdlib imports: {sorted(stdlib)}\n"
344
+ f"Third-party imports: {sorted(third_party)}\n"
345
+ )
346
+ todos_section = '\n'.join([f"Line {ln}: {txt}" for ln,txt in todos]) or 'None'
347
+ snippet_section = f"Longest function: {longest_func} ({max_len} lines)\n---\n{snippet}\n---" if snippet else ''
348
+ suggest_section = '\n'.join(suggestions) or 'No major issues detected.'
349
+ docstring = ast.get_docstring(tree)
350
+ if output_format == 'json':
351
+ return json.dumps({
352
+ 'file': src_file,
353
+ 'classes': class_names,
354
+ 'functions': func_names,
355
+ 'lines': num_lines,
356
+ 'docstring_coverage': doc_cov,
357
+ 'typehint_coverage': hint_cov,
358
+ 'todos': todos,
359
+ 'longest_func': longest_func,
360
+ 'longest_func_len': max_len,
361
+ 'longest_func_snippet': snippet,
362
+ 'suggestions': suggestions,
363
+ 'imports': {'stdlib': sorted(stdlib), 'third_party': sorted(third_party)},
364
+ 'docstring': docstring,
365
+ }, indent=2)
366
+ text = (
367
+ summary_table +
368
+ f"\nTODOs/FIXMEs:\n{todos_section}\n" +
369
+ (f"\n{snippet_section}\n" if snippet else '') +
370
+ f"\nSuggestions:\n{suggest_section}\n" +
371
+ (f"\nTop-level docstring: {docstring}\n" if docstring else '')
372
+ )
373
+ if output_format == 'text':
374
+ return text
375
+ # Default: ANSI/emoji box
376
+ return self.ux.ansi_emoji_box(
377
+ "Self Analysis",
378
+ text,
379
+ summary="Ultra-enhanced code analysis.",
380
+ op_type="analyze_self",
381
+ params={"file": src_file},
382
+ result_count=len(func_names) + len(class_names)
383
+ )
384
+
385
+ def _generate_code_from_prompt(self, prompt: str, src_file: str) -> str:
386
+ """
387
+ Placeholder for LLM/agent call. Should return the full new code for src_file based on prompt.
388
+ """
389
+ # TODO: Integrate with your LLM/agent backend.
390
+ # For now, just return the current code (no-op)
391
+ with open(src_file, "r") as f:
392
+ return f.read()
393
+
394
+ def prune_jobs(self, keep_running=True):
395
+ """Remove jobs that are finished (unless keep_running=False, then clear all)."""
396
+ to_remove = []
397
+ for pid, job in self._jobs.items():
398
+ if job["status"] == "finished" or not keep_running:
399
+ to_remove.append(pid)
400
+ for pid in to_remove:
401
+ del self._jobs[pid]
402
+ self._save_jobs()
403
+ return self.ux.ansi_emoji_box(
404
+ "Prune Jobs",
405
+ f"Removed {len(to_remove)} finished jobs.",
406
+ summary="Job table pruned.",
407
+ op_type="prune_jobs",
408
+ params={"keep_running": keep_running},
409
+ result_count=len(to_remove)
410
+ )
411
+
412
+ async def run_and_print(self, messages):
413
+ spinner = WhingeSpinner()
414
+ spinner.start()
415
+ try:
416
+ all_results = []
417
+ async for response in self.run(messages):
418
+ content = response["messages"][0]["content"] if (isinstance(response, dict) and "messages" in response and response["messages"]) else str(response)
419
+ all_results.append(content)
420
+ # Enhanced progressive output
421
+ if isinstance(response, dict) and (response.get("progress") or response.get("matches")):
422
+ display_operation_box(
423
+ title="Progressive Operation",
424
+ content="\n".join(response.get("matches", [])),
425
+ style="bold cyan" if response.get("type") == "code_search" else "bold magenta",
426
+ result_count=len(response.get("matches", [])) if response.get("matches") is not None else None,
427
+ params={k: v for k, v in response.items() if k not in {'matches', 'progress', 'total', 'truncated', 'done'}},
428
+ progress_line=response.get('progress'),
429
+ total_lines=response.get('total'),
430
+ spinner_state=spinner.current_spinner_state() if hasattr(spinner, 'current_spinner_state') else None,
431
+ op_type=response.get("type", "search"),
432
+ emoji="🔍" if response.get("type") == "code_search" else "🧠"
433
+ )
434
+ finally:
435
+ spinner.stop()
436
+ display_operation_box(
437
+ title="WhingeSurf Output",
438
+ content="\n".join(all_results),
439
+ style="bold green",
440
+ result_count=len(all_results),
441
+ params={"prompt": messages[0]["content"]},
442
+ op_type="whinge_surf"
443
+ )
444
+
445
+ # SELF-IMPROVEMENT: add a proof of self-improvement (2025-04-19 05:17:27)
446
+
447
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 20:20:22)
448
+
449
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 20:22:57)
450
+
451
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 20:24:30)
452
+
453
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 20:26:19)
454
+
455
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 20:28:02)
456
+
457
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 20:30:18)
458
+
459
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 20:31:26)
460
+
461
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 20:32:37)
462
+
463
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 20:35:24)
464
+
465
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 20:36:26)
466
+
467
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 20:39:09)
468
+
469
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 20:40:10)
470
+
471
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 20:43:04)
472
+
473
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 20:44:05)
474
+
475
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 21:34:27)
476
+
477
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 21:36:05)
478
+
479
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 21:36:58)
480
+
481
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 21:38:09)
482
+
483
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 21:39:00)
484
+
485
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 21:41:18)
486
+
487
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 21:42:13)
488
+
489
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 21:44:26)
490
+
491
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 21:45:29)
492
+
493
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 21:54:16)
494
+
495
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 21:59:18)
496
+
497
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:00:25)
498
+
499
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:02:11)
500
+
501
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:04:15)
502
+
503
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:05:25)
504
+
505
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:06:26)
506
+
507
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:07:26)
508
+
509
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:09:13)
510
+
511
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:10:29)
512
+
513
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:13:18)
514
+
515
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:13:42)
516
+
517
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:16:03)
518
+
519
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:18:39)
520
+
521
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:20:36)
522
+
523
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:25:35)
524
+
525
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:26:31)
526
+
527
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:30:05)
528
+
529
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:33:27)
530
+
531
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:33:50)
532
+
533
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:35:57)
534
+
535
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:37:40)
536
+
537
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:40:29)
538
+
539
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:42:50)
540
+
541
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:52:23)
542
+
543
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:53:37)
544
+
545
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:54:56)
546
+
547
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:58:00)
548
+
549
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 22:59:01)
550
+
551
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 23:00:03)
552
+
553
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 23:01:06)
554
+
555
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 23:02:36)
556
+
557
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 23:09:42)
558
+
559
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 23:10:42)
560
+
561
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 23:17:37)
562
+
563
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 23:32:39)
564
+
565
+ # SELF-IMPROVEMENT: Add a test comment (2025-04-19 23:36:00)
@@ -0,0 +1,99 @@
1
+ import argparse
2
+ import os
3
+ import sys
4
+ import asyncio
5
+ from swarm.blueprints.whinge_surf.blueprint_whinge_surf import WhingeSurfBlueprint
6
+
7
+ def main():
8
+ parser = argparse.ArgumentParser(description="whinge-surf: background subprocess butler & self-updater")
9
+ parser.add_argument('--run', nargs='+', help='Run a subprocess in the background (supply command as args)')
10
+ parser.add_argument('--status', type=int, help='Check status of a subprocess by PID')
11
+ parser.add_argument('--output', type=int, help='Get output from a subprocess by PID')
12
+ parser.add_argument('--kill', type=int, help='Kill/cancel a subprocess by PID')
13
+ parser.add_argument('--self-update', type=str, help='Prompt to update whinge-surf code (self-improvement)')
14
+ parser.add_argument('--no-test', action='store_true', help='Skip running tests after self-update')
15
+ parser.add_argument('--analyze-self', action='store_true', help='Analyze whinge-surf source code and print summary')
16
+ parser.add_argument('--analyze-output', choices=['ansi', 'text', 'json'], default='ansi', help='Output format for analysis')
17
+ parser.add_argument('--list-jobs', action='store_true', help='List all subprocess jobs')
18
+ parser.add_argument('--show-output', type=int, help='Show output of a subprocess by PID')
19
+ parser.add_argument('--tail', type=int, help='Tail live output of a subprocess by PID')
20
+ parser.add_argument('--resource-usage', type=int, help='Show resource usage for a subprocess by PID')
21
+ parser.add_argument('--prune-jobs', action='store_true', help='Remove finished jobs from the job table')
22
+ args = parser.parse_args()
23
+
24
+ ws = WhingeSurfBlueprint()
25
+
26
+ if args.run:
27
+ # If a single string is passed, treat it as a shell command
28
+ if len(args.run) == 1:
29
+ cmd = ["/bin/sh", "-c", args.run[0]]
30
+ else:
31
+ cmd = args.run
32
+ pid = ws.run_subprocess_in_background(cmd)
33
+ print(ws.ux.ansi_emoji_box(
34
+ "Subprocess Started",
35
+ f"PID: {pid}\nCommand: {' '.join(cmd)}",
36
+ op_type="run",
37
+ params={'cmd': cmd},
38
+ ))
39
+ return
40
+ if args.status is not None:
41
+ status = ws.check_subprocess_status(args.status)
42
+ print(ws.ux.ansi_emoji_box(
43
+ "Subprocess Status",
44
+ str(status) if status else f"No such PID: {args.status}",
45
+ op_type="status",
46
+ params={'pid': args.status},
47
+ ))
48
+ return
49
+ if args.output is not None:
50
+ output = ws.get_subprocess_output(args.output)
51
+ print(ws.ux.ansi_emoji_box(
52
+ "Subprocess Output",
53
+ output if output is not None else f"No such PID: {args.output}",
54
+ op_type="output",
55
+ params={'pid': args.output},
56
+ ))
57
+ return
58
+ if args.kill is not None:
59
+ result = ws.kill_subprocess(args.kill)
60
+ print(ws.ux.ansi_emoji_box(
61
+ "Subprocess Kill",
62
+ result,
63
+ op_type="kill",
64
+ params={'pid': args.kill},
65
+ ))
66
+ return
67
+ if args.list_jobs:
68
+ print(ws.list_jobs())
69
+ return
70
+ if args.show_output is not None:
71
+ print(ws.show_output(args.show_output))
72
+ return
73
+ if args.tail is not None:
74
+ ws.tail_output(args.tail)
75
+ return
76
+ if args.resource_usage is not None:
77
+ print(ws.resource_usage(args.resource_usage))
78
+ return
79
+ if args.analyze_self:
80
+ print(ws.analyze_self(output_format=args.analyze_output))
81
+ return
82
+ if args.prune_jobs:
83
+ print(ws.prune_jobs())
84
+ return
85
+ if args.self_update:
86
+ result = ws.self_update_from_prompt(args.self_update, test=not args.no_test)
87
+ print(result)
88
+ return
89
+ parser.print_help()
90
+
91
+ if __name__ == "__main__":
92
+ import sys
93
+ if sys.argv[0].endswith("whinge_surf_cli.py") or sys.argv[0].endswith("whinge_surf_cli"): # legacy
94
+ print("[INFO] For future use, invoke this CLI as 'whinge' instead of 'whinge_surf_cli'.")
95
+ main()
96
+ elif sys.argv[0].endswith("whinge"): # preferred new name
97
+ main()
98
+ else:
99
+ main()