aicomputer 0.1.15 → 0.1.17

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
@@ -95,6 +95,7 @@ computer ssh
95
95
  computer ssh my-box
96
96
  computer ssh --setup
97
97
  computer mount
98
+ computer mount --background
98
99
  computer mount status
99
100
  computer agent agents my-box
100
101
  computer agent sessions list my-box
@@ -117,10 +118,18 @@ ssh agentcomputer.ai
117
118
  ssh my-box@agentcomputer.ai
118
119
  ```
119
120
 
120
- Run `computer mount` to start a foreground controller that mirrors all SSH-ready
121
- machine homes under `~/agentcomputer/<handle>` while the command is running.
121
+ Run `computer mount` to start the mount controller in the foreground and mirror
122
+ all SSH-ready machine homes under `~/agentcomputer/<handle>`.
123
+ On macOS, the foreground command also opens Finder to `~/agentcomputer` after
124
+ the controller starts.
125
+
126
+ Run `computer mount --background` to start the same controller detached from
127
+ your terminal. It prints the controller PID immediately so you can inspect it
128
+ later with `computer mount status`.
129
+
122
130
  This uses the same SSH setup as `computer ssh --setup` and requires Mutagen plus
123
- OpenSSH client tools (`ssh` and `scp`) to already be installed locally.
131
+ OpenSSH client tools (`ssh` and `scp`) to already be installed locally. The
132
+ temporary `~/agentcomputer` root is removed when the command exits.
124
133
 
125
134
  Use `computer agent` to inspect agents on one machine and manage remote sessions. Use `computer acp serve` when you want to expose one remote session through a local ACP bridge.
126
135
 
@@ -1,15 +1,15 @@
1
1
  import {
2
+ MOUNT_SERVICE_LABEL,
2
3
  ensureHandleDirectories,
3
4
  ensureMountDirectories,
4
- getMountHandlePaths,
5
5
  readMountStatusSnapshot,
6
6
  removeMountHandleState,
7
7
  writeMountHandleMeta,
8
8
  writeMountStatusSnapshot
9
- } from "./chunk-5JVJROSI.js";
9
+ } from "./chunk-KXLTHWW3.js";
10
10
 
11
11
  // src/lib/mount-reconcile.ts
12
- import { readdir, mkdir, rename, rm } from "fs/promises";
12
+ import { readdir, mkdir, rm } from "fs/promises";
13
13
  import { join as join3 } from "path";
14
14
 
15
15
  // src/lib/config.ts
@@ -248,16 +248,6 @@ function timeAgo(dateStr) {
248
248
  const days = Math.floor(hours / 24);
249
249
  return `${days}d ago`;
250
250
  }
251
- function minuteSecondAgo(dateStr) {
252
- const timestamp = new Date(dateStr).getTime();
253
- if (!Number.isFinite(timestamp)) {
254
- return "unknown";
255
- }
256
- const totalSeconds = Math.max(0, Math.floor((Date.now() - timestamp) / 1e3));
257
- const totalMinutes = Math.floor(totalSeconds / 60);
258
- const seconds = totalSeconds % 60;
259
- return `${totalMinutes}:${String(seconds).padStart(2, "0")} ago`;
260
- }
261
251
  function formatStatus(status) {
262
252
  switch (status) {
263
253
  case "running":
@@ -312,9 +302,15 @@ function describeSSHChoice(computer) {
312
302
  }
313
303
 
314
304
  // src/lib/mount-mutagen.ts
315
- import { chmodSync, existsSync as existsSync2, readFileSync as readFileSync2, symlinkSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
305
+ import { chmodSync, readFileSync as readFileSync2, symlinkSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
316
306
  import { spawnSync } from "child_process";
317
- import { basename, join as join2 } from "path";
307
+ import { basename, join as join2, relative, resolve } from "path";
308
+ var SYNC_NAME_PREFIX = "agentcomputer-mount-";
309
+ var DEFAULT_IGNORE_PATHS = [
310
+ ".codex/tmp",
311
+ ".local",
312
+ ".ssh/sshd.log"
313
+ ];
318
314
  function ensureMutagenSshEnvironment(config, paths) {
319
315
  ensureMountDirectories(paths);
320
316
  const sshPath = resolveCommandPath("ssh");
@@ -324,47 +320,174 @@ function ensureMutagenSshEnvironment(config, paths) {
324
320
  writeExecutableLink(paths.sshToolsDir, basename(sshPath), sshPath);
325
321
  writeExecutableLink(paths.sshToolsDir, basename(scpPath), scpPath);
326
322
  }
327
- function writeHandleProjectFile(handle, config, paths) {
328
- const handlePaths = ensureHandleDirectories(handle, config.rootPath);
329
- const content = [
330
- "sync:",
331
- " defaults:",
332
- " flushOnCreate: true",
333
- " mirror:",
334
- ` alpha: ${yamlString(join2(paths.rootPath, handle))}`,
335
- ` beta: ${yamlString(`${handle}@${config.alias}:/home/node`)}`,
336
- ""
337
- ].join("\n");
338
- writeFileSync2(handlePaths.projectFilePath, content, { mode: 384 });
339
- return handlePaths.projectFilePath;
340
- }
341
- function startHandleProject(handle, config, paths) {
342
- const handlePaths = getMountHandlePaths(handle, config.rootPath);
343
- runMutagen(["project", "start", "-f", handlePaths.projectFilePath], config, paths, handle);
344
- runMutagen(["project", "flush", "-f", handlePaths.projectFilePath], config, paths, handle);
345
- }
346
- function terminateHandleProject(handle, config, paths) {
347
- const handlePaths = getMountHandlePaths(handle, config.rootPath);
348
- if (!existsSync2(handlePaths.projectFilePath)) {
349
- return;
350
- }
351
- runMutagen(["project", "terminate", "-f", handlePaths.projectFilePath], config, paths, handle, true);
323
+ function getHandleSessionName(handle) {
324
+ return `${SYNC_NAME_PREFIX}${handle}`;
325
+ }
326
+ function createHandleSession(handle, config, paths) {
327
+ ensureHandleDirectories(handle, config.rootPath);
328
+ const sessionName = getHandleSessionName(handle);
329
+ const args = [
330
+ "sync",
331
+ "create",
332
+ join2(paths.rootPath, handle),
333
+ `${handle}@${config.alias}:/home/node`,
334
+ "--name",
335
+ sessionName,
336
+ "--label",
337
+ `${MOUNT_SERVICE_LABEL}=true`,
338
+ "--label",
339
+ `${MOUNT_SERVICE_LABEL}.handle=${handle}`,
340
+ "--symlink-mode",
341
+ "posix-raw"
342
+ ];
343
+ for (const ignorePath of DEFAULT_IGNORE_PATHS) {
344
+ args.push("--ignore", ignorePath);
345
+ }
346
+ runMutagen(args, config, paths, handle);
347
+ runMutagen(["sync", "flush", sessionName, "--skip-wait"], config, paths, handle);
348
+ }
349
+ function terminateSession(session, config, paths) {
350
+ runMutagen(
351
+ ["sync", "terminate", session.identifier],
352
+ config,
353
+ paths,
354
+ session.handle,
355
+ true
356
+ );
352
357
  }
353
- function inspectHandleProject(handle, config, paths) {
354
- const handlePaths = getMountHandlePaths(handle, config.rootPath);
355
- if (!existsSync2(handlePaths.projectFilePath)) {
358
+ function listOwnedSessions(config, paths) {
359
+ const result = runMutagen(["sync", "list", "-l"], config, paths, "mount", true);
360
+ const raw = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
361
+ if (!raw) {
362
+ return [];
363
+ }
364
+ if (!result.ok && raw.includes("No synchronization sessions found")) {
365
+ return [];
366
+ }
367
+ const rootPath = resolve(paths.rootPath);
368
+ const sessions = parseMutagenSyncList(raw).filter((session) => session.alphaPath).map((session) => {
369
+ const alphaPath = resolve(session.alphaPath);
370
+ const handle = basename(alphaPath);
371
+ const expectedName = getHandleSessionName(handle);
372
+ const expectedBeta = `${handle}@${config.alias}:/home/node`;
373
+ const owned = alphaPath === rootPath || relative(rootPath, alphaPath) === "" || !relative(rootPath, alphaPath).startsWith("..") && !relative(rootPath, alphaPath).startsWith("../");
374
+ if (!owned || !session.identifier) {
375
+ return null;
376
+ }
356
377
  return {
357
- ok: false,
358
- stdout: "",
359
- stderr: "project file missing"
378
+ identifier: session.identifier,
379
+ name: session.name,
380
+ handle,
381
+ alphaPath,
382
+ betaUrl: session.betaUrl,
383
+ alphaConnected: session.alphaConnected,
384
+ betaConnected: session.betaConnected,
385
+ status: session.status,
386
+ lastError: session.lastError,
387
+ scanProblemCount: session.scanProblemCount,
388
+ conflictCount: session.conflictCount,
389
+ legacy: session.name !== expectedName || session.betaUrl !== expectedBeta || alphaPath !== resolve(join2(rootPath, handle))
360
390
  };
391
+ }).filter((session) => session !== null);
392
+ return sessions.sort((left, right) => left.handle.localeCompare(right.handle));
393
+ }
394
+ function selectPreferredSession(handle, sessions) {
395
+ const expectedName = getHandleSessionName(handle);
396
+ const exact = sessions.find((session) => session.name === expectedName);
397
+ return exact ?? sessions[0];
398
+ }
399
+ function parseMutagenSyncList(output) {
400
+ const sessions = [];
401
+ let current = null;
402
+ let section = "";
403
+ const finishCurrent = () => {
404
+ if (current?.identifier) {
405
+ sessions.push(current);
406
+ }
407
+ current = null;
408
+ section = "";
409
+ };
410
+ for (const line of output.split(/\r?\n/)) {
411
+ if (/^-{20,}$/.test(line.trim())) {
412
+ finishCurrent();
413
+ continue;
414
+ }
415
+ const trimmed = line.trim();
416
+ if (!trimmed) {
417
+ continue;
418
+ }
419
+ if (!current) {
420
+ current = {
421
+ alphaConnected: false,
422
+ betaConnected: false,
423
+ scanProblemCount: 0,
424
+ conflictCount: 0
425
+ };
426
+ }
427
+ if (trimmed.endsWith(":")) {
428
+ switch (trimmed.slice(0, -1)) {
429
+ case "Alpha":
430
+ case "Beta":
431
+ case "Scan problems":
432
+ case "Conflicts":
433
+ section = trimmed.slice(0, -1);
434
+ continue;
435
+ default:
436
+ continue;
437
+ }
438
+ }
439
+ if (trimmed.startsWith("Name: ")) {
440
+ current.name = trimmed.slice("Name: ".length);
441
+ continue;
442
+ }
443
+ if (trimmed.startsWith("Identifier: ")) {
444
+ current.identifier = trimmed.slice("Identifier: ".length);
445
+ continue;
446
+ }
447
+ if (trimmed.startsWith("Status: ")) {
448
+ current.status = trimmed.slice("Status: ".length);
449
+ continue;
450
+ }
451
+ if (trimmed.startsWith("Last error: ")) {
452
+ current.lastError = trimmed.slice("Last error: ".length);
453
+ continue;
454
+ }
455
+ if (section === "Alpha") {
456
+ if (trimmed.startsWith("URL: ")) {
457
+ current.alphaPath = trimmed.slice("URL: ".length);
458
+ continue;
459
+ }
460
+ if (trimmed.startsWith("Connected: ")) {
461
+ current.alphaConnected = parseConnected(trimmed);
462
+ continue;
463
+ }
464
+ }
465
+ if (section === "Beta") {
466
+ if (trimmed.startsWith("URL: ")) {
467
+ current.betaUrl = trimmed.slice("URL: ".length);
468
+ continue;
469
+ }
470
+ if (trimmed.startsWith("Connected: ")) {
471
+ current.betaConnected = parseConnected(trimmed);
472
+ continue;
473
+ }
474
+ }
475
+ if (section === "Scan problems") {
476
+ current.scanProblemCount += 1;
477
+ continue;
478
+ }
479
+ if (section === "Conflicts") {
480
+ if (trimmed.startsWith("(alpha)")) {
481
+ current.conflictCount += 1;
482
+ }
483
+ continue;
484
+ }
361
485
  }
362
- return runMutagen(["project", "list", "-f", handlePaths.projectFilePath], config, paths, handle, true);
486
+ finishCurrent();
487
+ return sessions;
363
488
  }
364
- function cleanupKnownHandleProjects(config, paths, handles) {
365
- for (const handle of handles) {
366
- terminateHandleProject(handle, config, paths);
367
- }
489
+ function parseConnected(line) {
490
+ return line.slice("Connected: ".length).trim().toLowerCase() === "yes";
368
491
  }
369
492
  function runMutagen(args, config, paths, handle, ignoreFailure = false) {
370
493
  const result = spawnSync("mutagen", args, {
@@ -383,7 +506,8 @@ function runMutagen(args, config, paths, handle, ignoreFailure = false) {
383
506
  return {
384
507
  ok: result.status === 0,
385
508
  stdout,
386
- stderr
509
+ stderr,
510
+ status: result.status
387
511
  };
388
512
  }
389
513
  function resolveCommandPath(command) {
@@ -422,35 +546,41 @@ exec "${escapeShell(target)}" "$@"
422
546
  chmodSync(linkPath, 493);
423
547
  }
424
548
  }
425
- function yamlString(value) {
426
- return JSON.stringify(value);
427
- }
428
549
  function escapeShell(value) {
429
550
  return value.replaceAll("\\", "\\\\").replaceAll("`", "\\`").replaceAll("$", "\\$").replaceAll('"', '\\"');
430
551
  }
431
552
 
432
553
  // src/lib/mount-reconcile.ts
433
554
  function computeMountPlan(input) {
434
- const previousByHandle = new Map(
435
- (input.previousSnapshot?.mounts ?? []).map((mount) => [mount.handle, mount])
436
- );
437
- const desiredHandles = new Set(
438
- [...input.desired, ...input.pending].map((entry) => entry.handle)
439
- );
440
- const toStart = [];
441
- const toCheck = [];
442
- for (const entry of input.desired) {
443
- const previous = previousByHandle.get(entry.handle);
444
- if (previous?.state === "mounted") {
445
- toCheck.push(entry);
555
+ const desiredHandles = new Set(input.desired.map((entry) => entry.handle));
556
+ const sessionGroups = /* @__PURE__ */ new Map();
557
+ for (const session of input.ownedSessions) {
558
+ const group = sessionGroups.get(session.handle);
559
+ if (group) {
560
+ group.push(session);
446
561
  } else {
447
- toStart.push(entry);
562
+ sessionGroups.set(session.handle, [session]);
563
+ }
564
+ }
565
+ const toCreate = [];
566
+ const toInspect = [];
567
+ const toReset = /* @__PURE__ */ new Set();
568
+ for (const entry of input.desired) {
569
+ const sessions = sessionGroups.get(entry.handle) ?? [];
570
+ const reusable = sessions.length === 1 && !sessions[0].legacy;
571
+ if (reusable) {
572
+ toInspect.push(entry);
573
+ continue;
448
574
  }
575
+ if (sessions.length > 0) {
576
+ toReset.add(entry.handle);
577
+ }
578
+ toCreate.push(entry);
449
579
  }
450
580
  const toStop = /* @__PURE__ */ new Set();
451
- for (const mount of input.previousSnapshot?.mounts ?? []) {
452
- if (!desiredHandles.has(mount.handle)) {
453
- toStop.add(mount.handle);
581
+ for (const session of input.ownedSessions) {
582
+ if (!desiredHandles.has(session.handle)) {
583
+ toStop.add(session.handle);
454
584
  }
455
585
  }
456
586
  for (const entry of input.rootEntries) {
@@ -458,9 +588,13 @@ function computeMountPlan(input) {
458
588
  toStop.add(entry);
459
589
  }
460
590
  }
591
+ for (const handle of toReset) {
592
+ toStop.delete(handle);
593
+ }
461
594
  return {
462
- toStart,
463
- toCheck,
595
+ toCreate,
596
+ toInspect,
597
+ toReset: Array.from(toReset).sort(),
464
598
  toStop: Array.from(toStop).sort(),
465
599
  pending: input.pending
466
600
  };
@@ -497,52 +631,81 @@ async function planMountReconcile(config, paths) {
497
631
  ready: true
498
632
  });
499
633
  }
500
- const previousSnapshot = readMountStatusSnapshot(config.rootPath);
634
+ const ownedSessions = listOwnedSessions(config, paths);
501
635
  const rootEntries = await listRootHandleDirectories(paths.rootPath);
502
- return computeMountPlan({
503
- desired,
504
- pending,
505
- previousSnapshot,
506
- rootEntries
507
- });
636
+ return {
637
+ plan: computeMountPlan({
638
+ desired,
639
+ pending,
640
+ ownedSessions,
641
+ rootEntries
642
+ }),
643
+ ownedSessions
644
+ };
508
645
  }
509
646
  async function reconcileMounts(config, paths, controllerPid = process.pid) {
510
647
  ensureMutagenSshEnvironment(config, paths);
511
- const plan = await planMountReconcile(config, paths);
648
+ const { plan, ownedSessions } = await planMountReconcile(config, paths);
512
649
  const mounts = [];
513
- const errors = [];
514
- for (const handle of plan.toStop) {
650
+ const issues = [];
651
+ const ownedByHandle = groupSessionsByHandle(ownedSessions);
652
+ for (const handle of [...plan.toStop, ...plan.toReset]) {
653
+ const sessions = ownedByHandle.get(handle) ?? [];
515
654
  try {
516
- terminateHandleProject(handle, config, paths);
517
- await moveHandleOutOfRoot(handle, paths);
655
+ terminateOwnedSessions(sessions, config, paths);
656
+ await removeHandleFromRoot(handle, paths);
518
657
  removeMountHandleState(handle, config.rootPath);
519
658
  } catch (error) {
520
- errors.push(
659
+ issues.push(
521
660
  error instanceof Error ? `${handle}: ${error.message}` : `${handle}: teardown failed`
522
661
  );
523
662
  }
524
663
  }
525
- for (const entry of plan.toCheck) {
664
+ for (const entry of plan.toCreate) {
526
665
  try {
527
- const inspection = inspectHandleProject(entry.handle, config, paths);
528
- if (!inspection.ok) {
529
- await ensureMountedHandle(entry, config, paths);
666
+ await ensureMountedHandle(entry, config, paths);
667
+ const session = selectPreferredSession(
668
+ entry.handle,
669
+ listOwnedSessions(config, paths).filter((candidate) => candidate.handle === entry.handle)
670
+ );
671
+ const inspected = inspectSnapshotEntry(session, entry.mountPath);
672
+ mounts.push(inspected.snapshot);
673
+ if (inspected.issue) {
674
+ issues.push(inspected.issue);
530
675
  }
531
- mounts.push(snapshotEntry(entry.handle, entry.mountPath, "mounted"));
532
676
  } catch (error) {
533
- const message = error instanceof Error ? error.message : "sync check failed";
677
+ const message = error instanceof Error ? error.message : "sync start failed";
534
678
  mounts.push(snapshotEntry(entry.handle, entry.mountPath, "error", message));
535
- errors.push(`${entry.handle}: ${message}`);
679
+ issues.push(`${entry.handle}: ${message}`);
536
680
  }
537
681
  }
538
- for (const entry of plan.toStart) {
682
+ const postCreateSessions = groupSessionsByHandle(listOwnedSessions(config, paths));
683
+ for (const entry of plan.toInspect) {
539
684
  try {
540
- await ensureMountedHandle(entry, config, paths);
541
- mounts.push(snapshotEntry(entry.handle, entry.mountPath, "mounted"));
685
+ const sessions = postCreateSessions.get(entry.handle) ?? [];
686
+ if (sessions.length === 0) {
687
+ await ensureMountedHandle(entry, config, paths);
688
+ const createdSession = selectPreferredSession(
689
+ entry.handle,
690
+ listOwnedSessions(config, paths).filter((candidate) => candidate.handle === entry.handle)
691
+ );
692
+ const inspected2 = inspectSnapshotEntry(createdSession, entry.mountPath);
693
+ mounts.push(inspected2.snapshot);
694
+ if (inspected2.issue) {
695
+ issues.push(inspected2.issue);
696
+ }
697
+ continue;
698
+ }
699
+ const session = selectPreferredSession(entry.handle, sessions);
700
+ const inspected = inspectSnapshotEntry(session, entry.mountPath);
701
+ mounts.push(inspected.snapshot);
702
+ if (inspected.issue) {
703
+ issues.push(inspected.issue);
704
+ }
542
705
  } catch (error) {
543
- const message = error instanceof Error ? error.message : "sync start failed";
706
+ const message = error instanceof Error ? error.message : "sync inspection failed";
544
707
  mounts.push(snapshotEntry(entry.handle, entry.mountPath, "error", message));
545
- errors.push(`${entry.handle}: ${message}`);
708
+ issues.push(`${entry.handle}: ${message}`);
546
709
  }
547
710
  }
548
711
  for (const entry of plan.pending) {
@@ -550,47 +713,93 @@ async function reconcileMounts(config, paths, controllerPid = process.pid) {
550
713
  }
551
714
  const now = (/* @__PURE__ */ new Date()).toISOString();
552
715
  const previousSnapshot = readMountStatusSnapshot(config.rootPath);
716
+ const healthy = mounts.every((mount) => mount.state === "mounted" || mount.state === "pending");
553
717
  const snapshot = {
554
718
  updatedAt: now,
555
719
  controllerPid,
556
720
  running: true,
557
- lastSuccessfulSyncAt: errors.length === 0 ? now : previousSnapshot?.lastSuccessfulSyncAt,
558
- lastError: errors[0],
721
+ lastHealthySyncAt: healthy ? now : previousSnapshot?.lastHealthySyncAt,
722
+ lastSuccessfulSyncAt: healthy ? now : previousSnapshot?.lastSuccessfulSyncAt,
723
+ lastIssueAt: issues.length > 0 ? now : previousSnapshot?.lastIssueAt,
724
+ lastIssue: issues[0],
725
+ lastError: mounts.some((mount) => mount.state === "error") ? issues[0] : void 0,
559
726
  mounts: sortSnapshots(mounts)
560
727
  };
561
728
  writeMountStatusSnapshot(snapshot, config.rootPath);
562
729
  return snapshot;
563
730
  }
564
731
  async function teardownManagedSessions(config, paths) {
565
- const handles = await listKnownHandleStates(paths);
566
- cleanupKnownHandleProjects(config, paths, handles);
732
+ const ownedSessions = listOwnedSessions(config, paths);
733
+ terminateOwnedSessions(ownedSessions, config, paths);
734
+ const handles = new Set(ownedSessions.map((session) => session.handle));
735
+ for (const handle of await listKnownHandleStates(paths)) {
736
+ handles.add(handle);
737
+ }
738
+ for (const handle of await listRootHandleDirectories(paths.rootPath)) {
739
+ handles.add(handle);
740
+ }
567
741
  for (const handle of handles) {
568
742
  removeMountHandleState(handle, config.rootPath);
569
743
  }
744
+ await rm(paths.rootPath, { recursive: true, force: true });
570
745
  }
571
746
  async function ensureMountedHandle(entry, config, paths) {
572
747
  await mkdir(entry.mountPath, { recursive: true });
573
- ensureHandleDirectories(entry.handle, config.rootPath);
574
- writeHandleProjectFile(entry.handle, config, paths);
575
- startHandleProject(entry.handle, config, paths);
748
+ createHandleSession(entry.handle, config, paths);
576
749
  writeMountHandleMeta(
577
750
  entry.handle,
578
751
  {
579
752
  handle: entry.handle,
753
+ sessionName: getHandleSessionName(entry.handle),
580
754
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
581
755
  lastStartedAt: (/* @__PURE__ */ new Date()).toISOString()
582
756
  },
583
757
  config.rootPath
584
758
  );
585
759
  }
586
- async function moveHandleOutOfRoot(handle, paths) {
587
- const sourcePath = join3(paths.rootPath, handle);
588
- const stalePath = join3(paths.staleDir, `${handle}-${Date.now()}`);
589
- try {
590
- await rename(sourcePath, stalePath);
591
- } catch {
592
- await rm(sourcePath, { recursive: true, force: true });
760
+ function inspectSnapshotEntry(session, mountPath) {
761
+ const status = (session.status ?? "").toLowerCase();
762
+ const problemParts = [];
763
+ if (session.conflictCount > 0) {
764
+ problemParts.push(
765
+ `${session.conflictCount} conflict${session.conflictCount === 1 ? "" : "s"}`
766
+ );
767
+ }
768
+ if (session.scanProblemCount > 0) {
769
+ problemParts.push(
770
+ `${session.scanProblemCount} scan problem${session.scanProblemCount === 1 ? "" : "s"}`
771
+ );
772
+ }
773
+ if (problemParts.length > 0) {
774
+ const message = problemParts.join(", ");
775
+ return {
776
+ snapshot: snapshotEntry(session.handle, mountPath, "degraded", message),
777
+ issue: `${session.handle}: ${message}`
778
+ };
779
+ }
780
+ if (!session.alphaConnected) {
781
+ const message = session.lastError ?? "local sync endpoint disconnected";
782
+ return {
783
+ snapshot: snapshotEntry(session.handle, mountPath, "error", message),
784
+ issue: `${session.handle}: ${message}`
785
+ };
786
+ }
787
+ if (!session.betaConnected || status.includes("connecting") || status.includes("reconnect")) {
788
+ const message = session.lastError ?? "reconnecting to remote machine";
789
+ return {
790
+ snapshot: snapshotEntry(session.handle, mountPath, "reconnecting", message),
791
+ issue: `${session.handle}: ${message}`
792
+ };
593
793
  }
794
+ if (session.lastError && !status.includes("watching")) {
795
+ return {
796
+ snapshot: snapshotEntry(session.handle, mountPath, "degraded", session.lastError),
797
+ issue: `${session.handle}: ${session.lastError}`
798
+ };
799
+ }
800
+ return {
801
+ snapshot: snapshotEntry(session.handle, mountPath, "mounted")
802
+ };
594
803
  }
595
804
  function snapshotEntry(handle, mountPath, state, message) {
596
805
  return {
@@ -604,6 +813,26 @@ function snapshotEntry(handle, mountPath, state, message) {
604
813
  function sortSnapshots(mounts) {
605
814
  return mounts.slice().sort((left, right) => left.handle.localeCompare(right.handle));
606
815
  }
816
+ function groupSessionsByHandle(sessions) {
817
+ const grouped = /* @__PURE__ */ new Map();
818
+ for (const session of sessions) {
819
+ const group = grouped.get(session.handle);
820
+ if (group) {
821
+ group.push(session);
822
+ } else {
823
+ grouped.set(session.handle, [session]);
824
+ }
825
+ }
826
+ return grouped;
827
+ }
828
+ function terminateOwnedSessions(sessions, config, paths) {
829
+ for (const session of sessions) {
830
+ terminateSession(session, config, paths);
831
+ }
832
+ }
833
+ async function removeHandleFromRoot(handle, paths) {
834
+ await rm(join3(paths.rootPath, handle), { recursive: true, force: true });
835
+ }
607
836
  async function listRootHandleDirectories(rootPath) {
608
837
  try {
609
838
  return (await readdir(rootPath, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
@@ -645,7 +874,6 @@ export {
645
874
  vncURL,
646
875
  padEnd,
647
876
  timeAgo,
648
- minuteSecondAgo,
649
877
  formatStatus,
650
878
  promptForSSHComputer,
651
879
  computeMountPlan,
@@ -35,7 +35,6 @@ function getMountHandlePaths(handle, rootPath = getDefaultMountRoot()) {
35
35
  const stateDir = join(paths.handlesDir, handle);
36
36
  return {
37
37
  stateDir,
38
- projectFilePath: join(stateDir, "mutagen.yml"),
39
38
  metaPath: join(stateDir, "meta.json"),
40
39
  sshToolsDir: join(stateDir, "ssh-tools")
41
40
  };
@@ -53,7 +52,6 @@ function defaultMountServiceConfig() {
53
52
  function ensureMountDirectories(paths) {
54
53
  for (const directory of [
55
54
  paths.stateDir,
56
- paths.rootPath,
57
55
  paths.handlesDir,
58
56
  paths.sshToolsDir,
59
57
  paths.staleDir
package/dist/index.js CHANGED
@@ -23,7 +23,6 @@ import {
23
23
  hasEnvAPIKey,
24
24
  listComputers,
25
25
  listPublishedPorts,
26
- minuteSecondAgo,
27
26
  padEnd,
28
27
  promptForSSHComputer,
29
28
  publishPort,
@@ -34,7 +33,7 @@ import {
34
33
  timeAgo,
35
34
  vncURL,
36
35
  webURL
37
- } from "./chunk-KQQUR2YX.js";
36
+ } from "./chunk-5IEWKH52.js";
38
37
  import {
39
38
  defaultMountServiceConfig,
40
39
  ensureMountDirectories,
@@ -46,7 +45,7 @@ import {
46
45
  writeMountConfig,
47
46
  writeMountControllerLock,
48
47
  writeMountStatusSnapshot
49
- } from "./chunk-5JVJROSI.js";
48
+ } from "./chunk-KXLTHWW3.js";
50
49
 
51
50
  // src/index.ts
52
51
  import { Command as Command14 } from "commander";
@@ -2461,7 +2460,7 @@ _computer() {
2461
2460
  'open:Open in browser'
2462
2461
  'ssh:SSH into a computer'
2463
2462
  'ports:Manage published ports'
2464
- 'mount:Mirror SSH-ready machine homes into ~/agentcomputer while running'
2463
+ 'mount:Mirror SSH-ready machine homes into ~/agentcomputer'
2465
2464
  'agent:Manage cloud agent sessions'
2466
2465
  'acp:Run a local ACP bridge for remote agent sessions'
2467
2466
  'rm:Delete a computer'
@@ -2601,6 +2600,7 @@ _computer() {
2601
2600
  ;;
2602
2601
  mount)
2603
2602
  _arguments -C \\
2603
+ '--background[Run the mount controller in the background and print its PID]' \\
2604
2604
  '--alias[SSH host alias]:alias:' \\
2605
2605
  '--host[SSH gateway host]:host:' \\
2606
2606
  '--port[SSH gateway port]:port:' \\
@@ -2772,9 +2772,9 @@ var BASH_SCRIPT = `_computer() {
2772
2772
  ;;
2773
2773
  mount)
