@magclaw/cli-core 0.1.24 → 0.1.26

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +169 -25
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magclaw/cli-core",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "Shared local MagClaw CLI implementation used by daemon and computer packages.",
5
5
  "type": "module",
6
6
  "bin": {
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;
@@ -3458,14 +3520,15 @@ class MagClawDaemon {
3458
3520
  const commandId = String(message.commandId || '').trim();
3459
3521
  const service = await readServiceState(this.paths.profile, this.env);
3460
3522
  const serviceStatus = backgroundServiceStatus(this.paths.profile, this.env);
3523
+ const serviceRunMode = daemonServiceRunMode(service, serviceStatus);
3461
3524
  const packageInfo = runtimePackageInfo(this.env, service);
3462
3525
  const runMode = {
3463
- mode: service.mode || serviceStatus.mode || 'foreground',
3464
- background: Boolean(service.background),
3465
- active: Boolean(serviceStatus.active),
3466
- label: serviceStatus.label || '',
3467
- serviceName: serviceStatus.serviceName || '',
3468
- taskName: serviceStatus.taskName || '',
3526
+ mode: serviceRunMode.mode,
3527
+ background: serviceRunMode.background,
3528
+ active: serviceRunMode.active,
3529
+ label: serviceRunMode.background ? serviceStatus.label || '' : '',
3530
+ serviceName: serviceRunMode.background ? serviceStatus.serviceName || '' : '',
3531
+ taskName: serviceRunMode.background ? serviceStatus.taskName || '' : '',
3469
3532
  packageName: packageInfo.name,
3470
3533
  packageVersion: packageInfo.version,
3471
3534
  packageKind: packageInfo.kind,
@@ -3473,6 +3536,12 @@ class MagClawDaemon {
3473
3536
  packageBin: packageInfo.bin,
3474
3537
  };
3475
3538
  logWarning('daemon', `Remote close requested (${message.reason || 'closed_from_cloud'}).`);
3539
+ await writeServiceState(this.paths.profile, {
3540
+ remoteClosed: true,
3541
+ remoteClosedAt: now(),
3542
+ remoteCloseReason: message.reason || 'closed_from_cloud',
3543
+ remoteCloseCommandId: commandId,
3544
+ }, this.env);
3476
3545
  for (const session of this.sessions.values()) session.stop();
3477
3546
  this.sessions.clear();
3478
3547
  this.send({ type: 'daemon:close:ack',
@@ -3482,8 +3551,15 @@ class MagClawDaemon {
3482
3551
  service: runMode,
3483
3552
  at: now(),
3484
3553
  });
3485
- const background = stopBackground(this.paths.profile, this.env, { disable: message.disableBackground !== false });
3486
- logInfo('daemon', `Close request stopped background service mode=${background.mode || 'foreground'} ok=${Boolean(background.ok)}.`);
3554
+ const shouldStopBackground = Boolean(runMode.background);
3555
+ const background = shouldStopBackground
3556
+ ? stopBackground(this.paths.profile, this.env, { disable: message.disableBackground !== false })
3557
+ : { ok: true, mode: runMode.mode || 'foreground', skipped: true };
3558
+ if (shouldStopBackground) {
3559
+ logInfo('daemon', `Close request stopped background service mode=${background.mode || 'foreground'} ok=${Boolean(background.ok)}.`);
3560
+ } else {
3561
+ logInfo('daemon', 'Foreground close request did not stop background service.');
3562
+ }
3487
3563
  this.close();
3488
3564
  process.exitCode = 0;
3489
3565
  setTimeout(() => process.exit(0), 50).unref?.();
@@ -3494,6 +3570,7 @@ class MagClawDaemon {
3494
3570
  const owner = await ensureMachineFingerprint(this.paths.profile, this.env);
3495
3571
  const service = await readServiceState(this.paths.profile, this.env);
3496
3572
  const serviceStatus = backgroundServiceStatus(this.paths.profile, this.env);
3573
+ const serviceRunMode = daemonServiceRunMode(service, serviceStatus);
3497
3574
  const upgrade = await readUpgradeHandoff(this.paths.profile, this.env);
3498
3575
  const packageInfo = runtimePackageInfo(this.env, service);
3499
3576
  return {
@@ -3513,12 +3590,12 @@ class MagClawDaemon {
3513
3590
  packageBin: packageInfo.bin,
3514
3591
  cliCoreVersion: CLI_CORE_VERSION,
3515
3592
  service: {
3516
- mode: service.mode || serviceStatus.mode || 'foreground',
3517
- background: Boolean(service.background),
3518
- active: Boolean(serviceStatus.active),
3519
- label: serviceStatus.label || '',
3520
- serviceName: serviceStatus.serviceName || '',
3521
- taskName: serviceStatus.taskName || '',
3593
+ mode: serviceRunMode.mode,
3594
+ background: serviceRunMode.background,
3595
+ active: serviceRunMode.active,
3596
+ label: serviceRunMode.background ? serviceStatus.label || '' : '',
3597
+ serviceName: serviceRunMode.background ? serviceStatus.serviceName || '' : '',
3598
+ taskName: serviceRunMode.background ? serviceStatus.taskName || '' : '',
3522
3599
  launcher: service.launcher || '',
3523
3600
  packageSpec: service.packageSpec || packageInfo.spec || '',
3524
3601
  packageName: service.packageName || packageInfo.name,
@@ -3746,7 +3823,7 @@ class MagClawDaemon {
3746
3823
  ? await session.listSkills()
3747
3824
  : { agent: { id: agent.id, name: agent.name || agent.id }, global: [], workspace: [], plugin: [], tools: [] };
3748
3825
  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.status || 'idle' });
3826
+ this.send({ type: 'agent:ack', commandId: message.commandId, agentId: agent.id, status: readOnlySessionAckStatus(session) });
3750
3827
  } catch (error) {
3751
3828
  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
3829
  this.send({ type: 'agent:error', commandId: message.commandId, agentId: agent.id, error: error.message });
@@ -3771,7 +3848,7 @@ class MagClawDaemon {
3771
3848
  const session = await this.sessionForWorkspaceRequest(message);
3772
3849
  const tree = await session.listWorkspace(message.path || message.payload?.path || '');
3773
3850
  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.status || 'idle' });
3851
+ this.send({ type: 'agent:ack', commandId: message.commandId, agentId: session.agent.id, status: readOnlySessionAckStatus(session) });
3775
3852
  } catch (error) {
3776
3853
  this.send({ type: 'agent:error', commandId: message.commandId, agentId: message.agentId || null, error: error.message });
3777
3854
  }
@@ -3782,7 +3859,7 @@ class MagClawDaemon {
3782
3859
  const session = await this.sessionForWorkspaceRequest(message);
3783
3860
  const file = await session.readWorkspaceFile(message.path || message.payload?.path || 'MEMORY.md');
3784
3861
  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.status || 'idle' });
3862
+ this.send({ type: 'agent:ack', commandId: message.commandId, agentId: session.agent.id, status: readOnlySessionAckStatus(session) });
3786
3863
  } catch (error) {
3787
3864
  this.send({ type: 'agent:error', commandId: message.commandId, agentId: message.agentId || null, error: error.message });
3788
3865
  }
@@ -4032,11 +4109,25 @@ class MagClawDaemon {
4032
4109
  async connectOnce() {
4033
4110
  if (this.closed) return;
4034
4111
  await this.refreshConfigFromDisk();
4035
- const url = toWebSocketUrl(this.config.serverUrl, this.config);
4112
+ const service = await readServiceState(this.paths.profile, this.env);
4113
+ const serviceStatus = backgroundServiceStatus(this.paths.profile, this.env);
4114
+ const serviceRunMode = daemonServiceRunMode(service, serviceStatus);
4115
+ const packageInfo = runtimePackageInfo(this.env, service);
4116
+ const url = toWebSocketUrl(this.config.serverUrl, {
4117
+ ...this.config,
4118
+ packageName: packageInfo.name,
4119
+ packageVersion: packageInfo.version,
4120
+ packageKind: packageInfo.kind,
4121
+ packageSpec: packageInfo.spec,
4122
+ packageBin: packageInfo.bin,
4123
+ cliCoreVersion: CLI_CORE_VERSION,
4124
+ serviceMode: serviceRunMode.mode,
4125
+ serviceBackground: serviceRunMode.background,
4126
+ serviceActive: serviceRunMode.active,
4127
+ });
4036
4128
  const requestModule = url.protocol === 'wss:' ? https : http;
4037
4129
  const requestUrl = new URL(url.href.replace(/^ws/, 'http'));
4038
4130
  const key = crypto.randomBytes(16).toString('base64');
4039
- const packageInfo = runtimePackageInfo(this.env);
4040
4131
  logInfo('daemon', `Connecting MagClaw daemon v${packageInfo.version || DAEMON_VERSION} profile "${this.paths.profile}" to ${this.config.serverUrl}...`);
4041
4132
  if (this.closed) return;
4042
4133
  return new Promise((resolve, reject) => {
@@ -4218,6 +4309,10 @@ async function writeLauncher(profile, env = process.env) {
4218
4309
  installedDaemonVersion: packageVersion || DAEMON_VERSION,
4219
4310
  installedPackageVersion: packageVersion || DAEMON_VERSION,
4220
4311
  commandMode: useNpmLauncher ? 'npm' : 'local',
4312
+ remoteClosed: false,
4313
+ remoteClosedAt: '',
4314
+ remoteCloseReason: '',
4315
+ remoteCloseCommandId: '',
4221
4316
  }, env);
4222
4317
  const code = [
4223
4318
  '#!/usr/bin/env node',
@@ -4234,6 +4329,11 @@ async function writeLauncher(profile, env = process.env) {
4234
4329
  `const defaultPackageSpec = ${JSON.stringify(service.packageSpec)};`,
4235
4330
  "let service = {};",
4236
4331
  "try { service = JSON.parse(fs.readFileSync(serviceFile, 'utf8')); } catch {}",
4332
+ "if (service.remoteClosed) {",
4333
+ " const stamp = new Date().toISOString().replace('T', ' ').slice(0, 19);",
4334
+ " console.error(`${stamp} INFO DAEMON profile ${profile} is closed from MagClaw Cloud; run magclaw start/restart/connect to reconnect.`);",
4335
+ " process.exit(0);",
4336
+ "}",
4237
4337
  "const packageSpec = String(service.packageSpec || defaultPackageSpec || '@magclaw/daemon@latest');",
4238
4338
  "const packageName = String(service.packageName || (packageSpec.startsWith('@magclaw/computer@') || packageSpec === '@magclaw/computer' ? '@magclaw/computer' : '@magclaw/daemon'));",
4239
4339
  "const packageKind = String(service.packageKind || (packageName === '@magclaw/computer' ? 'computer' : 'daemon'));",
@@ -4446,6 +4546,38 @@ function backgroundServiceStatus(profile, env = process.env) {
4446
4546
  return { mode: 'foreground', active: false };
4447
4547
  }
4448
4548
 
4549
+ function daemonServiceRunMode(service = {}, serviceStatus = {}) {
4550
+ const background = service.background === true;
4551
+ return {
4552
+ mode: background ? (service.mode || serviceStatus.mode || 'foreground') : 'foreground',
4553
+ background,
4554
+ active: background ? Boolean(serviceStatus.active) : false,
4555
+ };
4556
+ }
4557
+
4558
+ function launchctlResultIsNotLoaded(result) {
4559
+ const detail = String(result?.stderr || result?.stdout || '').toLowerCase();
4560
+ return detail.includes('no such process') || detail.includes('could not find service') || detail.includes('not found');
4561
+ }
4562
+
4563
+ function launchctlServiceIsStopped(serviceTarget) {
4564
+ const result = spawnSync('launchctl', ['print', serviceTarget], { encoding: 'utf8' });
4565
+ if (result.status !== 0) return launchctlResultIsNotLoaded(result);
4566
+ const output = String(result.stdout || result.stderr || '');
4567
+ if (/\bstate\s*=\s*running\b/i.test(output)) return false;
4568
+ if (/\bpid\s*=\s*[1-9][0-9]*\b/i.test(output)) return false;
4569
+ return true;
4570
+ }
4571
+
4572
+ function waitForBackgroundServiceStopped(serviceTarget, timeoutMs = 2000) {
4573
+ const deadline = Date.now() + Math.max(250, Number(timeoutMs) || 2000);
4574
+ while (Date.now() < deadline) {
4575
+ if (launchctlServiceIsStopped(serviceTarget)) return true;
4576
+ sleepSync(100);
4577
+ }
4578
+ return launchctlServiceIsStopped(serviceTarget);
4579
+ }
4580
+
4449
4581
  async function waitForPidExit(pid, timeoutMs = 2000) {
4450
4582
  const deadline = Date.now() + timeoutMs;
4451
4583
  while (Date.now() < deadline) {
@@ -4498,23 +4630,32 @@ function stopBackground(profile, env = process.env, options = {}) {
4498
4630
  if (process.platform === 'darwin') {
4499
4631
  const paths = profilePaths(profile, env);
4500
4632
  const label = launchAgentLabel(paths.profile);
4633
+ const serviceTarget = `gui/${process.getuid()}/${label}`;
4501
4634
  const plist = path.join(os.homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
4502
4635
  if (options.disable) {
4503
- const disabled = spawnSync('launchctl', ['disable', `gui/${process.getuid()}/${label}`], { encoding: 'utf8' });
4636
+ const disabled = spawnSync('launchctl', ['disable', serviceTarget], { encoding: 'utf8' });
4504
4637
  const stopped = spawnSync('launchctl', ['stop', label], { encoding: 'utf8' });
4638
+ const stoppedConfirmed = stopped.status === 0 && waitForBackgroundServiceStopped(serviceTarget);
4639
+ const needsBootout = disabled.status !== 0 || !stoppedConfirmed;
4640
+ const bootout = needsBootout
4641
+ ? spawnSync('launchctl', ['bootout', serviceTarget], { encoding: 'utf8' })
4642
+ : null;
4643
+ const bootoutOk = bootout ? (bootout.status === 0 || launchctlResultIsNotLoaded(bootout)) : false;
4505
4644
  if (stopped.status !== 0) spawnSync('launchctl', ['bootout', `gui/${process.getuid()}`, plist], { stdio: 'ignore' });
4506
4645
  return {
4507
- ok: disabled.status === 0 || stopped.status === 0,
4646
+ ok: disabled.status === 0 && (stoppedConfirmed || bootoutOk),
4508
4647
  mode: 'launchd',
4509
4648
  label,
4649
+ serviceTarget,
4510
4650
  file: plist,
4511
4651
  disabled: disabled.status === 0,
4512
- stopped: stopped.status === 0,
4513
- error: disabled.status === 0 && stopped.status === 0 ? '' : String(stopped.stderr || disabled.stderr || stopped.stdout || disabled.stdout || '').trim(),
4652
+ stopped: stoppedConfirmed || bootoutOk,
4653
+ bootout: bootout ? bootout.status === 0 : false,
4654
+ error: disabled.status === 0 && (stoppedConfirmed || bootoutOk) ? '' : String(bootout?.stderr || stopped.stderr || disabled.stderr || bootout?.stdout || stopped.stdout || disabled.stdout || '').trim(),
4514
4655
  };
4515
4656
  }
4516
- spawnSync('launchctl', ['bootout', `gui/${process.getuid()}`, plist], { stdio: 'ignore' });
4517
- return { ok: true, mode: 'launchd', label, file: plist };
4657
+ const bootout = spawnSync('launchctl', ['bootout', serviceTarget], { encoding: 'utf8' });
4658
+ return { ok: bootout.status === 0 || launchctlResultIsNotLoaded(bootout), mode: 'launchd', label, serviceTarget, file: plist, error: bootout.stderr || bootout.stdout || '' };
4518
4659
  }
4519
4660
  if (process.platform === 'linux') {
4520
4661
  const paths = profilePaths(profile, env);
@@ -5141,6 +5282,7 @@ async function runComputerSetup(flags, env = process.env) {
5141
5282
  fingerprint: owner.fingerprint,
5142
5283
  }, env);
5143
5284
  await saveProfile(config.profile, config, env);
5285
+ await clearRemoteClosedServiceState(config.profile, env);
5144
5286
  const cli = await tryInstallCliShim(flags, env);
5145
5287
  const result = await startBackground(config.profile, env);
5146
5288
  printJson({
@@ -5179,6 +5321,7 @@ async function buildConfig(flags, env = process.env) {
5179
5321
 
5180
5322
  async function runForegroundDaemon(config, env = process.env) {
5181
5323
  const releaseLock = await acquireDaemonLock(config.profile, config, env);
5324
+ await markForegroundServiceState(config.profile, env);
5182
5325
  const daemon = new MagClawDaemon(config, env);
5183
5326
  let forceExitTimer = null;
5184
5327
  const shutdown = (signal) => {
@@ -5205,6 +5348,7 @@ async function runConnect(flags, env = process.env) {
5205
5348
  throw new Error('Run connect with --api-key, --pair-token for legacy pairing, or use a saved profile with a machine token.');
5206
5349
  }
5207
5350
  await saveProfile(config.profile, config, env);
5351
+ await clearRemoteClosedServiceState(config.profile, env);
5208
5352
  const cli = await tryInstallCliShim(flags, env);
5209
5353
  if (flags.background) {
5210
5354
  const result = await startBackground(config.profile, env);