linkgravity 1.7.4 → 1.8.0
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 +127 -24
- package/bin/completion.js +15 -2
- package/package.json +1 -1
- package/src/api/ui_routes.py +3 -3
- package/src/cogs/general_cog.py +15 -0
- package/src/cogs/voice_cog.py +1 -1
- package/src/core/session_manager.py +10 -2
- package/src/main_slack.py +30 -3
- package/src/main_telegram.py +25 -2
- 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], {
|
|
@@ -342,7 +356,10 @@ function launchBlocker() {
|
|
|
342
356
|
return null;
|
|
343
357
|
}
|
|
344
358
|
|
|
345
|
-
|
|
359
|
+
// pm2 keys an app by name AND script path, so an install that moved (node version switch, npm prefix
|
|
360
|
+
// change, npm link) registers a second app under the same name instead of replacing the first, and
|
|
361
|
+
// every name-based pm2 command then acts on both at once.
|
|
362
|
+
function getPm2Procs() {
|
|
346
363
|
const jlist = spawnSync(process.execPath, [PM2_BIN, 'jlist'], { stdio: 'pipe' });
|
|
347
364
|
if (jlist.status !== 0) return null;
|
|
348
365
|
|
|
@@ -353,7 +370,7 @@ function getPm2Proc() {
|
|
|
353
370
|
if (out[i] !== '[') continue;
|
|
354
371
|
try {
|
|
355
372
|
const procs = JSON.parse(out.slice(i));
|
|
356
|
-
if (Array.isArray(procs)) return procs.
|
|
373
|
+
if (Array.isArray(procs)) return procs.filter((p) => p.name === LGY_PM2_NAME);
|
|
357
374
|
} catch (e) {}
|
|
358
375
|
}
|
|
359
376
|
|
|
@@ -363,6 +380,71 @@ function getPm2Proc() {
|
|
|
363
380
|
return null;
|
|
364
381
|
}
|
|
365
382
|
|
|
383
|
+
function isOurRegistration(proc) {
|
|
384
|
+
const registered = proc.pm2_env.pm_exec_path || '';
|
|
385
|
+
if (!isWin) return registered === LGY_SCRIPT_PATH;
|
|
386
|
+
return registered.toLowerCase() === LGY_SCRIPT_PATH.toLowerCase();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function printRegistrations(procs) {
|
|
390
|
+
for (const proc of procs) {
|
|
391
|
+
const mine = isOurRegistration(proc) ? ' <- this install' : '';
|
|
392
|
+
console.log(
|
|
393
|
+
` ${String(proc.pm2_env.status).padEnd(8)} ${proc.pm2_env.pm_exec_path}${mine}`,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
let duplicatesReported = false;
|
|
399
|
+
function pickPm2Proc(procs) {
|
|
400
|
+
if (!procs || procs.length === 0) return null;
|
|
401
|
+
|
|
402
|
+
if (procs.length > 1 && !duplicatesReported) {
|
|
403
|
+
duplicatesReported = true;
|
|
404
|
+
console.log(
|
|
405
|
+
`\n${color.yellow}⚠${color.reset} pm2 has ${procs.length} apps registered as '${LGY_PM2_NAME}' - only this install's should be:`,
|
|
406
|
+
);
|
|
407
|
+
printRegistrations(procs);
|
|
408
|
+
console.log(
|
|
409
|
+
` Everything below reports on one of them. Run ${color.cyan}lgy start${color.reset} to drop the stale ones.\n`,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const isOnline = (proc) => proc.pm2_env.status === 'online';
|
|
414
|
+
return (
|
|
415
|
+
procs.find((proc) => isOnline(proc) && isOurRegistration(proc)) ||
|
|
416
|
+
procs.find(isOnline) ||
|
|
417
|
+
procs.find(isOurRegistration) ||
|
|
418
|
+
procs[0]
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function getPm2Proc() {
|
|
423
|
+
return pickPm2Proc(getPm2Procs());
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function startDaemon() {
|
|
427
|
+
runPm2([
|
|
428
|
+
'start',
|
|
429
|
+
LGY_SCRIPT_PATH,
|
|
430
|
+
'--interpreter',
|
|
431
|
+
daemonPython,
|
|
432
|
+
'--name',
|
|
433
|
+
LGY_PM2_NAME,
|
|
434
|
+
'--update-env',
|
|
435
|
+
]);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// The saved autostart list keeps the removed paths until pm2 save runs, so a reboot restores them.
|
|
439
|
+
function clearRegistrations(procs) {
|
|
440
|
+
console.log(
|
|
441
|
+
`\n${color.yellow}⚠${color.reset} pm2 has ${procs.length} app(s) registered as '${LGY_PM2_NAME}', not all from this install:`,
|
|
442
|
+
);
|
|
443
|
+
printRegistrations(procs);
|
|
444
|
+
console.log(' Removing all of them and registering this install alone.\n');
|
|
445
|
+
runPm2(['delete', LGY_PM2_NAME]);
|
|
446
|
+
}
|
|
447
|
+
|
|
366
448
|
// Best-effort: pm2 has no API for "is this registered to start on boot", so this checks the OS directly and returns null (unknown) if that check itself isn't available.
|
|
367
449
|
function isAutostartEnabled() {
|
|
368
450
|
if (isWin) return null;
|
|
@@ -403,8 +485,11 @@ if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
|
|
|
403
485
|
const pkg = require('../package.json');
|
|
404
486
|
console.log(`linkgravity v${pkg.version}`);
|
|
405
487
|
} else if (cmd === 'start') {
|
|
406
|
-
const
|
|
407
|
-
|
|
488
|
+
const registered = getPm2Procs() || [];
|
|
489
|
+
const stale = registered.filter((proc) => !isOurRegistration(proc));
|
|
490
|
+
const existing = registered.find(isOurRegistration);
|
|
491
|
+
|
|
492
|
+
if (!stale.length && existing && existing.pm2_env.status === 'online') {
|
|
408
493
|
console.log(
|
|
409
494
|
`\n${color.yellow}⚠${color.reset} LinkGravity is already running. ` +
|
|
410
495
|
`Use ${color.cyan}lgy restart${color.reset} to apply changes, or ${color.cyan}lgy stop${color.reset} first.\n`,
|
|
@@ -425,16 +510,11 @@ if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
|
|
|
425
510
|
|
|
426
511
|
repairHookRegistration();
|
|
427
512
|
|
|
513
|
+
if (stale.length) clearRegistrations(registered);
|
|
514
|
+
|
|
428
515
|
info('Starting LinkGravity daemon...');
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
LGY_SCRIPT_PATH,
|
|
432
|
-
'--interpreter',
|
|
433
|
-
daemonPython,
|
|
434
|
-
'--name',
|
|
435
|
-
LGY_PM2_NAME,
|
|
436
|
-
'--update-env',
|
|
437
|
-
]);
|
|
516
|
+
startDaemon();
|
|
517
|
+
if (stale.length) runPm2(['save']);
|
|
438
518
|
verifyStartup().then((ok) => process.exit(ok ? 0 : 1));
|
|
439
519
|
} else if (cmd === 'stop') {
|
|
440
520
|
info('Stopping LinkGravity daemon...');
|
|
@@ -442,6 +522,18 @@ if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
|
|
|
442
522
|
runPm2(['reset', LGY_PM2_NAME]);
|
|
443
523
|
success('Daemon stopped successfully.\n');
|
|
444
524
|
} else if (cmd === 'restart') {
|
|
525
|
+
const registered = getPm2Procs() || [];
|
|
526
|
+
if (registered.some((proc) => !isOurRegistration(proc))) {
|
|
527
|
+
console.log(
|
|
528
|
+
`\n${color.yellow}⚠${color.reset} pm2 has ${registered.length} app(s) registered as '${LGY_PM2_NAME}', and a restart would start every one of them:`,
|
|
529
|
+
);
|
|
530
|
+
printRegistrations(registered);
|
|
531
|
+
console.log(
|
|
532
|
+
` Run ${color.cyan}lgy start${color.reset} instead - it drops the stale ones first.\n`,
|
|
533
|
+
);
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}
|
|
536
|
+
|
|
445
537
|
info('Restarting LinkGravity daemon...');
|
|
446
538
|
runPm2(['restart', LGY_PM2_NAME, '--update-env']);
|
|
447
539
|
runPm2(['reset', LGY_PM2_NAME]);
|
|
@@ -636,12 +728,15 @@ if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
|
|
|
636
728
|
|
|
637
729
|
if (latestVersion === currentVersion) {
|
|
638
730
|
repairHookRegistration();
|
|
731
|
+
repairShellCompletion();
|
|
639
732
|
success(`Already up to date (v${currentVersion}).\n`);
|
|
640
733
|
process.exit(0);
|
|
641
734
|
}
|
|
642
735
|
|
|
643
|
-
const
|
|
736
|
+
const registeredBeforeUpdate = getPm2Procs() || [];
|
|
737
|
+
const procBeforeUpdate = pickPm2Proc(registeredBeforeUpdate);
|
|
644
738
|
const wasOnline = !!procBeforeUpdate && procBeforeUpdate.pm2_env.status === 'online';
|
|
739
|
+
const hadStale = registeredBeforeUpdate.some((proc) => !isOurRegistration(proc));
|
|
645
740
|
|
|
646
741
|
info(`Updating: v${currentVersion} -> v${latestVersion}...`);
|
|
647
742
|
const installResult = runNpm(['install', '-g', 'linkgravity@latest'], { stdio: 'inherit' });
|
|
@@ -657,6 +752,7 @@ if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
|
|
|
657
752
|
delete require.cache[require.resolve('../npm-scripts/ensure-env')];
|
|
658
753
|
require('../npm-scripts/ensure-env').ensureEnvironment();
|
|
659
754
|
repairHookRegistration({ fresh: true });
|
|
755
|
+
repairShellCompletion();
|
|
660
756
|
|
|
661
757
|
if (!procBeforeUpdate) {
|
|
662
758
|
const blocker = launchBlocker();
|
|
@@ -666,15 +762,20 @@ if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
|
|
|
666
762
|
process.exit(0);
|
|
667
763
|
}
|
|
668
764
|
info("Daemon wasn't running - starting it fresh...");
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
'
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
765
|
+
startDaemon();
|
|
766
|
+
verifyStartup().then((ok) => process.exit(ok ? 0 : 1));
|
|
767
|
+
} else if (hadStale) {
|
|
768
|
+
clearRegistrations(registeredBeforeUpdate);
|
|
769
|
+
if (!wasOnline) {
|
|
770
|
+
runPm2(['save']);
|
|
771
|
+
success(
|
|
772
|
+
`Daemon was stopped - leaving it stopped. Run 'lgy start' when you're ready.\n`,
|
|
773
|
+
);
|
|
774
|
+
process.exit(0);
|
|
775
|
+
}
|
|
776
|
+
info('Starting the daemon from this install...');
|
|
777
|
+
startDaemon();
|
|
778
|
+
runPm2(['save']);
|
|
678
779
|
verifyStartup().then((ok) => process.exit(ok ? 0 : 1));
|
|
679
780
|
} else if (wasOnline) {
|
|
680
781
|
info('Restarting daemon to apply the update...');
|
|
@@ -753,7 +854,9 @@ if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
|
|
|
753
854
|
spawnSync(process.argv[0], [process.argv[1], action], { stdio: 'inherit' });
|
|
754
855
|
})();
|
|
755
856
|
} else {
|
|
756
|
-
|
|
857
|
+
// Not stdout: an eval "$(lgy ...)" line in a shell rc would run this message as commands.
|
|
858
|
+
console.error(
|
|
757
859
|
`\n❌ Unknown command: ${cmd || 'none'}\n💡 Run 'lgy help' to see available commands.`,
|
|
758
860
|
);
|
|
861
|
+
process.exit(1);
|
|
759
862
|
}
|
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
|
@@ -188,8 +188,8 @@ async def handle_approve_request(request):
|
|
|
188
188
|
if not sub_cmd:
|
|
189
189
|
continue
|
|
190
190
|
|
|
191
|
-
is_auto_allowed =
|
|
192
|
-
if "\n" not in sub_cmd and "|" not in sub_cmd:
|
|
191
|
+
is_auto_allowed = session_manager.is_auto_mode()
|
|
192
|
+
if not is_auto_allowed and "\n" not in sub_cmd and "|" not in sub_cmd:
|
|
193
193
|
try:
|
|
194
194
|
tokens = shlex.split(sub_cmd)
|
|
195
195
|
for scope in session_manager.persistent_allowed.get("commands", []):
|
|
@@ -270,7 +270,7 @@ async def handle_approve_request(request):
|
|
|
270
270
|
return allow_response(tool_name, tool_input)
|
|
271
271
|
|
|
272
272
|
else:
|
|
273
|
-
if is_tool_allowed(tool_name, tool_input):
|
|
273
|
+
if session_manager.is_auto_mode() or is_tool_allowed(tool_name, tool_input):
|
|
274
274
|
if target_thread and tool_msg_text:
|
|
275
275
|
await send_ordered(
|
|
276
276
|
target_thread_id, lambda: _send_chunked(adapter, target_thread, tool_msg_formatted)
|
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/cogs/voice_cog.py
CHANGED
|
@@ -583,7 +583,7 @@ class VoiceCog(commands.Cog):
|
|
|
583
583
|
wake_syllables = len(re.sub(r"[^\w가-힣]", "", matched_wake_word or ""))
|
|
584
584
|
min_prefix_similarity = 0.55 if wake_syllables <= 2 else 0.35
|
|
585
585
|
if is_waking_up and prefix_similarity is not None and prefix_similarity < min_prefix_similarity:
|
|
586
|
-
self.logger.
|
|
586
|
+
self.logger.debug(
|
|
587
587
|
f"STT: ignoring wake - '{text}' doesn't resemble '{matched_wake_word}' "
|
|
588
588
|
f"(prefix similarity {prefix_similarity:.2f}, needed {min_prefix_similarity:.2f})"
|
|
589
589
|
)
|
|
@@ -54,14 +54,22 @@ class SessionManager:
|
|
|
54
54
|
def _load_persistent(self) -> dict:
|
|
55
55
|
from config import logger
|
|
56
56
|
|
|
57
|
-
data = safe_load_json(self.persistent_file, {"tools": [], "commands": []}, logger=logger)
|
|
57
|
+
data = safe_load_json(self.persistent_file, {"tools": [], "commands": [], "auto_mode": False}, logger=logger)
|
|
58
58
|
if isinstance(data, list):
|
|
59
|
-
return {"tools": data, "commands": []}
|
|
59
|
+
return {"tools": data, "commands": [], "auto_mode": False}
|
|
60
|
+
data.setdefault("auto_mode", False)
|
|
60
61
|
return data
|
|
61
62
|
|
|
62
63
|
def save_persistent(self):
|
|
63
64
|
atomic_write_json(self.persistent_file, self.persistent_allowed)
|
|
64
65
|
|
|
66
|
+
def is_auto_mode(self) -> bool:
|
|
67
|
+
return bool(self.persistent_allowed.get("auto_mode"))
|
|
68
|
+
|
|
69
|
+
def set_auto_mode(self, enabled: bool):
|
|
70
|
+
self.persistent_allowed["auto_mode"] = enabled
|
|
71
|
+
self.save_persistent()
|
|
72
|
+
|
|
65
73
|
def register_queue(self, thread_id: str, queue: asyncio.Queue):
|
|
66
74
|
self.active_queues[str(thread_id)] = queue
|
|
67
75
|
|
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)
|
|
@@ -258,7 +282,7 @@ async def run_slack(stop_event: asyncio.Event) -> None:
|
|
|
258
282
|
|
|
259
283
|
app, adapter = build_app()
|
|
260
284
|
try:
|
|
261
|
-
await adapter.resolve_bot_user_id()
|
|
285
|
+
bot_user_id = await adapter.resolve_bot_user_id()
|
|
262
286
|
except SlackApiError as e:
|
|
263
287
|
logger.critical(f"Slack auth_test failed - check slack_bot_token: {e}")
|
|
264
288
|
return
|
|
@@ -266,6 +290,9 @@ async def run_slack(stop_event: asyncio.Event) -> None:
|
|
|
266
290
|
handler = AsyncSocketModeHandler(app, SLACK_APP_TOKEN)
|
|
267
291
|
logger.info("✅ Slack bot starting (Socket Mode)...")
|
|
268
292
|
await handler.connect_async()
|
|
293
|
+
# cli.js's verifyStartup() waits for this exact sentence - without it a Slack-only install
|
|
294
|
+
# never reports a successful startup and every lgy start/restart/update times out.
|
|
295
|
+
logger.info(f"✅ Bot is fully online and ready! Logged in as {bot_user_id}")
|
|
269
296
|
platform_health.set_status("slack", "running")
|
|
270
297
|
try:
|
|
271
298
|
await stop_event.wait()
|
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)
|
|
@@ -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 [
|