openagent-framework 0.2.7__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.
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/PKG-INFO +1 -1
- openagent_framework-0.2.8/openagent/channels/__init__.py +22 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/channels/base.py +80 -0
- openagent_framework-0.2.8/openagent/channels/commands.py +172 -0
- openagent_framework-0.2.8/openagent/channels/discord.py +421 -0
- openagent_framework-0.2.8/openagent/channels/queue.py +171 -0
- openagent_framework-0.2.8/openagent/channels/telegram.py +380 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/channels/whatsapp.py +121 -44
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/server.py +16 -1
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent_framework.egg-info/PKG-INFO +1 -1
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent_framework.egg-info/SOURCES.txt +2 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/pyproject.toml +1 -1
- openagent_framework-0.2.7/openagent/channels/__init__.py +0 -3
- openagent_framework-0.2.7/openagent/channels/discord.py +0 -151
- openagent_framework-0.2.7/openagent/channels/telegram.py +0 -225
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/docs/README.md +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/__init__.py +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/agent.py +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/bootstrap.py +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/channels/senders.py +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/cli.py +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/config.py +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcp/__init__.py +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcp/client.py +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcp/oauth.py +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/chrome-devtools/.gitignore +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/chrome-devtools/package.json +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/computer-control/.gitignore +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/computer-control/package.json +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/computer-control/src/index.ts +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/computer-control/src/main.ts +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/computer-control/src/tools/computer.ts +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/computer-control/src/tools/index.ts +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/computer-control/src/utils/response.ts +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/computer-control/src/xdotoolStringToKeys.ts +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/computer-control/tsconfig.json +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/editor/.gitignore +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/editor/package.json +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/editor/src/index.ts +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/editor/tsconfig.json +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/messaging/.gitignore +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/messaging/index.ts +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/messaging/package.json +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/messaging/tsconfig.json +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/shell/.gitignore +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/shell/package.json +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/shell/src/index.ts +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/shell/tsconfig.json +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/web-search/.gitignore +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/web-search/package.json +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/web-search/src/browser-pool.ts +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/web-search/src/content-extractor.ts +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/web-search/src/enhanced-content-extractor.ts +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/web-search/src/index.ts +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/web-search/src/rate-limiter.ts +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/web-search/src/search-engine.ts +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/web-search/src/types.ts +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/web-search/src/utils.ts +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/mcps/web-search/tsconfig.json +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/memory/__init__.py +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/memory/db.py +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/memory/manager.py +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/models/__init__.py +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/models/base.py +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/models/claude_api.py +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/models/claude_cli.py +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/models/zhipu.py +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/prompts.py +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/scheduler.py +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/service.py +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/services/__init__.py +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/services/base.py +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/services/manager.py +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent/services/syncthing.py +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent_framework.egg-info/dependency_links.txt +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent_framework.egg-info/entry_points.txt +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent_framework.egg-info/requires.txt +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/openagent_framework.egg-info/top_level.txt +0 -0
- {openagent_framework-0.2.7 → openagent_framework-0.2.8}/setup.cfg +0 -0
|
@@ -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```")
|