linkgravity 1.0.0 → 1.0.2
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 +6 -1
- package/bin/setup.js +62 -15
- package/package.json +1 -1
- package/src/cogs/general_cog.py +23 -3
- package/src/cogs/voice/enrollment.py +0 -2
- package/src/cogs/voice_cog.py +19 -1
- package/src/config.py +3 -0
- package/src/core/agy_runner.py +1 -3
- package/src/handlers/thread_reply.py +3 -2
- package/src/messengers/discord_adapter.py +2 -0
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 (
|
|
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 =
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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
package/src/cogs/general_cog.py
CHANGED
|
@@ -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
|
|
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(
|
|
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/cogs/voice_cog.py
CHANGED
|
@@ -6,7 +6,7 @@ import discord
|
|
|
6
6
|
from discord import app_commands
|
|
7
7
|
from discord.ext import commands, tasks
|
|
8
8
|
|
|
9
|
-
from config import logger
|
|
9
|
+
from config import allowed, logger
|
|
10
10
|
|
|
11
11
|
from .voice.enrollment import EnrollmentManager
|
|
12
12
|
from .voice.stt_session import SttSessionTracker
|
|
@@ -152,6 +152,10 @@ class VoiceCog(commands.Cog):
|
|
|
152
152
|
|
|
153
153
|
@app_commands.command(name="join", description="Summon the bot to your current voice channel")
|
|
154
154
|
async def cmd_join(self, interaction: discord.Interaction):
|
|
155
|
+
if not allowed(interaction.user.id):
|
|
156
|
+
await interaction.response.send_message("❌ Permission Denied", ephemeral=True)
|
|
157
|
+
return
|
|
158
|
+
|
|
155
159
|
if not isinstance(interaction.channel, discord.Thread):
|
|
156
160
|
await interaction.response.send_message(
|
|
157
161
|
"❌ This command can only be used inside a thread created with `/new`.", ephemeral=True
|
|
@@ -260,6 +264,10 @@ class VoiceCog(commands.Cog):
|
|
|
260
264
|
):
|
|
261
265
|
import aiohttp
|
|
262
266
|
|
|
267
|
+
if not allowed(interaction.user.id):
|
|
268
|
+
await interaction.response.send_message("❌ Permission Denied", ephemeral=True)
|
|
269
|
+
return
|
|
270
|
+
|
|
263
271
|
if (
|
|
264
272
|
wake_word is None
|
|
265
273
|
and active_times is None
|
|
@@ -330,6 +338,10 @@ class VoiceCog(commands.Cog):
|
|
|
330
338
|
|
|
331
339
|
@app_commands.command(name="leave", description="Make the bot leave the voice channel")
|
|
332
340
|
async def cmd_leave(self, interaction: discord.Interaction):
|
|
341
|
+
if not allowed(interaction.user.id):
|
|
342
|
+
await interaction.response.send_message("❌ Permission Denied", ephemeral=True)
|
|
343
|
+
return
|
|
344
|
+
|
|
333
345
|
guild_id = interaction.guild_id
|
|
334
346
|
await interaction.response.send_message("👋 Disconnected from voice channel.")
|
|
335
347
|
try:
|
|
@@ -365,6 +377,12 @@ class VoiceCog(commands.Cog):
|
|
|
365
377
|
try:
|
|
366
378
|
guild_id = data.get("guild_id")
|
|
367
379
|
user_id = data.get("user_id")
|
|
380
|
+
|
|
381
|
+
if not allowed(int(user_id)):
|
|
382
|
+
self.logger.debug(f"STT: ignoring speech from non-allowed user {user_id}")
|
|
383
|
+
await self.stt_session.clear_partial_msg(str(guild_id))
|
|
384
|
+
return
|
|
385
|
+
|
|
368
386
|
# Node runs STT itself before calling this endpoint, so this is
|
|
369
387
|
# already-recognized text, not raw audio - keeps STT off every VAD hit.
|
|
370
388
|
text = data.get("text")
|
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
|
|
package/src/core/agy_runner.py
CHANGED
|
@@ -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(
|
|
123
|
-
|
|
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",
|