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 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**, four times, for `/new`, `/model`, `/credit`, and `/permissions` (any description/hint text is fine - only the command name matters).
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
- const existing = fs.existsSync(rc) ? fs.readFileSync(rc, 'utf8') : '';
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 { shell, file: dest, rc: rcUpdated ? path.join(os.homedir(), target.rc) : null };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linkgravity",
3
- "version": "1.7.5",
3
+ "version": "1.8.0",
4
4
  "description": "Discord/Telegram bot bridge for the Antigravity (agy) CLI, with voice interaction support",
5
5
  "scripts": {
6
6
  "start": "node npm-scripts/run-dev.js",
@@ -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 = False
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)
@@ -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
- from cogs.general_cog import load_cached_models
86
+ import time
87
87
 
88
- cached_models = load_cached_models()
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)
@@ -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
- from cogs.general_cog import load_cached_models
98
+ import time
99
99
 
100
- cached_models = load_cached_models()
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 [