@openagents-org/agent-connector 0.2.7 → 0.2.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openagents-org/agent-connector",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Agent management CLI and library for OpenAgents — install, configure, and run AI coding agents",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Autostart — register agent-connector daemon as a system service.
3
+ *
4
+ * - macOS: launchd plist
5
+ * - Linux: systemd user unit
6
+ * - Windows: Task Scheduler XML
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { execSync } = require('child_process');
14
+ const { whichBinary, IS_WINDOWS } = require('./paths');
15
+
16
+ const IS_MACOS = process.platform === 'darwin';
17
+ const IS_LINUX = process.platform === 'linux' && !IS_MACOS;
18
+ const HOME = process.env.HOME || process.env.USERPROFILE || '';
19
+
20
+ const SERVICE_LABEL = 'org.openagents.connector';
21
+
22
+ /**
23
+ * Enable autostart on login.
24
+ */
25
+ function enable(configDir) {
26
+ const nodeBin = whichBinary('node') || process.execPath;
27
+ const cliPath = path.resolve(__dirname, '..', 'bin', 'agent-connector.js');
28
+
29
+ if (IS_MACOS) return _enableMacOS(nodeBin, cliPath, configDir);
30
+ if (IS_LINUX) return _enableLinux(nodeBin, cliPath, configDir);
31
+ if (IS_WINDOWS) return _enableWindows(nodeBin, cliPath, configDir);
32
+ throw new Error(`Autostart not supported on ${process.platform}`);
33
+ }
34
+
35
+ /**
36
+ * Disable autostart.
37
+ */
38
+ function disable() {
39
+ if (IS_MACOS) return _disableMacOS();
40
+ if (IS_LINUX) return _disableLinux();
41
+ if (IS_WINDOWS) return _disableWindows();
42
+ throw new Error(`Autostart not supported on ${process.platform}`);
43
+ }
44
+
45
+ /**
46
+ * Check if autostart is enabled.
47
+ */
48
+ function isEnabled() {
49
+ if (IS_MACOS) {
50
+ const plistPath = path.join(HOME, 'Library', 'LaunchAgents', `${SERVICE_LABEL}.plist`);
51
+ return fs.existsSync(plistPath);
52
+ }
53
+ if (IS_LINUX) {
54
+ const unitPath = path.join(HOME, '.config', 'systemd', 'user', 'openagents-connector.service');
55
+ return fs.existsSync(unitPath);
56
+ }
57
+ if (IS_WINDOWS) {
58
+ try {
59
+ execSync(`schtasks /Query /TN "OpenAgents Connector"`, { stdio: 'pipe', timeout: 5000 });
60
+ return true;
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+ return false;
66
+ }
67
+
68
+ // ---- macOS: launchd ----
69
+
70
+ function _enableMacOS(nodeBin, cliPath, configDir) {
71
+ const plistDir = path.join(HOME, 'Library', 'LaunchAgents');
72
+ fs.mkdirSync(plistDir, { recursive: true });
73
+
74
+ const plistPath = path.join(plistDir, `${SERVICE_LABEL}.plist`);
75
+ const logPath = path.join(configDir, 'daemon.log');
76
+
77
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
78
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
79
+ <plist version="1.0">
80
+ <dict>
81
+ <key>Label</key>
82
+ <string>${SERVICE_LABEL}</string>
83
+ <key>ProgramArguments</key>
84
+ <array>
85
+ <string>${nodeBin}</string>
86
+ <string>${cliPath}</string>
87
+ <string>up</string>
88
+ <string>--foreground</string>
89
+ </array>
90
+ <key>RunAtLoad</key>
91
+ <true/>
92
+ <key>KeepAlive</key>
93
+ <true/>
94
+ <key>StandardOutPath</key>
95
+ <string>${logPath}</string>
96
+ <key>StandardErrorPath</key>
97
+ <string>${logPath}</string>
98
+ <key>EnvironmentVariables</key>
99
+ <dict>
100
+ <key>PATH</key>
101
+ <string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
102
+ </dict>
103
+ </dict>
104
+ </plist>`;
105
+
106
+ fs.writeFileSync(plistPath, plist, 'utf-8');
107
+ execSync(`launchctl load -w "${plistPath}"`, { stdio: 'pipe', timeout: 5000 });
108
+ return { enabled: true, path: plistPath };
109
+ }
110
+
111
+ function _disableMacOS() {
112
+ const plistPath = path.join(HOME, 'Library', 'LaunchAgents', `${SERVICE_LABEL}.plist`);
113
+ try {
114
+ execSync(`launchctl unload "${plistPath}"`, { stdio: 'pipe', timeout: 5000 });
115
+ } catch {}
116
+ try { fs.unlinkSync(plistPath); } catch {}
117
+ return { enabled: false };
118
+ }
119
+
120
+ // ---- Linux: systemd user unit ----
121
+
122
+ function _enableLinux(nodeBin, cliPath, configDir) {
123
+ const unitDir = path.join(HOME, '.config', 'systemd', 'user');
124
+ fs.mkdirSync(unitDir, { recursive: true });
125
+
126
+ const unitPath = path.join(unitDir, 'openagents-connector.service');
127
+
128
+ const unit = `[Unit]
129
+ Description=OpenAgents Connector Daemon
130
+ After=network.target
131
+
132
+ [Service]
133
+ Type=simple
134
+ ExecStart=${nodeBin} ${cliPath} up --foreground
135
+ Restart=on-failure
136
+ RestartSec=10
137
+ Environment=PATH=/usr/local/bin:/usr/bin:/bin
138
+
139
+ [Install]
140
+ WantedBy=default.target
141
+ `;
142
+
143
+ fs.writeFileSync(unitPath, unit, 'utf-8');
144
+ execSync('systemctl --user daemon-reload', { stdio: 'pipe', timeout: 5000 });
145
+ execSync('systemctl --user enable openagents-connector.service', { stdio: 'pipe', timeout: 5000 });
146
+ execSync('systemctl --user start openagents-connector.service', { stdio: 'pipe', timeout: 5000 });
147
+ return { enabled: true, path: unitPath };
148
+ }
149
+
150
+ function _disableLinux() {
151
+ try {
152
+ execSync('systemctl --user stop openagents-connector.service', { stdio: 'pipe', timeout: 5000 });
153
+ } catch {}
154
+ try {
155
+ execSync('systemctl --user disable openagents-connector.service', { stdio: 'pipe', timeout: 5000 });
156
+ } catch {}
157
+ const unitPath = path.join(HOME, '.config', 'systemd', 'user', 'openagents-connector.service');
158
+ try { fs.unlinkSync(unitPath); } catch {}
159
+ return { enabled: false };
160
+ }
161
+
162
+ // ---- Windows: Task Scheduler ----
163
+
164
+ function _enableWindows(nodeBin, cliPath, configDir) {
165
+ const taskName = 'OpenAgents Connector';
166
+ const cmd = `schtasks /Create /SC ONLOGON /TN "${taskName}" /TR "\\"${nodeBin}\\" \\"${cliPath}\\" up --foreground" /RL HIGHEST /F`;
167
+ execSync(cmd, { stdio: 'pipe', timeout: 10000 });
168
+ return { enabled: true, method: 'Task Scheduler' };
169
+ }
170
+
171
+ function _disableWindows() {
172
+ try {
173
+ execSync('schtasks /Delete /TN "OpenAgents Connector" /F', { stdio: 'pipe', timeout: 5000 });
174
+ } catch {}
175
+ return { enabled: false };
176
+ }
177
+
178
+ module.exports = { enable, disable, isEnabled };
package/src/cli.js CHANGED
@@ -327,6 +327,17 @@ async function cmdLogs(connector, flags, positional) {
327
327
  }
328
328
  }
329
329
 
330
+ async function cmdAutostart(connector, flags) {
331
+ const autostart = require('./autostart');
332
+ if (flags.disable) {
333
+ autostart.disable();
334
+ print('Autostart disabled.');
335
+ } else {
336
+ const result = autostart.enable(connector._config ? connector._config.configDir : require('path').join(require('os').homedir(), '.openagents'));
337
+ print(`Autostart enabled.${result.path ? ` Config: ${result.path}` : ''}`);
338
+ }
339
+ }
340
+
330
341
  async function cmdWorkspace(connector, flags, positional) {
331
342
  const sub = positional[0] || 'list';
332
343
  const subArgs = positional.slice(1);
@@ -461,6 +472,7 @@ Commands:
461
472
  connect <agent> <token> Connect agent to workspace
462
473
  disconnect <agent> Disconnect agent from workspace
463
474
  env <type> [--set K=V] View/set env vars for agent type
475
+ autostart [--disable] Enable/disable auto-start on login
464
476
  test-llm <type> Test LLM connection
465
477
  logs [agent] [--lines N] View daemon logs
466
478
  workspace create [name] Create a new workspace
@@ -502,6 +514,7 @@ async function main() {
502
514
  connect: () => cmdConnect(connector, flags, positional),
503
515
  disconnect: () => cmdDisconnect(connector, flags, positional),
504
516
  logs: () => cmdLogs(connector, flags, positional),
517
+ autostart: () => cmdAutostart(connector, flags),
505
518
  workspace: () => cmdWorkspace(connector, flags, positional),
506
519
  env: () => cmdEnv(connector, flags, positional),
507
520
  'test-llm': () => cmdTestLLM(connector, flags, positional),
package/src/installer.js CHANGED
@@ -94,6 +94,60 @@ class Installer {
94
94
  return { success: true, output };
95
95
  }
96
96
 
97
+ /**
98
+ * Install with streaming output via callback.
99
+ * @param {string} agentType
100
+ * @param {function(string)} onData - called with each chunk of output
101
+ * @returns {Promise<{success: boolean, command: string}>}
102
+ */
103
+ async installStreaming(agentType, onData) {
104
+ const { spawn } = require('child_process');
105
+ const entry = this.registry.getEntry(agentType);
106
+ if (!entry || !entry.install) {
107
+ throw new Error(`No install definition for agent type: ${agentType}`);
108
+ }
109
+
110
+ let cmd = this._getInstallCommand(entry.install);
111
+ if (!cmd) {
112
+ throw new Error(`No install command for ${agentType} on ${Installer.platform()}`);
113
+ }
114
+
115
+ // Add verbose logging for npm so user sees download progress on stderr
116
+ if (cmd.includes('npm install') && !cmd.includes('--loglevel')) {
117
+ cmd = cmd.replace('npm install', 'npm install --loglevel=verbose');
118
+ }
119
+
120
+ if (onData) onData(`$ ${cmd}\n\n`);
121
+
122
+ const env = this._buildShellEnv();
123
+ const shell = process.platform === 'win32'
124
+ ? (process.env.ComSpec || 'C:\\Windows\\System32\\cmd.exe')
125
+ : true;
126
+
127
+ return new Promise((resolve, reject) => {
128
+ const proc = spawn(cmd, [], { shell, env, stdio: ['ignore', 'pipe', 'pipe'] });
129
+
130
+ if (proc.stdout) proc.stdout.setEncoding('utf-8');
131
+ if (proc.stderr) proc.stderr.setEncoding('utf-8');
132
+
133
+ if (proc.stdout) proc.stdout.on('data', (d) => { if (onData) onData(d); });
134
+ if (proc.stderr) proc.stderr.on('data', (d) => { if (onData) onData(d); });
135
+
136
+ proc.on('error', (err) => reject(err));
137
+ proc.on('close', (code) => {
138
+ if (code === 0) {
139
+ this._markInstalled(agentType);
140
+ if (onData) onData(`\nDone! ${agentType} is now installed.\n`);
141
+ resolve({ success: true, command: cmd });
142
+ } else {
143
+ const msg = `Install failed with exit code ${code}`;
144
+ if (onData) onData(`\n${msg}\n`);
145
+ reject(new Error(msg));
146
+ }
147
+ });
148
+ });
149
+ }
150
+
97
151
  /**
98
152
  * Uninstall an agent runtime.
99
153
  * @returns {Promise<{success: boolean, output: string}>}
@@ -115,6 +169,58 @@ class Installer {
115
169
  return { success: true, output };
116
170
  }
117
171
 
172
+ /**
173
+ * Uninstall with streaming output via callback.
174
+ */
175
+ async uninstallStreaming(agentType, onData) {
176
+ const { spawn } = require('child_process');
177
+ const entry = this.registry.getEntry(agentType);
178
+ if (!entry || !entry.install) {
179
+ throw new Error(`No install definition for agent type: ${agentType}`);
180
+ }
181
+
182
+ const installCmd = this._getInstallCommand(entry.install);
183
+ let cmd = this._deriveUninstallCommand(installCmd);
184
+ if (!cmd) {
185
+ throw new Error(`Cannot derive uninstall command for ${agentType}`);
186
+ }
187
+
188
+ // Add verbose logging for npm so user sees progress on stderr
189
+ if (cmd.includes('npm uninstall') && !cmd.includes('--loglevel')) {
190
+ cmd = cmd.replace('npm uninstall', 'npm uninstall --loglevel=verbose');
191
+ }
192
+
193
+ if (onData) onData(`$ ${cmd}\n\n`);
194
+
195
+ const env = this._buildShellEnv();
196
+ const shell = process.platform === 'win32'
197
+ ? (process.env.ComSpec || 'C:\\Windows\\System32\\cmd.exe')
198
+ : true;
199
+
200
+ return new Promise((resolve, reject) => {
201
+ const proc = spawn(cmd, [], { shell, env, stdio: ['ignore', 'pipe', 'pipe'] });
202
+
203
+ if (proc.stdout) proc.stdout.setEncoding('utf-8');
204
+ if (proc.stderr) proc.stderr.setEncoding('utf-8');
205
+
206
+ if (proc.stdout) proc.stdout.on('data', (d) => { if (onData) onData(d); });
207
+ if (proc.stderr) proc.stderr.on('data', (d) => { if (onData) onData(d); });
208
+
209
+ proc.on('error', (err) => reject(err));
210
+ proc.on('close', (code) => {
211
+ if (code === 0) {
212
+ this._markUninstalled(agentType);
213
+ if (onData) onData(`\nDone! ${agentType} has been uninstalled.\n`);
214
+ resolve({ success: true, command: cmd });
215
+ } else {
216
+ const msg = `Uninstall failed with exit code ${code}`;
217
+ if (onData) onData(`\n${msg}\n`);
218
+ reject(new Error(msg));
219
+ }
220
+ });
221
+ });
222
+ }
223
+
118
224
  /**
119
225
  * Get install command for current platform.
120
226
  */
@@ -223,25 +329,43 @@ class Installer {
223
329
  } catch {}
224
330
  }
225
331
 
226
- // -- Shell exec --
332
+ // -- Shell env + exec --
333
+
334
+ _buildShellEnv() {
335
+ const env = { ...process.env };
336
+ const sep = process.platform === 'win32' ? ';' : ':';
337
+ const extraDirs = [];
338
+ try { extraDirs.push(path.dirname(process.execPath)); } catch {}
339
+ if (process.platform === 'win32') {
340
+ const appData = env.APPDATA || '';
341
+ if (appData) extraDirs.push(path.join(appData, 'npm'));
342
+ extraDirs.push(env.ProgramFiles ? path.join(env.ProgramFiles, 'nodejs') : 'C:\\Program Files\\nodejs');
343
+ extraDirs.push(env.SystemRoot ? path.join(env.SystemRoot, 'System32') : 'C:\\Windows\\System32');
344
+ extraDirs.push(env.ProgramFiles ? path.join(env.ProgramFiles, 'Git', 'cmd') : 'C:\\Program Files\\Git\\cmd');
345
+ } else {
346
+ extraDirs.push('/usr/local/bin', '/opt/homebrew/bin');
347
+ }
348
+ for (const d of extraDirs) {
349
+ if (d && !(env.PATH || '').includes(d)) {
350
+ env.PATH = d + sep + (env.PATH || '');
351
+ }
352
+ }
353
+ return env;
354
+ }
227
355
 
228
356
  _execShell(cmd, timeoutMs = 300000) {
229
357
  return new Promise((resolve, reject) => {
230
- const env = getEnhancedEnv();
231
- // Also include Electron's own binary dir
232
- const execDir = path.dirname(process.execPath);
233
- if (execDir && !(env.PATH || '').includes(execDir)) {
234
- const sep = process.platform === 'win32' ? ';' : ':';
235
- env.PATH = execDir + sep + (env.PATH || '');
236
- }
358
+ const env = this._buildShellEnv();
237
359
 
238
- // On Windows, force UTF-8 codepage to avoid GBK garbled output
239
- const shellCmd = process.platform === 'win32' ? `chcp 65001 >nul && ${cmd}` : cmd;
360
+ let shell = true;
361
+ if (process.platform === 'win32') {
362
+ shell = env.ComSpec || 'C:\\Windows\\System32\\cmd.exe';
363
+ }
240
364
 
241
- exec(shellCmd, {
365
+ exec(cmd, {
242
366
  encoding: 'utf-8',
243
367
  timeout: timeoutMs,
244
- shell: true,
368
+ shell,
245
369
  env,
246
370
  }, (error, stdout, stderr) => {
247
371
  const output = ((stdout || '') + '\n' + (stderr || '')).trim();
package/src/paths.js CHANGED
@@ -36,11 +36,19 @@ function getExtraBinDirs() {
36
36
  // Common: ~/.local/bin (pipx, user installs)
37
37
  _push(dirs, path.join(HOME, '.local', 'bin'));
38
38
 
39
+ // Also add the directory containing the current node binary
40
+ try {
41
+ const nodeDir = path.dirname(process.execPath);
42
+ if (nodeDir) _push(dirs, nodeDir);
43
+ } catch {}
44
+
39
45
  // Filter to existing directories only, deduplicate
40
46
  const seen = new Set();
41
47
  const currentPATH = process.env.PATH || '';
42
48
  return dirs.filter(d => {
43
- if (!d || seen.has(d) || currentPATH.includes(d)) return false;
49
+ if (!d || seen.has(d)) return false;
50
+ // Skip if already in PATH (case-insensitive on Windows)
51
+ if (IS_WINDOWS ? currentPATH.toLowerCase().includes(d.toLowerCase()) : currentPATH.includes(d)) return false;
44
52
  seen.add(d);
45
53
  try {
46
54
  return fs.statSync(d).isDirectory();
@@ -71,10 +79,14 @@ function getEnhancedEnv(baseEnv) {
71
79
  }
72
80
  if (IS_WINDOWS) {
73
81
  // Force UTF-8 output from child processes on non-English Windows locales
74
- // (prevents GBK/Shift-JIS garbled text in error messages)
75
82
  env.PYTHONIOENCODING = env.PYTHONIOENCODING || 'utf-8';
76
83
  env.PYTHONUTF8 = env.PYTHONUTF8 || '1';
77
84
  env.LANG = env.LANG || 'en_US.UTF-8';
85
+ // Ensure ComSpec points to cmd.exe (Electron may not set it)
86
+ if (!env.ComSpec) {
87
+ const sysRoot = env.SystemRoot || 'C:\\Windows';
88
+ env.ComSpec = path.join(sysRoot, 'System32', 'cmd.exe');
89
+ }
78
90
  }
79
91
  return env;
80
92
  }
@@ -104,6 +116,10 @@ function _addWindowsPaths(dirs) {
104
116
  const appData = process.env.APPDATA || '';
105
117
  const localAppData = process.env.LOCALAPPDATA || '';
106
118
  const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
119
+ const sysRoot = process.env.SystemRoot || 'C:\\Windows';
120
+
121
+ // System32 (cmd.exe, powershell, etc) — Electron may not have it
122
+ _push(dirs, path.join(sysRoot, 'System32'));
107
123
 
108
124
  // npm global bin
109
125
  if (appData) _push(dirs, path.join(appData, 'npm'));