agent-relay 2.1.6 → 2.1.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.
Files changed (52) hide show
  1. package/README.md +9 -1
  2. package/dist/index.cjs +78 -13
  3. package/dist/src/cli/commands/doctor.d.ts.map +1 -1
  4. package/dist/src/cli/commands/doctor.js +32 -7
  5. package/dist/src/cli/commands/doctor.js.map +1 -1
  6. package/dist/src/cli/index.d.ts.map +1 -1
  7. package/dist/src/cli/index.js +260 -27
  8. package/dist/src/cli/index.js.map +1 -1
  9. package/install.sh +545 -34
  10. package/package.json +18 -18
  11. package/packages/api-types/package.json +1 -1
  12. package/packages/benchmark/package.json +4 -4
  13. package/packages/bridge/package.json +8 -8
  14. package/packages/cli-tester/package.json +1 -1
  15. package/packages/config/package.json +2 -2
  16. package/packages/continuity/package.json +2 -2
  17. package/packages/daemon/dist/server.d.ts.map +1 -1
  18. package/packages/daemon/dist/server.js +45 -4
  19. package/packages/daemon/dist/server.js.map +1 -1
  20. package/packages/daemon/package.json +12 -12
  21. package/packages/daemon/src/server.ts +42 -4
  22. package/packages/hooks/package.json +4 -4
  23. package/packages/mcp/dist/bin.js +34 -3
  24. package/packages/mcp/dist/bin.js.map +1 -1
  25. package/packages/mcp/dist/install.d.ts.map +1 -1
  26. package/packages/mcp/dist/install.js +7 -8
  27. package/packages/mcp/dist/install.js.map +1 -1
  28. package/packages/mcp/package.json +4 -4
  29. package/packages/mcp/src/bin.ts +39 -3
  30. package/packages/mcp/src/install.ts +7 -8
  31. package/packages/memory/package.json +2 -2
  32. package/packages/policy/package.json +2 -2
  33. package/packages/protocol/package.json +1 -1
  34. package/packages/resiliency/package.json +1 -1
  35. package/packages/sdk/package.json +3 -3
  36. package/packages/spawner/package.json +1 -1
  37. package/packages/state/package.json +1 -1
  38. package/packages/storage/dist/sqlite-adapter.d.ts +1 -0
  39. package/packages/storage/dist/sqlite-adapter.d.ts.map +1 -1
  40. package/packages/storage/dist/sqlite-adapter.js +31 -3
  41. package/packages/storage/dist/sqlite-adapter.js.map +1 -1
  42. package/packages/storage/package.json +2 -2
  43. package/packages/storage/src/sqlite-adapter.ts +34 -6
  44. package/packages/telemetry/package.json +1 -1
  45. package/packages/trajectory/package.json +2 -2
  46. package/packages/user-directory/package.json +2 -2
  47. package/packages/utils/package.json +3 -3
  48. package/packages/wrapper/dist/relay-pty-orchestrator.d.ts.map +1 -1
  49. package/packages/wrapper/dist/relay-pty-orchestrator.js +12 -0
  50. package/packages/wrapper/dist/relay-pty-orchestrator.js.map +1 -1
  51. package/packages/wrapper/package.json +6 -6
  52. package/packages/wrapper/src/relay-pty-orchestrator.ts +12 -0
@@ -27,29 +27,79 @@ import fs from 'node:fs';
27
27
  import path from 'node:path';
28
28
  import readline from 'node:readline';
29
29
  import { promisify } from 'node:util';
30
- import { exec, spawn as spawnProcess } from 'node:child_process';
30
+ import { exec, execSync, spawn as spawnProcess } from 'node:child_process';
31
31
  import { fileURLToPath } from 'node:url';
