linkgravity 1.0.0 → 1.0.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/bin/cli.js CHANGED
@@ -176,7 +176,12 @@ if (cmd === 'start') {
176
176
  i++;
177
177
  } else if (args[i] === '-f') {
178
178
  isFollow = true;
179
- } else if (args[i] === '-t' || args[i] === '--stamp' || args[i] === '--timestamp' || args[i] === '--timestamps') {
179
+ } else if (
180
+ args[i] === '-t' ||
181
+ args[i] === '--stamp' ||
182
+ args[i] === '--timestamp' ||
183
+ args[i] === '--timestamps'
184
+ ) {
180
185
  showStamps = true;
181
186
  } else {
182
187
  pm2Args.push(args[i]);
package/bin/setup.js CHANGED
@@ -3,6 +3,7 @@ const fs = require('fs');
3
3
  const path = require('path');
4
4
  const os = require('os');
5
5
  const { spawnSync } = require('child_process');
6
+ const { python: pythonExe } = require('../npm-scripts/venv-paths');
6
7
 
7
8
  const color = {
8
9
  reset: '\x1b[0m',
@@ -70,19 +71,37 @@ async function collectSessionScopes(existingScopes) {
70
71
  }
71
72
  isFirst = false;
72
73
 
73
- const channelIds = await p.text({
74
- message:
75
- 'Restrict to specific channel ID(s) in this server? Right-click a CHANNEL → Copy Channel ID. ' +
76
- 'Comma-separated, or leave empty to allow the WHOLE server:',
77
- });
78
- if (p.isCancel(channelIds)) {
79
- p.cancel('Setup cancelled.');
80
- process.exit(0);
74
+ const channelIds = [];
75
+ let isFirstChannel = true;
76
+ while (true) {
77
+ const channelId = await p.text({
78
+ message: isFirstChannel
79
+ ? 'Restrict to a specific channel in this server? Right-click a CHANNEL → Copy Channel ID. ' +
80
+ '(leave empty to allow the WHOLE server):'
81
+ : 'Another channel ID to restrict to (leave empty if done):',
82
+ });
83
+ if (p.isCancel(channelId)) {
84
+ p.cancel('Setup cancelled.');
85
+ process.exit(0);
86
+ }
87
+ if (!channelId) break;
88
+ isFirstChannel = false;
89
+ channelIds.push(channelId.trim());
90
+
91
+ const addAnotherChannel = await p.confirm({
92
+ message: 'Add another channel?',
93
+ initialValue: false,
94
+ });
95
+ if (p.isCancel(addAnotherChannel)) {
96
+ p.cancel('Setup cancelled.');
97
+ process.exit(0);
98
+ }
99
+ if (!addAnotherChannel) break;
81
100
  }
82
101
 
83
102
  scopes.push({
84
103
  guild_id: guildId.trim(),
85
- channel_ids: channelIds ? splitIds(channelIds) : [],
104
+ channel_ids: channelIds,
86
105
  });
87
106
 
88
107
  const addAnother = await p.confirm({ message: 'Add another server?', initialValue: false });
@@ -102,7 +121,7 @@ async function collectUserIds(existingIds) {
102
121
 
103
122
  p.note(
104
123
  'ONLY these users can use the bot (leave completely empty on first setup to allow EVERYONE). ' +
105
- "Not related to DMs - this only gates the channel/threads configured above.",
124
+ 'Not related to DMs - this only gates the channel/threads configured above.',
106
125
  'Allowed Discord Users',
107
126
  );
108
127
 
@@ -170,9 +189,9 @@ async function runSetup() {
170
189
  const userIds = await collectUserIds(existingUserIds);
171
190
 
172
191
  p.note(
173
- 'Wake words aren\'t set here anymore - they need a voice recording to register '
174
- + '(so only your voice triggers them), which this terminal wizard can\'t do. '
175
- + 'Set them from Discord with `/sound wake_words:<word>` once the bot is running.',
192
+ "Wake words aren't set here anymore - they need a voice recording to register " +
193
+ "(so only your voice triggers them), which this terminal wizard can't do. " +
194
+ 'Set them from Discord with `/sound wake_words:<word>` once the bot is running.',
176
195
  'Wake Words',
177
196
  );
178
197
 
@@ -249,12 +268,40 @@ async function runSetup() {
249
268
  p.note('Configuration saved to lgy.json successfully!', 'Success');
250
269
 
251
270
  console.log(`${color.cyan}▶${color.reset} Restarting daemon to apply changes...`);
252
- spawnSync('npx', ['-y', 'pm2', 'restart', 'lgy', '--update-env'], {
271
+ const restartResult = spawnSync('npx', ['-y', 'pm2', 'restart', 'lgy', '--update-env'], {
253
272
  stdio: 'pipe',
254
273
  env: { ...process.env, PYTHONUNBUFFERED: '1' },
255
274
  });
256
275
 
257
- p.outro('Daemon restarted.');
276
+ if (restartResult.status === 0) {
277
+ p.outro('Daemon restarted.');
278
+ } else {
279
+ const stderr = (restartResult.stderr || '').toString();
280
+ if (stderr.includes('not found')) {
281
+ // Nothing to restart yet (first-ever setup, or pm2's process list was
282
+ // reset e.g. after a reboot without `pm2 save`) - start it instead of
283
+ // reporting a false "restarted" success.
284
+ const botPath = path.join(__dirname, '..', 'src', 'main.py');
285
+ const startResult = spawnSync(
286
+ 'npx',
287
+ ['-y', 'pm2', 'start', botPath, '--interpreter', pythonExe, '--name', 'lgy'],
288
+ { stdio: 'pipe', env: { ...process.env, PYTHONUNBUFFERED: '1' } },
289
+ );
290
+ if (startResult.status === 0) {
291
+ p.outro("Daemon wasn't running yet - started it fresh instead.");
292
+ } else {
293
+ console.error((startResult.stderr || '').toString().trim());
294
+ p.outro(
295
+ `${color.yellow}⚠${color.reset} Failed to start the daemon - run \`lgy start\` manually to see the full error.`,
296
+ );
297
+ }
298
+ } else {
299
+ console.error(stderr.trim());
300
+ p.outro(
301
+ `${color.yellow}⚠${color.reset} Failed to restart the daemon - run \`lgy restart\` manually to see the full error.`,
302
+ );
303
+ }
304
+ }
258
305
  }
259
306
 
260
307
  module.exports = runSetup;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linkgravity",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Discord bot bridge for the Antigravity (agy) CLI, with voice interaction support",
5
5
  "scripts": {
6
6
  "postinstall": "node npm-scripts/postinstall.js",
@@ -7,7 +7,17 @@ import discord
7
7
  from discord import app_commands
8
8
  from discord.ext import commands
9
9
 
10
- from config import DATA_DIR, EMBED_COLOR, MODEL_CHOICES, allowed, is_allowed_session_channel, logger, session_manager
10
+ from config import (
11
+ DATA_DIR,
12
+ EMBED_COLOR,
13
+ MODEL_CHOICES,
14
+ allowed,
15
+ bot_settings,
16
+ is_allowed_session_channel,
17
+ logger,
18
+ save_bot_settings,
19
+ session_manager,
20
+ )
11
21
  from core.atomic_io import atomic_write_json, safe_load_json
12
22
  from utils.utils import get_default_cwd
13
23
 
@@ -116,7 +126,7 @@ class GeneralCog(commands.Cog):
116
126
  from utils.utils import get_current_model
117
127
 
118
128
  session = session_manager.get_session(str(interaction.channel_id)) or {}
119
- current_model = session.get("model") or get_current_model()
129
+ current_model = session.get("model") or bot_settings.get("default_model") or get_current_model()
120
130
 
121
131
  choices = []
122
132
  if current_model:
@@ -151,6 +161,8 @@ class GeneralCog(commands.Cog):
151
161
  if not model:
152
162
  await interaction.response.send_message(f"⚠️ Model matching `{model}` not found.", ephemeral=True)
153
163
  return
164
+ else:
165
+ model = bot_settings.get("default_model") or None
154
166
 
155
167
  await interaction.response.defer(ephemeral=True)
156
168
  import uuid
@@ -197,8 +209,16 @@ class GeneralCog(commands.Cog):
197
209
  partial = next((m for m in cached_models if model.lower() in m.lower()), None)
198
210
  final_model = exact or partial or model
199
211
  session_manager.update_session(str(interaction.channel_id), "model", final_model)
212
+
213
+ bot_settings["default_model"] = final_model
214
+ save_bot_settings(bot_settings)
215
+
200
216
  await interaction.response.send_message(
201
- embed=discord.Embed(description=f"🤖 Model changed: **{final_model}**", color=EMBED_COLOR)
217
+ embed=discord.Embed(
218
+ description=f"🤖 Model changed: **{final_model}**\n"
219
+ f"💾 Also set as the default for new `/new` sessions.",
220
+ color=EMBED_COLOR,
221
+ )
202
222
  )
203
223
  except discord.Forbidden:
204
224
  logger.error(f"Missing permissions to start thread in channel {interaction.channel_id}")
@@ -17,7 +17,6 @@ NODE_REQUEST_TIMEOUT = aiohttp.ClientTimeout(total=5)
17
17
 
18
18
 
19
19
  class RecordingPromptView(discord.ui.View):
20
-
21
20
  def __init__(self, manager: "EnrollmentManager", user_id: str):
22
21
  super().__init__(timeout=120)
23
22
  self.manager = manager
@@ -48,7 +47,6 @@ class RecordingPromptView(discord.ui.View):
48
47
 
49
48
 
50
49
  class SampleConfirmView(discord.ui.View):
51
-
52
50
  def __init__(self, manager: "EnrollmentManager", user_id: str):
53
51
  super().__init__(timeout=60)
54
52
  self.manager = manager
package/src/config.py CHANGED
@@ -19,6 +19,9 @@ DEFAULT_LGY_CONFIG = {
19
19
  "voice_threshold": 3000,
20
20
  "tts_voice": "ko-KR-SunHiNeural",
21
21
  "tts_enabled": True,
22
+ # Sticky default for new /new sessions - set whenever /model succeeds,
23
+ # so a new thread doesn't fall back to agy's own settings.json model.
24
+ "default_model": "",
22
25
  }
23
26
 
24
27
 
@@ -126,9 +126,7 @@ async def run_agy(
126
126
  libstdbuf_path = _find_libstdbuf() # works around agy's output-truncation bug
127
127
  if libstdbuf_path:
128
128
  existing_preload = env.get("LD_PRELOAD", "")
129
- env["LD_PRELOAD"] = (
130
- f"{libstdbuf_path}:{existing_preload}" if existing_preload else libstdbuf_path
131
- )
129
+ env["LD_PRELOAD"] = f"{libstdbuf_path}:{existing_preload}" if existing_preload else libstdbuf_path
132
130
  env["_STDBUF_O"] = str(_STDOUT_BUFFER_SIZE)
133
131
 
134
132
  kwargs = {
@@ -119,8 +119,9 @@ async def handle_thread_reply(bot, message: discord.Message):
119
119
  adapter = get_adapter()
120
120
  content = message.content.strip()
121
121
  if content.startswith("/new"):
122
- await adapter.send_message(thread,
123
- "To start a new session, please use the `/new` slash command (not typed as text) - it works from inside a thread too and will open the new one in the right place."
122
+ await adapter.send_message(
123
+ thread,
124
+ "To start a new session, please use the `/new` slash command (not typed as text) - it works from inside a thread too and will open the new one in the right place.",
124
125
  )
125
126
  return
126
127
 
@@ -187,6 +187,7 @@ class DiscordAdapter(MessengerAdapter):
187
187
  submit_btn.callback = submit_callback
188
188
  view.add_item(submit_btn)
189
189
  else:
190
+
190
191
  def make_option_callback(opt_text: str):
191
192
  async def callback(interaction: discord.Interaction):
192
193
  await _resolve(interaction, opt_text, "Selected")
@@ -200,6 +201,7 @@ class DiscordAdapter(MessengerAdapter):
200
201
  view.add_item(btn)
201
202
 
202
203
  if allow_write_in:
204
+
203
205
  class WriteInModal(discord.ui.Modal, title="Write in"):
204
206
  answer = discord.ui.TextInput(
205
207
  label="Enter your response",