@magclaw/cli-core 0.1.28 → 0.1.29

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/README.md CHANGED
@@ -5,3 +5,9 @@ Shared local MagClaw CLI implementation.
5
5
  This package owns the reusable command implementation used by
6
6
  `@magclaw/daemon` and `@magclaw/computer`. Install the public entry packages
7
7
  instead of using this package directly.
8
+
9
+ Public entry commands:
10
+
11
+ - `magclaw`: daemon/profile operations.
12
+ - `magclaw-computer`: browser-approved Computer setup and control-plane
13
+ operations.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magclaw/cli-core",
3
- "version": "0.1.28",
3
+ "version": "0.1.29",
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
@@ -385,6 +385,10 @@ function rootLockPaths(env = process.env) {
385
385
  };
386
386
  }
387
387
 
388
+ function computerChannelPath(env = process.env) {
389
+ return path.join(daemonRoot(env), 'channel');
390
+ }
391
+
388
392
  export function profilePaths(profile = DEFAULT_PROFILE, env = process.env) {
389
393
  const profileName = safeProfileName(profile);
390
394
  const dir = path.join(daemonRoot(env), 'profiles', profileName);
@@ -541,6 +545,10 @@ export function parseCli(argv = process.argv) {
541
545
  flags.help = true;
542
546
  continue;
543
547
  }
548
+ if (item === '-V') {
549
+ flags.version = true;
550
+ continue;
551
+ }
544
552
  if (!item.startsWith('--')) {
545
553
  positionals.push(item);
546
554
  continue;
@@ -612,6 +620,150 @@ function renderHelp() {
612
620
  ].join('\n');
613
621
  }
614
622
 
623
+ function renderComputerHelp(subcommand = '') {
624
+ const command = String(subcommand || '').trim();
625
+ const usage = {
626
+ login: [
627
+ 'Usage: magclaw-computer login [options] <serverSlug>',
628
+ '',
629
+ 'Start MagClaw browser approval for one server. This is an alias for setup.',
630
+ '',
631
+ 'Options:',
632
+ ' --server-url <url> MagClaw Cloud URL',
633
+ ' --name <name> Computer display name',
634
+ ' --no-start Save the approved profile without starting the daemon',
635
+ ' -h, --help Show this help',
636
+ ],
637
+ attach: [
638
+ 'Usage: magclaw-computer attach [options] <serverSlug>',
639
+ '',
640
+ 'Attach this Computer to one MagClaw server using browser approval.',
641
+ '',
642
+ 'Options:',
643
+ ' --server-url <url> MagClaw Cloud URL',
644
+ ' --name <name> Computer display name',
645
+ ' --no-run Save the approved profile without starting the daemon',
646
+ ' --foreground Run foreground if background service cannot start',
647
+ ' -h, --help Show this help',
648
+ ],
649
+ setup: [
650
+ 'Usage: magclaw-computer setup [options] <serverSlug>',
651
+ '',
652
+ 'Set up this Computer for one server, then start its daemon unless --no-start is set.',
653
+ '',
654
+ 'Options:',
655
+ ' --server-url <url> MagClaw Cloud URL',
656
+ ' --name <name> Computer display name',
657
+ ' --no-start Save the approved profile without starting the daemon',
658
+ ' --foreground Run foreground if background service cannot start',
659
+ ' -h, --help Show this help',
660
+ ],
661
+ detach: [
662
+ 'Usage: magclaw-computer detach <serverSlug>',
663
+ '',
664
+ 'Stop one local profile and remove its local attachment state.',
665
+ ],
666
+ status: [
667
+ 'Usage: magclaw-computer status [options] [serverSlug]',
668
+ '',
669
+ 'Show aggregate Computer state, or one server profile when a slug is provided.',
670
+ '',
671
+ 'Options:',
672
+ ' --json Emit the machine-readable report',
673
+ ' -h, --help Show this help',
674
+ ],
675
+ start: [
676
+ 'Usage: magclaw-computer start [options] [serverSlug]',
677
+ '',
678
+ 'Start one saved background daemon profile, or all saved profiles when no slug is provided.',
679
+ '',
680
+ 'Options:',
681
+ ' --foreground Run in this terminal for one selected profile',
682
+ ],
683
+ stop: [
684
+ 'Usage: magclaw-computer stop [options] [serverSlug]',
685
+ '',
686
+ 'Stop one daemon profile, or all saved profiles when no slug is provided.',
687
+ '',
688
+ 'Options:',
689
+ ' --disable Suppress background relaunch until the next start',
690
+ ],
691
+ doctor: [
692
+ 'Usage: magclaw-computer doctor [options] [serverSlug]',
693
+ '',
694
+ 'Diagnose local profiles, service state, runtime availability, and stale pidfiles.',
695
+ '',
696
+ 'Options:',
697
+ ' --json Emit the machine-readable report',
698
+ ' --cleanup Clear stale local locks while diagnosing',
699
+ ' --fix Alias for --cleanup',
700
+ ],
701
+ logs: [
702
+ 'Usage: magclaw-computer logs [options] [serverSlug]',
703
+ '',
704
+ 'Print recent daemon logs for one attached server profile.',
705
+ '',
706
+ 'Options:',
707
+ ' --lines <n> Number of trailing lines to print (default 120)',
708
+ ' --server <slug> Select a server profile',
709
+ ],
710
+ runners: [
711
+ 'Usage: magclaw-computer runners <command> [options]',
712
+ '',
713
+ 'Computer runner control plane.',
714
+ '',
715
+ 'Commands:',
716
+ ' list List local daemon profiles and known Computer bindings',
717
+ ' stop <agentId> Not available locally; stop Agents from the MagClaw web console',
718
+ ],
719
+ channel: [
720
+ 'Usage: magclaw-computer channel [set <channel>]',
721
+ '',
722
+ 'Show or set the local Computer release channel (latest | alpha | pinned:<semver>).',
723
+ ],
724
+ upgrade: [
725
+ 'Usage: magclaw-computer upgrade [options]',
726
+ '',
727
+ 'Upgrade the background Computer package for a saved profile.',
728
+ '',
729
+ 'Options:',
730
+ ' --dry-run Preview upgrade actions',
731
+ ' --channel <name> latest | alpha | pinned:<semver>',
732
+ ' --target-version <semver> Explicit target version',
733
+ ' --force Accepted for Slock parity; currently maps to the normal upgrade path',
734
+ ],
735
+ };
736
+ if (command && usage[command]) return `${usage[command].join('\n')}\n`;
737
+ return [
738
+ `MagClaw Computer CLI ${DAEMON_VERSION}`,
739
+ '',
740
+ 'Usage: magclaw-computer [options] [command]',
741
+ '',
742
+ 'MagClaw Computer - local-machine control plane (browser approval + per-server profiles).',
743
+ '',
744
+ 'Options:',
745
+ ' -V, --version output the version number',
746
+ ' -h, --help show help for command',
747
+ '',
748
+ 'Commands:',
749
+ ' login [options] <serverSlug> Browser-approved login for one server (alias for setup)',
750
+ ' attach [options] <serverSlug> Attach this Computer to one server',
751
+ ' setup [options] <serverSlug> Login/attach if needed, then start',
752
+ ' adopt-legacy [options] <serverSlug> Migrate from legacy pair-token style setup when possible',
753
+ ' detach <serverSlug> Remove one local server attachment',
754
+ ' status [options] [serverSlug] Show aggregate or per-profile state',
755
+ ' start [options] [serverSlug] Start one or all saved profiles',
756
+ ' stop [options] [serverSlug] Stop one or all saved profiles',
757
+ ' doctor [options] [serverSlug] Diagnose local profiles and runtime state',
758
+ ' logs [options] [serverSlug] Print one profile daemon log',
759
+ ' runners Computer runner control plane',
760
+ ' channel Show or set release channel',
761
+ ' upgrade [options] Upgrade the Computer package',
762
+ ' help [command] show help for command',
763
+ '',
764
+ ].join('\n');
765
+ }
766
+
615
767
  async function readJsonFile(file, fallback = {}) {
616
768
  if (!existsSync(file)) return fallback;
617
769
  try {
@@ -710,6 +862,27 @@ async function clearRemoteClosedServiceState(profile = DEFAULT_PROFILE, env = pr
710
862
  }, env);
711
863
  }
712
864
 
865
+ export function daemonRunLaunchedByBackgroundService(env = process.env) {
866
+ return String(env.MAGCLAW_DAEMON_BACKGROUND_SERVICE || '').trim() === '1';
867
+ }
868
+
869
+ function backgroundServiceModeForPlatform(platform = process.platform) {
870
+ if (platform === 'darwin') return 'launchd';
871
+ if (platform === 'linux') return 'systemd';
872
+ if (platform === 'win32') return 'schtasks';
873
+ return 'foreground';
874
+ }
875
+
876
+ export function serviceStatePatchForDaemonRun(service = {}, env = process.env, platform = process.platform) {
877
+ if (daemonRunLaunchedByBackgroundService(env)) {
878
+ return {
879
+ mode: service.mode || backgroundServiceModeForPlatform(platform),
880
+ background: true,
881
+ };
882
+ }
883
+ return { mode: 'foreground', background: false };
884
+ }
885
+
713
886
  async function markForegroundServiceState(profile = DEFAULT_PROFILE, env = process.env) {
714
887
  const packageInfo = runtimePackageInfo(env);
715
888
  return writeServiceState(profile, {
@@ -726,6 +899,22 @@ async function markForegroundServiceState(profile = DEFAULT_PROFILE, env = proce
726
899
  }, env);
727
900
  }
728
901
 
902
+ async function markDaemonRunServiceState(profile = DEFAULT_PROFILE, env = process.env) {
903
+ if (!daemonRunLaunchedByBackgroundService(env)) return markForegroundServiceState(profile, env);
904
+ const service = await readServiceState(profile, env);
905
+ const packageInfo = runtimePackageInfo(env, service);
906
+ return writeServiceState(profile, {
907
+ ...serviceStatePatchForDaemonRun(service, env),
908
+ packageSpec: packageInfo.spec,
909
+ packageName: packageInfo.name,
910
+ packageVersion: packageInfo.version,
911
+ packageKind: packageInfo.kind,
912
+ packageBin: packageInfo.bin,
913
+ installedDaemonVersion: packageInfo.version || DAEMON_VERSION,
914
+ installedPackageVersion: packageInfo.version || DAEMON_VERSION,
915
+ }, env);
916
+ }
917
+
729
918
  async function readUpgradeHandoff(profile = DEFAULT_PROFILE, env = process.env) {
730
919
  const paths = profilePaths(profile, env);
731
920
  const handoff = await readJsonFile(paths.upgradeHandoff, null);
@@ -3249,6 +3438,8 @@ class MagClawDaemon {
3249
3438
  this.pendingUpgradeRequest = null;
3250
3439
  this.upgradeIdleTimer = null;
3251
3440
  this.upgradeWorkerStarting = false;
3441
+ this.runtimeStatusTimer = null;
3442
+ this.runtimeStatusInFlight = false;
3252
3443
  }
3253
3444
 
3254
3445
  send(payload) {
@@ -3619,7 +3810,6 @@ class MagClawDaemon {
3619
3810
  }
3620
3811
 
3621
3812
  async readyPayload() {
3622
- const runtimes = await detectRuntimes(this.env);
3623
3813
  const owner = await ensureMachineFingerprint(this.paths.profile, this.env);
3624
3814
  const service = await readServiceState(this.paths.profile, this.env);
3625
3815
  const serviceStatus = backgroundServiceStatus(this.paths.profile, this.env);
@@ -3658,8 +3848,7 @@ class MagClawDaemon {
3658
3848
  cliCoreVersion: CLI_CORE_VERSION,
3659
3849
  },
3660
3850
  upgrade: upgrade || null,
3661
- runtimes: runtimes.filter((runtime) => runtime.installed).map((runtime) => runtime.id),
3662
- runtimeDetails: runtimes,
3851
+ runtimeScanPending: true,
3663
3852
  runningAgents: [...this.sessions.keys()],
3664
3853
  capabilities: CAPABILITIES,
3665
3854
  };
@@ -3670,10 +3859,68 @@ class MagClawDaemon {
3670
3859
  const sent = this.send(payload);
3671
3860
  logInfo(
3672
3861
  'daemon',
3673
- `Sent ready payload for computer ${payload.computerId || 'unpaired'} (runtimes=${payload.runtimes.join(', ') || 'none'}, runningAgents=${payload.runningAgents.length}, sent=${sent}).`,
3862
+ `Sent ready payload for computer ${payload.computerId || 'unpaired'} (runtimes=deferred, runningAgents=${payload.runningAgents.length}, sent=${sent}).`,
3674
3863
  );
3675
3864
  }
3676
3865
 
3866
+ runtimeStatusDelayMs() {
3867
+ return envInteger(this.env, 'MAGCLAW_DAEMON_RUNTIME_STATUS_DELAY_MS', 1000, { min: 0, max: 60_000 });
3868
+ }
3869
+
3870
+ clearRuntimeStatusTimer() {
3871
+ if (!this.runtimeStatusTimer) return;
3872
+ clearTimeout(this.runtimeStatusTimer);
3873
+ this.runtimeStatusTimer = null;
3874
+ }
3875
+
3876
+ scheduleRuntimeStatus(reason = 'ready_ack') {
3877
+ if (this.closed || this.runtimeStatusTimer || this.runtimeStatusInFlight) return;
3878
+ this.runtimeStatusTimer = setTimeout(() => {
3879
+ this.runtimeStatusTimer = null;
3880
+ this.sendRuntimeStatus(reason).catch((error) => {
3881
+ logWarning('daemon', `Failed to send runtime status: ${error.message}`);
3882
+ });
3883
+ }, this.runtimeStatusDelayMs());
3884
+ this.runtimeStatusTimer.unref?.();
3885
+ }
3886
+
3887
+ async runtimeStatusPayload(reason = 'ready_ack') {
3888
+ const runtimes = await detectRuntimes(this.env);
3889
+ const packageInfo = runtimePackageInfo(this.env);
3890
+ return {
3891
+ type: 'daemon:runtime_status',
3892
+ time: now(),
3893
+ reason,
3894
+ computerId: this.config.computerId || null,
3895
+ daemonVersion: packageInfo.version || DAEMON_VERSION,
3896
+ packageName: packageInfo.name,
3897
+ packageVersion: packageInfo.version,
3898
+ packageKind: packageInfo.kind,
3899
+ packageSpec: packageInfo.spec,
3900
+ packageBin: packageInfo.bin,
3901
+ cliCoreVersion: CLI_CORE_VERSION,
3902
+ runtimes: runtimes.filter((runtime) => runtime.installed).map((runtime) => runtime.id),
3903
+ runtimeDetails: runtimes,
3904
+ runningAgents: [...this.sessions.keys()],
3905
+ };
3906
+ }
3907
+
3908
+ async sendRuntimeStatus(reason = 'ready_ack') {
3909
+ if (this.closed || this.runtimeStatusInFlight) return false;
3910
+ this.runtimeStatusInFlight = true;
3911
+ try {
3912
+ const payload = await this.runtimeStatusPayload(reason);
3913
+ const sent = this.send(payload);
3914
+ logInfo(
3915
+ 'daemon',
3916
+ `Sent runtime status for computer ${payload.computerId || 'unpaired'} (runtimes=${payload.runtimes.join(', ') || 'none'}, sent=${sent}).`,
3917
+ );
3918
+ return sent;
3919
+ } finally {
3920
+ this.runtimeStatusInFlight = false;
3921
+ }
3922
+ }
3923
+
3677
3924
  sendHeartbeat() {
3678
3925
  const packageInfo = runtimePackageInfo(this.env);
3679
3926
  const sent = this.send({
@@ -3801,6 +4048,7 @@ class MagClawDaemon {
3801
4048
  break;
3802
4049
  case 'ready:ack':
3803
4050
  logInfo('daemon', `MagClaw daemon ready for computer ${message.computerId || this.config.computerId}.`);
4051
+ this.scheduleRuntimeStatus('ready_ack');
3804
4052
  break;
3805
4053
  case 'ping':
3806
4054
  this.send({ type: 'pong', time: now() });
@@ -4162,6 +4410,7 @@ class MagClawDaemon {
4162
4410
  this.sessions.clear();
4163
4411
  this.stopHeartbeat();
4164
4412
  this.clearInboundWatchdog();
4413
+ this.clearRuntimeStatusTimer();
4165
4414
  if (this.agentStartPumpTimer) {
4166
4415
  clearTimeout(this.agentStartPumpTimer);
4167
4416
  this.agentStartPumpTimer = null;
@@ -4214,6 +4463,7 @@ class MagClawDaemon {
4214
4463
  settled = true;
4215
4464
  this.stopHeartbeat();
4216
4465
  this.clearInboundWatchdog();
4466
+ this.clearRuntimeStatusTimer();
4217
4467
  if (this.socket && this.socket.destroyed) this.socket = null;
4218
4468
  if (this.request === req) this.request = null;
4219
4469
  callback(value);
@@ -4437,6 +4687,7 @@ async function writeLauncher(profile, env = process.env) {
4437
4687
  " MAGCLAW_DAEMON_PACKAGE_SPEC: packageSpec,",
4438
4688
  " MAGCLAW_DAEMON_PACKAGE_KIND: packageKind,",
4439
4689
  " MAGCLAW_DAEMON_PACKAGE_BIN: packageBin,",
4690
+ " MAGCLAW_DAEMON_BACKGROUND_SERVICE: '1',",
4440
4691
  " PATH: launchPath,",
4441
4692
  "};",
4442
4693
  "if (packageKind === 'computer') childEnv.MAGCLAW_COMPUTER_DAEMON = '1';",
@@ -4591,17 +4842,40 @@ async function startBackground(profile, env = process.env) {
4591
4842
  return { ok: false, mode: 'foreground', message: 'Background daemon is only automated on macOS launchd, Linux user systemd, and Windows schtasks.' };
4592
4843
  }
4593
4844
 
4845
+ export function parseLaunchdPrintStatus(result = {}) {
4846
+ const stdout = String(result.stdout || '');
4847
+ const stderr = String(result.stderr || '');
4848
+ const state = (stdout.match(/^\s*state\s*=\s*(.+?)\s*$/m)?.[1] || '').trim();
4849
+ const activeCountValue = Number(stdout.match(/^\s*active count\s*=\s*(\d+)\s*$/m)?.[1] || NaN);
4850
+ const stateStatus = state || (result.status === 0 ? 'loaded' : 'inactive');
4851
+ const active = result.status === 0 && (
4852
+ stateStatus.toLowerCase() === 'running'
4853
+ || (!state && Number.isFinite(activeCountValue) && activeCountValue > 0)
4854
+ );
4855
+ return {
4856
+ active,
4857
+ status: active ? 'running' : stateStatus,
4858
+ state,
4859
+ activeCount: Number.isFinite(activeCountValue) ? activeCountValue : null,
4860
+ error: result.status === 0 ? '' : String(stderr || stdout || '').trim(),
4861
+ };
4862
+ }
4863
+
4594
4864
  function backgroundServiceStatus(profile, env = process.env) {
4595
4865
  const paths = profilePaths(profile, env);
4596
4866
  if (process.platform === 'darwin') {
4597
4867
  const label = launchAgentLabel(paths.profile);
4598
4868
  const result = spawnSync('launchctl', ['print', `gui/${process.getuid()}/${label}`], { encoding: 'utf8' });
4869
+ const parsed = parseLaunchdPrintStatus(result);
4599
4870
  return {
4600
4871
  mode: 'launchd',
4601
- active: result.status === 0,
4872
+ active: parsed.active,
4602
4873
  label,
4603
4874
  file: path.join(os.homedir(), 'Library', 'LaunchAgents', `${label}.plist`),
4604
- error: result.status === 0 ? '' : String(result.stderr || result.stdout || '').trim(),
4875
+ status: parsed.status,
4876
+ state: parsed.state,
4877
+ activeCount: parsed.activeCount,
4878
+ error: parsed.error,
4605
4879
  };
4606
4880
  }
4607
4881
  if (process.platform === 'linux') {
@@ -4811,14 +5085,15 @@ async function status(profile) {
4811
5085
  };
4812
5086
  }
4813
5087
 
4814
- async function logs(profile) {
5088
+ async function logs(profile, options = {}) {
4815
5089
  const paths = profilePaths(profile);
5090
+ const lines = Math.max(1, Math.min(5000, Number(options.lines || 120) || 120));
4816
5091
  const files = [path.join(paths.logDir, 'daemon.log'), path.join(paths.logDir, 'daemon.err.log')];
4817
5092
  for (const file of files) {
4818
5093
  if (!existsSync(file)) continue;
4819
5094
  process.stdout.write(`\n==> ${file} <==\n`);
4820
5095
  const text = await readFile(file, 'utf8').catch(() => '');
4821
- process.stdout.write(text.split(/\r?\n/).slice(-120).join('\n'));
5096
+ process.stdout.write(text.split(/\r?\n/).slice(-lines).join('\n'));
4822
5097
  process.stdout.write('\n');
4823
5098
  }
4824
5099
  }
@@ -4857,6 +5132,9 @@ async function listProfiles(env = process.env) {
4857
5132
  label: service.label || '',
4858
5133
  serviceName: service.serviceName || '',
4859
5134
  taskName: service.taskName || '',
5135
+ status: service.status || '',
5136
+ state: service.state || '',
5137
+ activeCount: service.activeCount ?? null,
4860
5138
  },
4861
5139
  createdAt: config.createdAt || '',
4862
5140
  updatedAt: config.updatedAt || '',
@@ -5303,10 +5581,22 @@ async function postSetupJson(serverUrl, pathname, body = {}) {
5303
5581
  return data;
5304
5582
  }
5305
5583
 
5584
+ function hasComputerTarget(flags = {}) {
5585
+ return Boolean(flags.profileExplicit || flags.server || flags.serverSlug || flags.slug || flags._?.[1]);
5586
+ }
5587
+
5588
+ function profileFromComputerTarget(value = '') {
5589
+ return safeProfileName(normalizeSetupServerSlug(value) || value || DEFAULT_PROFILE);
5590
+ }
5591
+
5592
+ function computerTargetProfile(flags = {}, fallback = DEFAULT_PROFILE) {
5593
+ return profileFromComputerTarget(flags.server || flags.serverSlug || flags.slug || flags._?.[1] || flags.profile || fallback);
5594
+ }
5595
+
5306
5596
  async function runComputerSetup(flags, env = process.env) {
5307
5597
  const subcommand = String(flags._?.[0] || '').trim();
5308
- if (subcommand !== 'setup') {
5309
- throw new Error('Usage: magclaw computer setup /<server-slug> --server-url <url>');
5598
+ if (!['setup', 'attach', 'login'].includes(subcommand)) {
5599
+ throw new Error('Usage: magclaw-computer setup /<server-slug> --server-url <url>');
5310
5600
  }
5311
5601
  const serverSlug = normalizeSetupServerSlug(flags._?.[1] || flags.server || flags.serverSlug || flags.slug);
5312
5602
  if (!serverSlug) throw new Error('Run computer setup with a server slug, for example: magclaw computer setup /my-server');
@@ -5367,6 +5657,20 @@ async function runComputerSetup(flags, env = process.env) {
5367
5657
  await saveProfile(config.profile, config, env);
5368
5658
  await clearRemoteClosedServiceState(config.profile, env);
5369
5659
  const cli = await tryInstallCliShim(flags, env);
5660
+ if (flags.noStart || flags.noRun) {
5661
+ printJson({
5662
+ ok: true,
5663
+ started: false,
5664
+ cli,
5665
+ computerId: config.computerId,
5666
+ computerName: config.computerName || config.name || displayName,
5667
+ profile: config.profile,
5668
+ serverName: config.serverName,
5669
+ serverSlug: approved.serverSlug || serverSlug,
5670
+ next: `Run magclaw-computer start ${config.profile} when ready.`,
5671
+ });
5672
+ return;
5673
+ }
5370
5674
  const result = await startBackground(config.profile, env);
5371
5675
  printJson({
5372
5676
  ...result,
@@ -5383,6 +5687,255 @@ async function runComputerSetup(flags, env = process.env) {
5383
5687
  }
5384
5688
  }
5385
5689
 
5690
+ async function renderComputerAggregateStatus(env = process.env) {
5691
+ const report = await listProfiles(env);
5692
+ return {
5693
+ ok: true,
5694
+ root: report.root,
5695
+ loggedIn: report.profiles.some((profile) => profile.configured),
5696
+ supervisor: {
5697
+ model: 'per-profile-service',
5698
+ running: report.profiles.some((profile) => profile.running || profile.service?.active),
5699
+ managedProfiles: report.profiles.length,
5700
+ },
5701
+ profiles: report.profiles,
5702
+ };
5703
+ }
5704
+
5705
+ function formatComputerStatus(report = {}) {
5706
+ const profiles = report.profiles || [];
5707
+ if (report.profile) {
5708
+ return [
5709
+ `Profile: ${report.profile}`,
5710
+ `Configured: ${report.configured ? 'yes' : 'no'}`,
5711
+ `Daemon: ${report.running ? `running (pid ${report.pid})` : 'stopped'}`,
5712
+ `Service: ${report.service?.mode || 'foreground'}${report.service?.active ? ' active' : ''}`,
5713
+ `Server URL: ${report.serverUrl || '-'}`,
5714
+ `Computer ID: ${report.computerId || '-'}`,
5715
+ `Config: ${report.configPath}`,
5716
+ '',
5717
+ ].join('\n');
5718
+ }
5719
+ return [
5720
+ 'MagClaw Computers',
5721
+ `Profiles: ${profiles.length} Running: ${profiles.filter((profile) => profile.running || profile.service?.active).length}`,
5722
+ `Root: ${report.root || '-'}`,
5723
+ '',
5724
+ ...profiles.map((profile) => [
5725
+ `${profile.running || profile.service?.active ? 'online ' : 'offline'} ${profile.profile}`,
5726
+ ` server=${profile.serverSlug || profile.serverName || '-'} computer=${profile.computerId || '-'} service=${profile.service?.mode || 'foreground'}`,
5727
+ ].join('\n')),
5728
+ profiles.length ? '' : 'No profiles. Run `magclaw-computer setup /<serverSlug>` first.',
5729
+ '',
5730
+ ].join('\n');
5731
+ }
5732
+
5733
+ async function computerStatus(flags = {}, env = process.env) {
5734
+ if (hasComputerTarget(flags)) return status(computerTargetProfile(flags, flags.profile || DEFAULT_PROFILE));
5735
+ return renderComputerAggregateStatus(env);
5736
+ }
5737
+
5738
+ async function startAllComputerProfiles(env = process.env) {
5739
+ const report = await listProfiles(env);
5740
+ const results = [];
5741
+ for (const profile of report.profiles) {
5742
+ if (!profile.configured) continue;
5743
+ results.push({ profile: profile.profile, ...(await startSavedBackground({ profile: profile.profile }, env)) });
5744
+ }
5745
+ return { ok: results.every((item) => item.ok), count: results.length, results };
5746
+ }
5747
+
5748
+ async function stopAllComputerProfiles(flags = {}, env = process.env) {
5749
+ const report = await listProfiles(env);
5750
+ const results = [];
5751
+ for (const profile of report.profiles) {
5752
+ results.push({ profile: profile.profile, ...(await stopDaemon(profile.profile, env, { disable: Boolean(flags.disable) })) });
5753
+ }
5754
+ return { ok: results.every((item) => item.ok), count: results.length, results };
5755
+ }
5756
+
5757
+ async function detachComputerProfile(flags = {}, env = process.env) {
5758
+ const profile = computerTargetProfile(flags);
5759
+ if (!profile || profile === DEFAULT_PROFILE && !hasComputerTarget(flags)) {
5760
+ throw new Error('Usage: magclaw-computer detach <serverSlug>');
5761
+ }
5762
+ const paths = profilePaths(profile, env);
5763
+ const stopped = await uninstallBackground(profile, env);
5764
+ await rm(paths.dir, { recursive: true, force: true });
5765
+ return { ok: true, profile, detached: true, stopped };
5766
+ }
5767
+
5768
+ async function cleanupComputerResidue(env = process.env) {
5769
+ const report = await listProfiles(env);
5770
+ const cleaned = [];
5771
+ await activeComputerLock(env);
5772
+ for (const profile of report.profiles) {
5773
+ const paths = profilePaths(profile.profile, env);
5774
+ const before = existsSync(paths.lockFile);
5775
+ await activeDaemonLock(profile.profile, env);
5776
+ if (before && !existsSync(paths.lockFile)) cleaned.push(paths.lockFile);
5777
+ }
5778
+ return cleaned;
5779
+ }
5780
+
5781
+ async function computerDoctor(flags = {}, env = process.env) {
5782
+ const cleanup = Boolean(flags.cleanup || flags.fix);
5783
+ const target = hasComputerTarget(flags) ? computerTargetProfile(flags, flags.profile || DEFAULT_PROFILE) : '';
5784
+ const runtime = await doctor(env);
5785
+ const aggregate = await renderComputerAggregateStatus(env);
5786
+ const selected = target ? await status(target) : null;
5787
+ const cleaned = cleanup ? await cleanupComputerResidue(env) : [];
5788
+ const checks = [
5789
+ { name: 'MAGCLAW_DAEMON_HOME', ok: true, detail: aggregate.root },
5790
+ { name: 'profiles', ok: aggregate.profiles.length > 0, detail: `${aggregate.profiles.length} configured` },
5791
+ { name: 'runtime', ok: runtime.runtimes.some((item) => item.available), detail: runtime.runtimes.filter((item) => item.available).map((item) => item.id).join(', ') || 'none detected' },
5792
+ ];
5793
+ if (selected) {
5794
+ checks.push(
5795
+ { name: `profile ${target}`, ok: selected.configured, detail: selected.configPath },
5796
+ { name: `daemon ${target}`, ok: selected.running || selected.service?.active, detail: selected.running ? `running (pid ${selected.pid})` : (selected.service?.active ? 'service active' : 'stopped') },
5797
+ );
5798
+ }
5799
+ return {
5800
+ ok: checks.every((check) => check.ok !== false),
5801
+ checks,
5802
+ runtime,
5803
+ aggregate,
5804
+ ...(selected ? { profile: selected } : {}),
5805
+ cleanup: { requested: cleanup, staleLocksCleared: cleaned },
5806
+ };
5807
+ }
5808
+
5809
+ async function readComputerChannel(env = process.env) {
5810
+ const file = computerChannelPath(env);
5811
+ const value = existsSync(file) ? String(await readFile(file, 'utf8')).trim() : 'latest';
5812
+ return value || 'latest';
5813
+ }
5814
+
5815
+ function validateComputerChannel(value = '') {
5816
+ const channel = String(value || '').trim();
5817
+ if (channel === 'latest' || channel === 'alpha' || /^pinned:[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$/.test(channel)) return channel;
5818
+ throw new Error('Channel must be latest, alpha, or pinned:<semver>.');
5819
+ }
5820
+
5821
+ async function setComputerChannel(value, env = process.env) {
5822
+ const channel = validateComputerChannel(value);
5823
+ const file = computerChannelPath(env);
5824
+ await mkdir(path.dirname(file), { recursive: true });
5825
+ await writeFile(file, `${channel}\n`);
5826
+ return channel;
5827
+ }
5828
+
5829
+ async function computerChannel(flags = {}, env = process.env) {
5830
+ const action = String(flags._?.[1] || flags._?.[0] || '').trim();
5831
+ const value = String(flags._?.[2] || flags.channel || '').trim();
5832
+ if (action === 'set') {
5833
+ const channel = await setComputerChannel(value, env);
5834
+ return { ok: true, channel };
5835
+ }
5836
+ return { ok: true, channel: await readComputerChannel(env), file: computerChannelPath(env) };
5837
+ }
5838
+
5839
+ async function computerRunners(flags = {}, env = process.env) {
5840
+ const action = String(flags._?.[1] || 'list').trim();
5841
+ if (action === 'list') {
5842
+ const aggregate = await renderComputerAggregateStatus(env);
5843
+ return {
5844
+ ok: true,
5845
+ note: 'MagClaw local CLI can list Computer profiles. Per-agent runner stop/list remains a cloud console or agent-tool operation.',
5846
+ profiles: aggregate.profiles.map((profile) => ({
5847
+ profile: profile.profile,
5848
+ serverSlug: profile.serverSlug,
5849
+ computerId: profile.computerId,
5850
+ running: profile.running || profile.service?.active,
5851
+ })),
5852
+ };
5853
+ }
5854
+ if (action === 'stop') {
5855
+ throw new Error('Local runner stop is not available yet. Stop Agents from the MagClaw web console or agent runtime controls.');
5856
+ }
5857
+ throw new Error(`Unknown runners command: ${action}`);
5858
+ }
5859
+
5860
+ async function computerUpgrade(flags = {}, env = process.env) {
5861
+ const channel = flags.channel ? validateComputerChannel(flags.channel) : await readComputerChannel(env);
5862
+ const targetVersion = flags.targetVersion || flags.to || flags.version || (String(channel).startsWith('pinned:') ? String(channel).slice('pinned:'.length) : channel);
5863
+ await runManualUpgrade({
5864
+ ...flags,
5865
+ to: targetVersion,
5866
+ targetVersion,
5867
+ packageName: COMPUTER_PACKAGE_NAME,
5868
+ packageBin: 'magclaw-computer',
5869
+ }, {
5870
+ ...env,
5871
+ MAGCLAW_ENTRY_PACKAGE_NAME: COMPUTER_PACKAGE_NAME,
5872
+ MAGCLAW_DAEMON_PACKAGE_NAME: COMPUTER_PACKAGE_NAME,
5873
+ MAGCLAW_DAEMON_PACKAGE_KIND: 'computer',
5874
+ MAGCLAW_DAEMON_PACKAGE_BIN: 'magclaw-computer',
5875
+ });
5876
+ }
5877
+
5878
+ async function runComputerCommand(flags, env = process.env) {
5879
+ const subcommand = String(flags._?.[0] || 'help').trim();
5880
+ if (subcommand === 'help' || flags.help) {
5881
+ process.stdout.write(renderComputerHelp(subcommand === 'help' ? flags._?.[1] : subcommand));
5882
+ return;
5883
+ }
5884
+ switch (subcommand) {
5885
+ case 'login':
5886
+ case 'attach':
5887
+ case 'setup':
5888
+ await runComputerSetup(flags, env);
5889
+ break;
5890
+ case 'adopt-legacy':
5891
+ throw new Error('MagClaw legacy adoption is handled by `magclaw-computer setup /<serverSlug>` or `magclaw connect --pair-token <token>`.');
5892
+ case 'detach':
5893
+ printJson(await detachComputerProfile(flags, env));
5894
+ break;
5895
+ case 'status': {
5896
+ const report = await computerStatus(flags, env);
5897
+ if (flags.json) printJson(report);
5898
+ else process.stdout.write(formatComputerStatus(report));
5899
+ break;
5900
+ }
5901
+ case 'start':
5902
+ if (hasComputerTarget(flags)) {
5903
+ if (flags.foreground) {
5904
+ await runForegroundDaemon(await buildConfig({ ...flags, profile: computerTargetProfile(flags) }, env), env);
5905
+ } else {
5906
+ printJson(await startSavedBackground({ ...flags, profile: computerTargetProfile(flags) }, env));
5907
+ }
5908
+ } else {
5909
+ printJson(await startAllComputerProfiles(env));
5910
+ }
5911
+ break;
5912
+ case 'stop':
5913
+ if (hasComputerTarget(flags)) printJson(await stopDaemon(computerTargetProfile(flags), env, { disable: Boolean(flags.disable) }));
5914
+ else printJson(await stopAllComputerProfiles(flags, env));
5915
+ break;
5916
+ case 'doctor': {
5917
+ const report = await computerDoctor(flags, env);
5918
+ if (flags.json) printJson(report);
5919
+ else process.stdout.write(`${report.checks.map((check) => `${check.ok ? 'ok' : 'fail'} ${check.name}: ${check.detail}`).join('\n')}\n`);
5920
+ break;
5921
+ }
5922
+ case 'logs':
5923
+ await logs(computerTargetProfile(flags), { lines: flags.lines || flags.lineCount });
5924
+ break;
5925
+ case 'runners':
5926
+ printJson(await computerRunners(flags, env));
5927
+ break;
5928
+ case 'channel':
5929
+ printJson(await computerChannel(flags, env));
5930
+ break;
5931
+ case 'upgrade':
5932
+ await computerUpgrade(flags, env);
5933
+ break;
5934
+ default:
5935
+ throw new Error(`Unknown computer command: ${subcommand}`);
5936
+ }
5937
+ }
5938
+
5386
5939
  async function buildConfig(flags, env = process.env) {
5387
5940
  const diskConfig = await readProfile(flags.profile, env);
5388
5941
  const profile = flags.profile || diskConfig.profile || DEFAULT_PROFILE;
@@ -5405,7 +5958,7 @@ async function buildConfig(flags, env = process.env) {
5405
5958
 
5406
5959
  async function runForegroundDaemon(config, env = process.env) {
5407
5960
  const releaseLock = await acquireDaemonLock(config.profile, config, env);
5408
- await markForegroundServiceState(config.profile, env);
5961
+ await markDaemonRunServiceState(config.profile, env);
5409
5962
  const daemon = new MagClawDaemon(config, env);
5410
5963
  let forceExitTimer = null;
5411
5964
  const shutdown = (signal) => {
@@ -5482,6 +6035,14 @@ function requireExplicitProfile(command, flags = {}) {
5482
6035
 
5483
6036
  export async function main(argv = process.argv, env = process.env) {
5484
6037
  const { command, flags } = parseCli(argv);
6038
+ if (flags.version) {
6039
+ process.stdout.write(`${DAEMON_VERSION}\n`);
6040
+ return;
6041
+ }
6042
+ if (command === 'computer' && flags.help) {
6043
+ process.stdout.write(renderComputerHelp(flags._?.[0] || ''));
6044
+ return;
6045
+ }
5485
6046
  if (command === 'help' || flags.help) {
5486
6047
  process.stdout.write(renderHelp());
5487
6048
  return;
@@ -5491,7 +6052,7 @@ export async function main(argv = process.argv, env = process.env) {
5491
6052
  await runConnect(flags, env);
5492
6053
  break;
5493
6054
  case 'computer':
5494
- await runComputerSetup(flags, env);
6055
+ await runComputerCommand(flags, env);
5495
6056
  break;
5496
6057
  case 'start': {
5497
6058
  printJson(await startSavedBackground(flags, env));
@@ -57,7 +57,8 @@ function statusLabel(profile, color) {
57
57
 
58
58
  function serviceLabel(profile) {
59
59
  const mode = fallback(profile.service?.mode, 'foreground');
60
- return profile.service?.active ? `${mode}: active` : `${mode}: inactive`;
60
+ const status = fallback(profile.service?.status || (profile.service?.active ? 'active' : 'inactive'), 'inactive');
61
+ return `${mode}: ${status}`;
61
62
  }
62
63
 
63
64
  export function shouldUseColor({ env = process.env, stream = process.stdout, flags = {} } = {}) {