@openagents-org/agent-launcher 0.2.180 → 0.2.182
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/icons/antigravity.svg +1 -0
- package/icons/mini-swe-agent.svg +7 -0
- package/package.json +3 -2
- package/registry.json +1137 -622
- package/src/adapters/aider.js +10 -3
- package/src/adapters/amp.js +11 -3
- package/src/adapters/antigravity.js +10 -2
- package/src/adapters/base.js +46 -0
- package/src/adapters/claude.js +6 -1
- package/src/adapters/cline.js +5 -1
- package/src/adapters/codebuddy.js +6 -1
- package/src/adapters/codex.js +10 -2
- package/src/adapters/commandcode.js +5 -1
- package/src/adapters/copilot-stream-parser.js +58 -8
- package/src/adapters/copilot.js +20 -4
- package/src/adapters/cursor.js +5 -1
- package/src/adapters/deepseek.js +5 -1
- package/src/adapters/gemini.js +10 -2
- package/src/adapters/goose.js +6 -2
- package/src/adapters/hermes.js +23 -53
- package/src/adapters/kimi.js +5 -1
- package/src/adapters/llm-direct.js +6 -0
- package/src/adapters/mini.js +10 -3
- package/src/adapters/openclaw.js +5 -1
- package/src/adapters/opencode.js +6 -1
- package/src/adapters/openworker.js +15 -1
- package/src/adapters/pi.js +6 -1
- package/src/adapters/workspace-prompt.js +23 -12
- package/src/daemon.js +60 -1
- package/src/index.js +120 -0
- package/src/installer.js +57 -16
- package/src/paths.js +98 -34
- package/src/probe.js +4 -1
- package/src/workspace-client.js +38 -0
- package/src/wsl.js +579 -0
package/src/adapters/kimi.js
CHANGED
|
@@ -29,7 +29,11 @@
|
|
|
29
29
|
const fs = require('fs');
|
|
30
30
|
const os = require('os');
|
|
31
31
|
const path = require('path');
|
|
32
|
-
const { execSync
|
|
32
|
+
const { execSync } = require('child_process');
|
|
33
|
+
// spawn() here is the WSL bridge from ../wsl: same signature as
|
|
34
|
+
// child_process.spawn, and a straight pass-through unless the resolved CLI
|
|
35
|
+
// lives on the other side of the Windows/WSL boundary.
|
|
36
|
+
const { spawn } = require('../wsl');
|
|
33
37
|
|
|
34
38
|
const LlmDirectAdapter = require('./llm-direct');
|
|
35
39
|
const { formatAttachmentsForPrompt, SESSION_DEFAULT_RE, generateSessionTitle } = require('./utils');
|
|
@@ -71,6 +71,11 @@ class LlmDirectAdapter extends BaseAdapter {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/** The model this runner actually calls with — see `_model` above. */
|
|
75
|
+
modelLabel() {
|
|
76
|
+
return String(this.workspaceModel || this._model || '').trim() || null;
|
|
77
|
+
}
|
|
78
|
+
|
|
74
79
|
_buildSystemPrompt(channelName) {
|
|
75
80
|
return buildOpenclawSystemPrompt({
|
|
76
81
|
agentName: this.agentName,
|
|
@@ -79,6 +84,7 @@ class LlmDirectAdapter extends BaseAdapter {
|
|
|
79
84
|
endpoint: this.endpoint,
|
|
80
85
|
token: this.token,
|
|
81
86
|
mode: this._mode,
|
|
87
|
+
model: this.modelLabel(),
|
|
82
88
|
disabledModules: this.disabledModules,
|
|
83
89
|
...this.pinnedPromptOpts(channelName),
|
|
84
90
|
});
|
package/src/adapters/mini.js
CHANGED
|
@@ -50,7 +50,11 @@
|
|
|
50
50
|
const fs = require('fs');
|
|
51
51
|
const os = require('os');
|
|
52
52
|
const path = require('path');
|
|
53
|
-
const { execSync
|
|
53
|
+
const { execSync } = require('child_process');
|
|
54
|
+
// spawn() here is the WSL bridge from ../wsl: same signature as
|
|
55
|
+
// child_process.spawn, and a straight pass-through unless the resolved CLI
|
|
56
|
+
// lives on the other side of the Windows/WSL boundary.
|
|
57
|
+
const { spawn, resolveWslBinary } = require('../wsl');
|
|
54
58
|
|
|
55
59
|
const BaseAdapter = require('./base');
|
|
56
60
|
const { whichBinary, getEnhancedEnv, aiderBinDirs } = require('../paths');
|
|
@@ -178,7 +182,7 @@ class MiniSweAgentAdapter extends BaseAdapter {
|
|
|
178
182
|
// Shared cross-platform resolver runs `which`/`where` against an ENHANCED
|
|
179
183
|
// PATH (nvm/fnm/volta/homebrew + pip/pipx/uv user-install dirs) — covers the
|
|
180
184
|
// GUI/daemon "installed but not on PATH" case for a `pip install`.
|
|
181
|
-
const resolved = whichBinary('mini');
|
|
185
|
+
const resolved = whichBinary('mini', { allowWsl: false });
|
|
182
186
|
if (resolved) return this._resolveExePreference(resolved, IS_WINDOWS);
|
|
183
187
|
|
|
184
188
|
// Explicit fallback over the pip/pipx/uv user-install bin dirs plus the uv
|
|
@@ -201,7 +205,10 @@ class MiniSweAgentAdapter extends BaseAdapter {
|
|
|
201
205
|
if (fs.existsSync(c)) return c;
|
|
202
206
|
}
|
|
203
207
|
}
|
|
204
|
-
|
|
208
|
+
// Nothing native anywhere. Last of all, look inside WSL: a CLI the user
|
|
209
|
+
// installed in their distro is a real install, and the marked path it comes
|
|
210
|
+
// back as is what the spawn bridge in ../wsl turns into a wsl.exe run.
|
|
211
|
+
return resolveWslBinary('mini');
|
|
205
212
|
}
|
|
206
213
|
|
|
207
214
|
/**
|
package/src/adapters/openclaw.js
CHANGED
|
@@ -14,7 +14,11 @@
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const os = require('os');
|
|
17
|
-
const {
|
|
17
|
+
const { execSync } = require('child_process');
|
|
18
|
+
// spawn() here is the WSL bridge from ../wsl: same signature as
|
|
19
|
+
// child_process.spawn, and a straight pass-through unless the resolved CLI
|
|
20
|
+
// lives on the other side of the Windows/WSL boundary.
|
|
21
|
+
const { spawn } = require('../wsl');
|
|
18
22
|
|
|
19
23
|
const BaseAdapter = require('./base');
|
|
20
24
|
const { formatAttachmentsForPrompt } = require('./utils');
|
package/src/adapters/opencode.js
CHANGED
|
@@ -14,7 +14,11 @@ const fs = require('fs');
|
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const os = require('os');
|
|
16
16
|
const crypto = require('crypto');
|
|
17
|
-
const { execSync
|
|
17
|
+
const { execSync } = require('child_process');
|
|
18
|
+
// spawn() here is the WSL bridge from ../wsl: same signature as
|
|
19
|
+
// child_process.spawn, and a straight pass-through unless the resolved CLI
|
|
20
|
+
// lives on the other side of the Windows/WSL boundary.
|
|
21
|
+
const { spawn } = require('../wsl');
|
|
18
22
|
|
|
19
23
|
const BaseAdapter = require('./base');
|
|
20
24
|
const { formatAttachmentsForPrompt, redactSecrets } = require('./utils');
|
|
@@ -250,6 +254,7 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
250
254
|
endpoint: this.endpoint,
|
|
251
255
|
token: this.token,
|
|
252
256
|
mode: this._mode,
|
|
257
|
+
model: this.modelLabel(),
|
|
253
258
|
disabledModules: this.disabledModules,
|
|
254
259
|
...this.pinnedPromptOpts(channelName),
|
|
255
260
|
});
|
|
@@ -42,7 +42,11 @@ const net = require('net');
|
|
|
42
42
|
const os = require('os');
|
|
43
43
|
const path = require('path');
|
|
44
44
|
const crypto = require('crypto');
|
|
45
|
-
const {
|
|
45
|
+
const { execFileSync } = require('child_process');
|
|
46
|
+
// spawn() here is the WSL bridge from ../wsl: same signature as
|
|
47
|
+
// child_process.spawn, and a straight pass-through unless the resolved CLI
|
|
48
|
+
// lives on the other side of the Windows/WSL boundary.
|
|
49
|
+
const { spawn } = require('../wsl');
|
|
46
50
|
|
|
47
51
|
const BaseAdapter = require('./base');
|
|
48
52
|
const { formatAttachmentsForPrompt, SESSION_DEFAULT_RE, generateSessionTitle } = require('./utils');
|
|
@@ -203,6 +207,15 @@ class OpenWorkerAdapter extends BaseAdapter {
|
|
|
203
207
|
return String(this.agentEnv.OPENWORKER_API_KEY || '').trim();
|
|
204
208
|
}
|
|
205
209
|
|
|
210
|
+
/**
|
|
211
|
+
* What to tell the agent it is running on. `_model()` is the id we hand the
|
|
212
|
+
* server, provider prefix and all — the honest answer to "what model are
|
|
213
|
+
* you?", which OpenWorker's own weights cannot give.
|
|
214
|
+
*/
|
|
215
|
+
modelLabel() {
|
|
216
|
+
return this._model() || null;
|
|
217
|
+
}
|
|
218
|
+
|
|
206
219
|
/** The model for a turn: the workspace's selection wins over the saved env. */
|
|
207
220
|
_model() {
|
|
208
221
|
const raw = String(this.workspaceModel || this.agentEnv.OPENWORKER_MODEL || '').trim();
|
|
@@ -587,6 +600,7 @@ class OpenWorkerAdapter extends BaseAdapter {
|
|
|
587
600
|
endpoint: this.endpoint,
|
|
588
601
|
token: this.token,
|
|
589
602
|
mode: this._mode,
|
|
603
|
+
model: this.modelLabel(),
|
|
590
604
|
disabledModules: this.disabledModules,
|
|
591
605
|
browserEnabled,
|
|
592
606
|
});
|
package/src/adapters/pi.js
CHANGED
|
@@ -34,7 +34,11 @@ const crypto = require('crypto');
|
|
|
34
34
|
const fs = require('fs');
|
|
35
35
|
const os = require('os');
|
|
36
36
|
const path = require('path');
|
|
37
|
-
const { execFileSync, execSync
|
|
37
|
+
const { execFileSync, execSync } = require('child_process');
|
|
38
|
+
// spawn() here is the WSL bridge from ../wsl: same signature as
|
|
39
|
+
// child_process.spawn, and a straight pass-through unless the resolved CLI
|
|
40
|
+
// lives on the other side of the Windows/WSL boundary.
|
|
41
|
+
const { spawn } = require('../wsl');
|
|
38
42
|
|
|
39
43
|
const BaseAdapter = require('./base');
|
|
40
44
|
const { formatAttachmentsForPrompt, SESSION_DEFAULT_RE, generateSessionTitle } = require('./utils');
|
|
@@ -1391,6 +1395,7 @@ class PiAdapter extends BaseAdapter {
|
|
|
1391
1395
|
endpoint: this.endpoint,
|
|
1392
1396
|
token: this.token,
|
|
1393
1397
|
mode: this._mode,
|
|
1398
|
+
model: this.modelLabel(),
|
|
1394
1399
|
disabledModules: this.disabledModules,
|
|
1395
1400
|
browserEnabled,
|
|
1396
1401
|
});
|
|
@@ -121,7 +121,7 @@ function workspaceSkillName(agentName) {
|
|
|
121
121
|
* - `'skills'` → points at the agent's workspace skill (Bash + curl),
|
|
122
122
|
* since no MCP server is spawned and that tool would not exist.
|
|
123
123
|
*/
|
|
124
|
-
function buildWorkspaceIdentity(agentName, workspaceId, channelName, mode = 'execute', toolMode = 'mcp') {
|
|
124
|
+
function buildWorkspaceIdentity(agentName, workspaceId, channelName, mode = 'execute', toolMode = 'mcp', model = null) {
|
|
125
125
|
const priorContext = toolMode === 'skills'
|
|
126
126
|
? (
|
|
127
127
|
`When you need prior context, use the ${workspaceSkillName(agentName)} skill to ` +
|
|
@@ -135,6 +135,15 @@ function buildWorkspaceIdentity(agentName, workspaceId, channelName, mode = 'exe
|
|
|
135
135
|
'channel argument the tool falls back to a default channel that may ' +
|
|
136
136
|
'be different from where you are.\n'
|
|
137
137
|
);
|
|
138
|
+
// The model is the one fact about itself an agent cannot look up. Asked
|
|
139
|
+
// "what model are you?", a CLI-driven agent answers from whatever its
|
|
140
|
+
// weights remember about their own training — which is how an agent
|
|
141
|
+
// configured with deepseek came to tell a user it was Claude. The launcher
|
|
142
|
+
// knows the answer exactly (it is what was passed to the CLI), so state it
|
|
143
|
+
// rather than leaving the agent to guess.
|
|
144
|
+
const modelLine = String(model || '').trim()
|
|
145
|
+
? `- Model: ${String(model).trim()} (this is the model you are running on — do not guess otherwise)\n`
|
|
146
|
+
: '';
|
|
138
147
|
return (
|
|
139
148
|
`You are agent '${agentName}' connected to an OpenAgents workspace.\n` +
|
|
140
149
|
'Your text responses are automatically posted to the workspace chat ' +
|
|
@@ -142,7 +151,9 @@ function buildWorkspaceIdentity(agentName, workspaceId, channelName, mode = 'exe
|
|
|
142
151
|
'## Workspace Context\n' +
|
|
143
152
|
`- Workspace ID: ${workspaceId}\n` +
|
|
144
153
|
`- Channel: ${channelName} (this is the channel you are currently speaking in)\n` +
|
|
145
|
-
`- Mode: ${mode}\n
|
|
154
|
+
`- Mode: ${mode}\n` +
|
|
155
|
+
modelLine +
|
|
156
|
+
'\n' +
|
|
146
157
|
priorContext
|
|
147
158
|
);
|
|
148
159
|
}
|
|
@@ -931,10 +942,10 @@ function buildPinnedSections({ toolMode = 'mcp', channelName, mode = 'execute',
|
|
|
931
942
|
* writeAccess? }. Both default to off — other adapters that reuse this
|
|
932
943
|
* builder (e.g. Gemini) are unaffected unless they pass them.
|
|
933
944
|
*/
|
|
934
|
-
function buildClaudeSystemPrompt({ agentName, workspaceId, channelName, mode = 'execute', browserEnabled = false, toolMode = 'mcp', decisionLog = null, glossary = null }) {
|
|
945
|
+
function buildClaudeSystemPrompt({ agentName, workspaceId, channelName, mode = 'execute', browserEnabled = false, toolMode = 'mcp', decisionLog = null, glossary = null, model = null }) {
|
|
935
946
|
const skillName = workspaceSkillName(agentName);
|
|
936
947
|
const parts = [];
|
|
937
|
-
parts.push(buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, toolMode));
|
|
948
|
+
parts.push(buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, toolMode, model));
|
|
938
949
|
parts.push(toolMode === 'skills' ? buildClaudeSkillsToolBlock(skillName) : buildClaudeMcpToolBlock());
|
|
939
950
|
parts.push(buildBrowserDirective(browserEnabled));
|
|
940
951
|
parts.push(buildCollaborationPrompt(toolMode, skillName));
|
|
@@ -960,9 +971,9 @@ function buildClaudeSystemPrompt({ agentName, workspaceId, channelName, mode = '
|
|
|
960
971
|
* buildClaudeSystemPrompt (with skills-mode tool phrasing, since these
|
|
961
972
|
* agents reach the knowledge API through curl commands).
|
|
962
973
|
*/
|
|
963
|
-
function buildOpenclawSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules, browserEnabled = false, decisionLog = null, glossary = null }) {
|
|
974
|
+
function buildOpenclawSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules, browserEnabled = false, decisionLog = null, glossary = null, model = null }) {
|
|
964
975
|
const parts = [];
|
|
965
|
-
parts.push(buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, 'skills'));
|
|
976
|
+
parts.push(buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, 'skills', model));
|
|
966
977
|
parts.push(buildBrowserDirective(browserEnabled));
|
|
967
978
|
parts.push(buildCollaborationPrompt('skills', workspaceSkillName(agentName)));
|
|
968
979
|
parts.push(buildModePrompt(mode));
|
|
@@ -1009,8 +1020,8 @@ function buildOpenclawSkillMd({ endpoint, workspaceId, token, agentName, channel
|
|
|
1009
1020
|
* `decisionLog` / `glossary` opt in to knowledge pinning exactly as in
|
|
1010
1021
|
* buildOpenclawSystemPrompt.
|
|
1011
1022
|
*/
|
|
1012
|
-
function buildOpenCodeSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules, browserEnabled = false, decisionLog = null, glossary = null, isWindows = process.platform === 'win32' }) {
|
|
1013
|
-
const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, 'skills');
|
|
1023
|
+
function buildOpenCodeSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules, browserEnabled = false, decisionLog = null, glossary = null, isWindows = process.platform === 'win32', model = null }) {
|
|
1024
|
+
const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, 'skills', model);
|
|
1014
1025
|
const directive = buildBrowserDirective(browserEnabled);
|
|
1015
1026
|
const collab = buildCollaborationPrompt('skills', workspaceSkillName(agentName));
|
|
1016
1027
|
const modePrompt = buildModePrompt(mode) + buildWindowsShellHint(isWindows);
|
|
@@ -1031,8 +1042,8 @@ function buildOpenCodeSystemPrompt({ agentName, workspaceId, channelName, endpoi
|
|
|
1031
1042
|
* NOTE: this is APPENDED, never passed via `--system-prompt` — that flag would
|
|
1032
1043
|
* REPLACE Pi's own coding-assistant prompt and strip its tool instructions.
|
|
1033
1044
|
*/
|
|
1034
|
-
function buildPiSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules, browserEnabled = false }) {
|
|
1035
|
-
const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, 'skills');
|
|
1045
|
+
function buildPiSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules, browserEnabled = false, model = null }) {
|
|
1046
|
+
const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, 'skills', model);
|
|
1036
1047
|
const directive = buildBrowserDirective(browserEnabled);
|
|
1037
1048
|
const collab = buildCollaborationPrompt('skills', workspaceSkillName(agentName));
|
|
1038
1049
|
const modePrompt = buildModePrompt(mode);
|
|
@@ -1060,8 +1071,8 @@ function buildPiSystemPrompt({ agentName, workspaceId, channelName, endpoint, to
|
|
|
1060
1071
|
* conversation, so it is sent once and still in context on every later turn
|
|
1061
1072
|
* — which is why it must read as a briefing, not as a system directive.
|
|
1062
1073
|
*/
|
|
1063
|
-
function buildOpenWorkerSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules, browserEnabled = false }) {
|
|
1064
|
-
const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, 'skills');
|
|
1074
|
+
function buildOpenWorkerSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules, browserEnabled = false, model = null }) {
|
|
1075
|
+
const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, 'skills', model);
|
|
1065
1076
|
const directive = buildBrowserDirective(browserEnabled);
|
|
1066
1077
|
const collab = buildCollaborationPrompt('skills', workspaceSkillName(agentName));
|
|
1067
1078
|
const modePrompt = buildModePrompt(mode);
|
package/src/daemon.js
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const {
|
|
5
|
+
const { execSync, execFileSync } = require('child_process');
|
|
6
|
+
// spawn() here is the WSL bridge from ./wsl: same signature as
|
|
7
|
+
// child_process.spawn, and a straight pass-through unless the resolved CLI
|
|
8
|
+
// lives on the other side of the Windows/WSL boundary.
|
|
9
|
+
const { spawn } = require('./wsl');
|
|
6
10
|
const os = require('os');
|
|
7
11
|
const { WorkspaceClient } = require('./workspace-client');
|
|
8
12
|
const { getEnhancedEnv, whichBinary, IS_WINDOWS, defaultAgentWorkdir } = require('./paths');
|
|
@@ -43,6 +47,7 @@ class Daemon {
|
|
|
43
47
|
this._statusInterval = null;
|
|
44
48
|
this._cmdInterval = null;
|
|
45
49
|
this._nodeHeartbeatInterval = null; // device-level heartbeat (connect-a-node)
|
|
50
|
+
this._displayNameInterval = null; // pulls workspace-side renames back
|
|
46
51
|
this._nodeClients = new Map(); // workspace_id -> WorkspaceClient, one per pairing
|
|
47
52
|
this._runningCommands = new Set(); // node-command ids currently executing
|
|
48
53
|
this._runtimes = []; // detected agent runtimes for the node view
|
|
@@ -225,6 +230,50 @@ class Daemon {
|
|
|
225
230
|
}
|
|
226
231
|
}
|
|
227
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Reflect workspace-side renames back onto this device.
|
|
235
|
+
*
|
|
236
|
+
* The workspace is treated as authoritative for the label, which needs no
|
|
237
|
+
* timestamps to be correct: a rename started on the device writes to the
|
|
238
|
+
* workspace FIRST and only then to local config (see
|
|
239
|
+
* AgentConnector.setAgentDisplayName), so by the time this reads, the
|
|
240
|
+
* workspace already holds whatever the device just set. Anything different
|
|
241
|
+
* from local therefore came from the workspace and should win.
|
|
242
|
+
*
|
|
243
|
+
* Never throws — a workspace that is unreachable simply leaves the labels
|
|
244
|
+
* as they are until the next pass.
|
|
245
|
+
*/
|
|
246
|
+
async _syncDisplayNames() {
|
|
247
|
+
let pairings = [];
|
|
248
|
+
try {
|
|
249
|
+
pairings = require('./node-config').listPairings();
|
|
250
|
+
} catch { return; }
|
|
251
|
+
|
|
252
|
+
for (const n of pairings) {
|
|
253
|
+
if (!n.workspace_id || !n.token) continue;
|
|
254
|
+
let members = [];
|
|
255
|
+
try {
|
|
256
|
+
members = await this._nodeClientFor(n).getAgents(n.workspace_id, n.token);
|
|
257
|
+
} catch { continue; }
|
|
258
|
+
|
|
259
|
+
const byName = new Map(members.map((m) => [m.agentName, m]));
|
|
260
|
+
for (const a of this.config.getAgents()) {
|
|
261
|
+
// Scoped to this pairing's workspace, like _buildRoster: an agent in
|
|
262
|
+
// another workspace must never be relabelled from this one.
|
|
263
|
+
if (!this._agentOnNodeWorkspace(a, n)) continue;
|
|
264
|
+
const remote = byName.get(a.name);
|
|
265
|
+
if (!remote) continue;
|
|
266
|
+
const local = a.display_name || null;
|
|
267
|
+
const wanted = remote.displayName || null;
|
|
268
|
+
if (local === wanted) continue;
|
|
269
|
+
try {
|
|
270
|
+
this.config.updateAgent(a.name, { display_name: wanted || undefined });
|
|
271
|
+
this._log(`display name for ${a.name} updated from workspace`);
|
|
272
|
+
} catch {}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
228
277
|
/**
|
|
229
278
|
* True if agent `a` is bound to the node's currently-connected workspace.
|
|
230
279
|
* A missing network never matches (guards against undefined === undefined
|
|
@@ -608,6 +657,15 @@ async _runNodeCommand(n, cmd) {
|
|
|
608
657
|
15000,
|
|
609
658
|
);
|
|
610
659
|
|
|
660
|
+
// Pull display-name renames made in the workspace back onto the device, so
|
|
661
|
+
// the two sides agree whichever one the user typed into. Separate from the
|
|
662
|
+
// 10s heartbeat on purpose: a label changes rarely and this is an extra
|
|
663
|
+
// request per paired workspace, so it runs a sixth as often.
|
|
664
|
+
this._displayNameInterval = setInterval(
|
|
665
|
+
() => this._syncDisplayNames(),
|
|
666
|
+
60000,
|
|
667
|
+
);
|
|
668
|
+
|
|
611
669
|
// Detect installed/logged-in agent runtimes for the Add-agent gallery. Runs
|
|
612
670
|
// in a child process (off the event loop), refreshed periodically.
|
|
613
671
|
this._refreshRuntimes();
|
|
@@ -658,6 +716,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
658
716
|
if (this._nodeHeartbeatInterval) clearInterval(this._nodeHeartbeatInterval);
|
|
659
717
|
if (this._credReconcileInterval) clearInterval(this._credReconcileInterval);
|
|
660
718
|
if (this._runtimesInterval) clearInterval(this._runtimesInterval);
|
|
719
|
+
if (this._displayNameInterval) clearInterval(this._displayNameInterval);
|
|
661
720
|
if (this._probeStartupTimer) clearTimeout(this._probeStartupTimer);
|
|
662
721
|
if (this._probeInterval) clearInterval(this._probeInterval);
|
|
663
722
|
if (this._configWatcher) { try { this._configWatcher.close(); } catch {} }
|
package/src/index.js
CHANGED
|
@@ -84,6 +84,10 @@ class AgentConnector {
|
|
|
84
84
|
const network = networks.find((n) => n.slug === a.network || n.id === a.network);
|
|
85
85
|
return {
|
|
86
86
|
name: a.name,
|
|
87
|
+
// The label to show. `name` stays the identity — callers that key on
|
|
88
|
+
// it (sessions, working dirs, workspace membership) must keep using
|
|
89
|
+
// `name`, never this.
|
|
90
|
+
displayName: a.display_name || null,
|
|
87
91
|
type,
|
|
88
92
|
role: a.role || 'worker',
|
|
89
93
|
network: a.network || null,
|
|
@@ -100,6 +104,95 @@ class AgentConnector {
|
|
|
100
104
|
return { success: true };
|
|
101
105
|
}
|
|
102
106
|
|
|
107
|
+
/**
|
|
108
|
+
* The workspace an agent is bound to, as a ready-to-use client — or null
|
|
109
|
+
* when it is local-only or its network is no longer configured.
|
|
110
|
+
*
|
|
111
|
+
* Each network carries its own endpoint (a self-hosted workspace is not the
|
|
112
|
+
* official one), so the client is built per call rather than reusing
|
|
113
|
+
* `this.workspace`, the same way removeWorkspace does.
|
|
114
|
+
*/
|
|
115
|
+
_workspaceClientFor(agentName) {
|
|
116
|
+
const agent = this.config.getAgent(agentName);
|
|
117
|
+
if (!agent || !agent.network) return null;
|
|
118
|
+
const network = this.config
|
|
119
|
+
.getNetworks()
|
|
120
|
+
.find((n) => n.slug === agent.network || n.id === agent.network);
|
|
121
|
+
if (!network || !network.id) return null;
|
|
122
|
+
const endpoint = network.endpoint || (this.workspace && this.workspace.endpoint);
|
|
123
|
+
const { WorkspaceClient } = require('./workspace-client');
|
|
124
|
+
return { client: new WorkspaceClient(endpoint), network };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Set (or clear, with an empty value) an agent's display label.
|
|
129
|
+
*
|
|
130
|
+
* The agent's `name` is its identity — it keys daemon.yaml, the working
|
|
131
|
+
* directory under ~/.openagents/agents, its sessions and its workspace
|
|
132
|
+
* membership — so renaming NEVER touches it. What changes is the label the
|
|
133
|
+
* launcher and the workspace show, which is precisely what the workspace
|
|
134
|
+
* already models as `display_name` next to `agent_name`.
|
|
135
|
+
*
|
|
136
|
+
* When the agent is in a workspace the new label is pushed there too, so the
|
|
137
|
+
* two sides agree. A workspace that rejects it (a name another member
|
|
138
|
+
* already answers to) fails the whole call rather than leaving the label
|
|
139
|
+
* changed on one side only.
|
|
140
|
+
*/
|
|
141
|
+
async setAgentDisplayName(name, displayName) {
|
|
142
|
+
const agent = this.config.getAgent(name);
|
|
143
|
+
if (!agent) throw new Error(`Agent '${name}' not found`);
|
|
144
|
+
const label = (displayName || '').trim();
|
|
145
|
+
|
|
146
|
+
const bound = this._workspaceClientFor(name);
|
|
147
|
+
if (bound) {
|
|
148
|
+
await bound.client.setMemberDisplayName(
|
|
149
|
+
bound.network.id,
|
|
150
|
+
bound.network.token || '',
|
|
151
|
+
name,
|
|
152
|
+
label,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
// Local write comes second: if the workspace refused the label, nothing
|
|
156
|
+
// here has changed and the two sides are still in agreement.
|
|
157
|
+
this.config.updateAgent(name, { display_name: label || undefined });
|
|
158
|
+
return { success: true, displayName: label || null };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Drop an agent's member row from the workspace it joined.
|
|
163
|
+
*
|
|
164
|
+
* Deliberately SEPARATE from removeAgent rather than a flag on it. Removing
|
|
165
|
+
* an agent is a synchronous local edit that `agn remove` and the TUI call
|
|
166
|
+
* without awaiting, inside a try/catch — making it async would turn a thrown
|
|
167
|
+
* error into an unhandled rejection those callers can no longer see. So the
|
|
168
|
+
* network half lives here, and a caller that wants both awaits this first:
|
|
169
|
+
* failing to reach the workspace then leaves the agent intact locally, which
|
|
170
|
+
* is the recoverable order.
|
|
171
|
+
*
|
|
172
|
+
* A local-only agent is a no-op success — there is nothing over there.
|
|
173
|
+
*/
|
|
174
|
+
async removeAgentFromWorkspace(name) {
|
|
175
|
+
const bound = this._workspaceClientFor(name);
|
|
176
|
+
if (!bound) return { success: true, skipped: true };
|
|
177
|
+
try {
|
|
178
|
+
await bound.client.removeMember(
|
|
179
|
+
bound.network.id,
|
|
180
|
+
bound.network.token || '',
|
|
181
|
+
name,
|
|
182
|
+
);
|
|
183
|
+
} catch (e) {
|
|
184
|
+
// 404 is the goal state, reached by someone else: the member row is
|
|
185
|
+
// already gone, or the whole workspace is. Treating it as a failure
|
|
186
|
+
// would strand the agent on this device — removal happens local-last, so
|
|
187
|
+
// an error here means it cannot be removed at all. Anything else (a
|
|
188
|
+
// network blip, an auth failure) is real and must stop the removal so
|
|
189
|
+
// the user can retry rather than lose track of a live membership.
|
|
190
|
+
if (!e || e.status !== 404) throw e;
|
|
191
|
+
return { success: true, alreadyGone: true };
|
|
192
|
+
}
|
|
193
|
+
return { success: true };
|
|
194
|
+
}
|
|
195
|
+
|
|
103
196
|
removeAgent(name) {
|
|
104
197
|
this.config.removeAgent(name);
|
|
105
198
|
return { success: true };
|
|
@@ -193,11 +286,38 @@ class AgentConnector {
|
|
|
193
286
|
return { success: true };
|
|
194
287
|
}
|
|
195
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Local half of leaving a workspace: forget the binding, keep the agent.
|
|
291
|
+
*
|
|
292
|
+
* Synchronous, and deliberately still local-only — `agn disconnect` and the
|
|
293
|
+
* TUI call it without awaiting, inside a try/catch, so an async version
|
|
294
|
+
* would turn a throw into an unhandled rejection they can no longer see.
|
|
295
|
+
* Callers that want the membership dropped too use leaveWorkspace below.
|
|
296
|
+
*/
|
|
196
297
|
disconnectWorkspace(agentName) {
|
|
197
298
|
this.config.setAgentNetwork(agentName, null);
|
|
198
299
|
return { success: true };
|
|
199
300
|
}
|
|
200
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Leave a workspace properly: drop the member row over there, and unbind
|
|
304
|
+
* here — the agent itself stays, with its config, credentials and working
|
|
305
|
+
* directory, ready to join another workspace.
|
|
306
|
+
*
|
|
307
|
+
* Leaving only the local half is the bug this replaces. The device stopped
|
|
308
|
+
* showing the agent as connected while the workspace went on listing it as a
|
|
309
|
+
* member, and nothing in the launcher could reach that row any more. The
|
|
310
|
+
* workspace-level "remove a workspace" action learned this same lesson
|
|
311
|
+
* already (see removeWorkspace) — this is the agent-level equivalent.
|
|
312
|
+
*
|
|
313
|
+
* Workspace first, local second: a failure there leaves the agent bound and
|
|
314
|
+
* retryable rather than orphaning a membership.
|
|
315
|
+
*/
|
|
316
|
+
async leaveWorkspace(agentName) {
|
|
317
|
+
await this.removeAgentFromWorkspace(agentName);
|
|
318
|
+
return this.disconnectWorkspace(agentName);
|
|
319
|
+
}
|
|
320
|
+
|
|
201
321
|
async removeWorkspace(slug) {
|
|
202
322
|
const networks = this.config.getNetworks();
|
|
203
323
|
const network = networks.find(n => n.slug === slug || n.id === slug);
|
package/src/installer.js
CHANGED
|
@@ -5,6 +5,7 @@ const os = require('os');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { execSync, exec } = require('child_process');
|
|
7
7
|
const { whichBinary, getEnhancedEnv, getRuntimePrefix, clearBinaryLookupCache, aiderBinDirs, resolveBinaryInKnownDirs } = require('./paths');
|
|
8
|
+
const { isWslBinary, bridgedCommandString, wslHomeUnc } = require('./wsl');
|
|
8
9
|
const { EnvManager } = require('./env');
|
|
9
10
|
const { nodeDistUrls, installRegistry } = require('./mirrors');
|
|
10
11
|
const { readinessReason, REASON } = require('./adapters/health-status');
|
|
@@ -173,6 +174,13 @@ class Installer {
|
|
|
173
174
|
return { installed: false, managed: false, location: null };
|
|
174
175
|
}
|
|
175
176
|
|
|
177
|
+
// Installed inside a WSL distro. Never 'managed': the launcher did not put
|
|
178
|
+
// it there and cannot uninstall it, and the isolated-runtime bookkeeping
|
|
179
|
+
// below is all about Windows paths that an in-distro binary can never match.
|
|
180
|
+
if (isWslBinary(binaryPath)) {
|
|
181
|
+
return { installed: true, managed: false, location: 'wsl' };
|
|
182
|
+
}
|
|
183
|
+
|
|
176
184
|
// Verify it's not a stale shim pointing to a missing package
|
|
177
185
|
const openagentsDir = path.join(os.homedir(), '.openagents');
|
|
178
186
|
if (binaryPath.startsWith(openagentsDir)) {
|
|
@@ -580,6 +588,11 @@ class Installer {
|
|
|
580
588
|
* shebang), so it's applied on every platform for consistency.
|
|
581
589
|
*/
|
|
582
590
|
_versionProbeCommand(binary) {
|
|
591
|
+
// A binary inside WSL has to be asked through wsl.exe; running the Linux
|
|
592
|
+
// path as a Windows command yields "not recognized" and the marketplace
|
|
593
|
+
// then shows an installed agent with no version at all.
|
|
594
|
+
const bridged = bridgedCommandString(binary, ['--version']);
|
|
595
|
+
if (bridged) return bridged;
|
|
583
596
|
if (/\.(mjs|cjs|js)$/i.test(binary)) {
|
|
584
597
|
return `"${this._nodeBinary()}" "${binary}" --version`;
|
|
585
598
|
}
|
|
@@ -744,14 +757,14 @@ class Installer {
|
|
|
744
757
|
// authoritative; the legacy parser treats an empty {} as ready.
|
|
745
758
|
const credsReady = checkReady.creds_json_has_entries
|
|
746
759
|
? false
|
|
747
|
-
: this._checkCredsReady(checkReady);
|
|
760
|
+
: this._checkCredsReady(checkReady, agentType);
|
|
748
761
|
// Content-free credential probes (opt-in via check_ready). Detect agents
|
|
749
762
|
// whose sign-in is a local OAuth/credential FILE (e.g. Gemini's
|
|
750
763
|
// ~/.gemini/oauth_creds.json) or a service-account file path, WITHOUT
|
|
751
764
|
// parsing or logging the token. Additive: agents that don't declare
|
|
752
765
|
// creds_no_parse / creds_path_env get identical behavior to before.
|
|
753
|
-
const credsFile = this._evaluateCredsFile(checkReady);
|
|
754
|
-
const credsPathReady = this._checkCredsPathEnv(checkReady, savedEnv);
|
|
766
|
+
const credsFile = this._evaluateCredsFile(checkReady, agentType);
|
|
767
|
+
const credsPathReady = this._checkCredsPathEnv(checkReady, savedEnv, agentType);
|
|
755
768
|
|
|
756
769
|
let cliReady = false;
|
|
757
770
|
if (checkReady.status_command && binary) {
|
|
@@ -839,10 +852,10 @@ class Installer {
|
|
|
839
852
|
return keys.some((key) => !!(source && source[key]));
|
|
840
853
|
}
|
|
841
854
|
|
|
842
|
-
_checkCredsReady(checkReady) {
|
|
855
|
+
_checkCredsReady(checkReady, agentType) {
|
|
843
856
|
if (checkReady.creds_file) {
|
|
844
857
|
try {
|
|
845
|
-
const credsPath = checkReady.creds_file
|
|
858
|
+
const credsPath = this._expandHome(checkReady.creds_file, agentType);
|
|
846
859
|
if (fs.existsSync(credsPath)) {
|
|
847
860
|
const stat = fs.statSync(credsPath);
|
|
848
861
|
if (stat.isDirectory()) return fs.readdirSync(credsPath).length > 0;
|
|
@@ -862,14 +875,38 @@ class Installer {
|
|
|
862
875
|
return false;
|
|
863
876
|
}
|
|
864
877
|
|
|
865
|
-
/**
|
|
866
|
-
|
|
878
|
+
/**
|
|
879
|
+
* Expand a leading "~" to the home directory the AGENT writes its sign-in to.
|
|
880
|
+
*
|
|
881
|
+
* Normally the running user's. For an agent that resolved inside WSL it is
|
|
882
|
+
* the distro's $HOME, reached over the \\wsl.localhost mount — `claude login`
|
|
883
|
+
* run in the distro leaves ~/.claude there, and expanding against the Windows
|
|
884
|
+
* profile instead reports a signed-in agent as "Not logged in".
|
|
885
|
+
*/
|
|
886
|
+
_expandHome(p, agentType) {
|
|
867
887
|
if (typeof p !== 'string' || !p) return p;
|
|
868
|
-
|
|
869
|
-
if (p
|
|
888
|
+
const home = this._homeForAgent(agentType);
|
|
889
|
+
if (p === '~') return home;
|
|
890
|
+
if (p.startsWith('~/') || p.startsWith('~\\')) return path.join(home, p.slice(2));
|
|
870
891
|
return p;
|
|
871
892
|
}
|
|
872
893
|
|
|
894
|
+
/**
|
|
895
|
+
* Where this agent's dotfiles live, as a path this process can read. Falls
|
|
896
|
+
* back to the running user's home for every native install — and for a WSL
|
|
897
|
+
* one whose distro home could not be resolved, which is no worse than the
|
|
898
|
+
* behaviour before the WSL tier existed.
|
|
899
|
+
*/
|
|
900
|
+
_homeForAgent(agentType) {
|
|
901
|
+
if (!agentType) return os.homedir();
|
|
902
|
+
try {
|
|
903
|
+
if (isWslBinary(this._whichBinary(agentType))) {
|
|
904
|
+
return wslHomeUnc() || os.homedir();
|
|
905
|
+
}
|
|
906
|
+
} catch { /* resolution is best-effort; the host home is the safe answer */ }
|
|
907
|
+
return os.homedir();
|
|
908
|
+
}
|
|
909
|
+
|
|
873
910
|
/**
|
|
874
911
|
* Existence/readability of a credential file. Returns 'present' |
|
|
875
912
|
* 'unreadable' | 'absent'. A non-empty readable file (or a non-empty
|
|
@@ -881,9 +918,9 @@ class Installer {
|
|
|
881
918
|
* against the running user's home (the daemon/launcher user), not the
|
|
882
919
|
* renderer/browser user.
|
|
883
920
|
*/
|
|
884
|
-
_credsFileState(checkReady) {
|
|
921
|
+
_credsFileState(checkReady, agentType) {
|
|
885
922
|
if (!checkReady || !checkReady.creds_file) return 'absent';
|
|
886
|
-
const p = this._expandHome(checkReady.creds_file);
|
|
923
|
+
const p = this._expandHome(checkReady.creds_file, agentType);
|
|
887
924
|
let exists = false;
|
|
888
925
|
try { exists = fs.existsSync(p); } catch { return 'unreadable'; }
|
|
889
926
|
if (!exists) return 'absent';
|
|
@@ -922,11 +959,11 @@ class Installer {
|
|
|
922
959
|
* parse + match `creds_key` via _checkCredsReady) keep their exact behavior.
|
|
923
960
|
* Returns { ready, unreadable }.
|
|
924
961
|
*/
|
|
925
|
-
_evaluateCredsFile(checkReady) {
|
|
962
|
+
_evaluateCredsFile(checkReady, agentType) {
|
|
926
963
|
if (!checkReady || !checkReady.creds_file || !checkReady.creds_no_parse) {
|
|
927
964
|
return { ready: false, unreadable: false };
|
|
928
965
|
}
|
|
929
|
-
const state = this._credsFileState(checkReady);
|
|
966
|
+
const state = this._credsFileState(checkReady, agentType);
|
|
930
967
|
return { ready: state === 'present', unreadable: state === 'unreadable' };
|
|
931
968
|
}
|
|
932
969
|
|
|
@@ -936,14 +973,14 @@ class Installer {
|
|
|
936
973
|
* exists and is a readable file — a non-empty value alone is NOT enough.
|
|
937
974
|
* Checks both the process env and the agent's saved env.
|
|
938
975
|
*/
|
|
939
|
-
_checkCredsPathEnv(checkReady, savedEnv) {
|
|
976
|
+
_checkCredsPathEnv(checkReady, savedEnv, agentType) {
|
|
940
977
|
const names = checkReady && checkReady.creds_path_env;
|
|
941
978
|
if (!Array.isArray(names) || !names.length) return false;
|
|
942
979
|
for (const name of names) {
|
|
943
980
|
const raw = ((process.env[name] || (savedEnv && savedEnv[name]) || '') + '').trim();
|
|
944
981
|
if (!raw) continue;
|
|
945
982
|
try {
|
|
946
|
-
const p = this._expandHome(raw);
|
|
983
|
+
const p = this._expandHome(raw, agentType);
|
|
947
984
|
if (!fs.existsSync(p)) continue;
|
|
948
985
|
fs.accessSync(p, fs.constants.R_OK);
|
|
949
986
|
if (fs.statSync(p).isFile()) return true;
|
|
@@ -1568,8 +1605,12 @@ class Installer {
|
|
|
1568
1605
|
const managed = this._resolveManagedBinary(agentType, entry, binary, aliases);
|
|
1569
1606
|
if (managed) return managed;
|
|
1570
1607
|
|
|
1608
|
+
// allowWsl:false — whichBinary would otherwise end its own search inside
|
|
1609
|
+
// the distro, and this method still has two NATIVE tiers to run below. A
|
|
1610
|
+
// Windows copy must win over a WSL one; resolveBinaryInKnownDirs() reaches
|
|
1611
|
+
// the distro at the very end, once everything on this side has missed.
|
|
1571
1612
|
for (const candidate of [binary, ...aliases]) {
|
|
1572
|
-
const found = whichBinary(candidate);
|
|
1613
|
+
const found = whichBinary(candidate, { allowWsl: false });
|
|
1573
1614
|
if (found) return found;
|
|
1574
1615
|
}
|
|
1575
1616
|
// Fallback: resolve the installed package's OWN bin from its package.json
|