@magclaw/cli-core 0.1.24 → 0.1.25
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 +136 -11
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -404,6 +404,12 @@ export function profilePaths(profile = DEFAULT_PROFILE, env = process.env) {
|
|
|
404
404
|
};
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
+
function sleepSync(ms) {
|
|
408
|
+
const timeout = Math.max(0, Number(ms) || 0);
|
|
409
|
+
if (!timeout) return;
|
|
410
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, timeout);
|
|
411
|
+
}
|
|
412
|
+
|
|
407
413
|
function machineFingerprintValue() {
|
|
408
414
|
return `mfp_${crypto.createHash('sha256')
|
|
409
415
|
.update([
|
|
@@ -673,6 +679,10 @@ async function readServiceState(profile = DEFAULT_PROFILE, env = process.env) {
|
|
|
673
679
|
previousPackageSpec: state.previousPackageSpec || '',
|
|
674
680
|
installedDaemonVersion: state.installedDaemonVersion || DAEMON_VERSION,
|
|
675
681
|
installedPackageVersion: state.installedPackageVersion || packageVersion || state.installedDaemonVersion || DAEMON_VERSION,
|
|
682
|
+
remoteClosed: Boolean(state.remoteClosed),
|
|
683
|
+
remoteClosedAt: state.remoteClosedAt || '',
|
|
684
|
+
remoteCloseReason: state.remoteCloseReason || '',
|
|
685
|
+
remoteCloseCommandId: state.remoteCloseCommandId || '',
|
|
676
686
|
updatedAt: state.updatedAt || '',
|
|
677
687
|
};
|
|
678
688
|
}
|
|
@@ -691,6 +701,31 @@ async function writeServiceState(profile = DEFAULT_PROFILE, patch = {}, env = pr
|
|
|
691
701
|
return next;
|
|
692
702
|
}
|
|
693
703
|
|
|
704
|
+
async function clearRemoteClosedServiceState(profile = DEFAULT_PROFILE, env = process.env) {
|
|
705
|
+
return writeServiceState(profile, {
|
|
706
|
+
remoteClosed: false,
|
|
707
|
+
remoteClosedAt: '',
|
|
708
|
+
remoteCloseReason: '',
|
|
709
|
+
remoteCloseCommandId: '',
|
|
710
|
+
}, env);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
async function markForegroundServiceState(profile = DEFAULT_PROFILE, env = process.env) {
|
|
714
|
+
const packageInfo = runtimePackageInfo(env);
|
|
715
|
+
return writeServiceState(profile, {
|
|
716
|
+
mode: 'foreground',
|
|
717
|
+
background: false,
|
|
718
|
+
launcher: 'foreground',
|
|
719
|
+
packageSpec: packageInfo.spec,
|
|
720
|
+
packageName: packageInfo.name,
|
|
721
|
+
packageVersion: packageInfo.version,
|
|
722
|
+
packageKind: packageInfo.kind,
|
|
723
|
+
packageBin: packageInfo.bin,
|
|
724
|
+
installedDaemonVersion: packageInfo.version || DAEMON_VERSION,
|
|
725
|
+
installedPackageVersion: packageInfo.version || DAEMON_VERSION,
|
|
726
|
+
}, env);
|
|
727
|
+
}
|
|
728
|
+
|
|
694
729
|
async function readUpgradeHandoff(profile = DEFAULT_PROFILE, env = process.env) {
|
|
695
730
|
const paths = profilePaths(profile, env);
|
|
696
731
|
const handoff = await readJsonFile(paths.upgradeHandoff, null);
|
|
@@ -1187,6 +1222,27 @@ export function toWebSocketUrl(serverUrl, config = {}) {
|
|
|
1187
1222
|
if (/^mfp_[a-f0-9]{64}$/i.test(machineFingerprint)) {
|
|
1188
1223
|
url.searchParams.set('machine_fingerprint', machineFingerprint.toLowerCase());
|
|
1189
1224
|
}
|
|
1225
|
+
const packageName = String(config.packageName || '').trim();
|
|
1226
|
+
const packageVersion = String(config.packageVersion || config.daemonVersion || '').trim();
|
|
1227
|
+
const packageKind = String(config.packageKind || '').trim();
|
|
1228
|
+
const packageSpec = String(config.packageSpec || '').trim();
|
|
1229
|
+
const packageBin = String(config.packageBin || '').trim();
|
|
1230
|
+
const cliCoreVersion = String(config.cliCoreVersion || '').trim();
|
|
1231
|
+
const serviceMode = String(config.serviceMode || '').trim();
|
|
1232
|
+
const serviceBackground = Boolean(config.serviceBackground);
|
|
1233
|
+
const serviceActive = Boolean(config.serviceActive);
|
|
1234
|
+
if (packageName) url.searchParams.set('package_name', packageName);
|
|
1235
|
+
if (packageVersion) {
|
|
1236
|
+
url.searchParams.set('package_version', packageVersion);
|
|
1237
|
+
url.searchParams.set('daemon_version', packageVersion);
|
|
1238
|
+
}
|
|
1239
|
+
if (packageKind) url.searchParams.set('package_kind', packageKind);
|
|
1240
|
+
if (packageSpec) url.searchParams.set('package_spec', packageSpec);
|
|
1241
|
+
if (packageBin) url.searchParams.set('package_bin', packageBin);
|
|
1242
|
+
if (cliCoreVersion) url.searchParams.set('cli_core_version', cliCoreVersion);
|
|
1243
|
+
if (serviceMode) url.searchParams.set('service_mode', serviceMode);
|
|
1244
|
+
url.searchParams.set('service_background', String(serviceBackground));
|
|
1245
|
+
url.searchParams.set('service_active', String(serviceActive));
|
|
1190
1246
|
return url;
|
|
1191
1247
|
}
|
|
1192
1248
|
|
|
@@ -3098,6 +3154,12 @@ class ClaudeAgentSession {
|
|
|
3098
3154
|
}
|
|
3099
3155
|
}
|
|
3100
3156
|
|
|
3157
|
+
function readOnlySessionAckStatus(session) {
|
|
3158
|
+
const status = String(session?.status || '').toLowerCase();
|
|
3159
|
+
if ((!status || status === 'offline') && !session?.started) return 'idle';
|
|
3160
|
+
return session?.status || 'idle';
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3101
3163
|
class MagClawDaemon {
|
|
3102
3164
|
constructor(config, env = process.env) {
|
|
3103
3165
|
this.env = env;
|
|
@@ -3473,6 +3535,12 @@ class MagClawDaemon {
|
|
|
3473
3535
|
packageBin: packageInfo.bin,
|
|
3474
3536
|
};
|
|
3475
3537
|
logWarning('daemon', `Remote close requested (${message.reason || 'closed_from_cloud'}).`);
|
|
3538
|
+
await writeServiceState(this.paths.profile, {
|
|
3539
|
+
remoteClosed: true,
|
|
3540
|
+
remoteClosedAt: now(),
|
|
3541
|
+
remoteCloseReason: message.reason || 'closed_from_cloud',
|
|
3542
|
+
remoteCloseCommandId: commandId,
|
|
3543
|
+
}, this.env);
|
|
3476
3544
|
for (const session of this.sessions.values()) session.stop();
|
|
3477
3545
|
this.sessions.clear();
|
|
3478
3546
|
this.send({ type: 'daemon:close:ack',
|
|
@@ -3746,7 +3814,7 @@ class MagClawDaemon {
|
|
|
3746
3814
|
? await session.listSkills()
|
|
3747
3815
|
: { agent: { id: agent.id, name: agent.name || agent.id }, global: [], workspace: [], plugin: [], tools: [] };
|
|
3748
3816
|
this.send({ type: 'agent:skills:list_result', commandId: message.commandId, agentId: agent.id, skills });
|
|
3749
|
-
this.send({ type: 'agent:ack', commandId: message.commandId, agentId: agent.id, status: session
|
|
3817
|
+
this.send({ type: 'agent:ack', commandId: message.commandId, agentId: agent.id, status: readOnlySessionAckStatus(session) });
|
|
3750
3818
|
} catch (error) {
|
|
3751
3819
|
this.send({ type: 'agent:skills:list_result', commandId: message.commandId, agentId: agent.id, skills: { agent: { id: agent.id, name: agent.name || agent.id }, global: [], workspace: [], plugin: [], tools: [], error: error.message } });
|
|
3752
3820
|
this.send({ type: 'agent:error', commandId: message.commandId, agentId: agent.id, error: error.message });
|
|
@@ -3771,7 +3839,7 @@ class MagClawDaemon {
|
|
|
3771
3839
|
const session = await this.sessionForWorkspaceRequest(message);
|
|
3772
3840
|
const tree = await session.listWorkspace(message.path || message.payload?.path || '');
|
|
3773
3841
|
this.send({ type: 'agent:workspace:list_result', commandId: message.commandId, agentId: session.agent.id, tree });
|
|
3774
|
-
this.send({ type: 'agent:ack', commandId: message.commandId, agentId: session.agent.id, status: session
|
|
3842
|
+
this.send({ type: 'agent:ack', commandId: message.commandId, agentId: session.agent.id, status: readOnlySessionAckStatus(session) });
|
|
3775
3843
|
} catch (error) {
|
|
3776
3844
|
this.send({ type: 'agent:error', commandId: message.commandId, agentId: message.agentId || null, error: error.message });
|
|
3777
3845
|
}
|
|
@@ -3782,7 +3850,7 @@ class MagClawDaemon {
|
|
|
3782
3850
|
const session = await this.sessionForWorkspaceRequest(message);
|
|
3783
3851
|
const file = await session.readWorkspaceFile(message.path || message.payload?.path || 'MEMORY.md');
|
|
3784
3852
|
this.send({ type: 'agent:workspace:file_result', commandId: message.commandId, agentId: session.agent.id, file: file.file });
|
|
3785
|
-
this.send({ type: 'agent:ack', commandId: message.commandId, agentId: session.agent.id, status: session
|
|
3853
|
+
this.send({ type: 'agent:ack', commandId: message.commandId, agentId: session.agent.id, status: readOnlySessionAckStatus(session) });
|
|
3786
3854
|
} catch (error) {
|
|
3787
3855
|
this.send({ type: 'agent:error', commandId: message.commandId, agentId: message.agentId || null, error: error.message });
|
|
3788
3856
|
}
|
|
@@ -4032,11 +4100,24 @@ class MagClawDaemon {
|
|
|
4032
4100
|
async connectOnce() {
|
|
4033
4101
|
if (this.closed) return;
|
|
4034
4102
|
await this.refreshConfigFromDisk();
|
|
4035
|
-
const
|
|
4103
|
+
const service = await readServiceState(this.paths.profile, this.env);
|
|
4104
|
+
const serviceStatus = backgroundServiceStatus(this.paths.profile, this.env);
|
|
4105
|
+
const packageInfo = runtimePackageInfo(this.env, service);
|
|
4106
|
+
const url = toWebSocketUrl(this.config.serverUrl, {
|
|
4107
|
+
...this.config,
|
|
4108
|
+
packageName: packageInfo.name,
|
|
4109
|
+
packageVersion: packageInfo.version,
|
|
4110
|
+
packageKind: packageInfo.kind,
|
|
4111
|
+
packageSpec: packageInfo.spec,
|
|
4112
|
+
packageBin: packageInfo.bin,
|
|
4113
|
+
cliCoreVersion: CLI_CORE_VERSION,
|
|
4114
|
+
serviceMode: service.mode || serviceStatus.mode || 'foreground',
|
|
4115
|
+
serviceBackground: Boolean(service.background),
|
|
4116
|
+
serviceActive: Boolean(serviceStatus.active),
|
|
4117
|
+
});
|
|
4036
4118
|
const requestModule = url.protocol === 'wss:' ? https : http;
|
|
4037
4119
|
const requestUrl = new URL(url.href.replace(/^ws/, 'http'));
|
|
4038
4120
|
const key = crypto.randomBytes(16).toString('base64');
|
|
4039
|
-
const packageInfo = runtimePackageInfo(this.env);
|
|
4040
4121
|
logInfo('daemon', `Connecting MagClaw daemon v${packageInfo.version || DAEMON_VERSION} profile "${this.paths.profile}" to ${this.config.serverUrl}...`);
|
|
4041
4122
|
if (this.closed) return;
|
|
4042
4123
|
return new Promise((resolve, reject) => {
|
|
@@ -4218,6 +4299,10 @@ async function writeLauncher(profile, env = process.env) {
|
|
|
4218
4299
|
installedDaemonVersion: packageVersion || DAEMON_VERSION,
|
|
4219
4300
|
installedPackageVersion: packageVersion || DAEMON_VERSION,
|
|
4220
4301
|
commandMode: useNpmLauncher ? 'npm' : 'local',
|
|
4302
|
+
remoteClosed: false,
|
|
4303
|
+
remoteClosedAt: '',
|
|
4304
|
+
remoteCloseReason: '',
|
|
4305
|
+
remoteCloseCommandId: '',
|
|
4221
4306
|
}, env);
|
|
4222
4307
|
const code = [
|
|
4223
4308
|
'#!/usr/bin/env node',
|
|
@@ -4234,6 +4319,11 @@ async function writeLauncher(profile, env = process.env) {
|
|
|
4234
4319
|
`const defaultPackageSpec = ${JSON.stringify(service.packageSpec)};`,
|
|
4235
4320
|
"let service = {};",
|
|
4236
4321
|
"try { service = JSON.parse(fs.readFileSync(serviceFile, 'utf8')); } catch {}",
|
|
4322
|
+
"if (service.remoteClosed) {",
|
|
4323
|
+
" const stamp = new Date().toISOString().replace('T', ' ').slice(0, 19);",
|
|
4324
|
+
" console.error(`${stamp} INFO DAEMON profile ${profile} is closed from MagClaw Cloud; run magclaw start/restart/connect to reconnect.`);",
|
|
4325
|
+
" process.exit(0);",
|
|
4326
|
+
"}",
|
|
4237
4327
|
"const packageSpec = String(service.packageSpec || defaultPackageSpec || '@magclaw/daemon@latest');",
|
|
4238
4328
|
"const packageName = String(service.packageName || (packageSpec.startsWith('@magclaw/computer@') || packageSpec === '@magclaw/computer' ? '@magclaw/computer' : '@magclaw/daemon'));",
|
|
4239
4329
|
"const packageKind = String(service.packageKind || (packageName === '@magclaw/computer' ? 'computer' : 'daemon'));",
|
|
@@ -4446,6 +4536,29 @@ function backgroundServiceStatus(profile, env = process.env) {
|
|
|
4446
4536
|
return { mode: 'foreground', active: false };
|
|
4447
4537
|
}
|
|
4448
4538
|
|
|
4539
|
+
function launchctlResultIsNotLoaded(result) {
|
|
4540
|
+
const detail = String(result?.stderr || result?.stdout || '').toLowerCase();
|
|
4541
|
+
return detail.includes('no such process') || detail.includes('could not find service') || detail.includes('not found');
|
|
4542
|
+
}
|
|
4543
|
+
|
|
4544
|
+
function launchctlServiceIsStopped(serviceTarget) {
|
|
4545
|
+
const result = spawnSync('launchctl', ['print', serviceTarget], { encoding: 'utf8' });
|
|
4546
|
+
if (result.status !== 0) return launchctlResultIsNotLoaded(result);
|
|
4547
|
+
const output = String(result.stdout || result.stderr || '');
|
|
4548
|
+
if (/\bstate\s*=\s*running\b/i.test(output)) return false;
|
|
4549
|
+
if (/\bpid\s*=\s*[1-9][0-9]*\b/i.test(output)) return false;
|
|
4550
|
+
return true;
|
|
4551
|
+
}
|
|
4552
|
+
|
|
4553
|
+
function waitForBackgroundServiceStopped(serviceTarget, timeoutMs = 2000) {
|
|
4554
|
+
const deadline = Date.now() + Math.max(250, Number(timeoutMs) || 2000);
|
|
4555
|
+
while (Date.now() < deadline) {
|
|
4556
|
+
if (launchctlServiceIsStopped(serviceTarget)) return true;
|
|
4557
|
+
sleepSync(100);
|
|
4558
|
+
}
|
|
4559
|
+
return launchctlServiceIsStopped(serviceTarget);
|
|
4560
|
+
}
|
|
4561
|
+
|
|
4449
4562
|
async function waitForPidExit(pid, timeoutMs = 2000) {
|
|
4450
4563
|
const deadline = Date.now() + timeoutMs;
|
|
4451
4564
|
while (Date.now() < deadline) {
|
|
@@ -4498,23 +4611,32 @@ function stopBackground(profile, env = process.env, options = {}) {
|
|
|
4498
4611
|
if (process.platform === 'darwin') {
|
|
4499
4612
|
const paths = profilePaths(profile, env);
|
|
4500
4613
|
const label = launchAgentLabel(paths.profile);
|
|
4614
|
+
const serviceTarget = `gui/${process.getuid()}/${label}`;
|
|
4501
4615
|
const plist = path.join(os.homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
|
|
4502
4616
|
if (options.disable) {
|
|
4503
|
-
const disabled = spawnSync('launchctl', ['disable',
|
|
4617
|
+
const disabled = spawnSync('launchctl', ['disable', serviceTarget], { encoding: 'utf8' });
|
|
4504
4618
|
const stopped = spawnSync('launchctl', ['stop', label], { encoding: 'utf8' });
|
|
4619
|
+
const stoppedConfirmed = stopped.status === 0 && waitForBackgroundServiceStopped(serviceTarget);
|
|
4620
|
+
const needsBootout = disabled.status !== 0 || !stoppedConfirmed;
|
|
4621
|
+
const bootout = needsBootout
|
|
4622
|
+
? spawnSync('launchctl', ['bootout', serviceTarget], { encoding: 'utf8' })
|
|
4623
|
+
: null;
|
|
4624
|
+
const bootoutOk = bootout ? (bootout.status === 0 || launchctlResultIsNotLoaded(bootout)) : false;
|
|
4505
4625
|
if (stopped.status !== 0) spawnSync('launchctl', ['bootout', `gui/${process.getuid()}`, plist], { stdio: 'ignore' });
|
|
4506
4626
|
return {
|
|
4507
|
-
ok: disabled.status === 0
|
|
4627
|
+
ok: disabled.status === 0 && (stoppedConfirmed || bootoutOk),
|
|
4508
4628
|
mode: 'launchd',
|
|
4509
4629
|
label,
|
|
4630
|
+
serviceTarget,
|
|
4510
4631
|
file: plist,
|
|
4511
4632
|
disabled: disabled.status === 0,
|
|
4512
|
-
stopped:
|
|
4513
|
-
|
|
4633
|
+
stopped: stoppedConfirmed || bootoutOk,
|
|
4634
|
+
bootout: bootout ? bootout.status === 0 : false,
|
|
4635
|
+
error: disabled.status === 0 && (stoppedConfirmed || bootoutOk) ? '' : String(bootout?.stderr || stopped.stderr || disabled.stderr || bootout?.stdout || stopped.stdout || disabled.stdout || '').trim(),
|
|
4514
4636
|
};
|
|
4515
4637
|
}
|
|
4516
|
-
spawnSync('launchctl', ['bootout',
|
|
4517
|
-
return { ok:
|
|
4638
|
+
const bootout = spawnSync('launchctl', ['bootout', serviceTarget], { encoding: 'utf8' });
|
|
4639
|
+
return { ok: bootout.status === 0 || launchctlResultIsNotLoaded(bootout), mode: 'launchd', label, serviceTarget, file: plist, error: bootout.stderr || bootout.stdout || '' };
|
|
4518
4640
|
}
|
|
4519
4641
|
if (process.platform === 'linux') {
|
|
4520
4642
|
const paths = profilePaths(profile, env);
|
|
@@ -5141,6 +5263,7 @@ async function runComputerSetup(flags, env = process.env) {
|
|
|
5141
5263
|
fingerprint: owner.fingerprint,
|
|
5142
5264
|
}, env);
|
|
5143
5265
|
await saveProfile(config.profile, config, env);
|
|
5266
|
+
await clearRemoteClosedServiceState(config.profile, env);
|
|
5144
5267
|
const cli = await tryInstallCliShim(flags, env);
|
|
5145
5268
|
const result = await startBackground(config.profile, env);
|
|
5146
5269
|
printJson({
|
|
@@ -5179,6 +5302,7 @@ async function buildConfig(flags, env = process.env) {
|
|
|
5179
5302
|
|
|
5180
5303
|
async function runForegroundDaemon(config, env = process.env) {
|
|
5181
5304
|
const releaseLock = await acquireDaemonLock(config.profile, config, env);
|
|
5305
|
+
await markForegroundServiceState(config.profile, env);
|
|
5182
5306
|
const daemon = new MagClawDaemon(config, env);
|
|
5183
5307
|
let forceExitTimer = null;
|
|
5184
5308
|
const shutdown = (signal) => {
|
|
@@ -5205,6 +5329,7 @@ async function runConnect(flags, env = process.env) {
|
|
|
5205
5329
|
throw new Error('Run connect with --api-key, --pair-token for legacy pairing, or use a saved profile with a machine token.');
|
|
5206
5330
|
}
|
|
5207
5331
|
await saveProfile(config.profile, config, env);
|
|
5332
|
+
await clearRemoteClosedServiceState(config.profile, env);
|
|
5208
5333
|
const cli = await tryInstallCliShim(flags, env);
|
|
5209
5334
|
if (flags.background) {
|
|
5210
5335
|
const result = await startBackground(config.profile, env);
|