openagent-framework 0.2.6__tar.gz → 0.2.8__tar.gz

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 (79) hide show
  1. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/PKG-INFO +24 -1
  2. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/docs/README.md +23 -0
  3. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/__init__.py +1 -1
  4. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/agent.py +16 -2
  5. openagent_framework-0.2.8/openagent/channels/__init__.py +22 -0
  6. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/channels/base.py +80 -0
  7. openagent_framework-0.2.8/openagent/channels/commands.py +172 -0
  8. openagent_framework-0.2.8/openagent/channels/discord.py +421 -0
  9. openagent_framework-0.2.8/openagent/channels/queue.py +171 -0
  10. openagent_framework-0.2.8/openagent/channels/telegram.py +380 -0
  11. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/channels/whatsapp.py +121 -44
  12. openagent_framework-0.2.8/openagent/prompts.py +81 -0
  13. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/server.py +54 -19
  14. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent_framework.egg-info/PKG-INFO +24 -1
  15. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent_framework.egg-info/SOURCES.txt +3 -0
  16. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/pyproject.toml +1 -1
  17. openagent_framework-0.2.6/openagent/channels/__init__.py +0 -3
  18. openagent_framework-0.2.6/openagent/channels/discord.py +0 -151
  19. openagent_framework-0.2.6/openagent/channels/telegram.py +0 -225
  20. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/bootstrap.py +0 -0
  21. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/channels/senders.py +0 -0
  22. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/cli.py +0 -0
  23. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/config.py +0 -0
  24. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcp/__init__.py +0 -0
  25. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcp/client.py +0 -0
  26. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcp/oauth.py +0 -0
  27. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/chrome-devtools/.gitignore +0 -0
  28. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/chrome-devtools/package.json +0 -0
  29. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/computer-control/.gitignore +0 -0
  30. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/computer-control/package.json +0 -0
  31. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/computer-control/src/index.ts +0 -0
  32. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/computer-control/src/main.ts +0 -0
  33. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/computer-control/src/tools/computer.ts +0 -0
  34. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/computer-control/src/tools/index.ts +0 -0
  35. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/computer-control/src/utils/response.ts +0 -0
  36. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/computer-control/src/xdotoolStringToKeys.ts +0 -0
  37. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/computer-control/tsconfig.json +0 -0
  38. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/editor/.gitignore +0 -0
  39. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/editor/package.json +0 -0
  40. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/editor/src/index.ts +0 -0
  41. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/editor/tsconfig.json +0 -0
  42. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/messaging/.gitignore +0 -0
  43. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/messaging/index.ts +0 -0
  44. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/messaging/package.json +0 -0
  45. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/messaging/tsconfig.json +0 -0
  46. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/shell/.gitignore +0 -0
  47. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/shell/package.json +0 -0
  48. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/shell/src/index.ts +0 -0
  49. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/shell/tsconfig.json +0 -0
  50. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/web-search/.gitignore +0 -0
  51. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/web-search/package.json +0 -0
  52. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/web-search/src/browser-pool.ts +0 -0
  53. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/web-search/src/content-extractor.ts +0 -0
  54. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/web-search/src/enhanced-content-extractor.ts +0 -0
  55. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/web-search/src/index.ts +0 -0
  56. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/web-search/src/rate-limiter.ts +0 -0
  57. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/web-search/src/search-engine.ts +0 -0
  58. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/web-search/src/types.ts +0 -0
  59. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/web-search/src/utils.ts +0 -0
  60. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/mcps/web-search/tsconfig.json +0 -0
  61. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/memory/__init__.py +0 -0
  62. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/memory/db.py +0 -0
  63. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/memory/manager.py +0 -0
  64. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/models/__init__.py +0 -0
  65. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/models/base.py +0 -0
  66. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/models/claude_api.py +0 -0
  67. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/models/claude_cli.py +0 -0
  68. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/models/zhipu.py +0 -0
  69. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/scheduler.py +0 -0
  70. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/service.py +0 -0
  71. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/services/__init__.py +0 -0
  72. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/services/base.py +0 -0
  73. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/services/manager.py +0 -0
  74. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent/services/syncthing.py +0 -0
  75. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent_framework.egg-info/dependency_links.txt +0 -0
  76. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent_framework.egg-info/entry_points.txt +0 -0
  77. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent_framework.egg-info/requires.txt +0 -0
  78. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/openagent_framework.egg-info/top_level.txt +0 -0
  79. {openagent_framework-0.2.6 → openagent_framework-0.2.8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openagent-framework
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: Simplified LLM agent framework with MCP, memory, and multi-channel support
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.11
@@ -168,6 +168,29 @@ These are handled automatically by OpenAgent — you just write `openagent.yaml`
168
168
 
169
169
  ---
170
170
 
171
+ ## System prompt
172
+
173
+ OpenAgent injects a **framework-level system prompt** in front of your
174
+ user-defined `system_prompt` on every model call. It codifies guidelines
175
+ that apply to every deployment:
176
+
177
+ - How to use the memory vault (mcpvault tools, wikilinks, tags).
178
+ - Prefer MCP tools over `shell_exec` and ad-hoc scripts.
179
+ - Act autonomously under `permission_mode: bypass`; only stop for
180
+ ambiguous or irreversible actions.
181
+ - Be concise; lead with the answer, not the reasoning.
182
+
183
+ Your `system_prompt` in `openagent.yaml` should therefore stay **short
184
+ and project-specific** — just identity and a pointer to the memory
185
+ vault. Everything else (package names, host names, credentials,
186
+ procedures) belongs as `.md` notes inside the vault, where the agent
187
+ will find them via `search_notes`.
188
+
189
+ The framework prompt source lives at `openagent/prompts.py:FRAMEWORK_SYSTEM_PROMPT`
190
+ if you want to read or tweak the wording.
191
+
192
+ ---
193
+
171
194
  ## Memory
172
195
 
173
196
  OpenAgent's long-term memory is a plain **Obsidian-compatible markdown vault**. The agent reads and writes `.md` files directly via the [`mcpvault`](https://www.npmjs.com/package/@bitbonsai/mcpvault) MCP — no custom index, no full-text engine, no proprietary format. The files on disk *are* the database.
@@ -141,6 +141,29 @@ These are handled automatically by OpenAgent — you just write `openagent.yaml`
141
141
 
142
142
  ---
143
143
 
144
+ ## System prompt
145
+
146
+ OpenAgent injects a **framework-level system prompt** in front of your
147
+ user-defined `system_prompt` on every model call. It codifies guidelines
148
+ that apply to every deployment:
149
+
150
+ - How to use the memory vault (mcpvault tools, wikilinks, tags).
151
+ - Prefer MCP tools over `shell_exec` and ad-hoc scripts.
152
+ - Act autonomously under `permission_mode: bypass`; only stop for
153
+ ambiguous or irreversible actions.
154
+ - Be concise; lead with the answer, not the reasoning.
155
+
156
+ Your `system_prompt` in `openagent.yaml` should therefore stay **short
157
+ and project-specific** — just identity and a pointer to the memory
158
+ vault. Everything else (package names, host names, credentials,
159
+ procedures) belongs as `.md` notes inside the vault, where the agent
160
+ will find them via `search_notes`.
161
+
162
+ The framework prompt source lives at `openagent/prompts.py:FRAMEWORK_SYSTEM_PROMPT`
163
+ if you want to read or tweak the wording.
164
+
165
+ ---
166
+
144
167
  ## Memory
145
168
 
146
169
  OpenAgent's long-term memory is a plain **Obsidian-compatible markdown vault**. The agent reads and writes `.md` files directly via the [`mcpvault`](https://www.npmjs.com/package/@bitbonsai/mcpvault) MCP — no custom index, no full-text engine, no proprietary format. The files on disk *are* the database.
@@ -1,5 +1,5 @@
1
1
  from openagent.agent import Agent
2
2
  from openagent.config import load_config
3
3
 
4
- __version__ = "0.2.6"
4
+ __version__ = "0.2.7"
5
5
  __all__ = ["Agent", "load_config"]
@@ -8,6 +8,7 @@ from typing import Any, AsyncIterator, Callable, Awaitable
8
8
  from openagent.models.base import BaseModel, ModelResponse
9
9
  from openagent.memory.db import MemoryDB
10
10
  from openagent.mcp.client import MCPRegistry, MCPTools
11
+ from openagent.prompts import FRAMEWORK_SYSTEM_PROMPT
11
12
 
12
13
  logger = logging.getLogger(__name__)
13
14
 
@@ -192,7 +193,9 @@ class Agent:
192
193
  """Inner run logic, wrapped by run() for crash protection."""
193
194
  await _status("Loading context...")
194
195
 
195
- system = self.system_prompt
196
+ # Combine OpenAgent's framework-level guidelines with the user's
197
+ # project-specific system prompt from openagent.yaml.
198
+ system = self._combined_system_prompt()
196
199
 
197
200
  # Build messages
198
201
  if attachments:
@@ -244,6 +247,17 @@ class Agent:
244
247
 
245
248
  return response.content if response else "I wasn't able to complete the request."
246
249
 
250
+ def _combined_system_prompt(self) -> str:
251
+ """Prepend the framework-level prompt to the user's system prompt."""
252
+ user = (self.system_prompt or "").strip()
253
+ if not user:
254
+ return FRAMEWORK_SYSTEM_PROMPT
255
+ return (
256
+ FRAMEWORK_SYSTEM_PROMPT
257
+ + "\n\n── User-specific identity and project context ──\n\n"
258
+ + user
259
+ )
260
+
247
261
  async def stream_run(
248
262
  self,
249
263
  message: str,
@@ -256,7 +270,7 @@ class Agent:
256
270
 
257
271
  await self.initialize()
258
272
 
259
- system = self.system_prompt
273
+ system = self._combined_system_prompt()
260
274
  messages: list[dict[str, Any]] = [{"role": "user", "content": message}]
261
275
 
262
276
  async for chunk in self.model.stream(messages, system=system):
@@ -0,0 +1,22 @@
1
+ from openagent.channels.base import (
2
+ Attachment,
3
+ BLOCKED_EXTENSIONS,
4
+ BaseChannel,
5
+ is_blocked_attachment,
6
+ parse_response_markers,
7
+ split_preserving_code_blocks,
8
+ )
9
+ from openagent.channels.commands import CommandDispatcher, CommandResult
10
+ from openagent.channels.queue import UserQueueManager
11
+
12
+ __all__ = [
13
+ "Attachment",
14
+ "BLOCKED_EXTENSIONS",
15
+ "BaseChannel",
16
+ "CommandDispatcher",
17
+ "CommandResult",
18
+ "UserQueueManager",
19
+ "is_blocked_attachment",
20
+ "parse_response_markers",
21
+ "split_preserving_code_blocks",
22
+ ]
@@ -74,6 +74,86 @@ def format_attachments_for_prompt(attachments: list[Attachment], caption: str =
74
74
  return prefix
75
75
 
76
76
 
77
+ # File extensions we refuse to download from any channel for basic safety.
78
+ # Shell scripts are NOT on this list — an agent is expected to deal with them
79
+ # legitimately. This is about Windows executables and obviously malicious
80
+ # droppers, not about blocking all code.
81
+ BLOCKED_EXTENSIONS: frozenset[str] = frozenset({
82
+ ".exe", ".bat", ".cmd", ".com", ".msi", ".scr", ".pif",
83
+ ".vbs", ".vbe", ".jse", ".ws", ".wsf", ".wsh", ".ps1", ".hta",
84
+ ".cpl", ".lnk", ".reg", ".jar",
85
+ })
86
+
87
+
88
+ def is_blocked_attachment(filename: str | None) -> bool:
89
+ """Return True if the filename has a blocked extension (case-insensitive)."""
90
+ if not filename:
91
+ return False
92
+ return Path(filename).suffix.lower() in BLOCKED_EXTENSIONS
93
+
94
+
95
+ def split_preserving_code_blocks(text: str, max_len: int) -> list[str]:
96
+ """Split *text* into chunks of ≤ ``max_len`` characters, preserving
97
+ fenced ``` code blocks so no chunk ends with a dangling fence.
98
+
99
+ Strategy:
100
+
101
+ 1. Walk the text in windows of at most ``max_len`` chars, cutting on a
102
+ newline when possible.
103
+ 2. For each chunk, count the unescaped ``` fences. If odd, close with a
104
+ trailing ``` and prepend ``` to the next chunk so the code style
105
+ carries over to the reader.
106
+
107
+ This loses the original language tag after a mid-block split — Discord
108
+ and Telegram render ``` (no lang) as a plain monospace block, which is
109
+ still the right thing for long output.
110
+ """
111
+ if max_len <= 0:
112
+ return [text] if text else []
113
+ if len(text) <= max_len:
114
+ return [text] if text.strip() else []
115
+
116
+ chunks: list[str] = []
117
+ carry_prefix = ""
118
+ i = 0
119
+ n = len(text)
120
+
121
+ while i < n:
122
+ budget = max_len - len(carry_prefix)
123
+ if budget <= 16:
124
+ # carry_prefix too large vs max_len; fall back to hard cut
125
+ budget = max(max_len // 2, 16)
126
+ end = min(i + budget, n)
127
+ if end < n:
128
+ # prefer newline cut, then space, else hard cut
129
+ nl = text.rfind("\n", i, end)
130
+ if nl > i + budget // 4:
131
+ end = nl
132
+ else:
133
+ sp = text.rfind(" ", i, end)
134
+ if sp > i + budget // 4:
135
+ end = sp
136
+
137
+ body = text[i:end]
138
+ chunk = carry_prefix + body
139
+
140
+ fence_total = chunk.count("```")
141
+ if fence_total % 2 == 1:
142
+ chunk = chunk + "\n```"
143
+ carry_prefix = "```\n"
144
+ else:
145
+ carry_prefix = ""
146
+
147
+ if chunk.strip():
148
+ chunks.append(chunk)
149
+
150
+ i = end
151
+ while i < n and text[i] in ("\n", " "):
152
+ i += 1
153
+
154
+ return chunks
155
+
156
+
77
157
  class BaseChannel(ABC):
78
158
  """Abstract base for messaging channels (Telegram, Discord, WhatsApp, etc.).
79
159
 
@@ -0,0 +1,172 @@
1
+ """Cross-channel slash command dispatcher.
2
+
3
+ All channels (Telegram, Discord, WhatsApp) share a single command
4
+ vocabulary via ``CommandDispatcher``. Commands are parsed with a leading
5
+ ``/`` (e.g. ``/new``, ``/stop``, ``/status``, ``/queue clear``) and resolve
6
+ to a :class:`CommandResult` the channel renders in its native way.
7
+
8
+ The registered commands are:
9
+
10
+ - ``/new`` — reset the user's session (fresh context)
11
+ - ``/reset`` — alias of /new
12
+ - ``/stop`` — cancel the currently running task for the user
13
+ - ``/status`` — busy/idle + queue depth + session id tail
14
+ - ``/queue`` — show queue state; ``/queue clear`` empties it
15
+ - ``/help`` — list of commands
16
+ - ``/usage`` — Claude Code usage via ``ccusage`` (claude-cli backend only)
17
+
18
+ Unknown commands return ``None`` so the channel can either fall through to
19
+ normal message handling or reject them.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import asyncio
25
+ import logging
26
+ import shutil
27
+ from dataclasses import dataclass
28
+ from typing import TYPE_CHECKING
29
+
30
+ if TYPE_CHECKING:
31
+ from openagent.agent import Agent
32
+ from openagent.channels.queue import UserQueueManager
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ HELP_TEXT = (
38
+ "Comandi disponibili:\n"
39
+ "• /new — nuova sessione (contesto fresco)\n"
40
+ "• /stop — ferma l'operazione in corso\n"
41
+ "• /status — stato (busy/idle + coda)\n"
42
+ "• /queue — messaggi in coda\n"
43
+ "• /queue clear — svuota la coda\n"
44
+ "• /usage — uso Claude Code (solo backend claude-cli)\n"
45
+ "• /help — questo messaggio"
46
+ )
47
+
48
+
49
+ @dataclass
50
+ class CommandResult:
51
+ """Result of a command. Rendered verbatim by the channel."""
52
+ text: str
53
+ is_error: bool = False
54
+
55
+
56
+ class CommandDispatcher:
57
+ """Parse and dispatch slash commands for a single channel instance."""
58
+
59
+ def __init__(self, agent: Agent, queue: UserQueueManager):
60
+ self.agent = agent
61
+ self.queue = queue
62
+
63
+ @staticmethod
64
+ def is_command(text: str | None) -> bool:
65
+ return bool(text) and text.lstrip().startswith("/")
66
+
67
+ @staticmethod
68
+ def parse(text: str) -> tuple[str, str]:
69
+ """Return (command, argument) stripped of the leading slash."""
70
+ body = text.lstrip()[1:]
71
+ parts = body.split(maxsplit=1)
72
+ cmd = parts[0].lower() if parts else ""
73
+ # Telegram-style @botname suffix: /new@mybot → /new
74
+ if "@" in cmd:
75
+ cmd = cmd.split("@", 1)[0]
76
+ arg = parts[1] if len(parts) > 1 else ""
77
+ return cmd, arg
78
+
79
+ async def dispatch(self, text: str, user_id: str) -> CommandResult | None:
80
+ """Run a command. Returns None if the text isn't a known command."""
81
+ if not self.is_command(text):
82
+ return None
83
+ cmd, arg = self.parse(text)
84
+ method = getattr(self, f"cmd_{cmd.replace('-', '_')}", None)
85
+ if method is None:
86
+ return None
87
+ try:
88
+ return await method(arg, user_id)
89
+ except Exception as e: # noqa: BLE001
90
+ logger.exception("Command /%s failed", cmd)
91
+ return CommandResult(f"Errore eseguendo /{cmd}: {e}", is_error=True)
92
+
93
+ # ── commands ───────────────────────────────────────────────────────
94
+
95
+ async def cmd_new(self, arg: str, user_id: str) -> CommandResult:
96
+ self.queue.reset_session(user_id)
97
+ return CommandResult("🆕 Nuova sessione avviata. Contesto precedente archiviato.")
98
+
99
+ async def cmd_reset(self, arg: str, user_id: str) -> CommandResult:
100
+ return await self.cmd_new(arg, user_id)
101
+
102
+ async def cmd_stop(self, arg: str, user_id: str) -> CommandResult:
103
+ stopped = self.queue.stop_current(user_id)
104
+ if stopped:
105
+ return CommandResult("⏹ Operazione in corso cancellata.")
106
+ return CommandResult("Nessuna operazione in corso.")
107
+
108
+ async def cmd_status(self, arg: str, user_id: str) -> CommandResult:
109
+ busy = self.queue.is_busy(user_id)
110
+ depth = self.queue.queue_depth(user_id)
111
+ sid = self.queue.get_session_id(user_id)
112
+ state = "🟢 busy" if busy else "⚪ idle"
113
+ lines = [
114
+ "Stato:",
115
+ f"• Agent: {self.agent.name}",
116
+ f"• Stato: {state}",
117
+ f"• In coda: {depth}",
118
+ f"• Sessione: …{sid[-8:]}",
119
+ ]
120
+ return CommandResult("\n".join(lines))
121
+
122
+ async def cmd_queue(self, arg: str, user_id: str) -> CommandResult:
123
+ if arg.strip().lower() in {"clear", "clean", "reset"}:
124
+ n = self.queue.clear_queue(user_id)
125
+ return CommandResult(f"🧹 Coda svuotata ({n} messaggi rimossi).")
126
+ depth = self.queue.queue_depth(user_id)
127
+ busy = self.queue.is_busy(user_id)
128
+ if depth == 0 and not busy:
129
+ return CommandResult("Nessun messaggio in coda.")
130
+ tail = " (operazione in corso)" if busy else ""
131
+ return CommandResult(f"📋 In coda: {depth} messaggi{tail}")
132
+
133
+ async def cmd_help(self, arg: str, user_id: str) -> CommandResult:
134
+ return CommandResult(HELP_TEXT)
135
+
136
+ async def cmd_usage(self, arg: str, user_id: str) -> CommandResult:
137
+ from openagent.models.claude_cli import ClaudeCLI
138
+ if not isinstance(self.agent.model, ClaudeCLI):
139
+ return CommandResult(
140
+ "ℹ️ /usage funziona solo con il backend claude-cli.",
141
+ is_error=True,
142
+ )
143
+ npx = shutil.which("npx")
144
+ if not npx:
145
+ return CommandResult(
146
+ "❌ npx non trovato sul PATH (serve Node.js per ccusage).",
147
+ is_error=True,
148
+ )
149
+ try:
150
+ proc = await asyncio.create_subprocess_exec(
151
+ npx, "-y", "ccusage@latest",
152
+ stdout=asyncio.subprocess.PIPE,
153
+ stderr=asyncio.subprocess.PIPE,
154
+ )
155
+ try:
156
+ stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=45)
157
+ except asyncio.TimeoutError:
158
+ proc.kill()
159
+ return CommandResult("❌ ccusage timeout (45s).", is_error=True)
160
+ except Exception as e: # noqa: BLE001
161
+ return CommandResult(f"❌ Impossibile lanciare ccusage: {e}", is_error=True)
162
+
163
+ output = stdout.decode(errors="replace").strip()
164
+ if not output:
165
+ output = stderr.decode(errors="replace").strip()
166
+ if not output:
167
+ return CommandResult("❌ ccusage non ha restituito output.", is_error=True)
168
+ # Keep it under 3900 chars to stay under Discord/Telegram limits when
169
+ # wrapped in a code fence.
170
+ if len(output) > 3800:
171
+ output = output[:3800] + "\n… (troncato)"
172
+ return CommandResult(f"```\n{output}\n```")