linkgravity 1.5.6 → 1.5.7
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 +9 -0
- package/bin/setup.js +7 -0
- package/hooks/hook.py +14 -1
- package/npm-scripts/ensure-env.js +40 -0
- package/npm-scripts/prepare.js +5 -7
- package/package.json +1 -2
- package/src/api/server.py +11 -2
- package/src/config.py +6 -0
- package/voice-service/receiver.js +21 -5
- package/voice-service/tts.js +2 -1
- package/npm-scripts/postinstall.js +0 -48
package/bin/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
} = require('./platforms');
|
|
16
16
|
|
|
17
17
|
const { python: pythonExe, isWin } = require('../npm-scripts/venv-paths');
|
|
18
|
+
const { isEnvironmentReady } = require('../npm-scripts/ensure-env');
|
|
18
19
|
|
|
19
20
|
const cmd = process.argv[2];
|
|
20
21
|
|
|
@@ -378,6 +379,14 @@ if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
|
|
|
378
379
|
process.exit(1);
|
|
379
380
|
}
|
|
380
381
|
|
|
382
|
+
if (!isEnvironmentReady()) {
|
|
383
|
+
console.log(
|
|
384
|
+
`\n${color.yellow}⚠${color.reset} Python environment isn't set up yet - ` +
|
|
385
|
+
`run ${color.cyan}lgy setup${color.reset} first (it installs everything on its first run).\n`,
|
|
386
|
+
);
|
|
387
|
+
process.exit(1);
|
|
388
|
+
}
|
|
389
|
+
|
|
381
390
|
info('Starting LinkGravity daemon...');
|
|
382
391
|
runPm2(['start', LGY_SCRIPT_PATH, '--interpreter', pythonExe, '--name', LGY_PM2_NAME]);
|
|
383
392
|
verifyStartup().then((ok) => process.exit(ok ? 0 : 1));
|
package/bin/setup.js
CHANGED
|
@@ -446,6 +446,13 @@ async function runSetup() {
|
|
|
446
446
|
console.log();
|
|
447
447
|
p.intro(`${color.cyan}▶ LinkGravity Setup Wizard${color.reset}`);
|
|
448
448
|
|
|
449
|
+
const { ensureEnvironment, isEnvironmentReady } = require('../npm-scripts/ensure-env');
|
|
450
|
+
if (!isEnvironmentReady()) {
|
|
451
|
+
console.log();
|
|
452
|
+
ensureEnvironment();
|
|
453
|
+
console.log();
|
|
454
|
+
}
|
|
455
|
+
|
|
449
456
|
const registerHook = require('../npm-scripts/register-hook');
|
|
450
457
|
if (!registerHook.isHookRegistered()) {
|
|
451
458
|
const consent = await p.confirm({
|
package/hooks/hook.py
CHANGED
|
@@ -4,6 +4,17 @@ import os
|
|
|
4
4
|
import sys
|
|
5
5
|
import urllib.error
|
|
6
6
|
import urllib.request
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
LGY_CONFIG_FILE = Path.home() / ".gemini" / "linkgravity" / "lgy.json"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _load_approve_token():
|
|
13
|
+
try:
|
|
14
|
+
with open(LGY_CONFIG_FILE, encoding="utf-8") as f:
|
|
15
|
+
return json.load(f).get("approve_token", "")
|
|
16
|
+
except Exception:
|
|
17
|
+
return ""
|
|
7
18
|
|
|
8
19
|
|
|
9
20
|
def main():
|
|
@@ -34,7 +45,9 @@ def main():
|
|
|
34
45
|
).encode("utf-8")
|
|
35
46
|
|
|
36
47
|
req = urllib.request.Request(
|
|
37
|
-
"http://localhost:18080/approve",
|
|
48
|
+
"http://localhost:18080/approve",
|
|
49
|
+
data=payload,
|
|
50
|
+
headers={"Content-Type": "application/json", "X-LGY-Token": _load_approve_token()},
|
|
38
51
|
)
|
|
39
52
|
|
|
40
53
|
try:
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { execSync } = require('child_process');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const { pip: venvPip, python: venvPython, workspaceDir, repoRoot } = require('./venv-paths');
|
|
7
|
+
|
|
8
|
+
const isWin = os.platform() === 'win32';
|
|
9
|
+
const pyCmd = isWin ? 'python' : 'python3';
|
|
10
|
+
|
|
11
|
+
function isEnvironmentReady() {
|
|
12
|
+
return fs.existsSync(venvPython);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function ensureEnvironment() {
|
|
16
|
+
if (isEnvironmentReady()) return;
|
|
17
|
+
|
|
18
|
+
console.log('⚙️ Setting up Python Virtual Environment...');
|
|
19
|
+
console.log(
|
|
20
|
+
` (in ${path.join(workspaceDir, 'venv')} - not inside this install, so it survives`,
|
|
21
|
+
);
|
|
22
|
+
console.log(' package updates/reinstalls and works the same whether this is a global');
|
|
23
|
+
console.log(' `npm install -g linkgravity` or a local dev clone.)');
|
|
24
|
+
|
|
25
|
+
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
26
|
+
execSync(`${pyCmd} -m venv "${path.join(workspaceDir, 'venv')}"`, { stdio: 'inherit' });
|
|
27
|
+
|
|
28
|
+
console.log('📦 Installing Python dependencies...');
|
|
29
|
+
execSync(`"${venvPip}" install -r requirements.txt`, { stdio: 'inherit', cwd: repoRoot });
|
|
30
|
+
|
|
31
|
+
console.log('🎙️ Installing Voice Service dependencies...');
|
|
32
|
+
execSync('npm install', {
|
|
33
|
+
stdio: 'inherit',
|
|
34
|
+
cwd: path.join(repoRoot, 'voice-service'),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
console.log('✅ Environment ready.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { ensureEnvironment, isEnvironmentReady };
|
package/npm-scripts/prepare.js
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
// Runs at `prepare` time - which npm only triggers for a local `npm
|
|
3
3
|
// install` inside this repo (i.e. a git clone / contributor checkout),
|
|
4
4
|
// never for `npm install -g linkgravity` end users installing the published
|
|
5
|
-
// package from the registry. That's
|
|
6
|
-
//
|
|
7
|
-
// for everyone, including end users who don't need any of this.
|
|
5
|
+
// package from the registry. That's why dev-only setup (git hooks, lint
|
|
6
|
+
// tooling) lives here rather than running for every end user.
|
|
8
7
|
'use strict';
|
|
9
8
|
const { execSync } = require('child_process');
|
|
10
9
|
const fs = require('fs');
|
|
@@ -13,14 +12,13 @@ const { repoRoot, pip, preCommit } = require('./venv-paths');
|
|
|
13
12
|
if (!fs.existsSync(pip)) {
|
|
14
13
|
console.warn(
|
|
15
14
|
'⚠️ No venv found yet - skipping dev tooling install and git hook setup. ' +
|
|
16
|
-
'Run `
|
|
15
|
+
'Run `node bin/cli.js setup` to create it, then `npm install` again.',
|
|
17
16
|
);
|
|
18
17
|
return;
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
// 1. Dev-only Python tooling into the same venv
|
|
22
|
-
// created (
|
|
23
|
-
// order): ruff (editor/manual use) and pre-commit itself, which is
|
|
20
|
+
// 1. Dev-only Python tooling into the same venv `lgy setup` already
|
|
21
|
+
// created: ruff (editor/manual use) and pre-commit itself, which is
|
|
24
22
|
// what actually runs the hooks declared in .pre-commit-config.yaml.
|
|
25
23
|
try {
|
|
26
24
|
execSync(`"${pip}" install -r requirements-dev.txt`, { stdio: 'inherit', cwd: repoRoot });
|
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linkgravity",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.7",
|
|
4
4
|
"description": "Discord/Telegram bot bridge for the Antigravity (agy) CLI, with voice interaction support",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"postinstall": "node npm-scripts/postinstall.js",
|
|
7
6
|
"start": "node npm-scripts/run-dev.js",
|
|
8
7
|
"dev": "node npm-scripts/run-dev.js",
|
|
9
8
|
"format": "prettier --write \"bin/**/*.js\" \"npm-scripts/**/*.js\" \"voice-service/*.js\"",
|
package/src/api/server.py
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
from aiohttp import web
|
|
2
2
|
|
|
3
|
-
from config import logger, session_manager
|
|
3
|
+
from config import bot_settings, logger, session_manager
|
|
4
|
+
|
|
5
|
+
LGY_TOKEN_HEADER = "X-LGY-Token"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@web.middleware
|
|
9
|
+
async def auth_middleware(request, handler):
|
|
10
|
+
if request.headers.get(LGY_TOKEN_HEADER) != bot_settings.get("approve_token"):
|
|
11
|
+
return web.json_response({"error": "unauthorized"}, status=403)
|
|
12
|
+
return await handler(request)
|
|
4
13
|
|
|
5
14
|
|
|
6
15
|
def is_tool_allowed(tool_name, tool_input):
|
|
@@ -20,7 +29,7 @@ def is_tool_allowed(tool_name, tool_input):
|
|
|
20
29
|
|
|
21
30
|
|
|
22
31
|
async def setup_webhook_server(bot):
|
|
23
|
-
app = web.Application(client_max_size=50 * 1024 * 1024)
|
|
32
|
+
app = web.Application(client_max_size=50 * 1024 * 1024, middlewares=[auth_middleware])
|
|
24
33
|
app["bot"] = bot
|
|
25
34
|
|
|
26
35
|
from api.ui_routes import handle_approve_request
|
package/src/config.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import secrets
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
|
|
4
5
|
from core.atomic_io import atomic_write_json, safe_load_json
|
|
@@ -30,6 +31,7 @@ DEFAULT_LGY_CONFIG = {
|
|
|
30
31
|
"tts_enabled": True,
|
|
31
32
|
# Sticky default for /new sessions, set whenever /model succeeds.
|
|
32
33
|
"default_model": "",
|
|
34
|
+
"approve_token": "",
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
|
|
@@ -71,6 +73,10 @@ def save_bot_settings(data):
|
|
|
71
73
|
|
|
72
74
|
bot_settings = load_bot_settings()
|
|
73
75
|
|
|
76
|
+
if not bot_settings.get("approve_token"):
|
|
77
|
+
bot_settings["approve_token"] = secrets.token_hex(24)
|
|
78
|
+
save_bot_settings(bot_settings)
|
|
79
|
+
|
|
74
80
|
from core.logger import init_logger
|
|
75
81
|
from core.session_manager import SessionManager
|
|
76
82
|
|
|
@@ -5,6 +5,7 @@ const { googleSTT } = require('./stt');
|
|
|
5
5
|
const { getDetectorForUser, feedPCMToDetector, WAKE_MATCH_THRESHOLD } = require('./wakeword');
|
|
6
6
|
const { interruptTTS } = require('./tts');
|
|
7
7
|
const state = require('./state');
|
|
8
|
+
const { aglConfig } = require('./config');
|
|
8
9
|
const { activeStreams, enrollingUsers, isPlaying, wakeWordOptedOut, runtime, isGuildActive } =
|
|
9
10
|
state;
|
|
10
11
|
|
|
@@ -108,7 +109,10 @@ function setupReceiver(connection, guildId, client) {
|
|
|
108
109
|
partialSent = true;
|
|
109
110
|
fetch('http://127.0.0.1:18080/stt_partial', {
|
|
110
111
|
method: 'POST',
|
|
111
|
-
headers: {
|
|
112
|
+
headers: {
|
|
113
|
+
'Content-Type': 'application/json',
|
|
114
|
+
'X-LGY-Token': aglConfig.approve_token,
|
|
115
|
+
},
|
|
112
116
|
body: JSON.stringify({ guild_id: guildId, text }),
|
|
113
117
|
}).catch((err) =>
|
|
114
118
|
console.error(`[STT] Failed to send partial text to Python:`, err.message),
|
|
@@ -248,7 +252,10 @@ function setupReceiver(connection, guildId, client) {
|
|
|
248
252
|
`http://127.0.0.1:18080/enroll_sample?user_id=${encodeURIComponent(userId)}`,
|
|
249
253
|
{
|
|
250
254
|
method: 'POST',
|
|
251
|
-
headers: {
|
|
255
|
+
headers: {
|
|
256
|
+
'Content-Type': 'application/octet-stream',
|
|
257
|
+
'X-LGY-Token': aglConfig.approve_token,
|
|
258
|
+
},
|
|
252
259
|
body: wavBuffer,
|
|
253
260
|
},
|
|
254
261
|
);
|
|
@@ -263,7 +270,10 @@ function setupReceiver(connection, guildId, client) {
|
|
|
263
270
|
if (partialSent) {
|
|
264
271
|
fetch('http://127.0.0.1:18080/stt_partial_cancel', {
|
|
265
272
|
method: 'POST',
|
|
266
|
-
headers: {
|
|
273
|
+
headers: {
|
|
274
|
+
'Content-Type': 'application/json',
|
|
275
|
+
'X-LGY-Token': aglConfig.approve_token,
|
|
276
|
+
},
|
|
267
277
|
body: JSON.stringify({ guild_id: guildId }),
|
|
268
278
|
}).catch(() => {});
|
|
269
279
|
}
|
|
@@ -277,7 +287,10 @@ function setupReceiver(connection, guildId, client) {
|
|
|
277
287
|
if (partialSent) {
|
|
278
288
|
fetch('http://127.0.0.1:18080/stt_partial_cancel', {
|
|
279
289
|
method: 'POST',
|
|
280
|
-
headers: {
|
|
290
|
+
headers: {
|
|
291
|
+
'Content-Type': 'application/json',
|
|
292
|
+
'X-LGY-Token': aglConfig.approve_token,
|
|
293
|
+
},
|
|
281
294
|
body: JSON.stringify({ guild_id: guildId }),
|
|
282
295
|
}).catch(() => {});
|
|
283
296
|
}
|
|
@@ -291,7 +304,10 @@ function setupReceiver(connection, guildId, client) {
|
|
|
291
304
|
try {
|
|
292
305
|
await fetch('http://127.0.0.1:18080/stt_input', {
|
|
293
306
|
method: 'POST',
|
|
294
|
-
headers: {
|
|
307
|
+
headers: {
|
|
308
|
+
'Content-Type': 'application/json',
|
|
309
|
+
'X-LGY-Token': aglConfig.approve_token,
|
|
310
|
+
},
|
|
295
311
|
body: JSON.stringify({
|
|
296
312
|
user_id: userId,
|
|
297
313
|
guild_id: guildId,
|
package/voice-service/tts.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { Readable } = require('stream');
|
|
2
2
|
const { createAudioPlayer, createAudioResource, AudioPlayerStatus } = require('@discordjs/voice');
|
|
3
3
|
const { players, audioQueues, isPlaying, connections, suppressNotifyMap } = require('./state');
|
|
4
|
+
const { aglConfig } = require('./config');
|
|
4
5
|
|
|
5
6
|
function interruptTTS(guildId) {
|
|
6
7
|
const player = players.get(guildId);
|
|
@@ -25,7 +26,7 @@ async function notifyTtsFinished(guild_id) {
|
|
|
25
26
|
try {
|
|
26
27
|
await fetch('http://127.0.0.1:18080/tts_finished', {
|
|
27
28
|
method: 'POST',
|
|
28
|
-
headers: { 'Content-Type': 'application/json' },
|
|
29
|
+
headers: { 'Content-Type': 'application/json', 'X-LGY-Token': aglConfig.approve_token },
|
|
29
30
|
body: JSON.stringify({ guild_id }),
|
|
30
31
|
});
|
|
31
32
|
} catch (err) {
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
const { execSync } = require('child_process');
|
|
2
|
-
const os = require('os');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const { pip: venvPip, workspaceDir } = require('./venv-paths');
|
|
6
|
-
|
|
7
|
-
console.log('⚙️ Setting up Python Virtual Environment...');
|
|
8
|
-
console.log(` (in ${path.join(workspaceDir, 'venv')} - not inside this install, so it survives`);
|
|
9
|
-
console.log(' package updates/reinstalls and works the same whether this is a global');
|
|
10
|
-
console.log(' `npm install -g linkgravity` or a local dev clone.)');
|
|
11
|
-
|
|
12
|
-
// System python (not the venv's) - only used to create the venv below; every other
|
|
13
|
-
// script goes through venv-paths.js instead.
|
|
14
|
-
const isWin = os.platform() === 'win32';
|
|
15
|
-
const pyCmd = isWin ? 'python' : 'python3';
|
|
16
|
-
|
|
17
|
-
async function main() {
|
|
18
|
-
try {
|
|
19
|
-
// Fixed workspace dir, not cwd - see venv-paths.js for why.
|
|
20
|
-
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
21
|
-
execSync(`${pyCmd} -m venv "${path.join(workspaceDir, 'venv')}"`, { stdio: 'inherit' });
|
|
22
|
-
|
|
23
|
-
console.log('📦 Installing Python dependencies...');
|
|
24
|
-
execSync(`"${venvPip}" install -r requirements.txt`, { stdio: 'inherit' });
|
|
25
|
-
|
|
26
|
-
console.log('🎙️ Installing Voice Service dependencies...');
|
|
27
|
-
execSync('npm install', {
|
|
28
|
-
stdio: 'inherit',
|
|
29
|
-
cwd: path.join(__dirname, '..', 'voice-service'),
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// Own try/catch: agy may not be installed yet on a brand new machine, and that shouldn't fail the rest of the install.
|
|
33
|
-
try {
|
|
34
|
-
require('./register-hook')({ allowFirstTimeCreate: false });
|
|
35
|
-
} catch (err) {
|
|
36
|
-
console.warn(
|
|
37
|
-
`⚠️ Couldn't register the agy tool-approval hook: ${err.message.split('\n')[0]}`,
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
console.log('✅ Installation complete!');
|
|
42
|
-
} catch (error) {
|
|
43
|
-
console.error('❌ Installation failed. Please ensure Python 3.10+ is installed.');
|
|
44
|
-
process.exit(1);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
main();
|