linkgravity 1.7.5 → 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 +16 -0
- 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/core/session_manager.py +10 -2
- package/src/main_slack.py +26 -2
- 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], {
|
|
@@ -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
|
@@ -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
|
)
|
|
@@ -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)
|
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 [
|