32
32
  /**
33
- * Start dashboard via npx (downloads and runs if not installed).
33
+ * Find the dashboard binary if installed as standalone.
34
+ * Checks PATH and common installation locations.
35
+ */
36
+ function findDashboardBinary() {
37
+ const binaryName = 'relay-dashboard-server';
38
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
39
+ // Common locations to check
40
+ const searchPaths = [
41
+ // In PATH (using which/where)
42
+ binaryName,
43
+ // Common installation directories
44
+ path.join(homeDir, '.local', 'bin', binaryName),
45
+ path.join(homeDir, '.agent-relay', 'bin', binaryName),
46
+ '/usr/local/bin/' + binaryName,
47
+ ];
48
+ for (const searchPath of searchPaths) {
49
+ try {
50
+ // For absolute paths, check if file exists and is executable
51
+ if (path.isAbsolute(searchPath)) {
52
+ if (fs.existsSync(searchPath)) {
53
+ fs.accessSync(searchPath, fs.constants.X_OK);
54
+ return searchPath;
55
+ }
56
+ }
57
+ else {
58
+ // For relative paths (just binary name), check if it's in PATH
59
+ const result = execSync(`which ${searchPath} 2>/dev/null || where ${searchPath} 2>nul`, { encoding: 'utf8' }).trim();
60
+ if (result) {
61
+ return result.split('\n')[0]; // Take first result
62
+ }
63
+ }
64
+ }
65
+ catch {
66
+ // Continue to next path
67
+ }
68
+ }
69
+ return null;
70
+ }
71
+ /**
72
+ * Start dashboard (prefers standalone binary, falls back to npx).
34
73
  * Returns the spawned child process, port, and a promise that resolves when ready.
35
74
  */
