@magclaw/cli-core 0.1.28 → 0.1.30

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.30",
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;
@@ -580,7 +588,7 @@ function renderHelp() {
580
588
  ' status Show daemon status for one profile',
581
589
  ' list List local daemon profiles and connected Computers',
582
590
  ' logs Print recent daemon logs for one profile',
583
- ' install-cli Install or repair the durable magclaw command shim',
591
+ ' install-cli Install or repair durable magclaw command shims',
584
592
  ' upgrade Upgrade the background daemon package',
585
593
  ' doctor Show runtime and environment diagnostics',
586
594
  ' uninstall Stop and remove the background daemon service',
@@ -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);
@@ -921,64 +1110,88 @@ function defaultCliPackageSpec(env = process.env) {
921
1110
  return String(env.MAGCLAW_CLI_PACKAGE_SPEC || '@magclaw/cli-core@latest').trim() || '@magclaw/cli-core@latest';
922
1111
  }
923
1112
 
1113
+ function defaultComputerCliPackageSpec(env = process.env) {
1114
+ return String(env.MAGCLAW_COMPUTER_CLI_PACKAGE_SPEC || '@magclaw/computer@latest').trim() || '@magclaw/computer@latest';
1115
+ }
1116
+
924
1117
  function defaultCliNpmPath(env = process.env) {
925
1118
  return commandExists('npm', env) || (process.platform === 'win32' ? 'npm.cmd' : 'npm');
926
1119
  }
927
1120
 
928
- export function renderCliShimFiles({ platform = process.platform, npmPath = '', packageSpec = '@magclaw/cli-core@latest' } = {}) {
1121
+ function cliShimTargets({ packageSpec = '@magclaw/cli-core@latest', computerPackageSpec = '@magclaw/computer@latest' } = {}) {
1122
+ return [
1123
+ {
1124
+ command: 'magclaw',
1125
+ packageSpec: String(packageSpec || '@magclaw/cli-core@latest').trim() || '@magclaw/cli-core@latest',
1126
+ },
1127
+ {
1128
+ command: 'magclaw-computer',
1129
+ packageSpec: String(computerPackageSpec || '@magclaw/computer@latest').trim() || '@magclaw/computer@latest',
1130
+ },
1131
+ ];
1132
+ }
1133
+
1134
+ export function renderCliShimFiles({
1135
+ platform = process.platform,
1136
+ npmPath = '',
1137
+ packageSpec = '@magclaw/cli-core@latest',
1138
+ computerPackageSpec = '@magclaw/computer@latest',
1139
+ } = {}) {
929
1140
  const targetPackage = String(packageSpec || '@magclaw/cli-core@latest').trim() || '@magclaw/cli-core@latest';
1141
+ const targetComputerPackage = String(computerPackageSpec || '@magclaw/computer@latest').trim() || '@magclaw/computer@latest';
930
1142
  const targetNpm = String(npmPath || (platform === 'win32' ? 'npm.cmd' : 'npm')).trim() || (platform === 'win32' ? 'npm.cmd' : 'npm');
1143
+ const targets = cliShimTargets({ packageSpec: targetPackage, computerPackageSpec: targetComputerPackage });
931
1144
  if (platform === 'win32') {
932
1145
  const fallback = path.win32.basename(targetNpm) || 'npm.cmd';
933
- return [
1146
+ return targets.flatMap((target) => [
934
1147
  {
935
- name: 'magclaw.cmd',
1148
+ name: `${target.command}.cmd`,
936
1149
  executable: true,
937
1150
  content: [
938
1151
  '@echo off',
939
1152
  'setlocal',
940
1153
  `set "NPM_BIN=${cmdEnvValue(targetNpm)}"`,
941
1154
  `if not exist "%NPM_BIN%" set "NPM_BIN=${cmdEnvValue(fallback)}"`,
942
- `set "PACKAGE_SPEC=${cmdEnvValue(targetPackage)}"`,
1155
+ `set "PACKAGE_SPEC=${cmdEnvValue(target.packageSpec)}"`,
943
1156
  'set "ARGS=%*"',
944
- '"%NPM_BIN%" exec --yes --package "%PACKAGE_SPEC%" -- magclaw %ARGS%',
1157
+ `"%NPM_BIN%" exec --yes --package "%PACKAGE_SPEC%" -- ${target.command} %ARGS%`,
945
1158
  'exit /b %ERRORLEVEL%',
946
1159
  '',
947
1160
  ].join('\r\n'),
948
1161
  },
949
1162
  {
950
- name: 'magclaw.ps1',
1163
+ name: `${target.command}.ps1`,
951
1164
  executable: true,
952
1165
  content: [
953
1166
  `$npmBin = ${psSingleQuote(targetNpm)}`,
954
1167
  `if (-not (Test-Path -LiteralPath $npmBin)) { $npmBin = ${psSingleQuote(fallback)} }`,
955
- `$packageSpec = ${psSingleQuote(targetPackage)}`,
956
- '& $npmBin exec --yes --package $packageSpec -- magclaw @args',
1168
+ `$packageSpec = ${psSingleQuote(target.packageSpec)}`,
1169
+ `& $npmBin exec --yes --package $packageSpec -- ${target.command} @args`,
957
1170
  'exit $LASTEXITCODE',
958
1171
  '',
959
1172
  ].join('\n'),
960
1173
  },
961
- ];
1174
+ ]);
962
1175
  }
963
1176
  const fallback = path.basename(targetNpm) || 'npm';
964
- return [
1177
+ return targets.map((target) => (
965
1178
  {
966
- name: 'magclaw',
1179
+ name: target.command,
967
1180
  executable: true,
968
1181
  content: [
969
1182
  '#!/bin/sh',
970
1183
  'set -eu',
971
1184
  '# MagClaw CLI shim generated by @magclaw/cli-core.',
972
1185
  `NPM_BIN=${shSingleQuote(targetNpm)}`,
973
- `PACKAGE_SPEC=${shSingleQuote(targetPackage)}`,
1186
+ `PACKAGE_SPEC=${shSingleQuote(target.packageSpec)}`,
974
1187
  'if [ ! -x "$NPM_BIN" ]; then',
975
1188
  ` NPM_BIN=${shSingleQuote(fallback)}`,
976
1189
  'fi',
977
- 'exec "$NPM_BIN" exec --yes --package "$PACKAGE_SPEC" -- magclaw "$@"',
1190
+ `exec "$NPM_BIN" exec --yes --package "$PACKAGE_SPEC" -- ${target.command} "$@"`,
978
1191
  '',
979
1192
  ].join('\n'),
980
- },
981
- ];
1193
+ }
1194
+ ));
982
1195
  }
