@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 +1 -1
- package/src/adapters/base.js +4 -1
- package/src/adapters/claude.js +47 -8
- package/src/adapters/cursor.js +8 -1
- package/src/adapters/gemini.js +8 -1
- package/src/adapters/opencode.js +124 -4
- package/src/cli.js +5 -0
- package/src/daemon.js +30 -1
- package/src/installer.js +6 -1
- package/src/update-check.js +7 -2
package/package.json
CHANGED
package/src/adapters/base.js
CHANGED
|
@@ -93,7 +93,10 @@ class BaseAdapter {
|
|
|
93
93
|
|
|
94
94
|
// Sync workspace-managed skills into disabledModules
|
|
95
95
|
try {
|
|
96
|
-
const agents = await
|
|
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');
|
package/src/adapters/claude.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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];
|
package/src/adapters/cursor.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/src/adapters/gemini.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/src/adapters/opencode.js
CHANGED
|
@@ -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
|
|
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 (
|
|
264
|
-
if (ch === '\\' && inStr) {
|
|
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) => {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/update-check.js
CHANGED
|
@@ -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
|
-
|
|
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
|