2774
2774
  if [[ $cword -eq 2 ]]; then
2775
- COMPREPLY=($(compgen -W "$mount_commands" -- "$cur"))
2775
+ COMPREPLY=($(compgen -W "$mount_commands --background --alias --host --port --poll-interval --connect-timeout" -- "$cur"))
2776
2776
  elif [[ $cword -ge 3 ]]; then
2777
- COMPREPLY=($(compgen -W "--alias --host --port --poll-interval --connect-timeout" -- "$cur"))
2777
+ COMPREPLY=($(compgen -W "--background --alias --host --port --poll-interval --connect-timeout" -- "$cur"))
2778
2778
  fi
2779
2779
  ;;
2780
2780
  ports)
@@ -3988,7 +3988,8 @@ function printNextStep(machineHandle) {
3988
3988
  }
3989
3989
 
3990
3990
  // src/commands/mount.ts
3991
- import { Command as Command10 } from "commander";
3991
+ import { spawn as spawn4 } from "child_process";
3992
+ import { Command as Command10, Option } from "commander";
3992
3993
  import chalk8 from "chalk";
3993
3994
  import ora9 from "ora";
3994
3995
 
@@ -4005,7 +4006,8 @@ function getMountControllerState(rootPath = defaultMountServiceConfig().rootPath
4005
4006
  }
