@magclaw/cli-core 0.1.22 → 0.1.24

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 +282 -23
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magclaw/cli-core",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
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
@@ -39,6 +39,10 @@ const PACKAGE_JSON = (() => {
39
39
  }
40
40
  })();
41
41
  export const DAEMON_VERSION = String(PACKAGE_JSON.version || '0.0.0');
42
+ export const CLI_CORE_VERSION = DAEMON_VERSION;
43
+ const DAEMON_PACKAGE_NAME = '@magclaw/daemon';
44
+ const COMPUTER_PACKAGE_NAME = '@magclaw/computer';
45
+ const KNOWN_ENTRY_PACKAGE_NAMES = new Set([DAEMON_PACKAGE_NAME, COMPUTER_PACKAGE_NAME]);
42
46
  const SOURCE_CODEX_HOME = path.resolve(process.env.MAGCLAW_CODEX_HOME_SOURCE || process.env.CODEX_HOME || path.join(os.homedir(), '.codex'));
43
47
  const CODEX_HOME_SHARED_ENTRIES = ['auth.json', 'plugins', 'vendor_imports'];
44
48
  export const CAPABILITIES = [
@@ -48,6 +52,7 @@ export const CAPABILITIES = [
48
52
  'agent:stop',
49
53
  'agent:skills:list',
50
54
  'daemon:upgrade',
55
+ 'daemon:close',
51
56
  'daemon:release_notice',
52
57
  'machine:runtime_models:detect',
53
58
  ];
@@ -56,6 +61,71 @@ function now() {
56
61
  return new Date().toISOString();
57
62
  }
58
63
 
64
+ function packageInfoFromSpec(packageSpec = '') {
65
+ const match = String(packageSpec || '').trim().match(/^(@magclaw\/(?:daemon|computer))(?:@(.+))?$/);
66
+ return {
67
+ name: match?.[1] || '',
68
+ version: match?.[2] || '',
69
+ };
70
+ }
71
+
72
+ function normalizeEntryPackageName(value = '', fallback = DAEMON_PACKAGE_NAME) {
73
+ const clean = String(value || '').trim();
74
+ if (KNOWN_ENTRY_PACKAGE_NAMES.has(clean)) return clean;
75
+ return fallback;
76
+ }
77
+
78
+ function packageKindForPackageName(packageName = '') {
79
+ return normalizeEntryPackageName(packageName) === COMPUTER_PACKAGE_NAME ? 'computer' : 'daemon';
80
+ }
81
+
82
+ function packageBinForPackageName(packageName = '') {
83
+ return packageKindForPackageName(packageName) === 'computer' ? 'magclaw-computer' : 'magclaw';
84
+ }
85
+
86
+ function packageSpecForPackageName(packageName = DAEMON_PACKAGE_NAME, version = 'latest') {
87
+ const name = normalizeEntryPackageName(packageName);
88
+ const cleanVersion = String(version || '').trim() || 'latest';
89
+ return cleanVersion === 'latest' ? `${name}@latest` : `${name}@${cleanVersion}`;
90
+ }
91
+
92
+ function runtimePackageInfo(env = process.env, service = {}) {
93
+ const envSpec = String(env.MAGCLAW_DAEMON_PACKAGE_SPEC || '').trim();
94
+ const serviceSpec = String(service.packageSpec || '').trim();
95
+ const parsed = packageInfoFromSpec(envSpec || serviceSpec);
96
+ const packageName = normalizeEntryPackageName(
97
+ env.MAGCLAW_ENTRY_PACKAGE_NAME
98
+ || env.MAGCLAW_DAEMON_PACKAGE_NAME
99
+ || service.packageName
100
+ || parsed.name,
101
+ );
102
+ const packageVersion = String(
103
+ env.MAGCLAW_ENTRY_PACKAGE_VERSION
104
+ || env.MAGCLAW_DAEMON_PACKAGE_VERSION
105
+ || service.packageVersion
106
+ || parsed.version
107
+ || DAEMON_VERSION,
108
+ ).trim();
109
+ const packageKind = String(
110
+ env.MAGCLAW_DAEMON_PACKAGE_KIND
111
+ || service.packageKind
112
+ || packageKindForPackageName(packageName),
113
+ ).trim().toLowerCase() === 'computer' ? 'computer' : 'daemon';
114
+ const packageBin = String(
115
+ env.MAGCLAW_DAEMON_PACKAGE_BIN
116
+ || service.packageBin
117
+ || packageBinForPackageName(packageName),
118
+ ).trim() || packageBinForPackageName(packageName);
119
+ const packageSpec = envSpec || serviceSpec || packageSpecForPackageName(packageName, packageVersion || 'latest');
120
+ return {
121
+ name: packageName,
122
+ version: packageVersion,
123
+ kind: packageKind,
124
+ bin: packageBin,
125
+ spec: packageSpec,
126
+ };
127
+ }
128
+
59
129
  function localTimestamp(date = new Date()) {
60
130
  const pad = (value) => String(value).padStart(2, '0');
61
131
  return [
@@ -516,6 +586,7 @@ function renderHelp() {
516
586
  ' --server-url <url> MagClaw Cloud URL',
517
587
  ' --api-key <key> Machine API key for connect',
518
588
  ' --background Install and run as a background service',
589
+ ' --disable With stop: suppress background relaunch until next start',
519
590
  ' --bin-dir <path> install-cli target directory',
520
591
  ' --to <version> Upgrade target version (default: latest)',
521
592
  ' --wait-cloud Wait for Cloud heartbeat during manual upgrade',
@@ -584,17 +655,25 @@ async function saveProfile(profile, config, env = process.env) {
584
655
  async function readServiceState(profile = DEFAULT_PROFILE, env = process.env) {
585
656
  const paths = profilePaths(profile, env);
586
657
  const state = await readJsonFile(paths.service, {});
658
+ const parsed = packageInfoFromSpec(state.packageSpec || '');
659
+ const packageName = normalizeEntryPackageName(state.packageName || parsed.name || DAEMON_PACKAGE_NAME);
660
+ const packageVersion = String(state.packageVersion || parsed.version || state.installedPackageVersion || state.installedDaemonVersion || '').trim();
587
661
  return {
662
+ ...state,
588
663
  version: 1,
589
664
  profile: paths.profile,
590
665
  mode: state.mode || 'foreground',
591
666
  background: Boolean(state.background),
592
667
  launcher: state.launcher || '',
593
668
  packageSpec: state.packageSpec || '',
669
+ packageName,
670
+ packageVersion,
671
+ packageKind: String(state.packageKind || packageKindForPackageName(packageName)).toLowerCase() === 'computer' ? 'computer' : 'daemon',
672
+ packageBin: state.packageBin || packageBinForPackageName(packageName),
594
673
  previousPackageSpec: state.previousPackageSpec || '',
595
674
  installedDaemonVersion: state.installedDaemonVersion || DAEMON_VERSION,
675
+ installedPackageVersion: state.installedPackageVersion || packageVersion || state.installedDaemonVersion || DAEMON_VERSION,
596
676
  updatedAt: state.updatedAt || '',
597
- ...state,
598
677
  };
599
678
  }
600
679
 
@@ -3232,6 +3311,9 @@ class MagClawDaemon {
3232
3311
  this.upgradeWorkerStarting = true;
3233
3312
  const targetVersion = String(message.targetVersion || message.version || 'latest').trim() || 'latest';
3234
3313
  const previousVersion = String(message.previousVersion || DAEMON_VERSION).trim() || DAEMON_VERSION;
3314
+ const packageName = normalizeEntryPackageName(message.packageName || packageInfoFromSpec(message.packageSpec || '').name || this.env.MAGCLAW_ENTRY_PACKAGE_NAME || this.env.MAGCLAW_DAEMON_PACKAGE_NAME);
3315
+ const packageKind = packageKindForPackageName(packageName);
3316
+ const packageBin = String(message.packageBin || packageBinForPackageName(packageName)).trim() || packageBinForPackageName(packageName);
3235
3317
  const service = await readServiceState(this.paths.profile, this.env);
3236
3318
  const activeService = backgroundServiceStatus(this.paths.profile, this.env);
3237
3319
  if (!service.background || !activeService.active) {
@@ -3245,6 +3327,10 @@ class MagClawDaemon {
3245
3327
  message: error,
3246
3328
  previousVersion,
3247
3329
  targetVersion,
3330
+ packageName,
3331
+ packageKind,
3332
+ packageBin,
3333
+ packageSpec: message.packageSpec || packageSpecForPackageName(packageName, targetVersion),
3248
3334
  error,
3249
3335
  service: activeService,
3250
3336
  }, this.env);
@@ -3257,6 +3343,9 @@ class MagClawDaemon {
3257
3343
  message: error,
3258
3344
  previousVersion,
3259
3345
  targetVersion,
3346
+ packageName,
3347
+ packageKind,
3348
+ packageBin,
3260
3349
  });
3261
3350
  this.upgradeWorkerStarting = false;
3262
3351
  return false;
@@ -3270,7 +3359,10 @@ class MagClawDaemon {
3270
3359
  message: 'Upgrade worker is starting.',
3271
3360
  previousVersion,
3272
3361
  targetVersion,
3273
- packageSpec: message.packageSpec || '',
3362
+ packageName,
3363
+ packageKind,
3364
+ packageBin,
3365
+ packageSpec: message.packageSpec || packageSpecForPackageName(packageName, targetVersion),
3274
3366
  startedAt: now(),
3275
3367
  }, this.env);
3276
3368
  this.send({
@@ -3281,6 +3373,9 @@ class MagClawDaemon {
3281
3373
  progress: 1,
3282
3374
  previousVersion,
3283
3375
  targetVersion,
3376
+ packageName,
3377
+ packageKind,
3378
+ packageBin,
3284
3379
  message: 'Upgrade worker is starting.',
3285
3380
  });
3286
3381
  const args = [
@@ -3296,6 +3391,8 @@ class MagClawDaemon {
3296
3391
  previousVersion,
3297
3392
  ];
3298
3393
  if (message.packageSpec) args.push('--package-spec', String(message.packageSpec));
3394
+ args.push('--package-name', packageName);
3395
+ args.push('--package-bin', packageBin);
3299
3396
  const child = spawn(process.execPath, args, {
3300
3397
  cwd: process.cwd(),
3301
3398
  detached: true,
@@ -3315,6 +3412,9 @@ class MagClawDaemon {
3315
3412
  const commandId = String(message.commandId || '').trim();
3316
3413
  const targetVersion = String(message.targetVersion || message.version || 'latest').trim() || 'latest';
3317
3414
  const previousVersion = String(message.previousVersion || DAEMON_VERSION).trim() || DAEMON_VERSION;
3415
+ const packageName = normalizeEntryPackageName(message.packageName || packageInfoFromSpec(message.packageSpec || '').name || this.env.MAGCLAW_ENTRY_PACKAGE_NAME || this.env.MAGCLAW_DAEMON_PACKAGE_NAME);
3416
+ const packageKind = packageKindForPackageName(packageName);
3417
+ const packageBin = String(message.packageBin || packageBinForPackageName(packageName)).trim() || packageBinForPackageName(packageName);
3318
3418
  if (!commandId) {
3319
3419
  this.send({ type: 'daemon:upgrade:ack', status: 'failed', error: 'Missing commandId.' });
3320
3420
  return;
@@ -3327,7 +3427,10 @@ class MagClawDaemon {
3327
3427
  message: this.daemonIsIdleForUpgrade() ? 'Daemon accepted upgrade command.' : 'Waiting for all Agent work to become idle.',
3328
3428
  previousVersion,
3329
3429
  targetVersion,
3330
- packageSpec: message.packageSpec || '',
3430
+ packageName,
3431
+ packageKind,
3432
+ packageBin,
3433
+ packageSpec: message.packageSpec || packageSpecForPackageName(packageName, targetVersion),
3331
3434
  requestedAt: now(),
3332
3435
  }, this.env);
3333
3436
  if (!this.daemonIsIdleForUpgrade()) {
@@ -3340,6 +3443,9 @@ class MagClawDaemon {
3340
3443
  progress: 0,
3341
3444
  previousVersion,
3342
3445
  targetVersion,
3446
+ packageName,
3447
+ packageKind,
3448
+ packageBin,
3343
3449
  message: 'Waiting for all Agent work to become idle.',
3344
3450
  });
3345
3451
  this.scheduleUpgradeIdleCheck();
@@ -3348,12 +3454,48 @@ class MagClawDaemon {
3348
3454
  await this.startUpgradeWorker(message);
3349
3455
  }
3350
3456
 
3457
+ async handleDaemonClose(message) {
3458
+ const commandId = String(message.commandId || '').trim();
3459
+ const service = await readServiceState(this.paths.profile, this.env);
3460
+ const serviceStatus = backgroundServiceStatus(this.paths.profile, this.env);
3461
+ const packageInfo = runtimePackageInfo(this.env, service);
3462
+ 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 || '',
3469
+ packageName: packageInfo.name,
3470
+ packageVersion: packageInfo.version,
3471
+ packageKind: packageInfo.kind,
3472
+ packageSpec: packageInfo.spec,
3473
+ packageBin: packageInfo.bin,
3474
+ };
3475
+ logWarning('daemon', `Remote close requested (${message.reason || 'closed_from_cloud'}).`);
3476
+ for (const session of this.sessions.values()) session.stop();
3477
+ this.sessions.clear();
3478
+ this.send({ type: 'daemon:close:ack',
3479
+ commandId,
3480
+ status: 'stopping',
3481
+ reason: message.reason || 'closed_from_cloud',
3482
+ service: runMode,
3483
+ at: now(),
3484
+ });
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)}.`);
3487
+ this.close();
3488
+ process.exitCode = 0;
3489
+ setTimeout(() => process.exit(0), 50).unref?.();
3490
+ }
3491
+
3351
3492
  async readyPayload() {
3352
3493
  const runtimes = await detectRuntimes(this.env);
3353
3494
  const owner = await ensureMachineFingerprint(this.paths.profile, this.env);
3354
3495
  const service = await readServiceState(this.paths.profile, this.env);
3355
3496
  const serviceStatus = backgroundServiceStatus(this.paths.profile, this.env);
3356
3497
  const upgrade = await readUpgradeHandoff(this.paths.profile, this.env);
3498
+ const packageInfo = runtimePackageInfo(this.env, service);
3357
3499
  return {
3358
3500
  type: 'ready',
3359
3501
  computerId: this.config.computerId || null,
@@ -3363,7 +3505,13 @@ class MagClawDaemon {
3363
3505
  hostname: os.hostname(),
3364
3506
  os: `${os.platform()} ${os.release()}`,
3365
3507
  arch: os.arch(),
3366
- daemonVersion: DAEMON_VERSION,
3508
+ daemonVersion: packageInfo.version || DAEMON_VERSION,
3509
+ packageName: packageInfo.name,
3510
+ packageVersion: packageInfo.version,
3511
+ packageKind: packageInfo.kind,
3512
+ packageSpec: packageInfo.spec,
3513
+ packageBin: packageInfo.bin,
3514
+ cliCoreVersion: CLI_CORE_VERSION,
3367
3515
  service: {
3368
3516
  mode: service.mode || serviceStatus.mode || 'foreground',
3369
3517
  background: Boolean(service.background),
@@ -3372,7 +3520,12 @@ class MagClawDaemon {
3372
3520
  serviceName: serviceStatus.serviceName || '',
3373
3521
  taskName: serviceStatus.taskName || '',
3374
3522
  launcher: service.launcher || '',
3375
- packageSpec: service.packageSpec || '',
3523
+ packageSpec: service.packageSpec || packageInfo.spec || '',
3524
+ packageName: service.packageName || packageInfo.name,
3525
+ packageVersion: service.packageVersion || packageInfo.version,
3526
+ packageKind: service.packageKind || packageInfo.kind,
3527
+ packageBin: service.packageBin || packageInfo.bin,
3528
+ cliCoreVersion: CLI_CORE_VERSION,
3376
3529
  },
3377
3530
  upgrade: upgrade || null,
3378
3531
  runtimes: runtimes.filter((runtime) => runtime.installed).map((runtime) => runtime.id),
@@ -3392,11 +3545,18 @@ class MagClawDaemon {
3392
3545
  }
3393
3546
 
3394
3547
  sendHeartbeat() {
3548
+ const packageInfo = runtimePackageInfo(this.env);
3395
3549
  const sent = this.send({
3396
3550
  type: 'heartbeat',
3397
3551
  time: now(),
3398
3552
  computerId: this.config.computerId || null,
3399
- daemonVersion: DAEMON_VERSION,
3553
+ daemonVersion: packageInfo.version || DAEMON_VERSION,
3554
+ packageName: packageInfo.name,
3555
+ packageVersion: packageInfo.version,
3556
+ packageKind: packageInfo.kind,
3557
+ packageSpec: packageInfo.spec,
3558
+ packageBin: packageInfo.bin,
3559
+ cliCoreVersion: CLI_CORE_VERSION,
3400
3560
  runningAgents: [...this.sessions.keys()],
3401
3561
  });
3402
3562
  logInfo('daemon', `Sent heartbeat (runningAgents=${this.sessions.size}, sent=${sent}).`);
@@ -3534,6 +3694,9 @@ class MagClawDaemon {
3534
3694
  case 'daemon:upgrade':
3535
3695
  await this.handleDaemonUpgrade(message);
3536
3696
  break;
3697
+ case 'daemon:close':
3698
+ await this.handleDaemonClose(message);
3699
+ break;
3537
3700
  case 'agent:start':
3538
3701
  await this.handleAgentStart(message);
3539
3702
  break;
@@ -3873,7 +4036,9 @@ class MagClawDaemon {
3873
4036
  const requestModule = url.protocol === 'wss:' ? https : http;
3874
4037
  const requestUrl = new URL(url.href.replace(/^ws/, 'http'));
3875
4038
  const key = crypto.randomBytes(16).toString('base64');
3876
- logInfo('daemon', `Connecting MagClaw daemon profile "${this.paths.profile}" to ${this.config.serverUrl}...`);
4039
+ const packageInfo = runtimePackageInfo(this.env);
4040
+ logInfo('daemon', `Connecting MagClaw daemon v${packageInfo.version || DAEMON_VERSION} profile "${this.paths.profile}" to ${this.config.serverUrl}...`);
4041
+ if (this.closed) return;
3877
4042
  return new Promise((resolve, reject) => {
3878
4043
  let settled = false;
3879
4044
  const finish = (callback, value) => {
@@ -4020,13 +4185,38 @@ async function writeLauncher(profile, env = process.env) {
4020
4185
  const launcher = path.join(paths.runDir, 'launcher.js');
4021
4186
  const fallbackBin = executablePath();
4022
4187
  const previousService = await readServiceState(paths.profile, env);
4188
+ const preferPersistedPackage = Boolean(previousService.pendingCommandId);
4189
+ const envPackageInfo = runtimePackageInfo(env, {});
4190
+ const persistedPackageInfo = runtimePackageInfo({}, previousService);
4191
+ const packageInfo = preferPersistedPackage ? persistedPackageInfo : runtimePackageInfo(env, previousService);
4192
+ const packageSpec = preferPersistedPackage
4193
+ ? (previousService.packageSpec || persistedPackageInfo.spec)
4194
+ : (env.MAGCLAW_DAEMON_PACKAGE_SPEC || previousService.packageSpec || packageInfo.spec || packageSpecForPackageName(packageInfo.name, 'latest'));
4195
+ const packageName = normalizeEntryPackageName(
4196
+ packageInfoFromSpec(packageSpec).name
4197
+ || packageInfo.name
4198
+ || envPackageInfo.name
4199
+ || previousService.packageName,
4200
+ );
4201
+ const packageVersion = String(packageInfoFromSpec(packageSpec).version || packageInfo.version || previousService.packageVersion || '').trim();
4202
+ const packageKind = packageKindForPackageName(packageName);
4203
+ const packageBin = String(
4204
+ (preferPersistedPackage ? previousService.packageBin : env.MAGCLAW_DAEMON_PACKAGE_BIN)
4205
+ || previousService.packageBin
4206
+ || packageBinForPackageName(packageName),
4207
+ ).trim() || packageBinForPackageName(packageName);
4023
4208
  const service = await writeServiceState(paths.profile, {
4024
4209
  mode: process.platform === 'darwin' ? 'launchd' : process.platform === 'linux' ? 'systemd' : process.platform === 'win32' ? 'schtasks' : 'foreground',
4025
4210
  background: true,
4026
4211
  launcher,
4027
- packageSpec: env.MAGCLAW_DAEMON_PACKAGE_SPEC || previousService.packageSpec || '@magclaw/daemon@latest',
4212
+ packageSpec,
4213
+ packageName,
4214
+ packageVersion,
4215
+ packageKind,
4216
+ packageBin,
4028
4217
  previousPackageSpec: previousService.previousPackageSpec || '',
4029
- installedDaemonVersion: DAEMON_VERSION,
4218
+ installedDaemonVersion: packageVersion || DAEMON_VERSION,
4219
+ installedPackageVersion: packageVersion || DAEMON_VERSION,
4030
4220
  commandMode: useNpmLauncher ? 'npm' : 'local',
4031
4221
  }, env);
4032
4222
  const code = [
@@ -4045,14 +4235,31 @@ async function writeLauncher(profile, env = process.env) {
4045
4235
  "let service = {};",
4046
4236
  "try { service = JSON.parse(fs.readFileSync(serviceFile, 'utf8')); } catch {}",
4047
4237
  "const packageSpec = String(service.packageSpec || defaultPackageSpec || '@magclaw/daemon@latest');",
4238
+ "const packageName = String(service.packageName || (packageSpec.startsWith('@magclaw/computer@') || packageSpec === '@magclaw/computer' ? '@magclaw/computer' : '@magclaw/daemon'));",
4239
+ "const packageKind = String(service.packageKind || (packageName === '@magclaw/computer' ? 'computer' : 'daemon'));",
4240
+ "const packageBin = String(service.packageBin || (packageKind === 'computer' ? 'magclaw-computer' : 'magclaw'));",
4241
+ "const packageVersionMatch = packageSpec.match(/^@magclaw\\/(?:daemon|computer)@(.+)$/);",
4242
+ "const packageVersion = String(service.packageVersion || (packageVersionMatch ? packageVersionMatch[1] : ''));",
4048
4243
  'const command = useNpmLauncher ? npmPath : process.execPath;',
4049
4244
  "const args = useNpmLauncher",
4050
- " ? ['exec', '--yes', '--package', packageSpec, '--', 'magclaw', 'connect', '--profile', profile]",
4245
+ " ? ['exec', '--yes', '--package', packageSpec, '--', packageBin, 'connect', '--profile', profile]",
4051
4246
  " : [fallbackBin, 'connect', '--profile', profile];",
4052
4247
  "const launchPath = [nodeDir, npmDir, process.env.PATH || '/usr/bin:/bin:/usr/sbin:/sbin'].filter(Boolean).join(':');",
4248
+ "const childEnv = {",
4249
+ " ...process.env,",
4250
+ " MAGCLAW_DAEMON_HOME: daemonHome,",
4251
+ " MAGCLAW_ENTRY_PACKAGE_NAME: packageName,",
4252
+ " MAGCLAW_ENTRY_PACKAGE_VERSION: packageVersion,",
4253
+ " MAGCLAW_DAEMON_PACKAGE_NAME: packageName,",
4254
+ " MAGCLAW_DAEMON_PACKAGE_SPEC: packageSpec,",
4255
+ " MAGCLAW_DAEMON_PACKAGE_KIND: packageKind,",
4256
+ " MAGCLAW_DAEMON_PACKAGE_BIN: packageBin,",
4257
+ " PATH: launchPath,",
4258
+ "};",
4259
+ "if (packageKind === 'computer') childEnv.MAGCLAW_COMPUTER_DAEMON = '1';",
4053
4260
  'const child = spawn(command, args, {',
4054
4261
  " stdio: 'inherit',",
4055
- ' env: { ...process.env, MAGCLAW_DAEMON_HOME: daemonHome, PATH: launchPath },',
4262
+ ' env: childEnv,',
4056
4263
  '});',
4057
4264
  "child.on('exit', (code, signal) => {",
4058
4265
  ' if (signal) process.kill(process.pid, signal);',
@@ -4120,6 +4327,7 @@ async function startMacBackground(profile, env = process.env) {
4120
4327
  ].join('\n');
4121
4328
  await writeFile(plist, xml);
4122
4329
  spawnSync('launchctl', ['bootout', `gui/${process.getuid()}`, plist], { stdio: 'ignore' });
4330
+ spawnSync('launchctl', ['enable', `gui/${process.getuid()}/${label}`], { stdio: 'ignore' });
4123
4331
  const boot = spawnSync('launchctl', ['bootstrap', `gui/${process.getuid()}`, plist], { encoding: 'utf8' });
4124
4332
  if (boot.status !== 0) {
4125
4333
  const load = spawnSync('launchctl', ['load', plist], { encoding: 'utf8' });
@@ -4286,32 +4494,49 @@ async function stopActiveDaemon(profile, env = process.env) {
4286
4494
  return { ok: stopped, running: !stopped, pid, signal };
4287
4495
  }
4288
4496
 
4289
- function stopBackground(profile, env = process.env) {
4497
+ function stopBackground(profile, env = process.env, options = {}) {
4290
4498
  if (process.platform === 'darwin') {
4291
4499
  const paths = profilePaths(profile, env);
4292
4500
  const label = launchAgentLabel(paths.profile);
4293
4501
  const plist = path.join(os.homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
4502
+ if (options.disable) {
4503
+ const disabled = spawnSync('launchctl', ['disable', `gui/${process.getuid()}/${label}`], { encoding: 'utf8' });
4504
+ const stopped = spawnSync('launchctl', ['stop', label], { encoding: 'utf8' });
4505
+ if (stopped.status !== 0) spawnSync('launchctl', ['bootout', `gui/${process.getuid()}`, plist], { stdio: 'ignore' });
4506
+ return {
4507
+ ok: disabled.status === 0 || stopped.status === 0,
4508
+ mode: 'launchd',
4509
+ label,
4510
+ file: plist,
4511
+ 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(),
4514
+ };
4515
+ }
4294
4516
  spawnSync('launchctl', ['bootout', `gui/${process.getuid()}`, plist], { stdio: 'ignore' });
4295
4517
  return { ok: true, mode: 'launchd', label, file: plist };
4296
4518
  }
4297
4519
  if (process.platform === 'linux') {
4298
4520
  const paths = profilePaths(profile, env);
4299
4521
  const serviceName = systemdServiceName(paths.profile);
4300
- const result = spawnSync('systemctl', ['--user', 'disable', '--now', serviceName], { encoding: 'utf8' });
4522
+ const result = options.disable
4523
+ ? spawnSync('systemctl', ['--user', 'disable', '--now', serviceName], { encoding: 'utf8' })
4524
+ : spawnSync('systemctl', ['--user', 'stop', serviceName], { encoding: 'utf8' });
4301
4525
  return { ok: result.status === 0, mode: 'systemd', serviceName, error: result.stderr || '' };
4302
4526
  }
4303
4527
  if (process.platform === 'win32') {
4304
4528
  const paths = profilePaths(profile, env);
4305
4529
  const taskName = windowsTaskName(paths.profile);
4306
4530
  spawnSync('schtasks.exe', ['/End', '/TN', taskName], { stdio: 'ignore' });
4531
+ if (!options.disable) return { ok: true, mode: 'schtasks', taskName };
4307
4532
  const result = spawnSync('schtasks.exe', ['/Change', '/TN', taskName, '/DISABLE'], { encoding: 'utf8' });
4308
4533
  return { ok: result.status === 0, mode: 'schtasks', taskName, error: result.stderr || result.stdout || '' };
4309
4534
  }
4310
4535
  return { ok: false, mode: 'foreground' };
4311
4536
  }
4312
4537
 
4313
- async function stopDaemon(profile, env = process.env) {
4314
- const background = stopBackground(profile, env);
4538
+ async function stopDaemon(profile, env = process.env, options = {}) {
4539
+ const background = stopBackground(profile, env, options);
4315
4540
  const processResult = await stopActiveDaemon(profile, env);
4316
4541
  const backgroundRequired = background.mode !== 'foreground';
4317
4542
  return {
@@ -4573,13 +4798,24 @@ async function openUpgradeProgressSocket(url) {
4573
4798
  function packageSpecForUpgrade(targetVersion, flags = {}, env = process.env) {
4574
4799
  const explicit = String(flags.packageSpec || env.MAGCLAW_DAEMON_UPGRADE_PACKAGE_SPEC || '').trim();
4575
4800
  if (explicit) return explicit;
4801
+ const packageName = normalizeEntryPackageName(
4802
+ flags.packageName
4803
+ || flags.package
4804
+ || env.MAGCLAW_DAEMON_UPGRADE_PACKAGE_NAME
4805
+ || env.MAGCLAW_ENTRY_PACKAGE_NAME
4806
+ || env.MAGCLAW_DAEMON_PACKAGE_NAME
4807
+ || packageInfoFromSpec(env.MAGCLAW_DAEMON_PACKAGE_SPEC || '').name,
4808
+ );
4576
4809
  const target = String(targetVersion || '').trim();
4577
- return target && target !== 'latest' ? `@magclaw/daemon@${target}` : '@magclaw/daemon@latest';
4810
+ return packageSpecForPackageName(packageName, target && target !== 'latest' ? target : 'latest');
4578
4811
  }
4579
4812
 
4580
4813
  function npmPackageLooksRemote(packageSpec) {
4581
4814
  const value = String(packageSpec || '').trim();
4582
- return value.startsWith('@magclaw/daemon@') || value === '@magclaw/daemon';
4815
+ return value.startsWith('@magclaw/daemon@')
4816
+ || value === '@magclaw/daemon'
4817
+ || value.startsWith('@magclaw/computer@')
4818
+ || value === '@magclaw/computer';
4583
4819
  }
4584
4820
 
4585
4821
  function preflightPackage(packageSpec, env = process.env) {
@@ -4642,12 +4878,15 @@ async function runUpgradeWorker(flags, env = process.env) {
4642
4878
  const targetVersion = String(flags.targetVersion || flags.version || flags.to || flags.tag || 'latest').trim() || 'latest';
4643
4879
  const previousVersion = String(flags.previousVersion || DAEMON_VERSION).trim() || DAEMON_VERSION;
4644
4880
  const packageSpec = packageSpecForUpgrade(targetVersion, flags, env);
4881
+ const packageName = normalizeEntryPackageName(packageInfoFromSpec(packageSpec).name || flags.packageName || env.MAGCLAW_ENTRY_PACKAGE_NAME || env.MAGCLAW_DAEMON_PACKAGE_NAME);
4882
+ const packageKind = packageKindForPackageName(packageName);
4883
+ const packageBin = String(flags.packageBin || env.MAGCLAW_DAEMON_UPGRADE_PACKAGE_BIN || packageBinForPackageName(packageName)).trim() || packageBinForPackageName(packageName);
4645
4884
  const progressIntervalMs = Math.max(100, Math.min(5000, Number(flags.progressIntervalMs || env.MAGCLAW_DAEMON_UPGRADE_PROGRESS_MS || 500) || 500));
4646
4885
  const readyTimeoutMs = Math.max(5000, Math.min(10 * 60_000, Number(flags.readyTimeoutMs || env.MAGCLAW_DAEMON_UPGRADE_READY_TIMEOUT_MS || 120_000) || 120_000));
4647
4886
  const localOnly = Boolean(flags.localOnly || flags.local || flags.noWaitCloud);
4648
4887
  const assumeReady = Boolean(flags.assumeReady || env.MAGCLAW_DAEMON_UPGRADE_ASSUME_READY === '1');
4649
4888
  const serviceBefore = await readServiceState(profile, env);
4650
- const previousPackageSpec = serviceBefore.packageSpec || `@magclaw/daemon@${previousVersion}`;
4889
+ const previousPackageSpec = serviceBefore.packageSpec || packageSpecForPackageName(packageName, previousVersion);
4651
4890
  const dryRunPlan = {
4652
4891
  ok: true,
4653
4892
  dryRun: Boolean(flags.dryRun),
@@ -4657,6 +4896,9 @@ async function runUpgradeWorker(flags, env = process.env) {
4657
4896
  previousVersion,
4658
4897
  targetVersion,
4659
4898
  packageSpec,
4899
+ packageName,
4900
+ packageKind,
4901
+ packageBin,
4660
4902
  previousPackageSpec,
4661
4903
  service: serviceBefore,
4662
4904
  localOnly,
@@ -4731,6 +4973,10 @@ async function runUpgradeWorker(flags, env = process.env) {
4731
4973
  mode: serviceBefore.mode || (process.platform === 'darwin' ? 'launchd' : process.platform === 'linux' ? 'systemd' : process.platform === 'win32' ? 'schtasks' : 'foreground'),
4732
4974
  background: true,
4733
4975
  packageSpec,
4976
+ packageName,
4977
+ packageVersion: targetVersion === 'latest' ? '' : targetVersion,
4978
+ packageKind,
4979
+ packageBin,
4734
4980
  previousPackageSpec,
4735
4981
  pendingCommandId: commandId,
4736
4982
  pendingTargetVersion: targetVersion,
@@ -4749,19 +4995,19 @@ async function runUpgradeWorker(flags, env = process.env) {
4749
4995
  const complete = progressSocket ? await progressSocket.waitForComplete(readyTimeoutMs) : null;
4750
4996
  if (complete?.status === 'succeeded') {
4751
4997
  await emitProgress({ status: 'succeeded', phase: 'ready', progress: 100, message: 'Daemon upgrade completed.' });
4752
- await writeServiceState(profile, { installedDaemonVersion: targetVersion, pendingCommandId: '', pendingTargetVersion: '' }, env);
4998
+ await writeServiceState(profile, { installedDaemonVersion: targetVersion, installedPackageVersion: targetVersion, packageVersion: targetVersion, pendingCommandId: '', pendingTargetVersion: '' }, env);
4753
4999
  return { ok: true, commandId, targetVersion, packageSpec };
4754
5000
  }
4755
5001
  if (localOnly) {
4756
5002
  const ready = await waitForLocalDaemonReady(profile, readyTimeoutMs, env);
4757
5003
  if (!ready.ok) throw new Error(ready.error || 'Timed out waiting for the local daemon to become ready.');
4758
5004
  await emitProgress({ status: 'succeeded', phase: 'ready', progress: 100, message: 'Daemon upgrade completed locally.' });
4759
- await writeServiceState(profile, { installedDaemonVersion: targetVersion, pendingCommandId: '', pendingTargetVersion: '' }, env);
5005
+ await writeServiceState(profile, { installedDaemonVersion: targetVersion, installedPackageVersion: targetVersion, packageVersion: targetVersion, pendingCommandId: '', pendingTargetVersion: '' }, env);
4760
5006
  return { ok: true, commandId, targetVersion, packageSpec, localReady: ready };
4761
5007
  }
4762
5008
  if (!progressSocket && assumeReady) {
4763
5009
  await emitProgress({ status: 'succeeded', phase: 'ready', progress: 100, message: 'Daemon upgrade completed locally.' });
4764
- await writeServiceState(profile, { installedDaemonVersion: targetVersion, pendingCommandId: '', pendingTargetVersion: '' }, env);
5010
+ await writeServiceState(profile, { installedDaemonVersion: targetVersion, installedPackageVersion: targetVersion, packageVersion: targetVersion, pendingCommandId: '', pendingTargetVersion: '' }, env);
4765
5011
  return { ok: true, commandId, targetVersion, packageSpec, assumedReady: true };
4766
5012
  }
4767
5013
  throw new Error('Timed out waiting for upgraded daemon ready acknowledgement.');
@@ -4774,8 +5020,14 @@ async function runUpgradeWorker(flags, env = process.env) {
4774
5020
  await emitProgress({ status: 'rollback', phase: 'rollback', progress: 82, message: `Rolling back: ${upgradeError}`, error: upgradeError });
4775
5021
  let rollbackError = '';
4776
5022
  try {
5023
+ const previousPackageInfo = packageInfoFromSpec(previousPackageSpec);
5024
+ const rollbackPackageName = normalizeEntryPackageName(previousPackageInfo.name || serviceBefore.packageName || packageName);
4777
5025
  await writeServiceState(profile, {
4778
5026
  packageSpec: previousPackageSpec,
5027
+ packageName: rollbackPackageName,
5028
+ packageVersion: previousPackageInfo.version || serviceBefore.packageVersion || previousVersion,
5029
+ packageKind: packageKindForPackageName(rollbackPackageName),
5030
+ packageBin: serviceBefore.packageBin || packageBinForPackageName(rollbackPackageName),
4779
5031
  previousPackageSpec: packageSpec,
4780
5032
  pendingCommandId: '',
4781
5033
  pendingTargetVersion: '',
@@ -4838,6 +5090,7 @@ async function runComputerSetup(flags, env = process.env) {
4838
5090
  const profile = safeProfileName(flags.profile && flags.profile !== DEFAULT_PROFILE ? flags.profile : serverSlug);
4839
5091
  const owner = await ensureMachineFingerprint(profile, env);
4840
5092
  const displayName = String(flags.displayName || flags.name || os.hostname()).trim();
5093
+ const packageInfo = runtimePackageInfo(env);
4841
5094
  const started = await postSetupJson(serverUrl, '/api/cloud/computer/setup/start', {
4842
5095
  serverSlug,
4843
5096
  machineFingerprint: owner.fingerprint,
@@ -4845,7 +5098,13 @@ async function runComputerSetup(flags, env = process.env) {
4845
5098
  hostname: os.hostname(),
4846
5099
  os: os.platform(),
4847
5100
  arch: os.arch(),
4848
- daemonVersion: DAEMON_VERSION,
5101
+ daemonVersion: packageInfo.version || DAEMON_VERSION,
5102
+ packageName: packageInfo.name,
5103
+ packageVersion: packageInfo.version,
5104
+ packageKind: packageInfo.kind,
5105
+ packageSpec: packageInfo.spec,
5106
+ packageBin: packageInfo.bin,
5107
+ cliCoreVersion: CLI_CORE_VERSION,
4849
5108
  });
4850
5109
  process.stdout.write(`To finish login, open: ${started.verificationUri}\n`);
4851
5110
  process.stdout.write(`and enter the code: ${started.userCode}\n`);
@@ -5012,7 +5271,7 @@ export async function main(argv = process.argv, env = process.env) {
5012
5271
  }
5013
5272
  case 'stop':
5014
5273
  requireExplicitProfile('stop', flags);
5015
- printJson(await stopDaemon(flags.profile, env));
5274
+ printJson(await stopDaemon(flags.profile, env, { disable: Boolean(flags.disable) }));
5016
5275
  break;
5017
5276
  case 'restart':
5018
5277
  printJson(await restartSavedBackground(flags, env));