linkgravity 1.7.1 → 1.7.3

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
@@ -32,9 +32,12 @@ function success(msg) {
32
32
  console.log(`${color.green}✔${color.reset} ${msg}`);
33
33
  }
34
34
 
35
- // Resolved rather than run through npx: pm2 is a direct dependency, and npx wraps it in
36
- // "npm exec" + "sh -c", which leaves the real process orphaned when we try to kill it.
37
- const PM2_BIN = require.resolve('pm2/bin/pm2');
35
+ const { PM2_BIN, PM2_CWD, pm2Env } = require('./pm2');
36
+
37
+ // `npm` is npm.cmd on Windows, and node refuses to spawn a .cmd without a shell.
38
+ function runNpm(args, options = {}) {
39
+ return spawnSync('npm', args, { shell: isWin, ...options });
40
+ }
38
41
 
39
42
  function info(msg) {
40
43
  console.log(`\n${color.cyan}▶${color.reset} ${msg}`);
@@ -60,18 +63,8 @@ function runPm2(args, silent = true) {
60
63
  const stdioOpt = silent ? 'pipe' : 'inherit';
61
64
  const result = spawnSync(process.execPath, [PM2_BIN, ...args], {
62
65
  stdio: stdioOpt,
63
- cwd: path.join(__dirname, '..'),
64
- env: {
65
- ...process.env,
66
- // pm2 gives Python a pipe not a TTY, so it block-buffers stdout and can sit on log lines indefinitely - force line buffering.
67
- PYTHONUNBUFFERED: '1',
68
- // pm2 merges --update-env rather than replacing, so a LOG_LEVEL from an earlier run
69
- // survives unless a value is passed every time.
70
- LOG_LEVEL: process.env.LOG_LEVEL || 'INFO',
71
- // Version managers (fnm, nvm) put node on PATH from a shell hook the daemon never runs,
72
- // so the bot's own `node` lookup for voice-service would fail without this.
73
- PATH: `${path.dirname(process.execPath)}${path.delimiter}${process.env.PATH || ''}`,
74
- },
66
+ cwd: PM2_CWD,
67
+ env: pm2Env(),
75
68
  });
76
69
 
77
70
  if (result.error) {
@@ -148,7 +141,7 @@ function colorizeLevel(line) {
148
141
  }
149
142
 
150
143
  function runPm2LogsStream(args, printLine) {
151
- const cp = spawn(process.execPath, [PM2_BIN, ...args], { cwd: path.join(__dirname, '..') });
144
+ const cp = spawn(process.execPath, [PM2_BIN, ...args], { cwd: PM2_CWD });
152
145
 
153
146
  const isNoise = (line) =>
154
147
  line.trim().length === 0 ||
@@ -207,7 +200,7 @@ function verifyStartup() {
207
200
  );
208
201
 
209
202
  let cp = spawn(process.execPath, [PM2_BIN, 'logs', LGY_PM2_NAME, '--raw', '--lines', '0'], {
210
- cwd: path.join(__dirname, '..'),
203
+ cwd: PM2_CWD,
211
204
  });
212
205
 
213
206
  let settled = false;
@@ -359,10 +352,7 @@ function isAutostartEnabled() {
359
352
  }
360
353
 
361
354
  function checkLatestVersionFast(currentVersion) {
362
- const view = spawnSync('npm', ['view', 'linkgravity', 'version'], {
363
- stdio: 'pipe',
364
- timeout: 3000,
365
- });
355
+ const view = runNpm(['view', 'linkgravity', 'version'], { stdio: 'pipe', timeout: 3000 });
366
356
  if (view.error || view.status !== 0) return null;
367
357
  const latest = view.stdout.toString().trim();
368
358
  if (!latest) return null;
@@ -609,7 +599,7 @@ if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
609
599
  const currentVersion = pkg.version;
610
600
 
611
601
  info('Checking npm for the latest version...');
612
- const viewResult = spawnSync('npm', ['view', 'linkgravity', 'version'], { stdio: 'pipe' });
602
+ const viewResult = runNpm(['view', 'linkgravity', 'version'], { stdio: 'pipe' });
613
603
  if (viewResult.error || viewResult.status !== 0) {
614
604
  console.error(
615
605
  (viewResult.stderr || '').toString().trim() ||
@@ -629,9 +619,7 @@ if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
629
619
  const wasOnline = !!procBeforeUpdate && procBeforeUpdate.pm2_env.status === 'online';
630
620
 
631
621
  info(`Updating: v${currentVersion} -> v${latestVersion}...`);
632
- const installResult = spawnSync('npm', ['install', '-g', 'linkgravity@latest'], {
633
- stdio: 'inherit',
634
- });
622
+ const installResult = runNpm(['install', '-g', 'linkgravity@latest'], { stdio: 'inherit' });
635
623
  if (installResult.status !== 0) {
636
624
  console.error('npm install failed - update aborted, still on the old version.');
637
625
  process.exit(1);
package/bin/pm2.js ADDED
@@ -0,0 +1,22 @@
1
+ const path = require('path');
2
+
3
+ // Not `npx pm2`: its `npm exec` + `sh -c` wrapping survives cp.kill() as a zombie, and LPM
4
+ // Firewall reads it as a runtime package install.
5
+ const PM2_BIN = require.resolve('pm2/bin/pm2');
6
+ const PM2_CWD = path.join(__dirname, '..');
7
+
8
+ function pm2Env() {
9
+ return {
10
+ ...process.env,
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
+ PYTHONUNBUFFERED: '1',
13
+ // pm2 merges --update-env rather than replacing, so a LOG_LEVEL from an earlier run
14
+ // survives unless a value is passed every time.
15
+ LOG_LEVEL: process.env.LOG_LEVEL || 'INFO',
16
+ // Version managers (fnm, nvm) put node on PATH from a shell hook the daemon never runs,
17
+ // so the bot's own `node` lookup for voice-service would fail without this.
18
+ PATH: `${path.dirname(process.execPath)}${path.delimiter}${process.env.PATH || ''}`,
19
+ };
20
+ }
21
+
22
+ module.exports = { PM2_BIN, PM2_CWD, pm2Env };
package/bin/setup.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const p = require('@clack/prompts');
2
2
  const { spawnSync } = require('child_process');
3
3
  const { python: pythonExe } = require('../npm-scripts/venv-paths');
4
+ const { PM2_BIN, PM2_CWD, pm2Env } = require('./pm2');
4
5
  const {
5
6
  getSettings,
6
7
  updateSettings,
@@ -178,11 +179,19 @@ async function collectUserIds(existingIds, platformLabel, required = false) {
178
179
  return ids;
179
180
  }
180
181
 
182
+ function runPm2(args) {
183
+ return spawnSync(process.execPath, [PM2_BIN, ...args], {
184
+ stdio: 'pipe',
185
+ cwd: PM2_CWD,
186
+ env: pm2Env(),
187
+ });
188
+ }
189
+
181
190
  function stopDaemon(pm2Name, label) {
182
191
  console.log(`${color.cyan}▶${color.reset} Stopping ${label} daemon...`);
183
- spawnSync('npx', ['-y', 'pm2', 'delete', pm2Name], { stdio: 'pipe' });
192
+ runPm2(['delete', pm2Name]);
184
193
 
185
- const jlist = spawnSync('npx', ['-y', 'pm2', 'jlist'], { stdio: 'pipe' });
194
+ const jlist = runPm2(['jlist']);
186
195
  let stillRunning = false;
187
196
  if (jlist.status === 0) {
188
197
  try {
@@ -194,17 +203,14 @@ function stopDaemon(pm2Name, label) {
194
203
  p.outro(`${label} daemon stopped.`);
195
204
  } else {
196
205
  p.outro(
197
- `${color.yellow}⚠${color.reset} ${label} daemon is still running - run \`npx pm2 delete ${pm2Name}\` manually and check \`npx pm2 list\`.`,
206
+ `${color.yellow}⚠${color.reset} ${label} daemon is still running - run \`lgy stop\` manually and check \`lgy status\`.`,
198
207
  );
199
208
  }
200
209
  }
201
210
 
202
211
  function startOrRestartDaemon(pm2Name, scriptPath, label) {
203
212
  console.log(`${color.cyan}▶${color.reset} Restarting ${label} daemon to apply changes...`);
204
- const restartResult = spawnSync('npx', ['-y', 'pm2', 'restart', pm2Name, '--update-env'], {
205
- stdio: 'pipe',
206
- env: { ...process.env, PYTHONUNBUFFERED: '1' },
207
- });
213
+ const restartResult = runPm2(['restart', pm2Name, '--update-env']);
208
214
 
209
215
  if (restartResult.status === 0) {
210
216
  p.outro(`${label} daemon restarted.`);
@@ -214,11 +220,14 @@ function startOrRestartDaemon(pm2Name, scriptPath, label) {
214
220
  const stderr = (restartResult.stderr || '').toString();
215
221
  if (stderr.includes('not found')) {
216
222
  // Nothing to restart yet - start it instead of a false "restarted".
217
- const startResult = spawnSync(
218
- 'npx',
219
- ['-y', 'pm2', 'start', scriptPath, '--interpreter', pythonExe, '--name', pm2Name],
220
- { stdio: 'pipe', env: { ...process.env, PYTHONUNBUFFERED: '1' } },
221
- );
223
+ const startResult = runPm2([
224
+ 'start',
225
+ scriptPath,
226
+ '--interpreter',
227
+ pythonExe,
228
+ '--name',
229
+ pm2Name,
230
+ ]);
222
231
  if (startResult.status === 0) {
223
232
  p.outro(`${label} daemon wasn't running yet - started it fresh instead.`);
224
233
  } else {
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
- process.stdout.write(JSON.stringify(payload));
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() {
@@ -19,7 +19,7 @@ function log(line) {
19
19
  }
20
20
 
21
21
  function emit(payload) {
22
- process.stdout.write(JSON.stringify(payload));
22
+ process.stdout.write(JSON.stringify(payload), () => process.exit(0));
23
23
  }
24
24
 
25
25
  async function readStdin() {
@@ -3,7 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
- const { repoRoot, workspaceDir } = require('./venv-paths');
6
+ const { repoRoot, workspaceDir, isWin } = require('./venv-paths');
7
7
 
8
8
  const hooksJsonPath = path.join(os.homedir(), '.gemini', 'config', 'hooks.json');
9
9
 
@@ -59,12 +59,13 @@ function loadHooksConfig() {
59
59
  return { hooks: {} };
60
60
  }
61
61
  try {
62
- return JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8'));
62
+ // Windows tools write a BOM, which JSON.parse rejects.
63
+ return JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8').replace(/^\uFEFF/, ''));
63
64
  } catch (err) {
64
65
  const backupPath = `${hooksJsonPath}.corrupted-${Date.now()}`;
65
66
  fs.copyFileSync(hooksJsonPath, backupPath);
66
67
  console.warn(
67
- `⚠️ ${hooksJsonPath} was invalid JSON - backed up to ${backupPath} and starting fresh.`,
68
+ `⚠️ ${hooksJsonPath} was invalid JSON - copied to ${backupPath}. Any hooks it held, including other tools', are about to be replaced.`,
68
69
  );
69
70
  return { hooks: {} };
70
71
  }
@@ -168,7 +169,8 @@ function registerHook({ allowFirstTimeCreate = true, quiet = false } = {}) {
168
169
 
169
170
  for (const reg of HOOK_REGISTRATIONS) {
170
171
  const scriptPath = path.join(installedHooksDir, reg.fileName);
171
- const command = `${NODE_CMD} "${scriptPath}"`;
172
+ // Windows agy takes everything after the first token as one argument; POSIX agy uses a shell.
173
+ const command = isWin ? `${NODE_CMD} ${scriptPath}` : `${NODE_CMD} "${scriptPath}"`;
172
174
  const hookEntry = findHookEntry(config, reg.eventType, reg.name, reg.wrapInMatcher);
173
175
  const isNew = !hookEntry.command;
174
176
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linkgravity",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "description": "Discord/Telegram bot bridge for the Antigravity (agy) CLI, with voice interaction support",
5
5
  "scripts": {
6
6
  "start": "node npm-scripts/run-dev.js",