983
1196
 
984
1197
  async function chooseCliShimBinDir(options = {}, env = process.env) {
@@ -1014,17 +1227,21 @@ async function chooseCliShimBinDir(options = {}, env = process.env) {
1014
1227
  return { dir: fallback, explicit: false, pathReady: directoryIsInPath(fallback, env) };
1015
1228
  }
1016
1229
 
1017
- async function existingDurableMagclawCommand(env = process.env) {
1018
- const existing = commandExists('magclaw', env);
1230
+ function isGeneratedMagClawShim(content = '') {
1231
+ return Boolean(
1232
+ content.includes('MagClaw CLI shim generated by @magclaw/cli-core')
1233
+ || content.includes('MagClaw CLI shim generated by @magclaw/daemon')
1234
+ || content.includes('@magclaw/cli-core@')
1235
+ || content.includes('@magclaw/daemon@')
1236
+ || content.includes('@magclaw/computer@')
1237
+ );
1238
+ }
1239
+
1240
+ async function existingDurableMagclawCommand(command = 'magclaw', env = process.env) {
1241
+ const existing = commandExists(command, env);
1019
1242
  if (!existing || pathLooksEphemeralCli(existing)) return '';
1020
1243
  const content = await readFile(existing, 'utf8').catch(() => '');
1021
- if (
1022
- content
1023
- && !content.includes('MagClaw CLI shim generated by @magclaw/cli-core')
1024
- && !content.includes('MagClaw CLI shim generated by @magclaw/daemon')
1025
- && !content.includes('@magclaw/cli-core@')
1026
- && !content.includes('@magclaw/daemon@')
1027
- ) {
1244
+ if (content && !isGeneratedMagClawShim(content)) {
1028
1245
  return '';
1029
1246
  }
1030
1247
  return existing;
@@ -1033,12 +1250,7 @@ async function existingDurableMagclawCommand(env = process.env) {
1033
1250
  async function writeCliShimFile(file, content, { force = false } = {}) {
1034
1251
  if (existsSync(file) && !force) {
1035
1252
  const existing = await readFile(file, 'utf8').catch(() => '');
1036
- if (
1037
- !existing.includes('MagClaw CLI shim generated by @magclaw/cli-core')
1038
- && !existing.includes('MagClaw CLI shim generated by @magclaw/daemon')
1039
- && !existing.includes('@magclaw/cli-core@')
1040
- && !existing.includes('@magclaw/daemon@')
1041
- ) {
1253
+ if (!isGeneratedMagClawShim(existing)) {
1042
1254
  const error = new Error(`Refusing to overwrite existing non-MagClaw command: ${file}`);
1043
1255
  error.code = 'EEXIST';
1044
1256
  throw error;
@@ -1052,16 +1264,39 @@ async function installCliShim(options = {}, env = process.env) {
1052
1264
  if (env.MAGCLAW_INSTALL_CLI === '0' || options.installCli === false || options.noInstallCli) {
1053
1265
  return { ok: true, command: 'magclaw', installed: false, skipped: true, reason: 'disabled' };
1054
1266
  }
1055
- const existing = await existingDurableMagclawCommand(env);
1056
- if (existing && !options.binDir && !options.cliBinDir && !options.force) {
1057
- return { ok: true, command: 'magclaw', installed: false, path: existing, files: [existing], pathReady: true, reason: 'already_available' };
1267
+ const existing = await existingDurableMagclawCommand('magclaw', env);
1268
+ const existingComputer = await existingDurableMagclawCommand('magclaw-computer', env);
1269
+ const existingCommandsShareDir = Boolean(
1270
+ existing
1271
+ && existingComputer
1272
+ && path.dirname(existing) === path.dirname(existingComputer)
1273
+ );
1274
+ if (existingCommandsShareDir && !options.binDir && !options.cliBinDir && !options.force) {
1275
+ return {
1276
+ ok: true,
1277
+ command: 'magclaw',
1278
+ commands: ['magclaw', 'magclaw-computer'],
1279
+ installed: false,
1280
+ path: existing,
1281
+ files: [existing, existingComputer],
1282
+ pathReady: true,
1283
+ reason: 'already_available',
1284
+ };
1058
1285
  }
1059
1286
 
1060
- const target = await chooseCliShimBinDir(options, env);
1287
+ const target = (existing || existingComputer) && !options.binDir && !options.cliBinDir && !options.force
1288
+ ? { dir: path.dirname(existing || existingComputer), explicit: false, pathReady: true }
1289
+ : await chooseCliShimBinDir(options, env);
1061
1290
  await mkdir(target.dir, { recursive: true });
1062
1291
  const npmPath = String(options.npmPath || defaultCliNpmPath(env)).trim() || (process.platform === 'win32' ? 'npm.cmd' : 'npm');
1063
1292
  const packageSpec = String(options.packageSpec || options.cliPackageSpec || defaultCliPackageSpec(env)).trim() || '@magclaw/cli-core@latest';
1064
- const shimFiles = renderCliShimFiles({ platform: process.platform, npmPath, packageSpec });
1293
+ const computerPackageSpec = String(options.computerPackageSpec || options.computerCliPackageSpec || defaultComputerCliPackageSpec(env)).trim() || '@magclaw/computer@latest';
1294
+ const shimFiles = renderCliShimFiles({
1295
+ platform: process.platform,
1296
+ npmPath,
1297
+ packageSpec,
1298
+ computerPackageSpec,
1299
+ });
1065
1300
  const written = [];
1066
1301
  for (const shim of shimFiles) {
1067
1302
  const file = path.join(target.dir, shim.name);
@@ -1071,12 +1306,14 @@ async function installCliShim(options = {}, env = process.env) {
1071
1306
  return {
1072
1307
  ok: true,
1073
1308
  command: 'magclaw',
1309
+ commands: ['magclaw', 'magclaw-computer'],
1074
1310
  installed: true,
1075
1311
  binDir: target.dir,
1076
1312
  files: written,
1077
1313
  path: written[0] || '',
1078
1314
  pathReady: Boolean(target.pathReady),
1079
1315
  packageSpec,
1316
+ computerPackageSpec,
1080
1317
  npmPath,
1081
1318
  };
1082
1319
  }
@@ -3249,6 +3486,8 @@ class MagClawDaemon {
3249
3486
  this.pendingUpgradeRequest = null;
3250
3487
  this.upgradeIdleTimer = null;
3251
3488
  this.upgradeWorkerStarting = false;
3489
+ this.runtimeStatusTimer = null;
3490
+ this.runtimeStatusInFlight = false;
3252
3491
  }
3253
3492
 
3254
3493
  send(payload) {
@@ -3619,7 +3858,6 @@ class MagClawDaemon {
3619
3858
  }
3620
3859
 
3621
3860
  async readyPayload() {
3622
- const runtimes = await detectRuntimes(this.env);
3623
3861
  const owner = await ensureMachineFingerprint(this.paths.profile, this.env);
3624
3862
  const service = await readServiceState(this.paths.profile, this.env);
3625
3863
  const serviceStatus = backgroundServiceStatus(this.paths.profile, this.env);
@@ -3658,8 +3896,7 @@ class MagClawDaemon {
3658
3896
  cliCoreVersion: CLI_CORE_VERSION,
3659
3897
  },
3660
3898
  upgrade: upgrade || null,
3661
- runtimes: runtimes.filter((runtime) => runtime.installed).map((runtime) => runtime.id),
3662
- runtimeDetails: runtimes,
3899
+ runtimeScanPending: true,
3663
3900
  runningAgents: [...this.sessions.keys()],
3664
3901
  capabilities: CAPABILITIES,
3665
3902
  };
@@ -3670,10 +3907,68 @@ class MagClawDaemon {
3670
3907
  const sent = this.send(payload);
3671
3908
  logInfo(
3672
3909
  'daemon',
3673
- `Sent ready payload for computer ${payload.computerId || 'unpaired'} (runtimes=${payload.runtimes.join(', ') || 'none'}, runningAgents=${payload.runningAgents.length}, sent=${sent}).`,
3910
+ `Sent ready payload for computer ${payload.computerId || 'unpaired'} (runtimes=deferred, runningAgents=${payload.runningAgents.length}, sent=${sent}).`,
3674
3911
  );
3675
3912
  }
3676
3913
 
3914
+ runtimeStatusDelayMs() {
3915
+ return envInteger(this.env, 'MAGCLAW_DAEMON_RUNTIME_STATUS_DELAY_MS', 1000, { min: 0, max: 60_000 });
3916
+ }
3917
+
3918
+ clearRuntimeStatusTimer() {
3919
+ if (!this.runtimeStatusTimer) return;
3920
+ clearTimeout(this.runtimeStatusTimer);
3921
+ this.runtimeStatusTimer = null;
3922
+ }
3923
+
3924
+ scheduleRuntimeStatus(reason = 'ready_ack') {
3925
+ if (this.closed || this.runtimeStatusTimer || this.runtimeStatusInFlight) return;
3926
+ this.runtimeStatusTimer = setTimeout(() => {
3927
+ this.runtimeStatusTimer = null;
3928
+ this.sendRuntimeStatus(reason).catch((error) => {
3929
+ logWarning('daemon', `Failed to send runtime status: ${error.message}`);
3930
+ });
3931
+ }, this.runtimeStatusDelayMs());
3932
+ this.runtimeStatusTimer.unref?.();
3933
+ }
3934
+
3935
+ async runtimeStatusPayload(reason = 'ready_ack') {
3936
+ const runtimes = await detectRuntimes(this.env);
3937
+ const packageInfo = runtimePackageInfo(this.env);
3938
+ return {
3939
+ type: 'daemon:runtime_status',
3940
+ time: now(),
3941
+ reason,
3942
+ computerId: this.config.computerId || null,
3943
+ daemonVersion: packageInfo.version || DAEMON_VERSION,
3944
+ packageName: packageInfo.name,
3945
+ packageVersion: packageInfo.version,
3946
+ packageKind: packageInfo.kind,
3947
+ packageSpec: packageInfo.spec,
3948
+ packageBin: packageInfo.bin,
3949
+ cliCoreVersion: CLI_CORE_VERSION,
3950
+ runtimes: runtimes.filter((runtime) => runtime.installed).map((runtime) => runtime.id),
3951
+ runtimeDetails: runtimes,
3952
+ runningAgents: [...this.sessions.keys()],
3953
+ };
3954
+ }
3955
+
3956
+ async sendRuntimeStatus(reason = 'ready_ack') {
3957
+ if (this.closed || this.runtimeStatusInFlight) return false;
3958
+ this.runtimeStatusInFlight = true;
3959
+ try {
3960
+ const payload = await this.runtimeStatusPayload(reason);
3961
+ const sent = this.send(payload);
3962
+ logInfo(
3963
+ 'daemon',
3964
+ `Sent runtime status for computer ${payload.computerId || 'unpaired'} (runtimes=${payload.runtimes.join(', ') || 'none'}, sent=${sent}).`,
3965
+ );
3966
+ return sent;
3967
+ } finally {
3968
+ this.runtimeStatusInFlight = false;
3969
+ }
3970
+ }
3971
+
3677
3972
  sendHeartbeat() {
3678
3973
  const packageInfo = runtimePackageInfo(this.env);
3679
3974
  const sent = this.send({
@@ -3801,6 +4096,7 @@ class MagClawDaemon {
3801
4096
  break;
3802
4097
  case 'ready:ack':
3803
4098
  logInfo('daemon', `MagClaw daemon ready for computer ${message.computerId || this.config.computerId}.`);
4099
+ this.scheduleRuntimeStatus('ready_ack');
3804
4100
  break;
3805
4101
  case 'ping':
3806
4102
  this.send({ type: 'pong', time: now() });
@@ -4162,6 +4458,7 @@ class MagClawDaemon {
4162
4458
  this.sessions.clear();
4163
4459
  this.stopHeartbeat();
4164
4460
  this.clearInboundWatchdog();
4461
+ this.clearRuntimeStatusTimer();
4165
4462
  if (this.agentStartPumpTimer) {
4166
4463
  clearTimeout(this.agentStartPumpTimer);
4167
4464
  this.agentStartPumpTimer = null;
@@ -4214,6 +4511,7 @@ class MagClawDaemon {
4214
4511
  settled = true;
4215
4512
  this.stopHeartbeat();
4216
4513
  this.clearInboundWatchdog();
4514
+ this.clearRuntimeStatusTimer();
4217
4515
  if (this.socket && this.socket.destroyed) this.socket = null;
4218
4516
  if (this.request === req) this.request = null;
4219
4517
  callback(value);
@@ -4437,6 +4735,7 @@ async function writeLauncher(profile, env = process.env) {
4437
4735
  " MAGCLAW_DAEMON_PACKAGE_SPEC: packageSpec,",
4438
4736
  " MAGCLAW_DAEMON_PACKAGE_KIND: packageKind,",
4439
4737
  " MAGCLAW_DAEMON_PACKAGE_BIN: packageBin,",
4738
+ " MAGCLAW_DAEMON_BACKGROUND_SERVICE: '1',",
4440
4739
  " PATH: launchPath,",
4441
4740
  "};",
4442
4741
  "if (packageKind === 'computer') childEnv.MAGCLAW_COMPUTER_DAEMON = '1';",
@@ -4591,17 +4890,40 @@ async function startBackground(profile, env = process.env) {
4591
4890
  return { ok: false, mode: 'foreground', message: 'Background daemon is only automated on macOS launchd, Linux user systemd, and Windows schtasks.' };
4592
4891
  }
4593
4892
 
4893
+ export function parseLaunchdPrintStatus(result = {}) {
4894
+ const stdout = String(result.stdout || '');
4895
+ const stderr = String(result.stderr || '');
4896
+ const state = (stdout.match(/^\s*state\s*=\s*(.+?)\s*$/m)?.[1] || '').trim();
4897
+ const activeCountValue = Number(stdout.match(/^\s*active count\s*=\s*(\d+)\s*$/m)?.[1] || NaN);
4898
+ const stateStatus = state || (result.status === 0 ? 'loaded' : 'inactive');
4899
+ const active = result.status === 0 && (
4900
+ stateStatus.toLowerCase() === 'running'
4901
+ || (!state && Number.isFinite(activeCountValue) && activeCountValue > 0)
4902
+ );
4903
+ return {
4904
+ active,
4905
+ status: active ? 'running' : stateStatus,
4906
+ state,
4907
+ activeCount: Number.isFinite(activeCountValue) ? activeCountValue : null,
4908
+ error: result.status === 0 ? '' : String(stderr || stdout || '').trim(),
4909
+ };
4910
+ }
4911
+
4594
4912
  function backgroundServiceStatus(profile, env = process.env) {
4595
4913
  const paths = profilePaths(profile, env);
4596
4914
  if (process.platform === 'darwin') {
4597
4915
  const label = launchAgentLabel(paths.profile);
4598
4916
  const result = spawnSync('launchctl', ['print', `gui/${process.getuid()}/${label}`], { encoding: 'utf8' });
4917
+ const parsed = parseLaunchdPrintStatus(result);
4599
4918
  return {
4600
4919
  mode: 'launchd',
4601
- active: result.status === 0,
4920
+ active: parsed.active,
4602
4921
  label,
4603
4922
  file: path.join(os.homedir(), 'Library', 'LaunchAgents', `${label}.plist`),
4604
- error: result.status === 0 ? '' : String(result.stderr || result.stdout || '').trim(),
4923
+ status: parsed.status,
4924
+ state: parsed.state,
4925
+ activeCount: parsed.activeCount,
4926
+ error: parsed.error,
4605
4927
  };
4606
4928
  }
4607
4929
  if (process.platform === 'linux') {
@@ -4811,14 +5133,15 @@ async function status(profile) {
4811
5133
  };
4812
5134
  }
4813
5135
 
4814
- async function logs(profile) {
5136
+ async function logs(profile, options = {}) {
4815
5137
  const paths = profilePaths(profile);
5138
+ const lines = Math.max(1, Math.min(5000, Number(options.lines || 120) || 120));
4816
5139
  const files = [path.join(paths.logDir, 'daemon.log'), path.join(paths.logDir, 'daemon.err.log')];
4817
5140
  for (const file of files) {
4818
5141
  if (!existsSync(file)) continue;
4819
5142
  process.stdout.write(`\n==> ${file} <==\n`);
4820
5143
  const text = await readFile(file, 'utf8').catch(() => '');
4821
- process.stdout.write(text.split(/\r?\n/).slice(-120).join('\n'));
5144
+ process.stdout.write(text.split(/\r?\n/).slice(-lines).join('\n'));
4822
5145
  process.stdout.write('\n');
4823
5146
  }
4824
5147
  }
@@ -4857,6 +5180,9 @@ async function listProfiles(env = process.env) {
4857
5180
  label: service.label || '',
4858
5181
  serviceName: service.serviceName || '',
4859
5182
  taskName: service.taskName || '',
5183
+ status: service.status || '',
5184
+ state: service.state || '',
5185
+ activeCount: service.activeCount ?? null,
4860
5186
  },
4861
5187
  createdAt: config.createdAt || '',
4862
5188
  updatedAt: config.updatedAt || '',
@@ -5289,8 +5615,12 @@ function normalizeSetupServerSlug(value = '') {
5289
5615
  return String(value || '').trim().replace(/^\/+/, '').replace(/\/+$/, '');
5290
5616
  }
5291
5617
 
5618
+ function normalizeSetupServerUrl(value = '') {
5619
+ return String(value || DEFAULT_SERVER_URL).replace(/\/+$/, '');
5620
+ }
5621
+
5292
5622
  async function postSetupJson(serverUrl, pathname, body = {}) {
5293
- const url = `${String(serverUrl || DEFAULT_SERVER_URL).replace(/\/+$/, '')}${pathname}`;
5623
+ const url = `${normalizeSetupServerUrl(serverUrl)}${pathname}`;
5294
5624
  const response = await fetch(url, {
5295
5625
  method: 'POST',
5296
5626
  headers: { 'content-type': 'application/json' },
@@ -5303,15 +5633,144 @@ async function postSetupJson(serverUrl, pathname, body = {}) {
5303
5633
  return data;
5304
5634
  }
5305
5635
 
5636
+ function hasComputerTarget(flags = {}) {
5637
+ return Boolean(flags.profileExplicit || flags.server || flags.serverSlug || flags.slug || flags._?.[1]);
5638
+ }
5639
+
5640
+ function profileFromComputerTarget(value = '') {
5641
+ return safeProfileName(normalizeSetupServerSlug(value) || value || DEFAULT_PROFILE);
5642
+ }
5643
+
5644
+ function computerTargetProfile(flags = {}, fallback = DEFAULT_PROFILE) {
5645
+ return profileFromComputerTarget(flags.server || flags.serverSlug || flags.slug || flags._?.[1] || flags.profile || fallback);
5646
+ }
5647
+
5648
+ function savedComputerSetupMatches(config = {}, target = {}) {
5649
+ const token = String(config.token || config.machineToken || config.apiKey || '').trim();
5650
+ const computerId = String(config.computerId || '').trim();
5651
+ if (!token || !computerId) return false;
5652
+ if (normalizeSetupServerUrl(config.serverUrl) !== normalizeSetupServerUrl(target.serverUrl)) return false;
5653
+
5654
+ const targetProfile = safeProfileName(target.profile || target.serverSlug || DEFAULT_PROFILE);
5655
+ const configProfile = safeProfileName(config.profile || targetProfile);
5656
+ if (configProfile !== targetProfile) return false;
5657
+
5658
+ const targetSlug = normalizeSetupServerSlug(target.serverSlug);
5659
+ const configSlug = normalizeSetupServerSlug(config.serverSlug || config.slug || '');
5660
+ if (configSlug && targetSlug && configSlug !== targetSlug) return false;
5661
+ return true;
5662
+ }
5663
+
5664
+ async function reusableComputerSetupProfile(target = {}, env = process.env) {
5665
+ if (target.force || target.relogin || target.reauthorize) return null;
5666
+ const profile = safeProfileName(target.profile || target.serverSlug || DEFAULT_PROFILE);
5667
+ const config = await readProfile(profile, env);
5668
+ if (!savedComputerSetupMatches(config, { ...target, profile })) return null;
5669
+ const service = await readServiceState(profile, env);
5670
+ if (service.remoteClosed) return null;
5671
+ return {
5672
+ config: {
5673
+ ...config,
5674
+ profile,
5675
+ serverUrl: normalizeSetupServerUrl(config.serverUrl || target.serverUrl),
5676
+ token: String(config.token || config.machineToken || config.apiKey || '').trim(),
5677
+ pairToken: '',
5678
+ },
5679
+ service,
5680
+ serviceStatus: backgroundServiceStatus(profile, env),
5681
+ };
5682
+ }
5683
+
5684
+ async function finishReusableComputerSetup(existing, flags = {}, env = process.env) {
5685
+ const requestedSlug = normalizeSetupServerSlug(flags._?.[1] || flags.server || flags.serverSlug || flags.slug);
5686
+ const serverSlug = existing.config.serverSlug || requestedSlug;
5687
+ const config = await buildConfig({
5688
+ ...flags,
5689
+ profile: existing.config.profile,
5690
+ serverUrl: existing.config.serverUrl,
5691
+ apiKey: existing.config.token,
5692
+ computerId: existing.config.computerId,
5693
+ workspaceId: existing.config.workspaceId || existing.config.workspace,
5694
+ name: existing.config.name,
5695
+ serverName: existing.config.serverName || serverSlug,
5696
+ serverSlug,
5697
+ fingerprint: existing.config.fingerprint,
5698
+ }, env);
5699
+ await saveProfile(config.profile, config, env);
5700
+ const cli = await tryInstallCliShim(flags, env);
5701
+ const computerName = config.computerName || config.name || os.hostname();
5702
+ const basePayload = {
5703
+ cli,
5704
+ computerId: config.computerId,
5705
+ computerName,
5706
+ profile: config.profile,
5707
+ serverName: config.serverName,
5708
+ serverSlug: config.serverSlug,
5709
+ reused: true,
5710
+ reason: 'already_configured',
5711
+ };
5712
+ if (flags.noStart || flags.noRun) {
5713
+ printJson({
5714
+ ok: true,
5715
+ started: false,
5716
+ ...basePayload,
5717
+ next: `Run magclaw-computer start ${config.profile} when ready.`,
5718
+ });
5719
+ return;
5720
+ }
5721
+
5722
+ const serviceStatus = existing.serviceStatus || backgroundServiceStatus(config.profile, env);
5723
+ let result;
5724
+ if (serviceStatus.active) {
5725
+ result = {
5726
+ ok: true,
5727
+ mode: serviceStatus.mode,
5728
+ active: true,
5729
+ alreadyRunning: true,
5730
+ started: false,
5731
+ label: serviceStatus.label,
5732
+ serviceName: serviceStatus.serviceName,
5733
+ taskName: serviceStatus.taskName,
5734
+ file: serviceStatus.file,
5735
+ status: serviceStatus.status,
5736
+ state: serviceStatus.state,
5737
+ };
5738
+ } else {
5739
+ const started = await startBackground(config.profile, env);
5740
+ result = {
5741
+ ...started,
5742
+ started: Boolean(started.ok),
5743
+ };
5744
+ }
5745
+ printJson({
5746
+ ...result,
5747
+ ...basePayload,
5748
+ });
5749
+ if (!result.ok) {
5750
+ logWarning('daemon', 'Falling back to foreground mode.');
5751
+ await runForegroundDaemon(config, env);
5752
+ }
5753
+ }
5754
+
5306
5755
  async function runComputerSetup(flags, env = process.env) {
5307
5756
  const subcommand = String(flags._?.[0] || '').trim();
5308
- if (subcommand !== 'setup') {
5309
- throw new Error('Usage: magclaw computer setup /<server-slug> --server-url <url>');
5757
+ if (!['setup', 'attach', 'login'].includes(subcommand)) {
5758
+ throw new Error('Usage: magclaw-computer setup /<server-slug> --server-url <url>');
5310
5759
  }
5311
5760
  const serverSlug = normalizeSetupServerSlug(flags._?.[1] || flags.server || flags.serverSlug || flags.slug);
5312
5761
  if (!serverSlug) throw new Error('Run computer setup with a server slug, for example: magclaw computer setup /my-server');
5313
- const serverUrl = String(flags.serverUrl || env.MAGCLAW_PUBLIC_URL || DEFAULT_SERVER_URL).replace(/\/+$/, '');
5762
+ const serverUrl = normalizeSetupServerUrl(flags.serverUrl || env.MAGCLAW_PUBLIC_URL || DEFAULT_SERVER_URL);
5314
5763
  const profile = safeProfileName(flags.profile && flags.profile !== DEFAULT_PROFILE ? flags.profile : serverSlug);
5764
+ const existing = await reusableComputerSetupProfile({
5765
+ ...flags,
5766
+ profile,
5767
+ serverSlug,
5768
+ serverUrl,
5769
+ }, env);
5770
+ if (existing) {
5771
+ await finishReusableComputerSetup(existing, flags, env);
5772
+ return;
5773
+ }
5315
5774
  const owner = await ensureMachineFingerprint(profile, env);
5316
5775
  const displayName = String(flags.displayName || flags.name || os.hostname()).trim();
5317
5776
  const packageInfo = runtimePackageInfo(env);
@@ -5367,6 +5826,20 @@ async function runComputerSetup(flags, env = process.env) {
5367
5826
  await saveProfile(config.profile, config, env);
5368
5827
  await clearRemoteClosedServiceState(config.profile, env);
5369
5828
  const cli = await tryInstallCliShim(flags, env);
5829
+ if (flags.noStart || flags.noRun) {
5830
+ printJson({
5831
+ ok: true,
5832
+ started: false,
5833
+ cli,
5834
+ computerId: config.computerId,
5835
+ computerName: config.computerName || config.name || displayName,
5836
+ profile: config.profile,
5837
+ serverName: config.serverName,
5838
+ serverSlug: approved.serverSlug || serverSlug,
5839
+ next: `Run magclaw-computer start ${config.profile} when ready.`,
5840
+ });
5841
+ return;
5842
+ }
5370
5843
  const result = await startBackground(config.profile, env);
5371
5844
  printJson({
5372
5845
  ...result,
@@ -5383,6 +5856,255 @@ async function runComputerSetup(flags, env = process.env) {
5383
5856
  }
5384
5857
  }
5385
5858
 
5859
+ async function renderComputerAggregateStatus(env = process.env) {
5860
+ const report = await listProfiles(env);
5861
+ return {
5862
+ ok: true,
5863
+ root: report.root,
5864
+ loggedIn: report.profiles.some((profile) => profile.configured),
5865
+ supervisor: {
5866
+ model: 'per-profile-service',
5867
+ running: report.profiles.some((profile) => profile.running || profile.service?.active),
5868
+ managedProfiles: report.profiles.length,
5869
+ },
5870
+ profiles: report.profiles,
5871
+ };
5872
+ }
5873
+
5874
+ function formatComputerStatus(report = {}) {
5875
+ const profiles = report.profiles || [];
5876
+ if (report.profile) {
5877
+ return [
5878
+ `Profile: ${report.profile}`,
5879
+ `Configured: ${report.configured ? 'yes' : 'no'}`,
5880
+ `Daemon: ${report.running ? `running (pid ${report.pid})` : 'stopped'}`,
5881
+ `Service: ${report.service?.mode || 'foreground'}${report.service?.active ? ' active' : ''}`,
5882
+ `Server URL: ${report.serverUrl || '-'}`,
5883
+ `Computer ID: ${report.computerId || '-'}`,
5884
+ `Config: ${report.configPath}`,
5885
+ '',
5886
+ ].join('\n');
5887
+ }
5888
+ return [
5889
+ 'MagClaw Computers',
5890
+ `Profiles: ${profiles.length} Running: ${profiles.filter((profile) => profile.running || profile.service?.active).length}`,
5891
+ `Root: ${report.root || '-'}`,
5892
+ '',
5893
+ ...profiles.map((profile) => [
5894
+ `${profile.running || profile.service?.active ? 'online ' : 'offline'} ${profile.profile}`,
5895
+ ` server=${profile.serverSlug || profile.serverName || '-'} computer=${profile.computerId || '-'} service=${profile.service?.mode || 'foreground'}`,
5896
+ ].join('\n')),
5897
+ profiles.length ? '' : 'No profiles. Run `magclaw-computer setup /<serverSlug>` first.',
5898
+ '',
5899
+ ].join('\n');
5900
+ }
5901
+
5902
+ async function computerStatus(flags = {}, env = process.env) {
5903
+ if (hasComputerTarget(flags)) return status(computerTargetProfile(flags, flags.profile || DEFAULT_PROFILE));
5904
+ return renderComputerAggregateStatus(env);
5905
+ }
5906
+
5907
+ async function startAllComputerProfiles(env = process.env) {
5908
+ const report = await listProfiles(env);
5909
+ const results = [];
5910
+ for (const profile of report.profiles) {
5911
+ if (!profile.configured) continue;
5912
+ results.push({ profile: profile.profile, ...(await startSavedBackground({ profile: profile.profile }, env)) });
5913
+ }
5914
+ return { ok: results.every((item) => item.ok), count: results.length, results };
5915
+ }
5916
+
5917
+ async function stopAllComputerProfiles(flags = {}, env = process.env) {
5918
+ const report = await listProfiles(env);
5919
+ const results = [];
5920
+ for (const profile of report.profiles) {
5921
+ results.push({ profile: profile.profile, ...(await stopDaemon(profile.profile, env, { disable: Boolean(flags.disable) })) });
5922
+ }
5923
+ return { ok: results.every((item) => item.ok), count: results.length, results };
5924
+ }
5925
+
5926
+ async function detachComputerProfile(flags = {}, env = process.env) {
5927
+ const profile = computerTargetProfile(flags);
5928
+ if (!profile || profile === DEFAULT_PROFILE && !hasComputerTarget(flags)) {
5929
+ throw new Error('Usage: magclaw-computer detach <serverSlug>');
5930
+ }
5931
+ const paths = profilePaths(profile, env);
5932
+ const stopped = await uninstallBackground(profile, env);
5933
+ await rm(paths.dir, { recursive: true, force: true });
5934
+ return { ok: true, profile, detached: true, stopped };
5935
+ }
5936
+
5937
+ async function cleanupComputerResidue(env = process.env) {
5938
+ const report = await listProfiles(env);
5939
+ const cleaned = [];
5940
+ await activeComputerLock(env);
5941
+ for (const profile of report.profiles) {
5942
+ const paths = profilePaths(profile.profile, env);
5943
+ const before = existsSync(paths.lockFile);
5944
+ await activeDaemonLock(profile.profile, env);
5945
+ if (before && !existsSync(paths.lockFile)) cleaned.push(paths.lockFile);
5946
+ }
5947
+ return cleaned;
5948
+ }
5949
+
5950
+ async function computerDoctor(flags = {}, env = process.env) {
5951
+ const cleanup = Boolean(flags.cleanup || flags.fix);
5952
+ const target = hasComputerTarget(flags) ? computerTargetProfile(flags, flags.profile || DEFAULT_PROFILE) : '';
5953
+ const runtime = await doctor(env);
5954
+ const aggregate = await renderComputerAggregateStatus(env);
5955
+ const selected = target ? await status(target) : null;
5956
+ const cleaned = cleanup ? await cleanupComputerResidue(env) : [];
5957
+ const checks = [
5958
+ { name: 'MAGCLAW_DAEMON_HOME', ok: true, detail: aggregate.root },
5959
+ { name: 'profiles', ok: aggregate.profiles.length > 0, detail: `${aggregate.profiles.length} configured` },
5960
+ { name: 'runtime', ok: runtime.runtimes.some((item) => item.available), detail: runtime.runtimes.filter((item) => item.available).map((item) => item.id).join(', ') || 'none detected' },
5961
+ ];
5962
+ if (selected) {
5963
+ checks.push(
5964
+ { name: `profile ${target}`, ok: selected.configured, detail: selected.configPath },
5965
+ { name: `daemon ${target}`, ok: selected.running || selected.service?.active, detail: selected.running ? `running (pid ${selected.pid})` : (selected.service?.active ? 'service active' : 'stopped') },
5966
+ );
5967
+ }
5968
+ return {
5969
+ ok: checks.every((check) => check.ok !== false),
5970
+ checks,
5971
+ runtime,
5972
+ aggregate,
5973
+ ...(selected ? { profile: selected } : {}),
5974
+ cleanup: { requested: cleanup, staleLocksCleared: cleaned },
5975
+ };
5976
+ }
5977
+
5978
+ async function readComputerChannel(env = process.env) {
5979
+ const file = computerChannelPath(env);
5980
+ const value = existsSync(file) ? String(await readFile(file, 'utf8')).trim() : 'latest';
5981
+ return value || 'latest';
5982
+ }
5983
+
5984
+ function validateComputerChannel(value = '') {
5985
+ const channel = String(value || '').trim();
5986
+ if (channel === 'latest' || channel === 'alpha' || /^pinned:[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$/.test(channel)) return channel;
5987
+ throw new Error('Channel must be latest, alpha, or pinned:<semver>.');
5988
+ }
5989
+
5990
+ async function setComputerChannel(value, env = process.env) {
5991
+ const channel = validateComputerChannel(value);
5992
+ const file = computerChannelPath(env);
5993
+ await mkdir(path.dirname(file), { recursive: true });
5994
+ await writeFile(file, `${channel}\n`);
5995
+ return channel;
5996
+ }
5997
+
5998
+ async function computerChannel(flags = {}, env = process.env) {
5999
+ const action = String(flags._?.[1] || flags._?.[0] || '').trim();
6000
+ const value = String(flags._?.[2] || flags.channel || '').trim();
6001
+ if (action === 'set') {
6002
+ const channel = await setComputerChannel(value, env);
6003
+ return { ok: true, channel };
6004
+ }
6005
+ return { ok: true, channel: await readComputerChannel(env), file: computerChannelPath(env) };
6006
+ }
6007
+
6008
+ async function computerRunners(flags = {}, env = process.env) {
6009
+ const action = String(flags._?.[1] || 'list').trim();
6010
+ if (action === 'list') {
6011
+ const aggregate = await renderComputerAggregateStatus(env);
6012
+ return {
6013
+ ok: true,
6014
+ note: 'MagClaw local CLI can list Computer profiles. Per-agent runner stop/list remains a cloud console or agent-tool operation.',
6015
+ profiles: aggregate.profiles.map((profile) => ({
6016
+ profile: profile.profile,
6017
+ serverSlug: profile.serverSlug,
6018
+ computerId: profile.computerId,
6019
+ running: profile.running || profile.service?.active,
6020
+ })),
6021
+ };
6022
+ }
6023
+ if (action === 'stop') {
6024
+ throw new Error('Local runner stop is not available yet. Stop Agents from the MagClaw web console or agent runtime controls.');
6025
+ }
6026
+ throw new Error(`Unknown runners command: ${action}`);
6027
+ }
6028
+
6029
+ async function computerUpgrade(flags = {}, env = process.env) {
6030
+ const channel = flags.channel ? validateComputerChannel(flags.channel) : await readComputerChannel(env);
6031
+ const targetVersion = flags.targetVersion || flags.to || flags.version || (String(channel).startsWith('pinned:') ? String(channel).slice('pinned:'.length) : channel);
6032
+ await runManualUpgrade({
6033
+ ...flags,
6034
+ to: targetVersion,
6035
+ targetVersion,
6036
+ packageName: COMPUTER_PACKAGE_NAME,
6037
+ packageBin: 'magclaw-computer',
6038
+ }, {
6039
+ ...env,
6040
+ MAGCLAW_ENTRY_PACKAGE_NAME: COMPUTER_PACKAGE_NAME,
6041
+ MAGCLAW_DAEMON_PACKAGE_NAME: COMPUTER_PACKAGE_NAME,
6042
+ MAGCLAW_DAEMON_PACKAGE_KIND: 'computer',
6043
+ MAGCLAW_DAEMON_PACKAGE_BIN: 'magclaw-computer',
6044
+ });
6045
+ }
6046
+
6047
+ async function runComputerCommand(flags, env = process.env) {
6048
+ const subcommand = String(flags._?.[0] || 'help').trim();
6049
+ if (subcommand === 'help' || flags.help) {
6050
+ process.stdout.write(renderComputerHelp(subcommand === 'help' ? flags._?.[1] : subcommand));
6051
+ return;
6052
+ }
6053
+ switch (subcommand) {
6054
+ case 'login':
6055
+ case 'attach':
6056
+ case 'setup':
6057
+ await runComputerSetup(flags, env);
6058
+ break;
6059
+ case 'adopt-legacy':
6060
+ throw new Error('MagClaw legacy adoption is handled by `magclaw-computer setup /<serverSlug>` or `magclaw connect --pair-token <token>`.');
6061
+ case 'detach':
6062
+ printJson(await detachComputerProfile(flags, env));
6063
+ break;
6064
+ case 'status': {
6065
+ const report = await computerStatus(flags, env);
6066
+ if (flags.json) printJson(report);
6067
+ else process.stdout.write(formatComputerStatus(report));
6068
+ break;
6069
+ }
6070
+ case 'start':
6071
+ if (hasComputerTarget(flags)) {
6072
+ if (flags.foreground) {
6073
+ await runForegroundDaemon(await buildConfig({ ...flags, profile: computerTargetProfile(flags) }, env), env);
6074
+ } else {
6075
+ printJson(await startSavedBackground({ ...flags, profile: computerTargetProfile(flags) }, env));
6076
+ }
6077
+ } else {
6078
+ printJson(await startAllComputerProfiles(env));
6079
+ }
6080
+ break;
6081
+ case 'stop':
6082
+ if (hasComputerTarget(flags)) printJson(await stopDaemon(computerTargetProfile(flags), env, { disable: Boolean(flags.disable) }));
6083
+ else printJson(await stopAllComputerProfiles(flags, env));
6084
+ break;
6085
+ case 'doctor': {
6086
+ const report = await computerDoctor(flags, env);
6087
+ if (flags.json) printJson(report);
6088
+ else process.stdout.write(`${report.checks.map((check) => `${check.ok ? 'ok' : 'fail'} ${check.name}: ${check.detail}`).join('\n')}\n`);
6089
+ break;
6090
+ }
6091
+ case 'logs':
6092
+ await logs(computerTargetProfile(flags), { lines: flags.lines || flags.lineCount });
6093
+ break;
6094
+ case 'runners':
6095
+ printJson(await computerRunners(flags, env));
6096
+ break;
6097
+ case 'channel':
6098
+ printJson(await computerChannel(flags, env));
6099
+ break;
6100
+ case 'upgrade':
6101
+ await computerUpgrade(flags, env);
6102
+ break;
6103
+ default:
6104
+ throw new Error(`Unknown computer command: ${subcommand}`);
6105
+ }
6106
+ }
6107
+
5386
6108
  async function buildConfig(flags, env = process.env) {
5387
6109
  const diskConfig = await readProfile(flags.profile, env);
5388
6110
  const profile = flags.profile || diskConfig.profile || DEFAULT_PROFILE;
@@ -5405,7 +6127,7 @@ async function buildConfig(flags, env = process.env) {
5405
6127
 
5406
6128
  async function runForegroundDaemon(config, env = process.env) {
5407
6129
  const releaseLock = await acquireDaemonLock(config.profile, config, env);
5408
- await markForegroundServiceState(config.profile, env);
6130
+ await markDaemonRunServiceState(config.profile, env);
5409
6131
  const daemon = new MagClawDaemon(config, env);
5410
6132
  let forceExitTimer = null;
5411
6133
  const shutdown = (signal) => {
@@ -5482,6 +6204,14 @@ function requireExplicitProfile(command, flags = {}) {
5482
6204
 
5483
6205
  export async function main(argv = process.argv, env = process.env) {
5484
6206
  const { command, flags } = parseCli(argv);
6207
+ if (flags.version) {
6208
+ process.stdout.write(`${DAEMON_VERSION}\n`);
6209
+ return;
6210
+ }
6211
+ if (command === 'computer' && flags.help) {
6212
+ process.stdout.write(renderComputerHelp(flags._?.[0] || ''));
6213
+ return;
6214
+ }
5485
6215
  if (command === 'help' || flags.help) {
5486
6216
  process.stdout.write(renderHelp());
5487
6217
  return;
@@ -5491,7 +6221,7 @@ export async function main(argv = process.argv, env = process.env) {
5491
6221
  await runConnect(flags, env);
5492
6222
  break;
5493
6223
  case 'computer':
5494
- await runComputerSetup(flags, env);
6224
+ await runComputerCommand(flags, env);
5495
6225
  break;
5496
6226
  case 'start': {
5497
6227
  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 = {} } = {}) {