@magclaw/cli-core 0.1.26 → 0.1.28
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/cli.js +107 -23
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -12,7 +12,7 @@ import { renderListProfiles, shouldUseColor } from './list-renderer.js';
|
|
|
12
12
|
export const DEFAULT_PROFILE = 'default';
|
|
13
13
|
export const DEFAULT_SERVER_URL = 'http://127.0.0.1:6543';
|
|
14
14
|
const DEFAULT_DAEMON_HEARTBEAT_MS = 25_000;
|
|
15
|
-
const DEFAULT_DAEMON_INBOUND_WATCHDOG_MS =
|
|
15
|
+
const DEFAULT_DAEMON_INBOUND_WATCHDOG_MS = 15_000;
|
|
16
16
|
const DEFAULT_DAEMON_RECONNECT_MIN_MS = 1_000;
|
|
17
17
|
const DEFAULT_DAEMON_RECONNECT_MAX_MS = 30_000;
|
|
18
18
|
const DEFAULT_MAX_CONCURRENT_AGENT_STARTS = 5;
|
|
@@ -785,17 +785,66 @@ export function selectRuntimeCommandPath(output, fallback = '', platform = proce
|
|
|
785
785
|
.sort((left, right) => left.score - right.score || left.index - right.index)[0]?.file || paths[0];
|
|
786
786
|
}
|
|
787
787
|
|
|
788
|
+
function homeDirForEnv(env = process.env) {
|
|
789
|
+
return String(env.HOME || env.USERPROFILE || os.homedir() || '').trim();
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function uniquePathEntries(entries = []) {
|
|
793
|
+
const seen = new Set();
|
|
794
|
+
const result = [];
|
|
795
|
+
for (const entry of entries) {
|
|
796
|
+
const value = String(entry || '').trim();
|
|
797
|
+
if (!value) continue;
|
|
798
|
+
const key = durablePathKey(value);
|
|
799
|
+
if (seen.has(key)) continue;
|
|
800
|
+
seen.add(key);
|
|
801
|
+
result.push(value);
|
|
802
|
+
}
|
|
803
|
+
return result;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
export function runtimeSearchPathEntries(env = process.env) {
|
|
807
|
+
const home = homeDirForEnv(env);
|
|
808
|
+
const userEntries = home ? [
|
|
809
|
+
path.join(home, '.local', 'bin'),
|
|
810
|
+
path.join(home, 'bin'),
|
|
811
|
+
path.join(home, '.npm-global', 'bin'),
|
|
812
|
+
path.join(home, '.volta', 'bin'),
|
|
813
|
+
path.join(home, '.bun', 'bin'),
|
|
814
|
+
process.platform === 'win32' ? path.join(home, 'AppData', 'Roaming', 'npm') : '',
|
|
815
|
+
] : [];
|
|
816
|
+
const platformEntries = process.platform === 'darwin'
|
|
817
|
+
? ['/opt/homebrew/bin', '/opt/homebrew/sbin', '/usr/local/bin', '/usr/local/sbin']
|
|
818
|
+
: process.platform === 'win32'
|
|
819
|
+
? []
|
|
820
|
+
: ['/usr/local/bin', '/usr/local/sbin'];
|
|
821
|
+
return uniquePathEntries([
|
|
822
|
+
...pathDirs(env),
|
|
823
|
+
env.NVM_BIN,
|
|
824
|
+
env.VOLTA_HOME ? path.join(env.VOLTA_HOME, 'bin') : '',
|
|
825
|
+
env.BUN_INSTALL ? path.join(env.BUN_INSTALL, 'bin') : '',
|
|
826
|
+
path.dirname(process.execPath),
|
|
827
|
+
...userEntries,
|
|
828
|
+
...platformEntries,
|
|
829
|
+
]);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function runtimeDetectionEnv(env = process.env) {
|
|
833
|
+
return {
|
|
834
|
+
...env,
|
|
835
|
+
PATH: runtimeSearchPathEntries(env).join(path.delimiter),
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
788
839
|
function commandExists(command, env = process.env) {
|
|
840
|
+
const runtimeEnv = runtimeDetectionEnv(env);
|
|
789
841
|
const checker = process.platform === 'win32' ? 'where' : 'command';
|
|
790
842
|
const args = process.platform === 'win32' ? [command] : ['-v', command];
|
|
791
843
|
const result = process.platform === 'win32'
|
|
792
|
-
? commandOutput(checker, args, { env, timeoutMs: 1500 })
|
|
793
|
-
: commandOutput('/bin/sh', ['-lc', `command -v ${JSON.stringify(command)}`], { env, timeoutMs: 1500 });
|
|
844
|
+
? commandOutput(checker, args, { env: runtimeEnv, timeoutMs: 1500 })
|
|
845
|
+
: commandOutput('/bin/sh', ['-lc', `command -v ${JSON.stringify(command)}`], { env: runtimeEnv, timeoutMs: 1500 });
|
|
794
846
|
if (result.ok) return selectRuntimeCommandPath(result.stdout, command);
|
|
795
|
-
for (const candidate of
|
|
796
|
-
path.join(path.dirname(process.execPath), command),
|
|
797
|
-
path.join(os.homedir(), '.local', 'bin', command),
|
|
798
|
-
]) {
|
|
847
|
+
for (const candidate of runtimeSearchPathEntries(runtimeEnv).map((dir) => path.join(dir, command))) {
|
|
799
848
|
if (existsSync(candidate)) return candidate;
|
|
800
849
|
}
|
|
801
850
|
return '';
|
|
@@ -1046,13 +1095,15 @@ async function tryInstallCliShim(options = {}, env = process.env) {
|
|
|
1046
1095
|
}
|
|
1047
1096
|
|
|
1048
1097
|
function runtimeVersion(command, env = process.env) {
|
|
1049
|
-
const
|
|
1098
|
+
const runtimeEnv = runtimeDetectionEnv(env);
|
|
1099
|
+
const result = commandOutput(command, ['--version'], { env: runtimeEnv, timeoutMs: 3000 });
|
|
1050
1100
|
if (!result.ok && result.error?.code === 'ENOENT') return '';
|
|
1051
1101
|
return result.stdout || result.stderr || '';
|
|
1052
1102
|
}
|
|
1053
1103
|
|
|
1054
1104
|
function codexAppServerCapable(command, env = process.env) {
|
|
1055
|
-
const
|
|
1105
|
+
const runtimeEnv = runtimeDetectionEnv(env);
|
|
1106
|
+
const result = commandOutput(command, ['app-server', '--help'], { env: runtimeEnv, timeoutMs: 3000 });
|
|
1056
1107
|
if (result.error?.code === 'ENOENT') return false;
|
|
1057
1108
|
if (result.ok) return true;
|
|
1058
1109
|
const output = `${result.stdout}\n${result.stderr}`.toLowerCase();
|
|
@@ -1060,6 +1111,7 @@ function codexAppServerCapable(command, env = process.env) {
|
|
|
1060
1111
|
}
|
|
1061
1112
|
|
|
1062
1113
|
function defaultCodexCommand(env = process.env) {
|
|
1114
|
+
const runtimeEnv = runtimeDetectionEnv(env);
|
|
1063
1115
|
const macAppBinary = '/Applications/Codex.app/Contents/Resources/codex';
|
|
1064
1116
|
const candidates = [env.CODEX_PATH, env.MAGCLAW_CODEX_PATH, macAppBinary, 'codex']
|
|
1065
1117
|
.map((item) => String(item || '').trim())
|
|
@@ -1067,9 +1119,9 @@ function defaultCodexCommand(env = process.env) {
|
|
|
1067
1119
|
for (const candidate of [...new Set(candidates)]) {
|
|
1068
1120
|
const command = runtimeCommandHasPathSeparator(candidate)
|
|
1069
1121
|
? candidate
|
|
1070
|
-
: commandExists(candidate,
|
|
1122
|
+
: commandExists(candidate, runtimeEnv) || candidate;
|
|
1071
1123
|
if (runtimeCommandHasPathSeparator(command) && !existsSync(command)) continue;
|
|
1072
|
-
const result = commandOutput(command, ['--version'], { env, timeoutMs: 3000 });
|
|
1124
|
+
const result = commandOutput(command, ['--version'], { env: runtimeEnv, timeoutMs: 3000 });
|
|
1073
1125
|
if (result.ok) return command;
|
|
1074
1126
|
}
|
|
1075
1127
|
return candidates[0] || 'codex';
|
|
@@ -1088,7 +1140,7 @@ function codexRuntimeModels(command, env = process.env) {
|
|
|
1088
1140
|
reasoningEffort: ['low', 'medium', 'high', 'xhigh'],
|
|
1089
1141
|
defaultReasoningEffort: 'medium',
|
|
1090
1142
|
};
|
|
1091
|
-
const result = commandOutput(command, ['debug', 'models'], { env, timeoutMs: 5000 });
|
|
1143
|
+
const result = commandOutput(command, ['debug', 'models'], { env: runtimeDetectionEnv(env), timeoutMs: 5000 });
|
|
1092
1144
|
if (!result.ok) return fallback;
|
|
1093
1145
|
try {
|
|
1094
1146
|
const data = JSON.parse(result.stdout || '{}');
|
|
@@ -1121,14 +1173,15 @@ function codexRuntimeModels(command, env = process.env) {
|
|
|
1121
1173
|
}
|
|
1122
1174
|
|
|
1123
1175
|
export async function detectRuntimes(env = process.env) {
|
|
1124
|
-
const
|
|
1176
|
+
const runtimeEnv = runtimeDetectionEnv(env);
|
|
1177
|
+
const codexCommand = defaultCodexCommand(runtimeEnv);
|
|
1125
1178
|
const candidates = [
|
|
1126
1179
|
{
|
|
1127
1180
|
id: 'codex',
|
|
1128
1181
|
name: 'Codex CLI',
|
|
1129
1182
|
command: codexCommand,
|
|
1130
1183
|
createSupported: true,
|
|
1131
|
-
modelsFor: (command) => codexRuntimeModels(command,
|
|
1184
|
+
modelsFor: (command) => codexRuntimeModels(command, runtimeEnv),
|
|
1132
1185
|
},
|
|
1133
1186
|
{
|
|
1134
1187
|
id: 'claude-code',
|
|
@@ -1180,10 +1233,10 @@ export async function detectRuntimes(env = process.env) {
|
|
|
1180
1233
|
},
|
|
1181
1234
|
];
|
|
1182
1235
|
return candidates.map((item) => {
|
|
1183
|
-
const pathValue = runtimeCommandHasPathSeparator(item.command) ? (existsSync(item.command) ? item.command : '') : commandExists(item.command,
|
|
1236
|
+
const pathValue = runtimeCommandHasPathSeparator(item.command) ? (existsSync(item.command) ? item.command : '') : commandExists(item.command, runtimeEnv);
|
|
1184
1237
|
const runtimeCommand = pathValue || item.command;
|
|
1185
1238
|
const installed = Boolean(pathValue);
|
|
1186
|
-
const version = installed ? runtimeVersion(runtimeCommand,
|
|
1239
|
+
const version = installed ? runtimeVersion(runtimeCommand, runtimeEnv) : '';
|
|
1187
1240
|
const modelInfo = installed && item.modelsFor ? item.modelsFor(runtimeCommand) : {
|
|
1188
1241
|
models: item.models || [],
|
|
1189
1242
|
modelNames: (item.models || []).map((model) => ({ slug: model, name: model })),
|
|
@@ -1199,7 +1252,7 @@ export async function detectRuntimes(env = process.env) {
|
|
|
1199
1252
|
installed,
|
|
1200
1253
|
version,
|
|
1201
1254
|
createSupported: item.createSupported !== false,
|
|
1202
|
-
appServer: item.id === 'codex' && installed ? codexAppServerCapable(runtimeCommand,
|
|
1255
|
+
appServer: item.id === 'codex' && installed ? codexAppServerCapable(runtimeCommand, runtimeEnv) : false,
|
|
1203
1256
|
...modelInfo,
|
|
1204
1257
|
};
|
|
1205
1258
|
});
|
|
@@ -3560,7 +3613,7 @@ class MagClawDaemon {
|
|
|
3560
3613
|
} else {
|
|
3561
3614
|
logInfo('daemon', 'Foreground close request did not stop background service.');
|
|
3562
3615
|
}
|
|
3563
|
-
this.close();
|
|
3616
|
+
this.close({ notify: false, reason: 'cloud_close' });
|
|
3564
3617
|
process.exitCode = 0;
|
|
3565
3618
|
setTimeout(() => process.exit(0), 50).unref?.();
|
|
3566
3619
|
}
|
|
@@ -3803,7 +3856,7 @@ class MagClawDaemon {
|
|
|
3803
3856
|
break;
|
|
3804
3857
|
case 'token:revoked':
|
|
3805
3858
|
logError('daemon', 'Machine token was revoked by the server.');
|
|
3806
|
-
this.close();
|
|
3859
|
+
this.close({ notify: false, reason: 'token_revoked' });
|
|
3807
3860
|
process.exitCode = 2;
|
|
3808
3861
|
break;
|
|
3809
3862
|
default:
|
|
@@ -4081,8 +4134,29 @@ class MagClawDaemon {
|
|
|
4081
4134
|
});
|
|
4082
4135
|
}
|
|
4083
4136
|
|
|
4084
|
-
|
|
4137
|
+
sendStoppingNotice(reason = 'local_stop') {
|
|
4138
|
+
const packageInfo = runtimePackageInfo(this.env);
|
|
4139
|
+
const sent = this.send({
|
|
4140
|
+
type: 'daemon:stopping',
|
|
4141
|
+
time: now(),
|
|
4142
|
+
reason,
|
|
4143
|
+
computerId: this.config.computerId || null,
|
|
4144
|
+
daemonVersion: packageInfo.version || DAEMON_VERSION,
|
|
4145
|
+
packageName: packageInfo.name,
|
|
4146
|
+
packageVersion: packageInfo.version,
|
|
4147
|
+
packageKind: packageInfo.kind,
|
|
4148
|
+
packageSpec: packageInfo.spec,
|
|
4149
|
+
packageBin: packageInfo.bin,
|
|
4150
|
+
runningAgents: [...this.sessions.keys()],
|
|
4151
|
+
});
|
|
4152
|
+
if (sent) logInfo('daemon', `Sent stopping notice (${reason}).`);
|
|
4153
|
+
return sent;
|
|
4154
|
+
}
|
|
4155
|
+
|
|
4156
|
+
close(options = {}) {
|
|
4085
4157
|
if (this.closed) return;
|
|
4158
|
+
const notify = options.notify !== false;
|
|
4159
|
+
if (notify) this.sendStoppingNotice(options.reason || 'local_stop');
|
|
4086
4160
|
this.closed = true;
|
|
4087
4161
|
for (const session of this.sessions.values()) session.stop();
|
|
4088
4162
|
this.sessions.clear();
|
|
@@ -4102,7 +4176,10 @@ class MagClawDaemon {
|
|
|
4102
4176
|
this.request.destroy(new Error('MagClaw daemon is shutting down.'));
|
|
4103
4177
|
this.request = null;
|
|
4104
4178
|
}
|
|
4105
|
-
if (this.socket && !this.socket.destroyed)
|
|
4179
|
+
if (this.socket && !this.socket.destroyed) {
|
|
4180
|
+
if (notify) this.socket.end();
|
|
4181
|
+
else this.socket.destroy();
|
|
4182
|
+
}
|
|
4106
4183
|
this.socket = null;
|
|
4107
4184
|
}
|
|
4108
4185
|
|
|
@@ -4271,6 +4348,10 @@ async function writeLauncher(profile, env = process.env) {
|
|
|
4271
4348
|
const npmPath = commandExists('npm', env);
|
|
4272
4349
|
const nodeDir = path.dirname(process.execPath);
|
|
4273
4350
|
const npmDir = npmPath ? path.dirname(npmPath) : '';
|
|
4351
|
+
const launchPathEntries = runtimeSearchPathEntries({
|
|
4352
|
+
...env,
|
|
4353
|
+
PATH: [nodeDir, npmDir, env.PATH || '/usr/bin:/bin:/usr/sbin:/sbin'].filter(Boolean).join(path.delimiter),
|
|
4354
|
+
});
|
|
4274
4355
|
const commandMode = String(env.MAGCLAW_DAEMON_COMMAND_MODE || '').trim().toLowerCase();
|
|
4275
4356
|
const useNpmLauncher = Boolean(npmPath) && !['local', 'local-repo', 'repo', 'source'].includes(commandMode);
|
|
4276
4357
|
const launcher = path.join(paths.runDir, 'launcher.js');
|
|
@@ -4322,6 +4403,8 @@ async function writeLauncher(profile, env = process.env) {
|
|
|
4322
4403
|
`const useNpmLauncher = ${JSON.stringify(useNpmLauncher)};`,
|
|
4323
4404
|
`const nodeDir = ${JSON.stringify(nodeDir)};`,
|
|
4324
4405
|
`const npmDir = ${JSON.stringify(npmDir)};`,
|
|
4406
|
+
`const pathDelimiter = ${JSON.stringify(path.delimiter)};`,
|
|
4407
|
+
`const launchPathEntries = ${JSON.stringify(launchPathEntries)};`,
|
|
4325
4408
|
`const fallbackBin = ${JSON.stringify(fallbackBin)};`,
|
|
4326
4409
|
`const profile = ${JSON.stringify(paths.profile)};`,
|
|
4327
4410
|
`const daemonHome = ${JSON.stringify(daemonRoot(env))};`,
|
|
@@ -4344,7 +4427,7 @@ async function writeLauncher(profile, env = process.env) {
|
|
|
4344
4427
|
"const args = useNpmLauncher",
|
|
4345
4428
|
" ? ['exec', '--yes', '--package', packageSpec, '--', packageBin, 'connect', '--profile', profile]",
|
|
4346
4429
|
" : [fallbackBin, 'connect', '--profile', profile];",
|
|
4347
|
-
"const launchPath = [nodeDir, npmDir, process.env.PATH || '/usr/bin:/bin:/usr/sbin:/sbin'].filter(Boolean).join(
|
|
4430
|
+
"const launchPath = [...launchPathEntries, nodeDir, npmDir, process.env.PATH || '/usr/bin:/bin:/usr/sbin:/sbin'].filter(Boolean).join(pathDelimiter);",
|
|
4348
4431
|
"const childEnv = {",
|
|
4349
4432
|
" ...process.env,",
|
|
4350
4433
|
" MAGCLAW_DAEMON_HOME: daemonHome,",
|
|
@@ -5289,6 +5372,7 @@ async function runComputerSetup(flags, env = process.env) {
|
|
|
5289
5372
|
...result,
|
|
5290
5373
|
cli,
|
|
5291
5374
|
computerId: config.computerId,
|
|
5375
|
+
computerName: config.computerName || config.name || displayName,
|
|
5292
5376
|
profile: config.profile,
|
|
5293
5377
|
serverName: config.serverName,
|
|
5294
5378
|
serverSlug: approved.serverSlug || serverSlug,
|
|
@@ -5326,7 +5410,7 @@ async function runForegroundDaemon(config, env = process.env) {
|
|
|
5326
5410
|
let forceExitTimer = null;
|
|
5327
5411
|
const shutdown = (signal) => {
|
|
5328
5412
|
process.exitCode = signal === 'SIGINT' ? 130 : 143;
|
|
5329
|
-
daemon.close();
|
|
5413
|
+
daemon.close({ reason: signal === 'SIGINT' ? 'sigint' : 'sigterm' });
|
|
5330
5414
|
forceExitTimer ||= setTimeout(() => process.exit(process.exitCode || 1), 5000);
|
|
5331
5415
|
forceExitTimer.unref?.();
|
|
5332
5416
|
};
|