linkgravity 1.7.2 → 1.7.4
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 +52 -21
- package/bin/pm2.js +3 -0
- package/bin/setup.js +2 -2
- package/hooks/hook.js +3 -1
- package/hooks/stop_hook.js +1 -1
- package/npm-scripts/venv-paths.js +4 -0
- package/package.json +1 -1
- package/src/cogs/general_cog.py +2 -1
- package/src/config.py +18 -1
- package/src/core/agy_runner.py +1 -1
- package/src/main_discord.py +2 -2
- package/src/services/response.py +3 -3
package/bin/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ const {
|
|
|
14
14
|
LGY_SCRIPT_PATH,
|
|
15
15
|
} = require('./platforms');
|
|
16
16
|
|
|
17
|
-
const {
|
|
17
|
+
const { daemonPython, isWin } = require('../npm-scripts/venv-paths');
|
|
18
18
|
const { isEnvironmentReady } = require('../npm-scripts/ensure-env');
|
|
19
19
|
|
|
20
20
|
const cmd = process.argv[2];
|
|
@@ -203,15 +203,34 @@ function verifyStartup() {
|
|
|
203
203
|
cwd: PM2_CWD,
|
|
204
204
|
});
|
|
205
205
|
|
|
206
|
+
const baseline = getPm2Proc()?.pm2_env?.restart_time ?? 0;
|
|
207
|
+
|
|
206
208
|
let settled = false;
|
|
207
209
|
const finish = (ok) => {
|
|
208
210
|
if (settled) return;
|
|
209
211
|
settled = true;
|
|
210
212
|
clearTimeout(timer);
|
|
213
|
+
clearInterval(watchdog);
|
|
211
214
|
cp.kill();
|
|
212
215
|
resolve(ok);
|
|
213
216
|
};
|
|
214
217
|
|
|
218
|
+
// A process that exits on startup never logs anything for the stream below to match, so
|
|
219
|
+
// pm2's own counters are the only signal that it is dying and being restarted.
|
|
220
|
+
const watchdog = setInterval(() => {
|
|
221
|
+
const proc = getPm2Proc();
|
|
222
|
+
if (!proc) return;
|
|
223
|
+
const { status, restart_time: restarts = 0 } = proc.pm2_env;
|
|
224
|
+
if (status === 'errored' || restarts > baseline) {
|
|
225
|
+
console.log(
|
|
226
|
+
`\n\n${color.yellow}❌ The daemon keeps exiting - pm2 has restarted it ` +
|
|
227
|
+
`${restarts - baseline} time(s) (status: ${status}).${color.reset}`,
|
|
228
|
+
);
|
|
229
|
+
console.log(` Run ${color.cyan}lgy logs${color.reset} to see why.\n`);
|
|
230
|
+
finish(false);
|
|
231
|
+
}
|
|
232
|
+
}, 2000);
|
|
233
|
+
|
|
215
234
|
let timer = setTimeout(() => {
|
|
216
235
|
console.log(
|
|
217
236
|
`\n\n${color.yellow}⏳ Startup verification timed out. Run 'lgy logs' to check status manually.${color.reset}`,
|
|
@@ -302,6 +321,27 @@ function findAgyBin() {
|
|
|
302
321
|
return null;
|
|
303
322
|
}
|
|
304
323
|
|
|
324
|
+
// Returns null when the daemon can be started, or the reason it can't.
|
|
325
|
+
function launchBlocker() {
|
|
326
|
+
const settings = getSettings();
|
|
327
|
+
const anyConfigured = Object.keys(PLATFORMS).some(
|
|
328
|
+
(key) => platformState(key, settings).configured,
|
|
329
|
+
);
|
|
330
|
+
if (!anyConfigured) {
|
|
331
|
+
return (
|
|
332
|
+
'No messenger is configured yet - set up at least one of Discord, Telegram, or ' +
|
|
333
|
+
`Slack first: ${color.cyan}lgy setup${color.reset}`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
if (!findAgyBin()) {
|
|
337
|
+
return (
|
|
338
|
+
"Couldn't find the agy CLI (checked $AGY_BIN_PATH, ~/.local/bin/agy, and PATH). " +
|
|
339
|
+
'Install/configure agy first, or set the AGY_BIN_PATH environment variable to its location.'
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
305
345
|
function getPm2Proc() {
|
|
306
346
|
const jlist = spawnSync(process.execPath, [PM2_BIN, 'jlist'], { stdio: 'pipe' });
|
|
307
347
|
if (jlist.status !== 0) return null;
|
|
@@ -372,24 +412,9 @@ if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
|
|
|
372
412
|
process.exit(1);
|
|
373
413
|
}
|
|
374
414
|
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
(
|
|
378
|
-
);
|
|
379
|
-
if (!anyConfigured) {
|
|
380
|
-
console.log(
|
|
381
|
-
`\n${color.yellow}⚠${color.reset} No messenger is configured yet - ` +
|
|
382
|
-
`set up at least one of Discord, Telegram, or Slack first: ${color.cyan}lgy setup${color.reset}\n`,
|
|
383
|
-
);
|
|
384
|
-
process.exit(1);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
if (!findAgyBin()) {
|
|
388
|
-
console.log(
|
|
389
|
-
`\n${color.yellow}⚠${color.reset} Couldn't find the agy CLI ` +
|
|
390
|
-
`(checked $AGY_BIN_PATH, ~/.local/bin/agy, and PATH). Install/configure agy first, ` +
|
|
391
|
-
`or set the AGY_BIN_PATH environment variable to its location.\n`,
|
|
392
|
-
);
|
|
415
|
+
const blocker = launchBlocker();
|
|
416
|
+
if (blocker) {
|
|
417
|
+
console.log(`\n${color.yellow}⚠${color.reset} ${blocker}\n`);
|
|
393
418
|
process.exit(1);
|
|
394
419
|
}
|
|
395
420
|
|
|
@@ -405,7 +430,7 @@ if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
|
|
|
405
430
|
'start',
|
|
406
431
|
LGY_SCRIPT_PATH,
|
|
407
432
|
'--interpreter',
|
|
408
|
-
|
|
433
|
+
daemonPython,
|
|
409
434
|
'--name',
|
|
410
435
|
LGY_PM2_NAME,
|
|
411
436
|
'--update-env',
|
|
@@ -634,12 +659,18 @@ if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
|
|
|
634
659
|
repairHookRegistration({ fresh: true });
|
|
635
660
|
|
|
636
661
|
if (!procBeforeUpdate) {
|
|
662
|
+
const blocker = launchBlocker();
|
|
663
|
+
if (blocker) {
|
|
664
|
+
console.log(`\n${color.yellow}⚠${color.reset} ${blocker}\n`);
|
|
665
|
+
success('Update finished - the daemon was left stopped.\n');
|
|
666
|
+
process.exit(0);
|
|
667
|
+
}
|
|
637
668
|
info("Daemon wasn't running - starting it fresh...");
|
|
638
669
|
runPm2([
|
|
639
670
|
'start',
|
|
640
671
|
LGY_SCRIPT_PATH,
|
|
641
672
|
'--interpreter',
|
|
642
|
-
|
|
673
|
+
daemonPython,
|
|
643
674
|
'--name',
|
|
644
675
|
LGY_PM2_NAME,
|
|
645
676
|
'--update-env',
|
package/bin/pm2.js
CHANGED
|
@@ -10,6 +10,9 @@ function pm2Env() {
|
|
|
10
10
|
...process.env,
|
|
11
11
|
// pm2 gives Python a pipe not a TTY, so it block-buffers stdout and can sit on log lines indefinitely - force line buffering.
|
|
12
12
|
PYTHONUNBUFFERED: '1',
|
|
13
|
+
// Without this Python inherits the console codepage (cp949 on Korean Windows) and loguru
|
|
14
|
+
// drops every line it can't encode - including the one verifyStartup waits for.
|
|
15
|
+
PYTHONIOENCODING: 'utf-8',
|
|
13
16
|
// pm2 merges --update-env rather than replacing, so a LOG_LEVEL from an earlier run
|
|
14
17
|
// survives unless a value is passed every time.
|
|
15
18
|
LOG_LEVEL: process.env.LOG_LEVEL || 'INFO',
|
package/bin/setup.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const p = require('@clack/prompts');
|
|
2
2
|
const { spawnSync } = require('child_process');
|
|
3
|
-
const {
|
|
3
|
+
const { daemonPython } = require('../npm-scripts/venv-paths');
|
|
4
4
|
const { PM2_BIN, PM2_CWD, pm2Env } = require('./pm2');
|
|
5
5
|
const {
|
|
6
6
|
getSettings,
|
|
@@ -224,7 +224,7 @@ function startOrRestartDaemon(pm2Name, scriptPath, label) {
|
|
|
224
224
|
'start',
|
|
225
225
|
scriptPath,
|
|
226
226
|
'--interpreter',
|
|
227
|
-
|
|
227
|
+
daemonPython,
|
|
228
228
|
'--name',
|
|
229
229
|
pm2Name,
|
|
230
230
|
]);
|
package/hooks/hook.js
CHANGED
|
@@ -12,7 +12,9 @@ const APPROVE_PORT = 18080;
|
|
|
12
12
|
const TIMEOUT_MS = 3600 * 1000;
|
|
13
13
|
|
|
14
14
|
function emit(payload) {
|
|
15
|
-
|
|
15
|
+
// Exits explicitly: the keep-alive socket and its hour-long timer stay open after the
|
|
16
|
+
// response arrives, and agy SIGABRTs the process instead of waiting for them to expire.
|
|
17
|
+
process.stdout.write(JSON.stringify(payload), () => process.exit(0));
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
function loadApproveToken() {
|
package/hooks/stop_hook.js
CHANGED
|
@@ -40,6 +40,10 @@ module.exports = {
|
|
|
40
40
|
isWin,
|
|
41
41
|
venvBinDir,
|
|
42
42
|
python: venvBin('python'),
|
|
43
|
+
// Only for pm2's --interpreter: pm2 spawns with detached:true, which on Windows forces a
|
|
44
|
+
// console window and ignores its own windowsHide option. pythonw is GUI-subsystem so no
|
|
45
|
+
// console is ever allocated, and pm2 pipes stdio anyway so no output is lost.
|
|
46
|
+
daemonPython: venvBin(isWin ? 'pythonw' : 'python'),
|
|
43
47
|
pip: venvBin('pip'),
|
|
44
48
|
preCommit: venvBin('pre-commit'),
|
|
45
49
|
};
|
package/package.json
CHANGED
package/src/cogs/general_cog.py
CHANGED
|
@@ -51,7 +51,7 @@ async def fetch_models_background():
|
|
|
51
51
|
import time
|
|
52
52
|
|
|
53
53
|
try:
|
|
54
|
-
from config import AGY_BIN
|
|
54
|
+
from config import AGY_BIN, CREATION_FLAGS
|
|
55
55
|
|
|
56
56
|
logger.debug(f"Starting fetch_models_background using AGY_BIN: {AGY_BIN}")
|
|
57
57
|
env = os.environ.copy()
|
|
@@ -63,6 +63,7 @@ async def fetch_models_background():
|
|
|
63
63
|
stderr=asyncio.subprocess.PIPE,
|
|
64
64
|
stdin=asyncio.subprocess.DEVNULL,
|
|
65
65
|
env=env,
|
|
66
|
+
creationflags=CREATION_FLAGS,
|
|
66
67
|
)
|
|
67
68
|
try:
|
|
68
69
|
stdout, stderr = await asyncio.wait_for(p.communicate(), timeout=30.0)
|
package/src/config.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import secrets
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
|
|
5
7
|
from core.atomic_io import atomic_write_json, safe_load_json
|
|
@@ -158,7 +160,22 @@ MODEL_CHOICES = {
|
|
|
158
160
|
"pro": "Gemini 3.1 Pro",
|
|
159
161
|
}
|
|
160
162
|
|
|
161
|
-
|
|
163
|
+
|
|
164
|
+
def _resolve_agy_bin() -> str:
|
|
165
|
+
fallback = str(Path.home() / ".local/bin/agy")
|
|
166
|
+
for candidate in (os.getenv("AGY_BIN_PATH"), fallback):
|
|
167
|
+
if candidate and Path(candidate).is_file():
|
|
168
|
+
return candidate
|
|
169
|
+
# Windows installs agy under AppData and adds it to PATH instead.
|
|
170
|
+
return shutil.which("agy") or fallback
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# Must stay in sync with findAgyBin() in bin/cli.js.
|
|
174
|
+
AGY_BIN = _resolve_agy_bin()
|
|
175
|
+
|
|
176
|
+
# pm2 spawns the daemon with detached:true, so on Windows it owns no console; without this
|
|
177
|
+
# flag every child it launches gets a console window of its own.
|
|
178
|
+
CREATION_FLAGS = subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0
|
|
162
179
|
|
|
163
180
|
session_manager = SessionManager(DATA_DIR)
|
|
164
181
|
|
package/src/core/agy_runner.py
CHANGED
|
@@ -163,7 +163,7 @@ async def run_agy(
|
|
|
163
163
|
if os.name == "nt":
|
|
164
164
|
import subprocess
|
|
165
165
|
|
|
166
|
-
kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
166
|
+
kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.CREATE_NO_WINDOW
|
|
167
167
|
elif hasattr(os, "setsid"):
|
|
168
168
|
kwargs["preexec_fn"] = os.setsid
|
|
169
169
|
|
package/src/main_discord.py
CHANGED
|
@@ -11,7 +11,7 @@ from functools import partial
|
|
|
11
11
|
import discord
|
|
12
12
|
from discord.ext import commands
|
|
13
13
|
|
|
14
|
-
from config import DISCORD_TOKEN, logger, session_manager
|
|
14
|
+
from config import CREATION_FLAGS, DISCORD_TOKEN, logger, session_manager
|
|
15
15
|
from handlers.message_router import handle_message
|
|
16
16
|
from messengers.discord_adapter import DiscordAdapter
|
|
17
17
|
from messengers.registry import register_adapter
|
|
@@ -128,7 +128,7 @@ async def status_updater_task():
|
|
|
128
128
|
|
|
129
129
|
|
|
130
130
|
def _spawn_voice_process(voice_dir: str) -> subprocess.Popen:
|
|
131
|
-
return subprocess.Popen(["node", "index.js"], cwd=voice_dir)
|
|
131
|
+
return subprocess.Popen(["node", "index.js"], cwd=voice_dir, creationflags=CREATION_FLAGS)
|
|
132
132
|
|
|
133
133
|
|
|
134
134
|
async def _supervise_voice_process(voice_dir: str):
|
package/src/services/response.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
from typing import Any
|
|
3
3
|
|
|
4
|
-
from config import MAX_EMBED_LEN,
|
|
4
|
+
from config import MAX_EMBED_LEN, bot_settings, session_manager
|
|
5
5
|
from messengers.registry import get_adapter_for_platform
|
|
6
6
|
from services.discord_helpers import split_message
|
|
7
7
|
from utils.utils import get_current_model
|
|
@@ -18,8 +18,8 @@ async def send_agy_response(
|
|
|
18
18
|
adapter = get_adapter_for_platform(session.get("platform", "discord"))
|
|
19
19
|
session_manager.save_sessions()
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
model_display =
|
|
21
|
+
# Same precedence as the /model autocomplete in general_cog.
|
|
22
|
+
model_display = session.get("model") or bot_settings.get("default_model") or get_current_model()
|
|
23
23
|
|
|
24
24
|
parts = split_message(response_text, MAX_EMBED_LEN)
|
|
25
25
|
for idx, part in enumerate(parts):
|