@openagents-org/agent-launcher 0.2.141 → 0.2.143

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-launcher",
3
- "version": "0.2.141",
3
+ "version": "0.2.143",
4
4
  "description": "OpenAgents Launcher — install, configure, and run AI coding agents from your terminal",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -93,7 +93,10 @@ class BaseAdapter {
93
93
 
94
94
  // Sync workspace-managed skills into disabledModules
95
95
  try {
96
- const agents = await this.client.getAgents(this.workspaceId, this.token);
96
+ const agents = await Promise.race([
97
+ this.client.getAgents(this.workspaceId, this.token),
98
+ new Promise((_, reject) => setTimeout(() => reject(new Error('skill sync timed out (10s)')), 10000)),
99
+ ]);
97
100
  const self = agents.find((a) => a.agentName === this.agentName);
98
101
  if (self && self.enabledSkills) {
99
102
  const { skillsToDisabledModules } = require('../skill-catalog');
@@ -73,6 +73,10 @@ class ClaudeAdapter extends BaseAdapter {
73
73
  async _onControlAction(action, payload) {
74
74
  if (action === 'stop') {
75
75
  const channel = (payload && typeof payload === 'object') ? payload.channel : null;
76
+ if (channel) {
77
+ const pp = this._persistentProcs[channel];
78
+ if (pp) pp.userStopped = true;
79
+ }
76
80
  if (channel && this._channelProcesses[channel]) {
77
81
  this._log(`Stopping process for channel=${channel}`);
78
82
  this._stoppingChannels.add(channel);
@@ -84,6 +88,7 @@ class ClaudeAdapter extends BaseAdapter {
84
88
  await this.sendResponse(channel, 'Execution stopped by user.');
85
89
  } catch {}
86
90
  } else {
91
+ for (const pp of Object.values(this._persistentProcs)) pp.userStopped = true;
87
92
  await this._stopAllProcesses('Execution stopped by user.');
88
93
  }
89
94
  return;
@@ -226,7 +231,7 @@ class ClaudeAdapter extends BaseAdapter {
226
231
  */
227
232
  async _buildChannelRecap(channelName, currentMessage) {
228
233
  const messages = await this.client.getRecentMessages(
229
- this.workspaceId, channelName, this.token, 30
234
+ this.workspaceId, channelName, this.token, 60
230
235
  );
231
236
  if (!messages || messages.length === 0) return null;
232
237
 
@@ -236,20 +241,16 @@ class ClaudeAdapter extends BaseAdapter {
236
241
  if (mt === 'status' || mt === 'thinking' || mt === 'loading') continue;
237
242
  const text = (m.content || '').trim();
238
243
  if (!text) continue;
239
- // Don't echo the user's current message back at them.
240
244
  if (text === currentMessage) continue;
241
245
  const who = m.senderType === 'human'
242
246
  ? (m.senderName || 'user')
243
247
  : (m.senderName || 'agent');
244
- // Cap each line so a single huge paste doesn't blow up the prompt.
245
- const truncated = text.length > 800 ? text.slice(0, 800) + '…' : text;
248
+ const truncated = text.length > 2000 ? text.slice(0, 2000) + '…' : text;
246
249
  lines.push(`[${who}] ${truncated}`);
247
250
  }
248
251
  if (lines.length === 0) return null;
249
252
 
250
- // Keep only the tail; older context has diminishing value and we
251
- // don't want to balloon the system prompt.
252
- const tail = lines.slice(-15).join('\n');
253
+ const tail = lines.slice(-20).join('\n');
253
254
  return (
254
255
  'You previously worked in this channel but your prior session is no ' +
255
256
  'longer available, so here is the recent conversation for context:\n\n' +
@@ -728,6 +729,7 @@ class ClaudeAdapter extends BaseAdapter {
728
729
  lastStdoutTime: Date.now(),
729
730
  watchdogTimer: null,
730
731
  awaitingToolResult: false,
732
+ userStopped: false,
731
733
  };
732
734
 
733
735
  if (proc.stderr) {
@@ -803,6 +805,20 @@ class ClaudeAdapter extends BaseAdapter {
803
805
  if (pp.messageResolve) {
804
806
  pp.messageResolve({ resultEvent: event });
805
807
  pp.messageResolve = null;
808
+ } else {
809
+ // Background-triggered turn: the CLI produced output on its own after
810
+ // the user-initiated turn already resolved (e.g. a run_in_background
811
+ // task finished and fed its result back to the model). That text was
812
+ // streamed as `thinking` only — with no messageResolve waiting,
813
+ // _handleMessage never posts it as a `chat`, so the thread visually
814
+ // hangs on "thinking…/Working…". Finalize the accumulated text as a
815
+ // real answer so the turn settles on an actual message bubble.
816
+ const trailing = pp.lastResponseText.join('\n').trim();
817
+ if (trailing && !event.is_error) {
818
+ this._resetIdleTimer(pp.msgChannel);
819
+ try { await this.sendResponse(pp.msgChannel, trailing); } catch {}
820
+ }
821
+ pp.lastResponseText.length = 0;
806
822
  }
807
823
  } else if (eventType === 'system') {
808
824
  const subtype = event.subtype || '';
@@ -983,6 +999,12 @@ class ClaudeAdapter extends BaseAdapter {
983
999
  }
984
1000
  return;
985
1001
  }
1002
+ if (existingPP.userStopped) {
1003
+ if (!existingPP.everPostedAnything) {
1004
+ try { await this.sendResponse(msgChannel, 'Execution stopped by user.'); } catch {}
1005
+ }
1006
+ return;
1007
+ }
986
1008
  // Process died mid-message — fall through to spawn a fresh one
987
1009
  this._log(`Persistent process died, falling back to fresh spawn for ${msgChannel}`);
988
1010
  }
@@ -1034,11 +1056,22 @@ class ClaudeAdapter extends BaseAdapter {
1034
1056
 
1035
1057
  // Spawn a persistent process and send the first message via stdin
1036
1058
  let effectiveContent = content;
1059
+
1060
+ // When starting without a session (no --resume), prepend channel
1061
+ // history so the fresh CLI has conversation context.
1062
+ if (!this._channelSessions[msgChannel]) {
1063
+ try {
1064
+ const recap = await this._buildChannelRecap(msgChannel, content);
1065
+ if (recap) effectiveContent = `${recap}\n\n---\n\n${content}`;
1066
+ } catch {}
1067
+ }
1068
+
1037
1069
  for (let attempt = 0; attempt < 2; attempt++) {
1038
1070
  if (mcpConfigFile) { try { fs.unlinkSync(mcpConfigFile); } catch {} mcpConfigFile = null; }
1039
1071
 
1040
1072
  if (attempt > 0) {
1041
1073
  this._killPersistentProc(msgChannel);
1074
+ // Rebuild recap for retry without resume
1042
1075
  try {
1043
1076
  const recap = await this._buildChannelRecap(msgChannel, content);
1044
1077
  if (recap) effectiveContent = `${recap}\n\n---\n\n${content}`;
@@ -1065,7 +1098,13 @@ class ClaudeAdapter extends BaseAdapter {
1065
1098
  const result = await this._sendToPersistentProc(pp, effectiveContent);
1066
1099
 
1067
1100
  if (result.exited) {
1068
- this._log(`Process exited during first message (attempt ${attempt + 1})`);
1101
+ this._log(`Process exited during first message (attempt ${attempt + 1}), userStopped=${pp.userStopped}`);
1102
+ if (pp.userStopped) {
1103
+ if (!pp.everPostedAnything) {
1104
+ try { await this.sendResponse(msgChannel, 'Execution stopped by user.'); } catch {}
1105
+ }
1106
+ break;
1107
+ }
1069
1108
  if (attempt === 0 && this._channelSessions[msgChannel]) {
1070
1109
  this._log(`Stale session detected, retrying without resume`);
1071
1110
  delete this._channelSessions[msgChannel];
@@ -285,6 +285,13 @@ class CursorAdapter extends BaseAdapter {
285
285
  if (jsMatch) {
286
286
  return [nodeBin, path.resolve(cmdDir, jsMatch[1])];
287
287
  }
288
+ // .cmd shims that forward to a native .exe — resolve to the exe and spawn
289
+ // it directly. Wrapping such a .cmd in `cmd.exe /c` caps the command line
290
+ // at cmd.exe's 8191-char limit, truncating long args (e.g. system prompts).
291
+ const exeMatch = cmdContent.match(/%dp0%\\([^\s"*?]+\.exe)/i);
292
+ if (exeMatch) {
293
+ return [path.resolve(cmdDir, exeMatch[1])];
294
+ }
288
295
  } else {
289
296
  try {
290
297
  let target = binPath;
@@ -424,7 +431,7 @@ class CursorAdapter extends BaseAdapter {
424
431
  try {
425
432
  const resolved = this._resolveToNodeCmd(cmd[0]);
426
433
  if (resolved) {
427
- cmd = [resolved[0], resolved[1], ...cmd.slice(1)];
434
+ cmd = [...resolved, ...cmd.slice(1)];
428
435
  } else if (IS_WINDOWS && cmd[0].toLowerCase().endsWith('.cmd')) {
429
436
  cmd = ['cmd.exe', '/c', ...cmd];
430
437
  }
@@ -122,6 +122,13 @@ class GeminiAdapter extends BaseAdapter {
122
122
  if (jsMatch) {
123
123
  return [nodeBin, path.resolve(cmdDir, jsMatch[1])];
124
124
  }
125
+ // .cmd shims that forward to a native .exe — resolve to the exe and spawn
126
+ // it directly. Wrapping such a .cmd in `cmd.exe /c` caps the command line
127
+ // at cmd.exe's 8191-char limit, truncating long args (e.g. system prompts).
128
+ const exeMatch = cmdContent.match(/%dp0%\\([^\s"*?]+\.exe)/i);
129
+ if (exeMatch) {
130
+ return [path.resolve(cmdDir, exeMatch[1])];
131
+ }
125
132
  } else {
126
133
  try {
127
134
  let target = binPath;
@@ -265,7 +272,7 @@ class GeminiAdapter extends BaseAdapter {
265
272
  try {
266
273
  const resolved = this._resolveToNodeCmd(cmd[0]);
267
274
  if (resolved) {
268
- cmd = [resolved[0], resolved[1], ...cmd.slice(1)];
275
+ cmd = [...resolved, ...cmd.slice(1)];
269
276
  } else if (IS_WINDOWS && cmd[0].toLowerCase().endsWith('.cmd')) {
270
277
  cmd = ['cmd.exe', '/c', ...cmd];
271
278
  }
@@ -45,6 +45,10 @@ class OpenCodeAdapter extends BaseAdapter {
45
45
  this._migrateSessionsFile();
46
46
  this._loadSessions();
47
47
 
48
+ // Process tracking for stop control
49
+ this._channelProcesses = {}; // channel → child process
50
+ this._stoppingChannels = new Set();
51
+
48
52
  this._opencodeBinary = this._findOpencodeBinary();
49
53
  if (this._opencodeBinary) {
50
54
  this._log(`Using OpenCode subprocess mode: ${this._opencodeBinary}`);
@@ -91,6 +95,32 @@ class OpenCodeAdapter extends BaseAdapter {
91
95
  } catch {}
92
96
  }
93
97
 
98
+ async _onControlAction(action, payload) {
99
+ if (action === 'stop') {
100
+ const channel = (payload && typeof payload === 'object') ? payload.channel : null;
101
+ if (channel) {
102
+ const proc = this._channelProcesses[channel];
103
+ const hadQueuedWork = !!this._channelQueues[channel]?.length;
104
+ if (proc) {
105
+ this._log(`Stopping process for channel=${channel}`);
106
+ this._stoppingChannels.add(channel);
107
+ await this._stopProcess(proc);
108
+ delete this._channelProcesses[channel];
109
+ }
110
+ delete this._channelQueues[channel];
111
+ if (proc || hadQueuedWork) {
112
+ try {
113
+ await this.sendResponse(channel, 'Execution stopped by user.');
114
+ } catch {}
115
+ }
116
+ } else {
117
+ await this._stopAllProcesses('Execution stopped by user.');
118
+ }
119
+ return;
120
+ }
121
+ await super._onControlAction(action, payload);
122
+ }
123
+
94
124
  /**
95
125
  * Write workspace skill to OpenCode's skill directory for auto-discovery.
96
126
  */
@@ -223,6 +253,11 @@ class OpenCodeAdapter extends BaseAdapter {
223
253
  try {
224
254
  const responseText = await this._runOpencode(content, msgChannel);
225
255
 
256
+ if (this._stoppingChannels.has(msgChannel)) {
257
+ this._stoppingChannels.delete(msgChannel);
258
+ return;
259
+ }
260
+
226
261
  if (responseText) {
227
262
  await this.sendResponse(msgChannel, responseText);
228
263
  } else {
@@ -234,6 +269,10 @@ class OpenCodeAdapter extends BaseAdapter {
234
269
  );
235
270
  }
236
271
  } catch (e) {
272
+ if (this._stoppingChannels.has(msgChannel)) {
273
+ this._stoppingChannels.delete(msgChannel);
274
+ return;
275
+ }
237
276
  this._log(`Error handling message: ${e.message}`);
238
277
  await this.sendError(msgChannel, `Error processing message: ${e.message}`);
239
278
  }
@@ -256,12 +295,12 @@ class OpenCodeAdapter extends BaseAdapter {
256
295
  // Find matching brace
257
296
  let depth = 0;
258
297
  let inStr = false;
259
- let escape = false;
298
+ let escaped = false;
260
299
  let start = pos;
261
300
  for (let i = pos; i < raw.length; i++) {
262
301
  const ch = raw[i];
263
- if (escape) { escape = false; continue; }
264
- if (ch === '\\' && inStr) { escape = true; continue; }
302
+ if (escaped) { escaped = false; continue; }
303
+ if (ch === '\\' && inStr) { escaped = true; continue; }
265
304
  if (ch === '"') { inStr = !inStr; continue; }
266
305
  if (inStr) continue;
267
306
  if (ch === '{') depth++;
@@ -360,6 +399,72 @@ class OpenCodeAdapter extends BaseAdapter {
360
399
  }
361
400
  }
362
401
 
402
+ /**
403
+ * Override BaseAdapter.stop so daemon shutdown also tears down in-flight
404
+ * opencode subprocesses cleanly.
405
+ */
406
+ stop() {
407
+ this._stopAllProcesses(
408
+ 'Task interrupted — daemon restarting. Send another message to continue.'
409
+ ).catch(() => {});
410
+ super.stop();
411
+ }
412
+
413
+ async _stopProcess(proc) {
414
+ if (!proc || proc.exitCode !== null) return;
415
+ try {
416
+ if (IS_WINDOWS) {
417
+ try { proc.kill('SIGINT'); } catch {}
418
+ const exited = await new Promise((resolve) => {
419
+ if (proc.exitCode !== null) {
420
+ resolve(true);
421
+ return;
422
+ }
423
+ const timeout = setTimeout(() => resolve(false), 1500);
424
+ proc.once('exit', () => { clearTimeout(timeout); resolve(true); });
425
+ });
426
+ if (!exited) {
427
+ try { execSync(`taskkill /F /T /PID ${proc.pid}`, { timeout: 5000 }); } catch {}
428
+ }
429
+ } else {
430
+ try { process.kill(-proc.pid, 'SIGTERM'); } catch {
431
+ proc.kill('SIGTERM');
432
+ }
433
+ await new Promise((resolve) => {
434
+ let done = false;
435
+ const finish = () => {
436
+ if (done) return;
437
+ done = true;
438
+ resolve();
439
+ };
440
+ const timeout = setTimeout(() => {
441
+ try { process.kill(-proc.pid, 'SIGKILL'); } catch {
442
+ proc.kill('SIGKILL');
443
+ }
444
+ const reapTimeout = setTimeout(finish, 1000);
445
+ proc.once('exit', () => { clearTimeout(reapTimeout); finish(); });
446
+ }, 1500);
447
+ proc.once('exit', () => { clearTimeout(timeout); finish(); });
448
+ });
449
+ }
450
+ } catch {}
451
+ }
452
+
453
+ async _stopAllProcesses(completionMessage = 'Execution stopped.') {
454
+ const entries = Object.entries(this._channelProcesses);
455
+ if (!entries.length) return;
456
+ this._log(`Stopping ${entries.length} running process(es)...`);
457
+ for (const [channel, proc] of entries) {
458
+ this._stoppingChannels.add(channel);
459
+ await this._stopProcess(proc);
460
+ delete this._channelProcesses[channel];
461
+ delete this._channelQueues[channel];
462
+ try {
463
+ await this.sendResponse(channel, completionMessage);
464
+ } catch {}
465
+ }
466
+ }
467
+
363
468
  // ------------------------------------------------------------------
364
469
  // Subprocess execution
365
470
  // ------------------------------------------------------------------
@@ -444,16 +549,26 @@ class OpenCodeAdapter extends BaseAdapter {
444
549
  env: spawnEnv,
445
550
  cwd: this.agentHome,
446
551
  timeout: TIMEOUT_MS,
552
+ detached: !IS_WINDOWS,
447
553
  // Never let a console window appear. Besides being ugly, an attached
448
554
  // console makes opencode start its interactive TUI and hang instead of
449
555
  // running headless — the root cause of "stuck on thinking…" on Windows.
450
556
  windowsHide: true,
451
557
  });
452
558
 
559
+ this._channelProcesses[msgChannel] = proc;
560
+
453
561
  let stdout = '';
454
562
  let stderr = '';
455
563
  let settled = false;
456
- const finish = (fn, arg) => { if (!settled) { settled = true; fn(arg); } };
564
+ const finish = (fn, arg) => {
565
+ if (settled) return;
566
+ settled = true;
567
+ if (this._channelProcesses[msgChannel] === proc) {
568
+ delete this._channelProcesses[msgChannel];
569
+ }
570
+ fn(arg);
571
+ };
457
572
 
458
573
  if (proc.stdout) proc.stdout.on('data', (d) => { stdout += d; });
459
574
  if (proc.stderr) proc.stderr.on('data', (d) => { stderr += d; });
@@ -475,6 +590,11 @@ class OpenCodeAdapter extends BaseAdapter {
475
590
  // when the spawn timeout kills the child, code is null and signal is the
476
591
  // signal name — report that as a timeout rather than "exited with code null".
477
592
  proc.on('close', (code, signal) => {
593
+ if (this._stoppingChannels.has(msgChannel)) {
594
+ this._stoppingChannels.delete(msgChannel);
595
+ return finish(resolve, '');
596
+ }
597
+
478
598
  stdout = stdout.trim();
479
599
  stderr = stderr.trim();
480
600
 
package/src/cli.js CHANGED
@@ -41,6 +41,9 @@ function parseArgs(argv) {
41
41
  function getConnector(flags) {
42
42
  const opts = {};
43
43
  if (flags.config) opts.configDir = flags.config;
44
+ if (flags.endpoint || process.env.OPENAGENTS_ENDPOINT) {
45
+ opts.workspaceEndpoint = flags.endpoint || process.env.OPENAGENTS_ENDPOINT;
46
+ }
44
47
  return new AgentConnector(opts);
45
48
  }
46
49
 
@@ -358,6 +361,8 @@ async function cmdConnect(connector, flags, positional) {
358
361
  if (pid) {
359
362
  connector.sendDaemonCommand(`restart:${name}`);
360
363
  print('Daemon notified');
364
+ } else {
365
+ print('Daemon is not running. Run `agn up` to bring this agent online.');
361
366
  }
362
367
  } catch (e) {
363
368
  print(`Error: ${e.message}`);
package/src/daemon.js CHANGED
@@ -151,6 +151,14 @@ class Daemon {
151
151
  if (!this._adapters || !this._adapters[agentName]) break;
152
152
  await new Promise(r => setTimeout(r, 500));
153
153
  }
154
+ // Force-clear the adapter slot if it's still hanging. Without this,
155
+ // a hung adapter.run() promise prevents the slot from ever being
156
+ // released, and subsequent start/restart commands see "already running".
157
+ if (this._adapters && this._adapters[agentName]) {
158
+ this._log(`WARNING: ${agentName} adapter did not exit after stop — force-releasing slot`);
159
+ try { this._adapters[agentName].stop(); } catch {}
160
+ delete this._adapters[agentName];
161
+ }
154
162
  this._writeStatus();
155
163
  }
156
164
 
@@ -319,7 +327,17 @@ class Daemon {
319
327
  * Read daemon PID, returning null if not running.
320
328
  */
321
329
  static readDaemonPid(configDir) {
322
- return Daemon._readPid(path.join(configDir, 'daemon.pid'));
330
+ const pidFile = path.join(configDir, 'daemon.pid');
331
+ const statusFile = path.join(configDir, 'daemon.status.json');
332
+ const pid = Daemon._readPid(pidFile);
333
+ if (!pid) return null;
334
+
335
+ if (!Daemon._isAlive(pid)) {
336
+ Daemon._cleanupStaleDaemonFiles(pidFile, statusFile);
337
+ return null;
338
+ }
339
+
340
+ return pid;
323
341
  }
324
342
 
325
343
  // ---------------------------------------------------------------------------
@@ -865,6 +883,17 @@ class Daemon {
865
883
  }
866
884
  }
867
885
 
886
+ static _cleanupStaleDaemonFiles(pidFile, statusFile) {
887
+ Daemon._unlinkIfExists(pidFile);
888
+ Daemon._unlinkIfExists(statusFile);
889
+ }
890
+
891
+ static _unlinkIfExists(filePath) {
892
+ if (fs.existsSync(filePath)) {
893
+ fs.unlinkSync(filePath);
894
+ }
895
+ }
896
+
868
897
  static _isAlive(pid) {
869
898
  try {
870
899
  process.kill(pid, 0);
package/src/installer.js CHANGED
@@ -132,7 +132,12 @@ class Installer {
132
132
  : null;
133
133
  if (verifyCmd) {
134
134
  try {
135
- require('child_process').execSync(verifyCmd, { stdio: 'ignore', timeout: 5000 });
135
+ // Use the enhanced PATH so a freshly-installed CLI living in a dir the
136
+ // installer added (e.g. %LOCALAPPDATA%\cursor-agent) is found — otherwise
137
+ // `cursor-agent --version` exits "not recognized" and verify wrongly fails.
138
+ require('child_process').execSync(verifyCmd, {
139
+ stdio: 'ignore', timeout: 5000, env: getEnhancedEnv(), windowsHide: true,
140
+ });
136
141
  return true;
137
142
  } catch { return false; }
138
143
  }
@@ -140,9 +140,14 @@ function runUpdate(opts = {}) {
140
140
  const spawn = opts.spawn || spawnSync;
141
141
  const npmBin = opts.npmBin || findNpmBin({ platform });
142
142
  const prefix = opts.prefix !== undefined ? opts.prefix : detectInstallPrefix();
143
- const args = ['install', '--no-save', `${PKG_NAME}@latest`];
143
+ // Use a GLOBAL install (-g). A LOCAL install (`npm install <pkg> --prefix
144
+ // <dir>`) treats <dir> as a project root and prunes every node_modules entry
145
+ // not reachable from its package.json. When the prefix is the portable node
146
+ // dir, that deletes node's own bundled npm/corepack (and other agent runtimes),
147
+ // breaking every subsequent `agn` command. A global install only adds/updates
148
+ // the named package into <prefix>/node_modules and never prunes its siblings.
149
+ const args = ['install', '-g', `${PKG_NAME}@latest`];
144
150
  if (prefix) args.push('--prefix', prefix);
145
- else args.push('-g');
146
151
 
147
152
  // On Windows, npm.cmd is a batch file and must be invoked through cmd.exe.
148
153
  // Passing the args via cmd.exe with shell:false lets Node quote paths that