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 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", data=payload, headers={"Content-Type": "application/json"}
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 };
@@ -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 exactly why dev-only setup (git
6
- // hooks, lint tooling) lives here instead of postinstall.js, which runs
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 `npm install` again once the venv exists, or set it up manually.',
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 postinstall.js already
22
- // created (prepare always runs after postinstall in npm's lifecycle
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.6",
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: { 'Content-Type': 'application/json' },
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: { 'Content-Type': 'application/octet-stream' },
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: { 'Content-Type': 'application/json' },
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: { 'Content-Type': 'application/json' },
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: { 'Content-Type': 'application/json' },
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,
@@ -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();