linkgravity 1.7.5 → 1.8.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.
- package/README.md +1 -1
- package/bin/cli.js +16 -0
- package/bin/completion.js +15 -2
- package/package.json +1 -1
- package/src/api/ui_routes.py +13 -3
- package/src/cogs/general_cog.py +15 -0
- package/src/config.py +1 -1
- package/src/core/agy_runner.py +3 -3
- package/src/core/session_manager.py +39 -4
- package/src/handlers/thread_reply.py +18 -6
- package/src/main_slack.py +26 -2
- package/src/main_telegram.py +25 -2
- package/src/messengers/slack_adapter.py +9 -0
- package/src/services/permissions.py +11 -0
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ Slack has more moving parts than the others - two separate tokens, and a few set
|
|
|
60
60
|
- It's easy to grab the wrong token here - the page also shows a **User OAuth Token** (`xoxp-...`) further down, which is a different thing and won't work for this bot.
|
|
61
61
|
5. **App Home** (left sidebar) > under **Show Tabs**, turn on **Messages Tab**, then check **Allow users to send Slash commands and messages from the messages tab** - this is what lets you DM the bot at all. (If this section looks greyed out, it's because step 3 hasn't been saved/installed yet - go back and do that first.)
|
|
62
62
|
6. **Event Subscriptions** (left sidebar) > toggle **Enable Events** on > under **Subscribe to bot events**, add `message.channels`, `message.groups`, `message.im`, and `message.mpim` > **Save Changes**.
|
|
63
|
-
7. **Slash Commands** (left sidebar) > **Create New Command**,
|
|
63
|
+
7. **Slash Commands** (left sidebar) > **Create New Command**, five times, for `/new`, `/model`, `/credit`, `/permissions`, and `/automode` (any description/hint text is fine - only the command name matters).
|
|
64
64
|
8. Back on **OAuth & Permissions**, since scopes/events changed after the initial install, click **Reinstall to Workspace** to push those changes live. Any time you change scopes or events later, you'll need to repeat this step.
|
|
65
65
|
9. In Slack itself, for any **channel** (not DM) you want the bot usable in, run `/invite @<your bot's name>` there first - the bot can't post in a channel it hasn't been added to.
|
|
66
66
|
|
package/bin/cli.js
CHANGED
|
@@ -59,6 +59,20 @@ function repairHookRegistration({ fresh = false } = {}) {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
function repairShellCompletion() {
|
|
63
|
+
// Runs on every update too (not just the rarely-rerun setup) so existing installs get cleaned up.
|
|
64
|
+
try {
|
|
65
|
+
const result = require('./completion').installCompletion();
|
|
66
|
+
if (result?.cleaned) {
|
|
67
|
+
console.log(
|
|
68
|
+
`${color.dim}Cleaned up a stale completion line in your shell rc file.${color.reset}`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// Best-effort - a broken shell rc file shouldn't fail the update.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
62
76
|
function runPm2(args, silent = true) {
|
|
63
77
|
const stdioOpt = silent ? 'pipe' : 'inherit';
|
|
64
78
|
const result = spawnSync(process.execPath, [PM2_BIN, ...args], {
|
|
@@ -714,6 +728,7 @@ if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
|
|
|
714
728
|
|
|
715
729
|
if (latestVersion === currentVersion) {
|
|
716
730
|
repairHookRegistration();
|
|
731
|
+
repairShellCompletion();
|
|
717
732
|
success(`Already up to date (v${currentVersion}).\n`);
|
|
718
733
|
process.exit(0);
|
|
719
734
|
}
|
|
@@ -737,6 +752,7 @@ if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
|
|
|
737
752
|
delete require.cache[require.resolve('../npm-scripts/ensure-env')];
|
|
738
753
|
require('../npm-scripts/ensure-env').ensureEnvironment();
|
|
739
754
|
repairHookRegistration({ fresh: true });
|
|
755
|
+
repairShellCompletion();
|
|
740
756
|
|
|
741
757
|
if (!procBeforeUpdate) {
|
|
742
758
|
const blocker = launchBlocker();
|
package/bin/completion.js
CHANGED
|
@@ -29,13 +29,21 @@ function installCompletion(shell = path.basename(process.env.SHELL || '')) {
|
|
|
29
29
|
|
|
30
30
|
const dest = path.join(os.homedir(), ...target.dest);
|
|
31
31
|
let rcUpdated = false;
|
|
32
|
+
let cleaned = false;
|
|
32
33
|
try {
|
|
33
34
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
34
35
|
fs.copyFileSync(path.join(__dirname, 'completions', target.src), dest);
|
|
35
36
|
|
|
36
37
|
if (target.rc) {
|
|
37
38
|
const rc = path.join(os.homedir(), target.rc);
|
|
38
|
-
|
|
39
|
+
let existing = fs.existsSync(rc) ? fs.readFileSync(rc, 'utf8') : '';
|
|
40
|
+
// Strip any pre-marker `eval "$(lgy completion ...)"` line - lgy never had that subcommand.
|
|
41
|
+
const stale = /^\s*eval "\$\(lgy completion[^)]*\)"\s*$/gm;
|
|
42
|
+
if (stale.test(existing)) {
|
|
43
|
+
fs.writeFileSync(rc, existing.replace(stale, '').replace(/\n{3,}/g, '\n\n'));
|
|
44
|
+
existing = fs.readFileSync(rc, 'utf8');
|
|
45
|
+
cleaned = true;
|
|
46
|
+
}
|
|
39
47
|
if (!existing.includes(MARKER)) {
|
|
40
48
|
fs.appendFileSync(rc, zshRcBlock(path.dirname(dest)));
|
|
41
49
|
rcUpdated = true;
|
|
@@ -44,7 +52,12 @@ function installCompletion(shell = path.basename(process.env.SHELL || '')) {
|
|
|
44
52
|
} catch {
|
|
45
53
|
return null;
|
|
46
54
|
}
|
|
47
|
-
return {
|
|
55
|
+
return {
|
|
56
|
+
shell,
|
|
57
|
+
file: dest,
|
|
58
|
+
rc: rcUpdated ? path.join(os.homedir(), target.rc) : null,
|
|
59
|
+
cleaned,
|
|
60
|
+
};
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
module.exports = { installCompletion };
|
package/package.json
CHANGED
package/src/api/ui_routes.py
CHANGED
|
@@ -78,6 +78,8 @@ def _clean_inline(text: str) -> str:
|
|
|
78
78
|
async def handle_approve_request(request):
|
|
79
79
|
# Tracks approval_keys registered this request so the exception handler can clean them up.
|
|
80
80
|
registered_approval_keys = []
|
|
81
|
+
# Tracks prompts sent this request so a later exception can still finalize (disable) them.
|
|
82
|
+
sent_prompts = []
|
|
81
83
|
try:
|
|
82
84
|
data = await request.json()
|
|
83
85
|
conv_id = data.get("conversation_id")
|
|
@@ -151,6 +153,7 @@ async def handle_approve_request(request):
|
|
|
151
153
|
prompt = adapter.create_question_prompt(
|
|
152
154
|
future, question_text, options, multi_select=is_multi_select, allow_write_in=True
|
|
153
155
|
)
|
|
156
|
+
sent_prompts.append(prompt)
|
|
154
157
|
await send_ordered(target_thread_id, lambda: prompt.send(target_thread))
|
|
155
158
|
|
|
156
159
|
try:
|
|
@@ -188,8 +191,8 @@ async def handle_approve_request(request):
|
|
|
188
191
|
if not sub_cmd:
|
|
189
192
|
continue
|
|
190
193
|
|
|
191
|
-
is_auto_allowed =
|
|
192
|
-
if "\n" not in sub_cmd and "|" not in sub_cmd:
|
|
194
|
+
is_auto_allowed = session_manager.is_auto_mode()
|
|
195
|
+
if not is_auto_allowed and "\n" not in sub_cmd and "|" not in sub_cmd:
|
|
193
196
|
try:
|
|
194
197
|
tokens = shlex.split(sub_cmd)
|
|
195
198
|
for scope in session_manager.persistent_allowed.get("commands", []):
|
|
@@ -234,6 +237,7 @@ async def handle_approve_request(request):
|
|
|
234
237
|
prompt = adapter.create_tool_approval_prompt(
|
|
235
238
|
future, "⚠️ Tool Execution Approval Required", prompt_desc, scope_options
|
|
236
239
|
)
|
|
240
|
+
sent_prompts.append(prompt)
|
|
237
241
|
|
|
238
242
|
sub_cmd_display, sub_cmd_desc, _ = format_bash_display(sub_cmd)
|
|
239
243
|
sub_cmd_formatted = f"```text\n{sub_cmd_display}\n```{sub_cmd_desc}"
|
|
@@ -270,7 +274,7 @@ async def handle_approve_request(request):
|
|
|
270
274
|
return allow_response(tool_name, tool_input)
|
|
271
275
|
|
|
272
276
|
else:
|
|
273
|
-
if is_tool_allowed(tool_name, tool_input):
|
|
277
|
+
if session_manager.is_auto_mode() or is_tool_allowed(tool_name, tool_input):
|
|
274
278
|
if target_thread and tool_msg_text:
|
|
275
279
|
await send_ordered(
|
|
276
280
|
target_thread_id, lambda: _send_chunked(adapter, target_thread, tool_msg_formatted)
|
|
@@ -287,6 +291,7 @@ async def handle_approve_request(request):
|
|
|
287
291
|
prompt = adapter.create_tool_approval_prompt(
|
|
288
292
|
future, "⚠️ Tool Execution Approval Required", tool_msg_formatted, scope_options
|
|
289
293
|
)
|
|
294
|
+
sent_prompts.append(prompt)
|
|
290
295
|
|
|
291
296
|
async def _send_prompt():
|
|
292
297
|
await _send_chunked(adapter, target_thread, tool_msg_formatted)
|
|
@@ -316,4 +321,9 @@ async def handle_approve_request(request):
|
|
|
316
321
|
logger.exception(f"Error in handle_approve_request: {e}")
|
|
317
322
|
for key in registered_approval_keys:
|
|
318
323
|
session_manager.clear_pending_approval(key)
|
|
324
|
+
for prompt in sent_prompts:
|
|
325
|
+
try:
|
|
326
|
+
await prompt.finalize()
|
|
327
|
+
except Exception:
|
|
328
|
+
pass
|
|
319
329
|
return web.json_response({"decision": "allow"})
|
package/src/cogs/general_cog.py
CHANGED
|
@@ -275,6 +275,21 @@ class GeneralCog(commands.Cog):
|
|
|
275
275
|
await handle.send(interaction.channel)
|
|
276
276
|
await interaction.response.send_message("🔐 Permission list posted above.", ephemeral=True)
|
|
277
277
|
|
|
278
|
+
@app_commands.command(
|
|
279
|
+
name="automode", description="Auto-allow every approval globally, across all sessions (on/off)"
|
|
280
|
+
)
|
|
281
|
+
@app_commands.describe(state="on or off")
|
|
282
|
+
async def cmd_automode(self, interaction: discord.Interaction, state: str):
|
|
283
|
+
if not allowed(interaction.user.id):
|
|
284
|
+
return await interaction.response.send_message("❌ Denied", ephemeral=True)
|
|
285
|
+
if state.lower() not in ("on", "off"):
|
|
286
|
+
return await interaction.response.send_message("Use `on` or `off`.", ephemeral=True)
|
|
287
|
+
|
|
288
|
+
from services import permissions
|
|
289
|
+
|
|
290
|
+
msg = permissions.set_auto_mode(state.lower() == "on")
|
|
291
|
+
await interaction.response.send_message(msg)
|
|
292
|
+
|
|
278
293
|
@app_commands.command(
|
|
279
294
|
name="stop", description="Stop the currently generating response or task (Equivalent to ESC in CLI)"
|
|
280
295
|
)
|
package/src/config.py
CHANGED
|
@@ -148,7 +148,7 @@ TMP_VOICE_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
148
148
|
|
|
149
149
|
MAX_EMBED_LEN = 1900
|
|
150
150
|
STREAM_RATE_LIMIT_SEC = 0.5
|
|
151
|
-
APPROVAL_TIMEOUT_SEC =
|
|
151
|
+
APPROVAL_TIMEOUT_SEC = 86400
|
|
152
152
|
PERSISTENT_FILE = DATA_DIR / "persistent_tools.json"
|
|
153
153
|
SESSION_FILE = DATA_DIR / "sessions.json"
|
|
154
154
|
|
package/src/core/agy_runner.py
CHANGED
|
@@ -229,7 +229,7 @@ async def run_agy(
|
|
|
229
229
|
gather_task = asyncio.create_task(_gather_pipes())
|
|
230
230
|
wait_task = asyncio.create_task(proc.wait())
|
|
231
231
|
|
|
232
|
-
# Slices let the timeout pause
|
|
232
|
+
# Slices let the timeout pause indefinitely while any approval is pending (hooks/hook.js waits up to 24h).
|
|
233
233
|
from config import session_manager as _sm
|
|
234
234
|
|
|
235
235
|
poll_slice = 5.0
|
|
@@ -348,7 +348,7 @@ async def agy_new_conversation(
|
|
|
348
348
|
content: str, model: str = None, stream_queue: asyncio.Queue = None, thread_id: str = None, cwd: str = None
|
|
349
349
|
) -> tuple[str, str]:
|
|
350
350
|
# --print consumes the next token as the prompt, so the flag must come first.
|
|
351
|
-
args = ["--dangerously-skip-permissions", "--print", content]
|
|
351
|
+
args = ["--dangerously-skip-permissions", "--print", content, "--print-timeout", "24h"]
|
|
352
352
|
if model:
|
|
353
353
|
args.extend(["--model", clean_model_name(model)])
|
|
354
354
|
result_text = await run_agy(*args, stream_queue=stream_queue, thread_id=thread_id, cwd=cwd)
|
|
@@ -364,7 +364,7 @@ async def agy_send_message(
|
|
|
364
364
|
thread_id: str = None,
|
|
365
365
|
cwd: str = None,
|
|
366
366
|
) -> str:
|
|
367
|
-
args = ["--dangerously-skip-permissions", "--print", content, "--conversation", conv_id]
|
|
367
|
+
args = ["--dangerously-skip-permissions", "--print", content, "--conversation", conv_id, "--print-timeout", "24h"]
|
|
368
368
|
if model:
|
|
369
369
|
args.extend(["--model", clean_model_name(model)])
|
|
370
370
|
return await run_agy(*args, stream_queue=stream_queue, thread_id=thread_id, cwd=cwd)
|
|
@@ -20,6 +20,9 @@ class SessionManager:
|
|
|
20
20
|
# conv_id -> current approval_key. Keyed by approval_key (not conv_id)
|
|
21
21
|
# so a 2nd call can't overwrite the 1st's still-pending Future.
|
|
22
22
|
self.active_approval_by_conv: dict[str, str] = {}
|
|
23
|
+
# Same, but keyed by the platform session id - needed because a brand-new session's
|
|
24
|
+
# conv_id isn't known to us until its first turn fully finishes (see set_pending_approval).
|
|
25
|
+
self.active_approval_by_thread: dict[str, str] = {}
|
|
23
26
|
|
|
24
27
|
self.persistent_allowed: dict = self._load_persistent()
|
|
25
28
|
|
|
@@ -54,14 +57,22 @@ class SessionManager:
|
|
|
54
57
|
def _load_persistent(self) -> dict:
|
|
55
58
|
from config import logger
|
|
56
59
|
|
|
57
|
-
data = safe_load_json(self.persistent_file, {"tools": [], "commands": []}, logger=logger)
|
|
60
|
+
data = safe_load_json(self.persistent_file, {"tools": [], "commands": [], "auto_mode": False}, logger=logger)
|
|
58
61
|
if isinstance(data, list):
|
|
59
|
-
return {"tools": data, "commands": []}
|
|
62
|
+
return {"tools": data, "commands": [], "auto_mode": False}
|
|
63
|
+
data.setdefault("auto_mode", False)
|
|
60
64
|
return data
|
|
61
65
|
|
|
62
66
|
def save_persistent(self):
|
|
63
67
|
atomic_write_json(self.persistent_file, self.persistent_allowed)
|
|
64
68
|
|
|
69
|
+
def is_auto_mode(self) -> bool:
|
|
70
|
+
return bool(self.persistent_allowed.get("auto_mode"))
|
|
71
|
+
|
|
72
|
+
def set_auto_mode(self, enabled: bool):
|
|
73
|
+
self.persistent_allowed["auto_mode"] = enabled
|
|
74
|
+
self.save_persistent()
|
|
75
|
+
|
|
65
76
|
def register_queue(self, thread_id: str, queue: asyncio.Queue):
|
|
66
77
|
self.active_queues[str(thread_id)] = queue
|
|
67
78
|
|
|
@@ -111,12 +122,19 @@ class SessionManager:
|
|
|
111
122
|
self.active_tts_tasks.pop(str(thread_id), None)
|
|
112
123
|
|
|
113
124
|
def set_pending_approval(
|
|
114
|
-
self,
|
|
125
|
+
self,
|
|
126
|
+
approval_key: str,
|
|
127
|
+
future: asyncio.Future,
|
|
128
|
+
app_type: str = "tool",
|
|
129
|
+
conv_id: str | None = None,
|
|
130
|
+
thread_id: str | None = None,
|
|
115
131
|
):
|
|
116
132
|
self.pending_approvals[approval_key] = future
|
|
117
133
|
self.pending_approval_types[approval_key] = app_type
|
|
118
134
|
if conv_id:
|
|
119
135
|
self.active_approval_by_conv[conv_id] = approval_key
|
|
136
|
+
if thread_id:
|
|
137
|
+
self.active_approval_by_thread[thread_id] = approval_key
|
|
120
138
|
|
|
121
139
|
def get_pending_approval_by_conv(self, conv_id: str) -> asyncio.Future | None:
|
|
122
140
|
"""Looks up whichever approval is CURRENTLY active for a given
|
|
@@ -128,18 +146,35 @@ class SessionManager:
|
|
|
128
146
|
return None
|
|
129
147
|
return self.pending_approvals.get(approval_key)
|
|
130
148
|
|
|
149
|
+
def get_pending_approval_by_thread(self, thread_id: str) -> asyncio.Future | None:
|
|
150
|
+
"""Same as get_pending_approval_by_conv, but keyed by the platform session id - use this
|
|
151
|
+
for a session that might still be "pending" (conv_id not assigned yet)."""
|
|
152
|
+
approval_key = self.active_approval_by_thread.get(thread_id)
|
|
153
|
+
if not approval_key:
|
|
154
|
+
return None
|
|
155
|
+
return self.pending_approvals.get(approval_key)
|
|
156
|
+
|
|
131
157
|
def get_pending_approval_type_by_conv(self, conv_id: str) -> str:
|
|
132
158
|
approval_key = self.active_approval_by_conv.get(conv_id)
|
|
133
159
|
if not approval_key:
|
|
134
160
|
return "tool"
|
|
135
161
|
return self.pending_approval_types.get(approval_key, "tool")
|
|
136
162
|
|
|
163
|
+
def get_pending_approval_type_by_thread(self, thread_id: str) -> str:
|
|
164
|
+
approval_key = self.active_approval_by_thread.get(thread_id)
|
|
165
|
+
if not approval_key:
|
|
166
|
+
return "tool"
|
|
167
|
+
return self.pending_approval_types.get(approval_key, "tool")
|
|
168
|
+
|
|
137
169
|
def clear_pending_approval(self, approval_key: str):
|
|
138
170
|
self.pending_approvals.pop(approval_key, None)
|
|
139
171
|
self.pending_approval_types.pop(approval_key, None)
|
|
140
172
|
self.pending_approval_messages.pop(approval_key, None)
|
|
141
|
-
# Only remove the conv_id pointer if it still points at THIS key -
|
|
173
|
+
# Only remove the conv_id/thread_id pointer if it still points at THIS key -
|
|
142
174
|
# a newer call may have already overwritten it.
|
|
143
175
|
for conv, key in list(self.active_approval_by_conv.items()):
|
|
144
176
|
if key == approval_key:
|
|
145
177
|
del self.active_approval_by_conv[conv]
|
|
178
|
+
for thread, key in list(self.active_approval_by_thread.items()):
|
|
179
|
+
if key == approval_key:
|
|
180
|
+
del self.active_approval_by_thread[thread]
|
|
@@ -22,8 +22,15 @@ from utils.utils import (
|
|
|
22
22
|
async def handle_approval_reply(incoming: IncomingMessage, session: dict, content: str, pa) -> bool:
|
|
23
23
|
adapter = get_adapter_for_platform(incoming.platform)
|
|
24
24
|
thread = incoming.conversation_ref
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
conv_id = session.get("conversation_id")
|
|
26
|
+
# Brand-new session: conv_id isn't assigned yet, so fall back to the platform-keyed lookup.
|
|
27
|
+
approval_type = (
|
|
28
|
+
session_manager.get_pending_approval_type_by_conv(conv_id)
|
|
29
|
+
if conv_id
|
|
30
|
+
else session_manager.get_pending_approval_type_by_thread(incoming.conversation_id)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if approval_type == "ask_question":
|
|
27
34
|
pa.set_result(content)
|
|
28
35
|
await adapter.send_message(thread, f'✅ *Answer Received (Write in): "{content}"*')
|
|
29
36
|
return True
|
|
@@ -166,14 +173,19 @@ async def handle_thread_reply(bot, incoming: IncomingMessage):
|
|
|
166
173
|
|
|
167
174
|
agy_content = build_content_with_images(content, image_paths)
|
|
168
175
|
conv_id = session.get("conversation_id")
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
176
|
+
# Brand-new session: conv_id isn't assigned until the first turn fully finishes, so fall back to platform id.
|
|
177
|
+
pa = (
|
|
178
|
+
session_manager.get_pending_approval_by_conv(conv_id)
|
|
179
|
+
if conv_id
|
|
180
|
+
else session_manager.get_pending_approval_by_thread(incoming.conversation_id)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if pa and not pa.done():
|
|
172
184
|
handled = await handle_approval_reply(incoming, session, content, pa)
|
|
173
185
|
if handled:
|
|
174
186
|
return
|
|
175
187
|
|
|
176
|
-
has_pending_approval = bool(
|
|
188
|
+
has_pending_approval = bool(pa and not pa.done())
|
|
177
189
|
if session_manager.get_queue(incoming.conversation_id) is not None and not has_pending_approval:
|
|
178
190
|
from core.agy_runner import stop_active_process
|
|
179
191
|
|
package/src/main_slack.py
CHANGED
|
@@ -83,9 +83,14 @@ async def cmd_model(ack, body, respond, context) -> None:
|
|
|
83
83
|
return
|
|
84
84
|
conversation_id, session = found
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
import time
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
import cogs.general_cog as general_cog
|
|
89
|
+
|
|
90
|
+
if time.time() - general_cog.last_models_fetch > 3600 and not general_cog.fetching_models:
|
|
91
|
+
general_cog.fetching_models = True
|
|
92
|
+
await general_cog.fetch_models_background()
|
|
93
|
+
cached_models = general_cog.cached_models
|
|
89
94
|
current_model = session.get("model") or bot_settings.get("default_model")
|
|
90
95
|
|
|
91
96
|
def _apply_model(final_model: str) -> str:
|
|
@@ -150,6 +155,24 @@ async def cmd_permissions(ack, body, respond, context) -> None:
|
|
|
150
155
|
await handle.send(SlackConversationRef(channel=channel, thread_ts=None))
|
|
151
156
|
|
|
152
157
|
|
|
158
|
+
async def cmd_automode(ack, body, respond, context) -> None:
|
|
159
|
+
await ack()
|
|
160
|
+
user_id = body["user_id"]
|
|
161
|
+
|
|
162
|
+
if not allowed(user_id, "slack"):
|
|
163
|
+
await respond("❌ Denied")
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
state = (body.get("text") or "").strip().lower()
|
|
167
|
+
if state not in ("on", "off"):
|
|
168
|
+
await respond("Usage: /automode on|off")
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
from services import permissions
|
|
172
|
+
|
|
173
|
+
await respond(permissions.set_auto_mode(state == "on"))
|
|
174
|
+
|
|
175
|
+
|
|
153
176
|
async def cmd_credit(ack, body, respond, context) -> None:
|
|
154
177
|
await ack()
|
|
155
178
|
adapter: SlackAdapter = context["adapter"]
|
|
@@ -234,6 +257,7 @@ def build_app() -> tuple[AsyncApp, SlackAdapter]:
|
|
|
234
257
|
app.command("/model")(cmd_model)
|
|
235
258
|
app.command("/credit")(cmd_credit)
|
|
236
259
|
app.command("/permissions")(cmd_permissions)
|
|
260
|
+
app.command("/automode")(cmd_automode)
|
|
237
261
|
app.event("message")(on_message)
|
|
238
262
|
app.action(re.compile(".*"))(on_action)
|
|
239
263
|
app.view(re.compile(".*"))(on_view_submission)
|
package/src/main_telegram.py
CHANGED
|
@@ -95,9 +95,14 @@ async def cmd_model(update: Update, context) -> None:
|
|
|
95
95
|
await update.message.reply_text("⚠️ No active session here. Start one with /new first.")
|
|
96
96
|
return
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
import time
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
import cogs.general_cog as general_cog
|
|
101
|
+
|
|
102
|
+
if time.time() - general_cog.last_models_fetch > 3600 and not general_cog.fetching_models:
|
|
103
|
+
general_cog.fetching_models = True
|
|
104
|
+
await general_cog.fetch_models_background()
|
|
105
|
+
cached_models = general_cog.cached_models
|
|
101
106
|
current_model = session.get("model") or bot_settings.get("default_model")
|
|
102
107
|
|
|
103
108
|
def _apply_model(final_model: str) -> str:
|
|
@@ -157,6 +162,22 @@ async def cmd_permissions(update: Update, context) -> None:
|
|
|
157
162
|
await handle.send(update.effective_chat.id)
|
|
158
163
|
|
|
159
164
|
|
|
165
|
+
async def cmd_automode(update: Update, context) -> None:
|
|
166
|
+
user = update.effective_user
|
|
167
|
+
if not allowed(user.id, "telegram"):
|
|
168
|
+
await update.message.reply_text("❌ Denied")
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
state = (context.args[0] if context.args else "").lower()
|
|
172
|
+
if state not in ("on", "off"):
|
|
173
|
+
await update.message.reply_text("Usage: /automode on|off")
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
from services import permissions
|
|
177
|
+
|
|
178
|
+
await update.message.reply_text(permissions.set_auto_mode(state == "on"))
|
|
179
|
+
|
|
180
|
+
|
|
160
181
|
async def cmd_credit(update: Update, context) -> None:
|
|
161
182
|
user = update.effective_user
|
|
162
183
|
adapter: TelegramAdapter = context.bot_data["adapter"]
|
|
@@ -230,6 +251,7 @@ async def on_ready(app: Application) -> None:
|
|
|
230
251
|
BotCommand("model", "Change the AI model for this session"),
|
|
231
252
|
BotCommand("credit", "Turn AI Credits on/off"),
|
|
232
253
|
BotCommand("permissions", "View and remove allowed tools and commands"),
|
|
254
|
+
BotCommand("automode", "Auto-allow every approval globally (on/off)"),
|
|
233
255
|
]
|
|
234
256
|
)
|
|
235
257
|
logger.info(f"✅ Bot is fully online and ready! Logged in as @{app.bot.username}")
|
|
@@ -245,6 +267,7 @@ def build_application() -> Application:
|
|
|
245
267
|
app.add_handler(CommandHandler("model", cmd_model))
|
|
246
268
|
app.add_handler(CommandHandler("credit", cmd_credit))
|
|
247
269
|
app.add_handler(CommandHandler("permissions", cmd_permissions))
|
|
270
|
+
app.add_handler(CommandHandler("automode", cmd_automode))
|
|
248
271
|
app.add_handler(CallbackQueryHandler(adapter.handle_callback_query))
|
|
249
272
|
app.add_handler(MessageHandler(filters.ALL & ~filters.COMMAND, on_message))
|
|
250
273
|
app.add_error_handler(on_error)
|
|
@@ -164,6 +164,7 @@ class _SlackPromptHandle(PromptHandle):
|
|
|
164
164
|
self.channel: str | None = None
|
|
165
165
|
self.ts: str | None = None
|
|
166
166
|
self.outcome: ToolApprovalOutcome | None = None
|
|
167
|
+
self.resolved = False
|
|
167
168
|
|
|
168
169
|
async def send(self, conversation_ref: SlackConversationRef) -> dict:
|
|
169
170
|
try:
|
|
@@ -185,6 +186,12 @@ class _SlackPromptHandle(PromptHandle):
|
|
|
185
186
|
self._cleanup()
|
|
186
187
|
if self.ts is None:
|
|
187
188
|
return
|
|
189
|
+
if not self.resolved:
|
|
190
|
+
# No button click happened - resolve() would have already rewritten text/blocks otherwise.
|
|
191
|
+
self.blocks = [b for b in self.blocks if b.get("type") != "actions"]
|
|
192
|
+
self.blocks.append(
|
|
193
|
+
{"type": "section", "text": {"type": "mrkdwn", "text": "⏰ *Expired - no response in time*"}}
|
|
194
|
+
)
|
|
188
195
|
try:
|
|
189
196
|
await self.client.chat_update(channel=self.channel, ts=self.ts, text=self.text, blocks=self.blocks)
|
|
190
197
|
except SlackApiError as e:
|
|
@@ -390,6 +397,7 @@ class SlackAdapter(MessengerAdapter):
|
|
|
390
397
|
elements = []
|
|
391
398
|
|
|
392
399
|
async def resolve(decision: str, scope: ScopeOption | None, resp_body: dict, client: AsyncWebClient):
|
|
400
|
+
handle.resolved = True
|
|
393
401
|
handle.outcome = ToolApprovalOutcome(decision=decision, scope=scope)
|
|
394
402
|
if not decision_future.done():
|
|
395
403
|
decision_future.set_result(decision)
|
|
@@ -463,6 +471,7 @@ class SlackAdapter(MessengerAdapter):
|
|
|
463
471
|
keys: list[str] = []
|
|
464
472
|
|
|
465
473
|
async def resolve(chosen_text: str, note: str, body: dict, client: AsyncWebClient):
|
|
474
|
+
handle.resolved = True
|
|
466
475
|
if not answer_future.done():
|
|
467
476
|
answer_future.set_result(chosen_text)
|
|
468
477
|
new_text = f"✅ *{note}: {chosen_text}*"
|
|
@@ -5,6 +5,17 @@ from messengers.base import PermissionEntry
|
|
|
5
5
|
PAGE_SIZE = 20
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
def set_auto_mode(enabled: bool) -> str:
|
|
9
|
+
session_manager.set_auto_mode(enabled)
|
|
10
|
+
if enabled:
|
|
11
|
+
return (
|
|
12
|
+
"⚠️ **Auto-mode ON** — every tool/command approval will be auto-allowed globally, "
|
|
13
|
+
"across all sessions and platforms, until you run `/automode off`. Protected paths "
|
|
14
|
+
"are still blocked regardless."
|
|
15
|
+
)
|
|
16
|
+
return "🔒 Auto-mode OFF — approvals are back to normal."
|
|
17
|
+
|
|
18
|
+
|
|
8
19
|
def list_entries() -> list[PermissionEntry]:
|
|
9
20
|
allowed = session_manager.persistent_allowed
|
|
10
21
|
return [
|