4006
4007
  return { running: true, pid: lock.pid };
4007
4008
  }
4008
- async function runMountDaemon(config) {
4009
+ async function runMountDaemon(config, options = {}) {
4010
+ const { onStarted } = options;
4009
4011
  const paths = getMountPaths(config.rootPath);
4010
4012
  ensureMountDirectories(paths);
4011
4013
  await mkdir3(paths.rootPath, { recursive: true });
@@ -4019,6 +4021,7 @@ async function runMountDaemon(config) {
4019
4021
  },
4020
4022
  config.rootPath
4021
4023
  );
4024
+ onStarted?.();
4022
4025
  await teardownManagedSessions(config, paths);
4023
4026
  let running = false;
4024
4027
  let queued = false;
@@ -4046,7 +4049,10 @@ async function runMountDaemon(config) {
4046
4049
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4047
4050
  controllerPid: process.pid,
4048
4051
  running: true,
4052
+ lastHealthySyncAt: previous?.lastHealthySyncAt,
4049
4053
  lastSuccessfulSyncAt: previous?.lastSuccessfulSyncAt,
4054
+ lastIssueAt: (/* @__PURE__ */ new Date()).toISOString(),
4055
+ lastIssue: error instanceof Error ? error.message : "mount reconcile failed",
4050
4056
  lastError: error instanceof Error ? error.message : "mount reconcile failed",
4051
4057
  mounts: previous?.mounts ?? []
4052
4058
  },
@@ -4094,7 +4100,10 @@ async function runMountDaemon(config) {
4094
4100
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4095
4101
  controllerPid: previous?.controllerPid,
4096
4102
  running: false,
4103
+ lastHealthySyncAt: previous?.lastHealthySyncAt,
4097
4104
  lastSuccessfulSyncAt: previous?.lastSuccessfulSyncAt,
4105
+ lastIssueAt: previous?.lastIssueAt,
4106
+ lastIssue: void 0,
4098
4107
  lastError: void 0,
4099
4108
  mounts: []
4100
4109
  },
@@ -4140,40 +4149,39 @@ function processExists(pid) {
4140
4149
  }
4141
4150
 
4142
4151
  // src/commands/mount.ts
4143
- var mountCommand = new Command10("mount").description("Mirror SSH-ready machines under ~/agentcomputer while this command is running").option("--alias <alias>", "SSH host alias", "agentcomputer.ai").option("--host <host>", "SSH gateway host", "ssh.agentcomputer.ai").option("--port <port>", "SSH gateway port", "443").option("--poll-interval <ms>", "Reconcile interval in milliseconds", "5000").option("--connect-timeout <seconds>", "SSH connect timeout for Mutagen", "5").action(async (options) => {
4144
- const spinner = ora9("Starting machine mount controller...").start();
4152
+ var mountCommand = new Command10("mount").description("Mirror SSH-ready machines under ~/agentcomputer with a local mount controller").option("--alias <alias>", "SSH host alias", "agentcomputer.ai").option("--host <host>", "SSH gateway host", "ssh.agentcomputer.ai").option("--port <port>", "SSH gateway port", "443").option("--poll-interval <ms>", "Reconcile interval in milliseconds", "5000").option("--connect-timeout <seconds>", "SSH connect timeout for Mutagen", "5").option(
4153
+ "--background",
4154
+ "Run the mount controller in the background and print its PID"
4155
+ ).addOption(new Option("--daemonized").hideHelp()).action(async (options) => {
4156
+ const spinner = ora9(
4157
+ options.background ? "Starting machine mount controller in background..." : "Starting machine mount controller..."
4158
+ ).start();
4145
4159
  try {
4146
- const issues = getMountHostValidationIssues();
4147
- if (issues.length > 0) {
4148
- throw new Error(
4149
- [
4150
- ...issues.map((issue) => issue.message),
4151
- ...formatMountHostInstallGuidance(issues)
4152
- ].join("\n")
4153
- );
4160
+ if (options.daemonized) {
4161
+ const config2 = readMountConfig() ?? defaultMountServiceConfig();
4162
+ await runMountDaemon(config2, {
4163
+ onStarted: () => {
4164
+ spinner.succeed("Machine mount controller running");
4165
+ printMountStartSummary(config2, process.pid, "foreground");
4166
+ }
4167
+ });
4168
+ return;
4154
4169
  }
4155
- const sshSetup = await ensureSSHAccessConfigured(options);
4156
- const config = {
4157
- ...defaultMountServiceConfig(),
4158
- alias: sshSetup.alias,
4159
- host: sshSetup.host,
4160
- port: sshSetup.port,
4161
- pollIntervalMs: parsePositiveInt(options.pollInterval, "poll interval"),
4162
- connectTimeoutSeconds: parsePositiveInt(
4163
- options.connectTimeout,
4164
- "connect timeout"
4165
- )
4166
- };
4170
+ const config = await resolveMountServiceConfig(options);
4167
4171
  writeMountConfig(config);
4168
- spinner.succeed("Machine mount controller running");
4169
- console.log();
4170
- console.log(chalk8.dim(` Root: ${config.rootPath}`));
4171
- console.log(chalk8.dim(` SSH alias: ${config.alias}`));
4172
- console.log(chalk8.dim(` Poll: ${config.pollIntervalMs}ms`));
4173
- console.log();
4174
- console.log(chalk8.dim(" Press Ctrl-C to stop syncing."));
4175
- console.log();
4176
- await runMountDaemon(config);
4172
+ if (options.background) {
4173
+ const pid = await startMountControllerInBackground(config.rootPath);
4174
+ spinner.succeed("Machine mount controller running in background");
4175
+ printMountStartSummary(config, pid, "background");
4176
+ return;
4177
+ }
4178
+ await runMountDaemon(config, {
4179
+ onStarted: () => {
4180
+ spinner.succeed("Machine mount controller running");
4181
+ printMountStartSummary(config, process.pid, "foreground");
4182
+ revealMountRootInFinder(config.rootPath);
4183
+ }
4184
+ });
4177
4185
  } catch (error) {
4178
4186
  spinner.fail(
4179
4187
  error instanceof Error ? error.message : "Failed to start machine mount controller"
@@ -4197,24 +4205,24 @@ var mountCommand = new Command10("mount").description("Mirror SSH-ready machines
4197
4205
  console.log(` ${chalk8.dim("Root")} ${config.rootPath}`);
4198
4206
  console.log(` ${chalk8.dim("Alias")} ${config.alias}`);
4199
4207
  console.log(
4200
- ` ${chalk8.dim("Updated")} ${snapshot?.updatedAt ? minuteSecondAgo(snapshot.updatedAt) : chalk8.dim("never")}`
4208
+ ` ${chalk8.dim("Updated")} ${snapshot?.updatedAt ? timeAgo(snapshot.updatedAt) : chalk8.dim("never")}`
4201
4209
  );
4202
4210
  console.log(
4203
- ` ${chalk8.dim("Last sync")} ${snapshot?.lastSuccessfulSyncAt ? minuteSecondAgo(snapshot.lastSuccessfulSyncAt) : chalk8.dim("never")}`
4211
+ ` ${chalk8.dim("Healthy")} ${snapshot?.lastHealthySyncAt ? timeAgo(snapshot.lastHealthySyncAt) : chalk8.dim("never")}`
4204
4212
  );
4205
4213
  if (controller.running && snapshot?.mounts.length) {
4206
4214
  console.log();
4207
4215
  for (const mount of snapshot.mounts) {
4208
- const state = mount.state === "mounted" ? chalk8.green(mount.state) : mount.state === "pending" ? chalk8.yellow(mount.state) : chalk8.red(mount.state);
4216
+ const state = formatMountState(mount.state);
4209
4217
  console.log(` ${chalk8.white(mount.handle)} ${state} ${chalk8.dim(mount.mountPath)}`);
4210
4218
  if (mount.message) {
4211
4219
  console.log(` ${chalk8.dim(mount.message)}`);
4212
4220
  }
4213
4221
  }
4214
4222
  }
4215
- if (snapshot?.lastError) {
4223
+ if (snapshot?.lastIssue) {
4216
4224
  console.log();
4217
- console.log(` ${chalk8.dim("Last error")} ${chalk8.red(snapshot.lastError)}`);
4225
+ console.log(` ${chalk8.dim("Last issue")} ${chalk8.yellow(snapshot.lastIssue)}`);
4218
4226
  }
4219
4227
  console.log();
4220
4228
  })
@@ -4226,6 +4234,115 @@ function parsePositiveInt(raw, label) {
4226
4234
  }
4227
4235
  return Math.round(value);
4228
4236
  }
4237
+ async function resolveMountServiceConfig(options) {
4238
+ const issues = getMountHostValidationIssues();
4239
+ if (issues.length > 0) {
4240
+ throw new Error(
4241
+ [
4242
+ ...issues.map((issue) => issue.message),
4243
+ ...formatMountHostInstallGuidance(issues)
4244
+ ].join("\n")
4245
+ );
4246
+ }
4247
+ const sshSetup = await ensureSSHAccessConfigured(options);
4248
+ return {
4249
+ ...defaultMountServiceConfig(),
4250
+ alias: sshSetup.alias,
4251
+ host: sshSetup.host,
4252
+ port: sshSetup.port,
4253
+ pollIntervalMs: parsePositiveInt(options.pollInterval, "poll interval"),
4254
+ connectTimeoutSeconds: parsePositiveInt(
4255
+ options.connectTimeout,
4256
+ "connect timeout"
4257
+ )
4258
+ };
4259
+ }
4260
+ async function startMountControllerInBackground(rootPath) {
4261
+ const controller = getMountControllerState(rootPath);
4262
+ if (controller.running) {
4263
+ throw new Error(`computer mount is already running (pid ${controller.pid})`);
4264
+ }
4265
+ const entrypoint = process.argv[1];
4266
+ if (!entrypoint) {
4267
+ throw new Error("unable to determine CLI entrypoint for background mount");
4268
+ }
4269
+ const child = spawn4(process.execPath, [entrypoint, "mount", "--daemonized"], {
4270
+ cwd: process.cwd(),
4271
+ detached: true,
4272
+ env: process.env,
4273
+ stdio: "ignore"
4274
+ });
4275
+ if (!child.pid) {
4276
+ throw new Error("failed to start machine mount controller in background");
4277
+ }
4278
+ child.unref();
4279
+ await waitForBackgroundMountController(rootPath, child.pid);
4280
+ return child.pid;
4281
+ }
4282
+ async function waitForBackgroundMountController(rootPath, pid) {
4283
+ const deadline = Date.now() + 3e3;
4284
+ while (Date.now() < deadline) {
4285
+ const controller = getMountControllerState(rootPath);
4286
+ if (controller.running && controller.pid === pid) {
4287
+ return;
4288
+ }
4289
+ if (!processExists2(pid)) {
4290
+ break;
4291
+ }
4292
+ await new Promise((resolve) => setTimeout(resolve, 50));
4293
+ }
4294
+ throw new Error("failed to start machine mount controller in background");
4295
+ }
4296
+ function printMountStartSummary(config, pid, mode) {
4297
+ console.log();
4298
+ console.log(chalk8.dim(` PID: ${pid}`));
4299
+ console.log(chalk8.dim(` Root: ${config.rootPath}`));
4300
+ console.log(chalk8.dim(` SSH alias: ${config.alias}`));
4301
+ console.log(chalk8.dim(` Poll: ${config.pollIntervalMs}ms`));
4302
+ console.log();
4303
+ console.log(
4304
+ chalk8.dim(
4305
+ mode === "background" ? " Use `computer mount status` to inspect sync state." : " Press Ctrl-C to stop syncing."
4306
+ )
4307
+ );
4308
+ console.log();
4309
+ }
4310
+ function processExists2(pid) {
4311
+ if (!Number.isInteger(pid) || pid <= 0) {
4312
+ return false;
4313
+ }
4314
+ try {
4315
+ process.kill(pid, 0);
4316
+ return true;
4317
+ } catch {
4318
+ return false;
4319
+ }
4320
+ }
4321
+ function revealMountRootInFinder(rootPath) {
4322
+ if (process.platform !== "darwin") {
4323
+ return;
4324
+ }
4325
+ const child = spawn4("open", [rootPath], {
4326
+ stdio: "ignore",
4327
+ detached: true
4328
+ });
4329
+ child.on("error", () => {
4330
+ });
4331
+ child.unref();
4332
+ }
4333
+ function formatMountState(state) {
4334
+ switch (state) {
4335
+ case "mounted":
4336
+ return chalk8.green(state);
4337
+ case "reconnecting":
4338
+ return chalk8.cyan(state);
4339
+ case "degraded":
4340
+ case "pending":
4341
+ return chalk8.yellow(state);
4342
+ default:
4343
+ return chalk8.red(state);
4344
+ }
4345
+ }
4229
4346
 
4230
4347
  // src/commands/logout.ts
4231
4348
  import { Command as Command11 } from "commander";
@@ -10,7 +10,7 @@ interface MountServiceConfig {
10
10
  interface ManagedMountSnapshot {
11
11
  handle: string;
12
12
  mountPath: string;
13
- state: "mounted" | "pending" | "error";
13
+ state: "mounted" | "reconnecting" | "degraded" | "pending" | "error";
14
14
  message?: string;
15
15
  updatedAt: string;
16
16
  }
@@ -18,7 +18,10 @@ interface MountStatusSnapshot {
18
18
  updatedAt: string;
19
19
  controllerPid?: number;
20
20
  running: boolean;
21
+ lastHealthySyncAt?: string;
21
22
  lastSuccessfulSyncAt?: string;
23
+ lastIssueAt?: string;
24
+ lastIssue?: string;
22
25
  lastError?: string;
23
26
  mounts: ManagedMountSnapshot[];
24
27
  }
@@ -35,12 +38,12 @@ interface MountPaths {
35
38
  }
36
39
  interface MountHandlePaths {
37
40
  stateDir: string;
38
- projectFilePath: string;
39
41
  metaPath: string;
40
42
  sshToolsDir: string;
41
43
  }
42
44
  interface MountHandleMeta {
43
45
  handle: string;
46
+ sessionName?: string;
44
47
  startedAt: string;
45
48
  lastStartedAt?: string;
46
49
  }
@@ -17,7 +17,7 @@ import {
17
17
  writeMountControllerLock,
18
18
  writeMountHandleMeta,
19
19
  writeMountStatusSnapshot
20
- } from "../chunk-5JVJROSI.js";
20
+ } from "../chunk-KXLTHWW3.js";
21
21
  export {
22
22
  MOUNT_SERVICE_LABEL,
23
23
  defaultMountServiceConfig,
@@ -1,4 +1,19 @@
1
- import { MountStatusSnapshot, MountServiceConfig, MountPaths } from './mount-config.js';
1
+ import { MountServiceConfig, MountPaths, MountStatusSnapshot } from './mount-config.js';
2
+
3
+ interface OwnedMountSession {
4
+ identifier: string;
5
+ name?: string;
6
+ handle: string;
7
+ alphaPath: string;
8
+ betaUrl?: string;
9
+ alphaConnected: boolean;
10
+ betaConnected: boolean;
11
+ status?: string;
12
+ lastError?: string;
13
+ scanProblemCount: number;
14
+ conflictCount: number;
15
+ legacy: boolean;
16
+ }
2
17
 
3
18
  interface DesiredMount {
4
19
  handle: string;
@@ -7,18 +22,22 @@ interface DesiredMount {
7
22
  message?: string;
8
23
  }
9
24
  interface MountPlan {
10
- toStart: DesiredMount[];
11
- toCheck: DesiredMount[];
25
+ toCreate: DesiredMount[];
26
+ toInspect: DesiredMount[];
27
+ toReset: string[];
12
28
  toStop: string[];
13
29
  pending: DesiredMount[];
14
30
  }
15
31
  declare function computeMountPlan(input: {
16
32
  desired: DesiredMount[];
17
33
  pending: DesiredMount[];
18
- previousSnapshot: MountStatusSnapshot | null;
34
+ ownedSessions: Pick<OwnedMountSession, "handle" | "legacy">[];
19
35
  rootEntries: string[];
20
36
  }): MountPlan;
21
- declare function planMountReconcile(config: MountServiceConfig, paths: MountPaths): Promise<MountPlan>;
37
+ declare function planMountReconcile(config: MountServiceConfig, paths: MountPaths): Promise<{
38
+ plan: MountPlan;
39
+ ownedSessions: OwnedMountSession[];
40
+ }>;
22
41
  declare function reconcileMounts(config: MountServiceConfig, paths: MountPaths, controllerPid?: number): Promise<MountStatusSnapshot>;
23
42
  declare function teardownManagedSessions(config: MountServiceConfig, paths: MountPaths): Promise<void>;
24
43
 
@@ -3,8 +3,8 @@ import {
3
3
  planMountReconcile,
4
4
  reconcileMounts,
5
5
  teardownManagedSessions
6
- } from "../chunk-KQQUR2YX.js";
7
- import "../chunk-5JVJROSI.js";
6
+ } from "../chunk-5IEWKH52.js";
7
+ import "../chunk-KXLTHWW3.js";
8
8
  export {
9
9
  computeMountPlan,
10
10
  planMountReconcile,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicomputer",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Computer CLI - manage your Agent Computer machines from the terminal",
5
5
  "homepage": "https://agentcomputer.ai",
6
6
  "repository": {