36
75
  function startDashboardViaNpx(options) {
37
- console.log('Starting dashboard via npx (this may take a moment on first run)...');
38
- const dashboardProcess = spawnProcess('npx', [
39
- '--yes',
40
- '@agent-relay/dashboard-server',
76
+ const dashboardBinary = findDashboardBinary();
77
+ let dashboardProcess;
78
+ const args = [
41
79
  '--integrated',
42
80
  '--port', String(options.port),
43
81
  '--data-dir', options.dataDir,
44
82
  '--team-dir', options.teamDir,
45
83
  '--project-root', options.projectRoot,
46
- ], {
47
- stdio: ['ignore', 'pipe', 'pipe'],
48
- env: {
49
- ...process.env,
50
- // Pass any additional env vars needed
51
- },
52
- });
84
+ ];
85
+ if (dashboardBinary) {
86
+ console.log(`Starting dashboard using binary: ${dashboardBinary}`);
87
+ dashboardProcess = spawnProcess(dashboardBinary, args, {
88
+ stdio: ['ignore', 'pipe', 'pipe'],
89
+ env: { ...process.env },
90
+ });
91
+ }
92
+ else {
93
+ console.log('Starting dashboard via npx (this may take a moment on first run)...');
94
+ dashboardProcess = spawnProcess('npx', [
95
+ '--yes',
96
+ '@agent-relay/dashboard-server',
97
+ ...args,
98
+ ], {
99
+ stdio: ['ignore', 'pipe', 'pipe'],
100
+ env: { ...process.env },
101
+ });
102
+ }
53
103
  // Promise that resolves when dashboard is ready (or after timeout)
54
104
  let resolveReady;
55
105
  const ready = new Promise((resolve) => {
@@ -148,9 +198,20 @@ function findPackageJson(startDir) {
148
198
  }
149
199
  throw new Error('Could not find package.json');
150
200
  }
151
- const packageJsonPath = findPackageJson(__dirname);
152
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
153
- const VERSION = packageJson.version;
201
+ // Get version: prefer build-time injected version (for standalone binaries), fall back to package.json
202
+ const VERSION = (() => {
203
+ const envVersion = process.env.AGENT_RELAY_VERSION;
204
+ if (envVersion)
205
+ return envVersion;
206
+ try {
207
+ const packageJsonPath = findPackageJson(__dirname);
208
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
209
+ return packageJson.version;
210
+ }
211
+ catch {
212
+ return 'unknown';
213
+ }
214
+ })();
154
215
  const execAsync = promisify(exec);
155
216
  // Check for updates in background (non-blocking)
156
217
  // Only show notification for interactive commands, not when wrapping agents or running update
@@ -691,21 +752,193 @@ program
691
752
  program
692
753
  .command('down')
693
754
  .description('Stop daemon')
694
- .action(async () => {
755
+ .option('--force', 'Force cleanup even if process is stuck')
756
+ .option('--all', 'Kill all agent-relay processes system-wide')
757
+ .option('--timeout <ms>', 'Timeout waiting for graceful shutdown', '5000')
758
+ .action(async (options) => {
695
759
  const paths = getProjectPaths();
696
760
  const pidPath = pidFilePathForSocket(paths.socketPath);
761
+ const timeout = parseInt(options.timeout ?? '5000', 10);
762
+ // Helper to check if process is running
763
+ const isProcessRunning = (pid) => {
764
+ try {
765
+ process.kill(pid, 0); // Signal 0 just checks if process exists
766
+ return true;
767
+ }
768
+ catch {
769
+ return false;
770
+ }
771
+ };
772
+ // Helper to wait for process to die
773
+ const waitForProcessExit = async (pid, timeoutMs) => {
774
+ const start = Date.now();
775
+ while (Date.now() - start < timeoutMs) {
776
+ if (!isProcessRunning(pid)) {
777
+ return true;
778
+ }
779
+ await new Promise(resolve => setTimeout(resolve, 100));
780
+ }
781
+ return false;
782
+ };
783
+ // Helper to clean up stale files
784
+ const cleanupStaleFiles = () => {
785
+ // Clean up socket file
786
+ if (fs.existsSync(paths.socketPath)) {
787
+ try {
788
+ fs.unlinkSync(paths.socketPath);
789
+ console.log('Cleaned up stale socket');
790
+ }
791
+ catch { /* ignore */ }
792
+ }
793
+ // Clean up pid file
794
+ if (fs.existsSync(pidPath)) {
795
+ try {
796
+ fs.unlinkSync(pidPath);
797
+ console.log('Cleaned up stale pid file');
798
+ }
799
+ catch { /* ignore */ }
800
+ }
801
+ // Clean up runtime.json
802
+ const runtimePath = path.join(paths.dataDir, 'runtime.json');
803
+ if (fs.existsSync(runtimePath)) {
804
+ try {
805
+ fs.unlinkSync(runtimePath);
806
+ console.log('Cleaned up runtime config');
807
+ }
808
+ catch { /* ignore */ }
809
+ }
810
+ // Clean up stale mcp-identity-* files (but not mcp-identity)
811
+ try {
812
+ const files = fs.readdirSync(paths.dataDir);
813
+ for (const file of files) {
814
+ if (file.startsWith('mcp-identity-')) {
815
+ const identityPath = path.join(paths.dataDir, file);
816
+ // Check if the PID in the filename is still running
817
+ const match = file.match(/mcp-identity-(\d+)/);
818
+ if (match) {
819
+ const identityPid = parseInt(match[1], 10);
820
+ if (!isProcessRunning(identityPid)) {
821
+ fs.unlinkSync(identityPath);
822
+ console.log(`Cleaned up stale identity file: ${file}`);
823
+ }
824
+ }
825
+ }
826
+ }
827
+ }
828
+ catch { /* ignore errors reading directory */ }
829
+ };
830
+ // Handle --all flag: kill all agent-relay processes system-wide
831
+ if (options.all) {
832
+ console.log('Stopping all agent-relay processes...');
833
+ const { execSync } = await import('node:child_process');
834
+ try {
835
+ // Find all agent-relay processes
836
+ const psOutput = execSync('ps aux', { encoding: 'utf-8' });
837
+ const lines = psOutput.split('\n');
838
+ const agentRelayProcesses = [];
839
+ for (const line of lines) {
840
+ // Match agent-relay daemon processes (not MCP servers or other commands)
841
+ // Look for "agent-relay up" or "agent-relay.*up" patterns
842
+ if ((line.includes('agent-relay') && line.includes(' up')) &&
843
+ !line.includes('grep') &&
844
+ !line.includes('agent-relay-mcp')) {
845
+ const parts = line.trim().split(/\s+/);
846
+ // ps aux format: USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
847
+ const pid = parseInt(parts[1], 10);
848
+ if (!isNaN(pid) && pid !== process.pid && pid > 0) {
849
+ agentRelayProcesses.push({ pid, cmd: parts.slice(10).join(' ') });
850
+ }
851
+ }
852
+ }
853
+ if (agentRelayProcesses.length === 0) {
854
+ console.log('No agent-relay processes found');
855
+ }
856
+ else {
857
+ console.log(`Found ${agentRelayProcesses.length} agent-relay process(es)`);
858
+ for (const proc of agentRelayProcesses) {
859
+ try {
860
+ console.log(` Stopping PID ${proc.pid}...`);
861
+ process.kill(proc.pid, 'SIGTERM');
862
+ }
863
+ catch (err) {
864
+ if (err.code !== 'ESRCH') {
865
+ console.log(` Warning: ${err.message}`);
866
+ }
867
+ }
868
+ }
869
+ // Wait a bit for graceful shutdown
870
+ await new Promise(resolve => setTimeout(resolve, 2000));
871
+ // Force kill any remaining
872
+ if (options.force) {
873
+ for (const proc of agentRelayProcesses) {
874
+ if (isProcessRunning(proc.pid)) {
875
+ try {
876
+ console.log(` Force killing PID ${proc.pid}...`);
877
+ process.kill(proc.pid, 'SIGKILL');
878
+ }
879
+ catch { /* ignore */ }
880
+ }
881
+ }
882
+ }
883
+ console.log('Done');
884
+ }
885
+ }
886
+ catch (err) {
887
+ console.error(`Error finding processes: ${err.message}`);
888
+ }
889
+ return;
890
+ }
697
891
  if (!fs.existsSync(pidPath)) {
698
- console.log('Not running');
892
+ // No pid file, but might have stale files
893
+ if (options.force) {
894
+ cleanupStaleFiles();
895
+ console.log('Cleaned up (was not running)');
896
+ }
897
+ else {
898
+ console.log('Not running');
899
+ }
699
900
  return;
700
901
  }
701
902
  const pid = Number(fs.readFileSync(pidPath, 'utf-8').trim());
903
+ // Check if process is actually running
904
+ if (!isProcessRunning(pid)) {
905
+ cleanupStaleFiles();
906
+ console.log('Cleaned up stale state (process was not running)');
907
+ return;
908
+ }
909
+ // Try graceful shutdown
702
910
  try {
911
+ console.log(`Stopping daemon (pid: ${pid})...`);
703
912
  process.kill(pid, 'SIGTERM');
913
+ // Wait for graceful shutdown
914
+ const exited = await waitForProcessExit(pid, timeout);
915
+ if (!exited) {
916
+ if (options.force) {
917
+ console.log('Graceful shutdown timed out, forcing...');
918
+ try {
919
+ process.kill(pid, 'SIGKILL');
920
+ await waitForProcessExit(pid, 2000);
921
+ }
922
+ catch { /* ignore */ }
923
+ }
924
+ else {
925
+ console.log(`Graceful shutdown timed out after ${timeout}ms. Use --force to kill.`);
926
+ return;
927
+ }
928
+ }
929
+ // Clean up any remaining stale files
930
+ cleanupStaleFiles();
704
931
  console.log('Stopped');
705
932
  }
706
- catch {
707
- fs.unlinkSync(pidPath);
708
- console.log('Cleaned up stale pid');
933
+ catch (err) {
934
+ if (err.code === 'ESRCH') {
935
+ // Process doesn't exist
936
+ cleanupStaleFiles();
937
+ console.log('Cleaned up stale state');
938
+ }
939
+ else {
940
+ console.error(`Error stopping daemon: ${err.message}`);
941
+ }
709
942
  }
710
943
  });
711
944
  // System prompt for Dashboard agent - plain text to avoid shell escaping issues
@@ -1879,13 +2112,13 @@ program
1879
2112
  };
1880
2113
  // Try daemon socket first (preferred path)
1881
2114
  try {
1882
- const paths = getProjectPaths();
2115
+ const _paths = getProjectPaths();
1883
2116
  // TODO: Re-enable daemon-based spawning when client.spawn() is implemented
1884
2117
  // See: docs/SDK-MIGRATION-PLAN.md for planned implementation
1885
2118
  // For now, fall through to HTTP API
1886
2119
  throw new Error('Daemon-based spawn not yet implemented');
1887
2120
  }
1888
- catch (daemonErr) {
2121
+ catch {
1889
2122
  // Fall through to HTTP API
1890
2123
  // console.log('Daemon not available, trying HTTP API...');
1891
2124
  }
@@ -1933,9 +2166,9 @@ program
1933
2166
  const port = options.port || DEFAULT_DASHBOARD_PORT;
1934
2167
  // Try daemon socket first (preferred path)
1935
2168
  try {
1936
- const paths = getProjectPaths();
1937
- const client = new RelayClient({
1938
- socketPath: paths.socketPath,
2169
+ const _paths = getProjectPaths();
2170
+ const _client = new RelayClient({
2171
+ socketPath: _paths.socketPath,
1939
2172
  agentName: '__cli_releaser__',
1940
2173
  quiet: true,
1941
2174
  reconnect: false,
@@ -1948,7 +2181,7 @@ program
1948
2181
  // For now, fall through to HTTP API
1949
2182
  throw new Error('Daemon-based release not yet implemented');
1950
2183
  }
1951
- catch (daemonErr) {
2184
+ catch {
1952
2185
  // Fall through to HTTP API
1953
2186
  // console.log('Daemon not available, trying HTTP API...');
1954
2187
  }