@astrale-os/cli 0.6.3-alpha.0 → 0.8.0-alpha.0

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 (126) hide show
  1. package/README.md +3 -2
  2. package/dist/astrale.js +339 -242
  3. package/package.json +2 -3
  4. package/src/commands/__tests__/admin-instance.test.ts +49 -2
  5. package/src/commands/__tests__/domain-install-owned.test.ts +122 -0
  6. package/src/commands/__tests__/help-contract.test.ts +15 -0
  7. package/src/commands/domain/install.ts +15 -4
  8. package/src/commands/instance/active.ts +4 -7
  9. package/src/commands/instance/list.ts +4 -8
  10. package/src/commands/instance/status.ts +14 -9
  11. package/src/commands/instance/use.ts +13 -11
  12. package/src/commands/studio.ts +13 -0
  13. package/src/commands/view.ts +40 -13
  14. package/src/kernel/__tests__/client-owned-lookup.test.ts +51 -0
  15. package/src/kernel/client.ts +47 -20
  16. package/src/kernel/expand.ts +3 -22
  17. package/src/lib/__tests__/instance-candidates.test.ts +94 -19
  18. package/src/lib/__tests__/instance-target.test.ts +9 -1
  19. package/src/lib/__tests__/view-assets.test.ts +114 -0
  20. package/src/lib/__tests__/view-port-allocation.test.ts +82 -0
  21. package/src/lib/admin-instance.ts +27 -2
  22. package/src/lib/instance-candidates.ts +3 -3
  23. package/src/lib/instance-target.ts +1 -0
  24. package/src/lib/instance.ts +0 -11
  25. package/src/lib/login-flow.ts +4 -41
  26. package/src/lib/view/assets.ts +64 -0
  27. package/src/lib/view/port-allocation.ts +19 -0
  28. package/src/lib/view/server.ts +4 -37
  29. package/src/setup/__tests__/instance-step.test.ts +239 -0
  30. package/src/setup/steps/instance.ts +141 -61
  31. package/studio/client/dist/assets/index-BaqEuIQJ.js +109 -0
  32. package/studio/client/dist/assets/index-jRdExahh.css +1 -0
  33. package/studio/client/dist/index.html +2 -2
  34. package/studio/server/agent/ask.test.ts +74 -0
  35. package/studio/server/agent/ask.ts +20 -22
  36. package/studio/server/agent/bridge/client.test.ts +49 -0
  37. package/studio/server/agent/bridge/client.ts +37 -0
  38. package/studio/server/agent/bridge/grant.test.ts +62 -0
  39. package/studio/server/agent/bridge/grant.ts +84 -0
  40. package/studio/server/agent/bridge/routes.test.ts +151 -0
  41. package/studio/server/agent/bridge/routes.ts +148 -0
  42. package/studio/server/agent/{bridge-mcp.ts → bridge/stdio.ts} +27 -21
  43. package/studio/server/agent/conversation.test.ts +84 -0
  44. package/studio/server/agent/conversation.ts +110 -0
  45. package/studio/server/agent/{types.ts → harness/adapter.ts} +63 -12
  46. package/studio/server/agent/harness/claude/adapter.test.ts +338 -0
  47. package/studio/server/agent/harness/claude/adapter.ts +71 -0
  48. package/studio/server/agent/harness/claude/ask.ts +114 -0
  49. package/studio/server/agent/harness/claude/capabilities.ts +27 -0
  50. package/studio/server/agent/harness/claude/command.ts +63 -0
  51. package/studio/server/agent/harness/claude/events.ts +215 -0
  52. package/studio/server/agent/harness/claude/loadout.ts +132 -0
  53. package/studio/server/agent/harness/claude/mcp.test.ts +36 -0
  54. package/studio/server/agent/harness/claude/mcp.ts +41 -0
  55. package/studio/server/agent/harness/claude/skills.ts +43 -0
  56. package/studio/server/agent/harness/codex/adapter.test.ts +269 -0
  57. package/studio/server/agent/harness/codex/adapter.ts +116 -0
  58. package/studio/server/agent/harness/codex/ask.test.ts +176 -0
  59. package/studio/server/agent/harness/codex/ask.ts +181 -0
  60. package/studio/server/agent/harness/codex/command.test.ts +69 -0
  61. package/studio/server/agent/harness/codex/command.ts +43 -0
  62. package/studio/server/agent/harness/codex/events.test.ts +94 -0
  63. package/studio/server/agent/harness/codex/events.ts +132 -0
  64. package/studio/server/agent/harness/codex/exec.ts +113 -0
  65. package/studio/server/agent/harness/codex/loadout.ts +63 -0
  66. package/studio/server/agent/harness/codex/mcp.test.ts +32 -0
  67. package/studio/server/agent/harness/codex/mcp.ts +27 -0
  68. package/studio/server/agent/harness/codex/models.test.ts +167 -0
  69. package/studio/server/agent/harness/codex/models.ts +211 -0
  70. package/studio/server/agent/harness/codex/skills.ts +33 -0
  71. package/studio/server/agent/harness/gateway/config.test.ts +98 -0
  72. package/studio/server/{state/harness-gateway.ts → agent/harness/gateway/config.ts} +30 -8
  73. package/studio/server/agent/harness/gateway/token.test.ts +133 -0
  74. package/studio/server/agent/harness/gateway/token.ts +166 -0
  75. package/studio/server/agent/harness/mock/adapter.ts +190 -0
  76. package/studio/server/agent/harness/mock/domain-edit.ts +41 -0
  77. package/studio/server/agent/harness/process.test.ts +46 -0
  78. package/studio/server/agent/harness/process.ts +104 -0
  79. package/studio/server/agent/harness/registry.ts +37 -0
  80. package/studio/server/agent/harness/selection.test.ts +75 -0
  81. package/studio/server/agent/harness/selection.ts +77 -0
  82. package/studio/server/agent/harness/skills.test.ts +95 -0
  83. package/studio/server/agent/harness/skills.ts +100 -0
  84. package/studio/server/agent/layout.test.ts +95 -0
  85. package/studio/server/agent/notify.ts +12 -0
  86. package/studio/server/agent/{schema-map.ts → prompts/anchors.ts} +2 -2
  87. package/studio/server/agent/prompts/ask.ts +34 -0
  88. package/studio/server/agent/{prompt.ts → prompts/system.ts} +3 -107
  89. package/studio/server/agent/prompts/turn.ts +57 -0
  90. package/studio/server/agent/routes.test.ts +176 -0
  91. package/studio/server/agent/routes.ts +214 -0
  92. package/studio/server/agent/run/completion.ts +193 -0
  93. package/studio/server/agent/run/coordinator.test.ts +337 -0
  94. package/studio/server/agent/run/coordinator.ts +104 -0
  95. package/studio/server/agent/run/live-state.ts +73 -0
  96. package/studio/server/agent/run/preparation.ts +162 -0
  97. package/studio/server/agent/run/transcript.test.ts +47 -0
  98. package/studio/server/agent/run/transcript.ts +30 -0
  99. package/studio/server/agent/run/usage.test.ts +46 -0
  100. package/studio/server/{state → agent/run}/usage.ts +4 -4
  101. package/studio/server/agent/stream.ts +37 -0
  102. package/studio/server/agent/{session-id.ts → telemetry.ts} +1 -1
  103. package/studio/server/api-agent-loadout.test.ts +69 -0
  104. package/studio/server/api.ts +3 -155
  105. package/studio/server/index.ts +6 -5
  106. package/studio/server/state/comments.test.ts +29 -0
  107. package/studio/server/state/comments.ts +3 -3
  108. package/studio/server/state/settings.test.ts +78 -0
  109. package/studio/server/state/settings.ts +28 -1
  110. package/studio/shared/agent-effort.test.ts +12 -0
  111. package/studio/shared/agent-effort.ts +15 -0
  112. package/studio/shared/agent-models.test.ts +9 -0
  113. package/studio/shared/agent-models.ts +14 -0
  114. package/studio/shared/settings-values.test.ts +18 -0
  115. package/studio/shared/settings-values.ts +20 -0
  116. package/studio/shared/types.ts +58 -7
  117. package/src/connect-core.test.ts +0 -42
  118. package/src/connect-core.ts +0 -53
  119. package/studio/client/dist/assets/index-CyN5G8IA.js +0 -109
  120. package/studio/client/dist/assets/index-DKKMHBBC.css +0 -1
  121. package/studio/server/agent/bridge.ts +0 -188
  122. package/studio/server/agent/claude.ts +0 -678
  123. package/studio/server/agent/mock.ts +0 -186
  124. package/studio/server/agent/registry.ts +0 -29
  125. package/studio/server/agent/runner.ts +0 -487
  126. package/studio/server/state/harness-token.ts +0 -0
package/dist/astrale.js CHANGED
@@ -2576,7 +2576,7 @@ var package_default;
2576
2576
  var init_package = __esm(() => {
2577
2577
  package_default = {
2578
2578
  name: "@astrale-os/cli",
2579
- version: "0.6.3-alpha.0",
2579
+ version: "0.8.0-alpha.0",
2580
2580
  description: "Astrale CLI — connect to existing Astrale kernels",
2581
2581
  keywords: [
2582
2582
  "astrale",
@@ -2606,8 +2606,7 @@ var init_package = __esm(() => {
2606
2606
  type: "module",
2607
2607
  exports: {
2608
2608
  "./keys": "./src/lib/keys.ts",
2609
- "./paths": "./src/lib/paths.ts",
2610
- "./connect-core": "./src/connect-core.ts"
2609
+ "./paths": "./src/lib/paths.ts"
2611
2610
  },
2612
2611
  publishConfig: {
2613
2612
  access: "public",
@@ -59080,24 +59079,14 @@ async function obtainToken(idp, opts, scope2) {
59080
59079
  scope: scope2,
59081
59080
  audience: opts.audience
59082
59081
  });
59083
- if (opts.onVerification) {
59084
- opts.onVerification({
59085
- verificationUri: device.verification_uri ?? device.verification_uri_complete ?? "",
59086
- verificationUriComplete: device.verification_uri_complete,
59087
- userCode: device.user_code,
59088
- expiresIn: device.expires_in,
59089
- message: device.message
59090
- });
59091
- } else {
59092
- if (device.verification_uri_complete)
59093
- log.info(`Open: ${device.verification_uri_complete}`);
59094
- else if (device.verification_uri)
59095
- log.info(`Open: ${device.verification_uri}`);
59096
- if (device.user_code)
59097
- log.info(`Code: ${device.user_code}`);
59098
- if (device.message)
59099
- log.dim(` ${device.message}`);
59100
- }
59082
+ if (device.verification_uri_complete)
59083
+ log.info(`Open: ${device.verification_uri_complete}`);
59084
+ else if (device.verification_uri)
59085
+ log.info(`Open: ${device.verification_uri}`);
59086
+ if (device.user_code)
59087
+ log.info(`Code: ${device.user_code}`);
59088
+ if (device.message)
59089
+ log.dim(` ${device.message}`);
59101
59090
  return pollDeviceToken({
59102
59091
  idp,
59103
59092
  deviceCode: device.device_code,
@@ -65902,6 +65891,12 @@ var init_session = __esm(() => {
65902
65891
  function adminInstanceMethod(method) {
65903
65892
  return `${ADMIN_INSTANCE}:${method}`;
65904
65893
  }
65894
+ async function callOwnedInstances(client) {
65895
+ return await client.call(adminInstanceMethod("listMine"), {});
65896
+ }
65897
+ function findOwnedInstance(instances, identifier) {
65898
+ return instances.find((instance) => instance.id === identifier || instance.slug === identifier);
65899
+ }
65905
65900
  function formatInstanceLocation(info2) {
65906
65901
  return [info2.state && info2.state !== "ready" ? info2.state : undefined, info2.region, info2.hostId].filter(Boolean).join(" · ");
65907
65902
  }
@@ -66019,6 +66014,8 @@ function adminTargetToInstance(target) {
66019
66014
  };
66020
66015
  }
66021
66016
  function isManagedInstanceNotFound(error52) {
66017
+ if (error52 instanceof AstraleError && error52.code === "INSTANCE_NOT_FOUND")
66018
+ return true;
66022
66019
  if (!(error52 instanceof Error))
66023
66020
  return false;
66024
66021
  if (error52.name === "NotFoundError")
@@ -66384,13 +66381,13 @@ var init_ca_fetch = () => {};
66384
66381
  // src/kernel/client.ts
66385
66382
  async function resolveKernelTarget(opts, config2) {
66386
66383
  if (opts.url && !opts.instance) {
66387
- const resolved2 = await resolveInstanceTarget({ source: "url", url: opts.url }, { config: config2, admin: adminLookupOpts(opts) });
66384
+ const resolved2 = await resolveInstanceTarget({ source: "url", url: opts.url }, { config: config2 });
66388
66385
  return resolvedToKernelTarget(resolved2);
66389
66386
  }
66390
66387
  const resolved = await resolveInstanceTarget(opts.instance ? { source: "name", name: opts.instance } : { source: "active" }, {
66391
66388
  config: config2,
66392
- admin: adminLookupOpts(opts),
66393
- managed: (slug) => lookupManagedInstance(slug, opts)
66389
+ admin: {},
66390
+ managed: (slug) => lookupImplicitOwnedInstance(slug, opts)
66394
66391
  });
66395
66392
  return resolvedToKernelTarget(resolved, opts.url);
66396
66393
  }
@@ -66399,16 +66396,22 @@ async function withKernelClient(opts, fn2) {
66399
66396
  const target = await resolveKernelTarget(opts, config2);
66400
66397
  return withResolvedKernelClient(opts, config2, target, fn2);
66401
66398
  }
66402
- async function lookupManagedInstance(slug, opts) {
66403
- return await withAdminKernelClient(adminLookupOpts(opts), async (ctx) => await ctx.client.call(adminInstanceMethod("info"), { id: slug }));
66399
+ async function listOwnedInstances(opts) {
66400
+ return await withAdminKernelClient(opts, async (ctx) => callOwnedInstances(ctx.client));
66404
66401
  }
66405
- function adminLookupOpts(opts) {
66406
- return {
66407
- as: opts.as,
66408
- creds: opts.creds,
66409
- timeout: opts.timeout,
66410
- debug: opts.debug
66411
- };
66402
+ async function lookupOwnedInstance(slug, opts) {
66403
+ const instance = findOwnedInstance(await listOwnedInstances(opts), slug);
66404
+ if (instance)
66405
+ return instance;
66406
+ throw new AstraleError("INSTANCE_NOT_FOUND", `No owned instance matches "${slug}".`);
66407
+ }
66408
+ async function lookupImplicitOwnedInstance(slug, targetOpts, deps = {
66409
+ lookupOwned: lookupOwnedInstance
66410
+ }) {
66411
+ return await deps.lookupOwned(slug, {
66412
+ timeout: targetOpts.timeout,
66413
+ debug: targetOpts.debug
66414
+ });
66412
66415
  }
66413
66416
  function resolvedToKernelTarget(target, urlOverride) {
66414
66417
  return {
@@ -66564,30 +66567,97 @@ var init_provision_instance = __esm(() => {
66564
66567
  });
66565
66568
 
66566
66569
  // src/setup/steps/instance.ts
66567
- async function fetchManaged(ctx) {
66568
- try {
66569
- return await withSpinner("Checking for existing instances", !ctx.machine, () => withAdminKernelClient(ctx.opts, async (client) => await client.client.call(adminInstanceMethod("list"), {})));
66570
- } catch {
66571
- return [];
66572
- }
66573
- }
66574
66570
  function hero(slug, kernelUrl) {
66575
66571
  console.log("");
66576
66572
  console.log(renderInstanceHero(slug, guiOrigin(kernelUrl)));
66577
66573
  console.log("");
66578
66574
  }
66579
- async function adopt(info2) {
66580
- const { repointedFrom } = await upsertManagedBookmark(info2.slug, info2.slug, info2.url);
66581
- await setActive(info2.slug);
66575
+ async function adoptOwnedInstance(info2, deps = {
66576
+ upsert: upsertManagedBookmark,
66577
+ activate: setActive
66578
+ }) {
66579
+ const { repointedFrom } = await deps.upsert(info2.slug, info2.slug, info2.url, info2.organizationId);
66580
+ await deps.activate(info2.slug);
66582
66581
  if (repointedFrom) {
66583
66582
  log.warn(`Bookmark "${info2.slug}" repointed: ${repointedFrom} → ${normalizeInstanceKernelUrl(info2.url)}`);
66584
66583
  }
66585
66584
  log.success(`Active instance: ${info2.slug}`);
66586
66585
  hero(info2.slug, info2.url);
66587
66586
  }
66587
+ function defaultDependencies(ctx) {
66588
+ return {
66589
+ fetchOwned: (setupCtx) => withSpinner("Checking for existing instances", !setupCtx.machine, () => listOwnedInstances(setupCtx.opts)),
66590
+ adopt: adoptOwnedInstance,
66591
+ selectReady: (instances) => selectFrom("No active instance. Pick one:", instances.map((info2) => ({
66592
+ label: `${info2.slug} ${source_default.dim(guiOrigin(info2.url))}`,
66593
+ value: info2
66594
+ }))),
66595
+ confirmCreate: () => confirmDefaultYes("No instances yet. Provision your first one now?"),
66596
+ promptSlug: () => ctx.slug ? Promise.resolve(ctx.slug) : promptText("Pick a slug for your instance", { validate: slugError }),
66597
+ provision: (slug) => provisionInstance(slug, ctx.opts)
66598
+ };
66599
+ }
66600
+ async function ensureOwnedInstance(ctx, deps = defaultDependencies(ctx)) {
66601
+ let owned;
66602
+ try {
66603
+ owned = await deps.fetchOwned(ctx);
66604
+ } catch (cause) {
66605
+ const detail = cause instanceof Error ? cause.message : String(cause);
66606
+ throw new AstraleError("INSTANCE_DISCOVERY_FAILED", `Could not check your Astrale instances: ${detail}`, "No instance was created. Check `astrale admin status`, then rerun `astrale setup`.");
66607
+ }
66608
+ const ready = owned.filter((info2) => info2.state === "ready");
66609
+ if (ready.length === 1) {
66610
+ await deps.adopt(ready[0]);
66611
+ return "fixed";
66612
+ }
66613
+ if (ready.length > 1) {
66614
+ const choice = await deps.selectReady(ready);
66615
+ if (choice === null) {
66616
+ log.dim(" Skipped — no active instance set.");
66617
+ return "skipped";
66618
+ }
66619
+ await deps.adopt(choice);
66620
+ return "fixed";
66621
+ }
66622
+ if (owned.length > 0) {
66623
+ reportNotReady(owned);
66624
+ return "skipped";
66625
+ }
66626
+ if (!await deps.confirmCreate()) {
66627
+ log.dim(" Skipped — create one later: astrale instance create <slug>");
66628
+ return "skipped";
66629
+ }
66630
+ const slug = await deps.promptSlug();
66631
+ if (!slug) {
66632
+ log.dim(" Skipped — no slug given.");
66633
+ return "skipped";
66634
+ }
66635
+ const { created, selectionError } = await deps.provision(slug);
66636
+ if (selectionError) {
66637
+ const detail = selectionError instanceof Error ? selectionError.message : String(selectionError);
66638
+ throw new AstraleError("INSTANCE_SELECTION_FAILED", `Instance "${slug}" was provisioned, but the CLI could not select it: ${detail}`, `Fix local CLI storage, then run \`astrale instance use ${slug}\`.`);
66639
+ }
66640
+ hero(slug, created.url);
66641
+ return "fixed";
66642
+ }
66643
+ function reportNotReady(instances) {
66644
+ log.warn(`${instances.length === 1 ? "Your instance is" : "Your instances are"} not ready; setup will not create another.`);
66645
+ for (const info2 of instances) {
66646
+ const phase = info2.phase && info2.phase !== info2.state ? ` (${info2.phase})` : "";
66647
+ log.dim(` ${info2.slug}: ${info2.state}${phase} · astrale instance status ${info2.slug}`);
66648
+ if (info2.error)
66649
+ log.dim(` ${info2.error}`);
66650
+ }
66651
+ if (instances.some((info2) => info2.state === "failed")) {
66652
+ log.dim(" Inspect the failure, then deliberately delete/recreate it with `astrale instance` commands.");
66653
+ } else {
66654
+ log.dim(" Wait for provisioning to finish, then rerun `astrale setup`.");
66655
+ }
66656
+ }
66588
66657
  var instanceStep;
66589
66658
  var init_instance2 = __esm(() => {
66590
66659
  init_source();
66660
+ init_errors3();
66591
66661
  init_client2();
66592
66662
  init_instance();
66593
66663
  init_local_status();
@@ -66617,38 +66687,7 @@ var init_instance2 = __esm(() => {
66617
66687
  log.success(`Active instance: ${instance.active} (${guiOrigin(instance.url)})`);
66618
66688
  return "unchanged";
66619
66689
  }
66620
- const managed = await fetchManaged(ctx);
66621
- if (managed.length > 0) {
66622
- const choice = await selectFrom("No active instance. Pick one or create a new instance:", [
66623
- ...managed.map((info2) => ({
66624
- label: `${info2.slug} ${source_default.dim(guiOrigin(info2.url))}`,
66625
- value: info2
66626
- })),
66627
- { label: source_default.cyan("➕ Create a new instance"), value: "create" }
66628
- ]);
66629
- if (choice === null) {
66630
- log.dim(" Skipped — no active instance set.");
66631
- return "skipped";
66632
- }
66633
- if (choice !== "create") {
66634
- await adopt(choice);
66635
- return "fixed";
66636
- }
66637
- } else {
66638
- const yes = await confirmDefaultYes("No instances yet. Provision your first one now?");
66639
- if (!yes) {
66640
- log.dim(" Skipped — create one later: astrale instance create <slug>");
66641
- return "skipped";
66642
- }
66643
- }
66644
- const slug = ctx.slug ?? await promptText("Pick a slug for your instance", { validate: slugError });
66645
- if (!slug) {
66646
- log.dim(" Skipped — no slug given.");
66647
- return "skipped";
66648
- }
66649
- const { created } = await provisionInstance(slug, ctx.opts);
66650
- hero(slug, created.url);
66651
- return "fixed";
66690
+ return ensureOwnedInstance(ctx);
66652
66691
  }
66653
66692
  };
66654
66693
  });
@@ -67027,7 +67066,7 @@ async function resolveUseTarget2(name, opts) {
67027
67066
  return await resolveInstance(chosen.key);
67028
67067
  }
67029
67068
  assertManagedReady(chosen.info);
67030
- const { repointedFrom } = await upsertManagedBookmark(chosen.key, chosen.info.slug, chosen.url);
67069
+ const { repointedFrom } = await upsertManagedBookmark(chosen.key, chosen.info.slug, chosen.url, chosen.info.organizationId);
67031
67070
  if (repointedFrom) {
67032
67071
  log.warn(`Bookmark "${chosen.key}" repointed: ${repointedFrom} → ${chosen.url}`);
67033
67072
  }
@@ -67040,16 +67079,16 @@ async function fetchManagedInstances(name, opts) {
67040
67079
  return [];
67041
67080
  }
67042
67081
  try {
67043
- return await withAdminKernelClient(opts, async (ctx) => await ctx.client.call(adminInstanceMethod("list"), {}));
67082
+ return await listOwnedInstances(opts);
67044
67083
  } catch {
67045
67084
  return [];
67046
67085
  }
67047
67086
  }
67048
67087
  function assertManagedReady(info2) {
67049
- if (!info2.state || info2.state === "ready")
67088
+ if (info2.state === "ready")
67050
67089
  return;
67051
67090
  const detail = info2.phase && info2.phase !== info2.state ? ` (${info2.phase})` : "";
67052
- throw new AstraleError("INSTANCE_NOT_READY", `Instance "${info2.slug}" is ${info2.state}${detail}; it cannot be selected yet.`, info2.error ?? undefined);
67091
+ throw new AstraleError("INSTANCE_NOT_READY", `Instance "${info2.slug}" is ${info2.state}${detail}; it cannot be selected yet.`, info2.error ?? `Run: astrale instance status ${info2.slug}`);
67053
67092
  }
67054
67093
  async function pickCandidate(name, candidates) {
67055
67094
  return selectFrom(`Multiple instances match "${name}":`, candidates.map((candidate) => ({
@@ -68002,8 +68041,8 @@ async function buildSelfContext(opts) {
68002
68041
  } else {
68003
68042
  const resolved = await resolveInstanceTarget(opts.instance ? { source: "name", name: opts.instance } : { source: "active" }, {
68004
68043
  config: config2,
68005
- admin: adminLookupOpts2(opts),
68006
- managed: (instanceSlug) => lookupManagedInstance2(instanceSlug, opts)
68044
+ admin: {},
68045
+ managed: (instanceSlug) => lookupImplicitOwnedInstance(instanceSlug, opts)
68007
68046
  });
68008
68047
  slug = resolved.name;
68009
68048
  defaultIdentity = resolved.defaultIdentity;
@@ -68034,17 +68073,6 @@ async function buildSelfContext(opts) {
68034
68073
  }
68035
68074
  return { identity: identity4, instanceSlug: slug, credsJwt: opts.creds, instanceSigned, idpSubject };
68036
68075
  }
68037
- function adminLookupOpts2(opts) {
68038
- return {
68039
- as: opts.as,
68040
- creds: opts.creds,
68041
- timeout: opts.timeout,
68042
- debug: opts.debug
68043
- };
68044
- }
68045
- async function lookupManagedInstance2(slug, opts) {
68046
- return await withAdminKernelClient(adminLookupOpts2(opts), async (ctx) => await ctx.client.call(adminInstanceMethod("info"), { id: slug }));
68047
- }
68048
68076
  async function withSelfHint(fn2, meta3) {
68049
68077
  if (!meta3)
68050
68078
  return fn2();
@@ -69727,67 +69755,6 @@ async function findFreePort(start, span2 = 20, host = LOOPBACK) {
69727
69755
  var LOOPBACK = "127.0.0.1";
69728
69756
  var init_port = () => {};
69729
69757
 
69730
- // src/lib/view/resolve.ts
69731
- function parseViewSpec(spec) {
69732
- if (VIEW_PATH_RE.test(spec))
69733
- return { kind: "view", path: spec };
69734
- if (spec.startsWith("/") || spec.startsWith("@"))
69735
- return { kind: "target", path: spec };
69736
- throw new AstraleError("INVALID_ARGUMENT", `"${spec}" is neither a ViewPath (/:origin:view.slug) nor a node path (/… or @id)`);
69737
- }
69738
- async function resolveViewCandidates(ctx, nodePath) {
69739
- const result = await ctx.client.call(VIEW_RESOLVE_PATH, { node: nodePath });
69740
- if (!Array.isArray(result)) {
69741
- throw new AstraleError("UNEXPECTED_RESULT", `View:resolve returned a non-array for ${nodePath}`);
69742
- }
69743
- return result;
69744
- }
69745
- function candidateSlug(candidate) {
69746
- const fromPath = candidate.path?.split("/").pop() ?? "";
69747
- return candidate.name ?? fromPath;
69748
- }
69749
- function pickCandidate2(candidates, nodePath, slug) {
69750
- if (slug) {
69751
- const match = candidates.find((c) => candidateSlug(c) === slug || c.path.endsWith(`/${slug}`));
69752
- if (!match) {
69753
- throw new AstraleError("VIEW_NOT_FOUND", `No view "${slug}" on ${nodePath} — available: ${candidates.map(candidateSlug).join(", ") || "(none)"}`);
69754
- }
69755
- return match;
69756
- }
69757
- if (candidates.length === 0) {
69758
- throw new AstraleError("VIEW_NOT_FOUND", `No views resolve on ${nodePath}`);
69759
- }
69760
- if (candidates.length === 1)
69761
- return candidates[0];
69762
- return "ambiguous";
69763
- }
69764
- function applyViewUrlOverride(resolvedUrl, override) {
69765
- const parsed = parseUrl(override);
69766
- if (parsed.pathname !== "/")
69767
- return parsed.toString();
69768
- const original = parseUrl(resolvedUrl);
69769
- return new URL(original.pathname + original.search + original.hash, parsed.origin).toString();
69770
- }
69771
- function rewriteLocalViewUrl(url2) {
69772
- const parsed = parseUrl(url2);
69773
- if (parsed.hostname !== "host.docker.internal")
69774
- return url2;
69775
- parsed.hostname = "127.0.0.1";
69776
- return parsed.toString();
69777
- }
69778
- function parseUrl(raw) {
69779
- try {
69780
- return new URL(raw);
69781
- } catch {
69782
- throw new AstraleError("INVALID_URL", `Not a valid URL: ${raw}`);
69783
- }
69784
- }
69785
- var VIEW_RESOLVE_PATH = "/:kernel.astrale.ai:class.View:resolve", VIEW_PATH_RE;
69786
- var init_resolve7 = __esm(() => {
69787
- init_errors3();
69788
- VIEW_PATH_RE = /^\/:[^\s/:@]+:view\.[a-z][a-z0-9-]*$/;
69789
- });
69790
-
69791
69758
  // src/lib/view/session.ts
69792
69759
  import { mkdir as mkdir9, readdir as readdir2, readFile as readFile10, rm as rm2, writeFile as writeFile8 } from "node:fs/promises";
69793
69760
  import { join as join10 } from "node:path";
@@ -69858,11 +69825,132 @@ var init_session2 = __esm(() => {
69858
69825
  VIEW_DIR = join10(paths.home, "view");
69859
69826
  });
69860
69827
 
69861
- // src/lib/view/server.ts
69828
+ // src/lib/view/port-allocation.ts
69829
+ import { join as join11 } from "node:path";
69830
+ function withViewPortAllocationLock(fn2, lockPath = VIEW_PORT_LOCK) {
69831
+ return withFileLock(lockPath, fn2);
69832
+ }
69833
+ var VIEW_PORT_LOCK;
69834
+ var init_port_allocation = __esm(() => {
69835
+ init_fs_atomic();
69836
+ init_session2();
69837
+ VIEW_PORT_LOCK = join11(VIEW_DIR, "ports.lock");
69838
+ });
69839
+
69840
+ // src/lib/view/resolve.ts
69841
+ function parseViewSpec(spec) {
69842
+ if (VIEW_PATH_RE.test(spec))
69843
+ return { kind: "view", path: spec };
69844
+ if (spec.startsWith("/") || spec.startsWith("@"))
69845
+ return { kind: "target", path: spec };
69846
+ throw new AstraleError("INVALID_ARGUMENT", `"${spec}" is neither a ViewPath (/:origin:view.slug) nor a node path (/… or @id)`);
69847
+ }
69848
+ async function resolveViewCandidates(ctx, nodePath) {
69849
+ const result = await ctx.client.call(VIEW_RESOLVE_PATH, { node: nodePath });
69850
+ if (!Array.isArray(result)) {
69851
+ throw new AstraleError("UNEXPECTED_RESULT", `View:resolve returned a non-array for ${nodePath}`);
69852
+ }
69853
+ return result;
69854
+ }
69855
+ function candidateSlug(candidate) {
69856
+ const fromPath = candidate.path?.split("/").pop() ?? "";
69857
+ return candidate.name ?? fromPath;
69858
+ }
69859
+ function pickCandidate2(candidates, nodePath, slug) {
69860
+ if (slug) {
69861
+ const match = candidates.find((c) => candidateSlug(c) === slug || c.path.endsWith(`/${slug}`));
69862
+ if (!match) {
69863
+ throw new AstraleError("VIEW_NOT_FOUND", `No view "${slug}" on ${nodePath} — available: ${candidates.map(candidateSlug).join(", ") || "(none)"}`);
69864
+ }
69865
+ return match;
69866
+ }
69867
+ if (candidates.length === 0) {
69868
+ throw new AstraleError("VIEW_NOT_FOUND", `No views resolve on ${nodePath}`);
69869
+ }
69870
+ if (candidates.length === 1)
69871
+ return candidates[0];
69872
+ return "ambiguous";
69873
+ }
69874
+ function applyViewUrlOverride(resolvedUrl, override) {
69875
+ const parsed = parseUrl(override);
69876
+ if (parsed.pathname !== "/")
69877
+ return parsed.toString();
69878
+ const original = parseUrl(resolvedUrl);
69879
+ return new URL(original.pathname + original.search + original.hash, parsed.origin).toString();
69880
+ }
69881
+ function rewriteLocalViewUrl(url2) {
69882
+ const parsed = parseUrl(url2);
69883
+ if (parsed.hostname !== "host.docker.internal")
69884
+ return url2;
69885
+ parsed.hostname = "127.0.0.1";
69886
+ return parsed.toString();
69887
+ }
69888
+ function parseUrl(raw) {
69889
+ try {
69890
+ return new URL(raw);
69891
+ } catch {
69892
+ throw new AstraleError("INVALID_URL", `Not a valid URL: ${raw}`);
69893
+ }
69894
+ }
69895
+ var VIEW_RESOLVE_PATH = "/:kernel.astrale.ai:class.View:resolve", VIEW_PATH_RE;
69896
+ var init_resolve7 = __esm(() => {
69897
+ init_errors3();
69898
+ VIEW_PATH_RE = /^\/:[^\s/:@]+:view\.[a-z][a-z0-9-]*$/;
69899
+ });
69900
+
69901
+ // src/lib/view/assets.ts
69862
69902
  import { existsSync as existsSync5 } from "node:fs";
69903
+ import { copyFile as copyFile2 } from "node:fs/promises";
69904
+ import { dirname as dirname8, join as join12 } from "node:path";
69905
+ import { fileURLToPath } from "node:url";
69906
+ function viewerDistDir(moduleUrl = import.meta.url, entry = process.argv[1] ?? ".") {
69907
+ const override = process.env.ASTRALE_VIEWER_DIR;
69908
+ if (override)
69909
+ return override;
69910
+ const moduleDirectory = dirname8(fileURLToPath(moduleUrl));
69911
+ const published = join12(moduleDirectory, "..", "viewer", "dist");
69912
+ const source = join12(moduleDirectory, "..", "..", "..", "viewer", "dist");
69913
+ const legacy = join12(dirname8(entry), "..", "viewer", "dist");
69914
+ const complete = [published, source, legacy].find(hasViewerBundle);
69915
+ if (complete)
69916
+ return complete;
69917
+ if (hasViewerSource(join12(source, "..")))
69918
+ return source;
69919
+ return published;
69920
+ }
69921
+ async function ensureViewerAssets(moduleUrl = import.meta.url, entry = process.argv[1] ?? ".") {
69922
+ const dist = viewerDistDir(moduleUrl, entry);
69923
+ if (hasViewerBundle(dist))
69924
+ return dist;
69925
+ const srcDir = join12(dist, "..");
69926
+ const bun = globalThis.Bun;
69927
+ if (bun && hasViewerSource(srcDir)) {
69928
+ const result = await bun.build({
69929
+ entrypoints: [join12(srcDir, "main.ts")],
69930
+ outdir: dist,
69931
+ target: "browser",
69932
+ minify: false
69933
+ });
69934
+ if (!result.success)
69935
+ throw new Error(`viewer build failed: ${result.logs.join(`
69936
+ `)}`);
69937
+ await copyFile2(join12(srcDir, "index.html"), join12(dist, "index.html"));
69938
+ return dist;
69939
+ }
69940
+ throw new Error(`viewer bundle missing at ${dist} — reinstall the CLI (or run \`bun scripts/build.ts\` in a dev checkout)`);
69941
+ }
69942
+ function hasViewerBundle(directory) {
69943
+ return existsSync5(join12(directory, "main.js")) && existsSync5(join12(directory, "index.html"));
69944
+ }
69945
+ function hasViewerSource(directory) {
69946
+ return existsSync5(join12(directory, "main.ts")) && existsSync5(join12(directory, "index.html"));
69947
+ }
69948
+ var init_assets = () => {};
69949
+
69950
+ // src/lib/view/server.ts
69863
69951
  import { readFile as readFile11 } from "node:fs/promises";
69864
69952
  import { createServer } from "node:http";
69865
- import { dirname as dirname8, join as join11 } from "node:path";
69953
+ import { join as join13 } from "node:path";
69866
69954
  import { Readable } from "node:stream";
69867
69955
  function startViewServer(config2) {
69868
69956
  const { session, proxy } = config2;
@@ -69920,11 +70008,11 @@ function startViewServer(config2) {
69920
70008
  return;
69921
70009
  }
69922
70010
  if (sub === "/" || sub === "/index.html") {
69923
- await serveAsset(res, join11(hostDir, "index.html"), "text/html; charset=utf-8");
70011
+ await serveAsset(res, join13(hostDir, "index.html"), "text/html; charset=utf-8");
69924
70012
  return;
69925
70013
  }
69926
70014
  if (sub === "/main.js") {
69927
- await serveAsset(res, join11(hostDir, "main.js"), "text/javascript; charset=utf-8");
70015
+ await serveAsset(res, join13(hostDir, "main.js"), "text/javascript; charset=utf-8");
69928
70016
  return;
69929
70017
  }
69930
70018
  if (sub === "/config.json" && req.method === "GET") {
@@ -70009,34 +70097,6 @@ function startViewServer(config2) {
70009
70097
  server.listen(session.port, "127.0.0.1");
70010
70098
  return server;
70011
70099
  }
70012
- function viewerDistDir() {
70013
- const override = process.env.ASTRALE_VIEWER_DIR;
70014
- if (override)
70015
- return override;
70016
- return join11(dirname8(process.argv[1] ?? "."), "..", "viewer", "dist");
70017
- }
70018
- async function ensureViewerAssets() {
70019
- const dist = viewerDistDir();
70020
- if (existsSync5(join11(dist, "main.js")) && existsSync5(join11(dist, "index.html")))
70021
- return dist;
70022
- const srcDir = join11(dist, "..");
70023
- const bun = globalThis.Bun;
70024
- if (bun && existsSync5(join11(srcDir, "main.ts"))) {
70025
- const result = await bun.build({
70026
- entrypoints: [join11(srcDir, "main.ts")],
70027
- outdir: dist,
70028
- target: "browser",
70029
- minify: false
70030
- });
70031
- if (!result.success)
70032
- throw new Error(`viewer build failed: ${result.logs.join(`
70033
- `)}`);
70034
- const { copyFile: copyFile2 } = await import("node:fs/promises");
70035
- await copyFile2(join11(srcDir, "index.html"), join11(dist, "index.html"));
70036
- return dist;
70037
- }
70038
- throw new Error(`viewer bundle missing at ${dist} — reinstall the CLI (or run \`bun scripts/build.ts\` in a dev checkout)`);
70039
- }
70040
70100
  function jwtExpiry(token) {
70041
70101
  try {
70042
70102
  const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString("utf8"));
@@ -70093,7 +70153,9 @@ var TOKEN_REFRESH_MARGIN_MS, FALLBACK_TOKEN_TTL_MS = 3600000, IDLE_SWEEP_MS = 60
70093
70153
  var init_server = __esm(() => {
70094
70154
  init_kernel2();
70095
70155
  init_ca_fetch();
70156
+ init_assets();
70096
70157
  init_session2();
70158
+ init_assets();
70097
70159
  TOKEN_REFRESH_MARGIN_MS = 5 * 60000;
70098
70160
  });
70099
70161
 
@@ -70174,7 +70236,7 @@ __export(exports_view, {
70174
70236
  import { randomBytes } from "node:crypto";
70175
70237
  import { closeSync, existsSync as existsSync6, openSync, statSync } from "node:fs";
70176
70238
  import { mkdir as mkdir10, readdir as readdir3, readFile as readFile12, writeFile as writeFile9 } from "node:fs/promises";
70177
- import { dirname as dirname9, join as join12 } from "node:path";
70239
+ import { dirname as dirname9, join as join14 } from "node:path";
70178
70240
  async function resolveSession(spec, opts) {
70179
70241
  const parsed = spec ? parseViewSpec(spec) : undefined;
70180
70242
  if (parsed?.kind === "target" && opts.target) {
@@ -70250,7 +70312,7 @@ async function resolveServeRuntime() {
70250
70312
  if (node2 && entry?.endsWith(".js") && existsSync6(entry))
70251
70313
  return { file: node2, args: [entry] };
70252
70314
  if (node2 && entry?.endsWith(".ts")) {
70253
- const dist = join12(dirname9(entry), "..", "dist", "astrale.js");
70315
+ const dist = join14(dirname9(entry), "..", "dist", "astrale.js");
70254
70316
  await ensureDevDist(entry, dist);
70255
70317
  if (existsSync6(dist))
70256
70318
  return { file: node2, args: [dist] };
@@ -70268,7 +70330,7 @@ async function ensureDevDist(entry, dist) {
70268
70330
  const bun = globalThis.Bun;
70269
70331
  if (!bun)
70270
70332
  return;
70271
- const srcDir = join12(dirname9(entry), "..", "src");
70333
+ const srcDir = join14(dirname9(entry), "..", "src");
70272
70334
  if (existsSync6(dist) && !await newerThan(srcDir, statSync(dist).mtimeMs))
70273
70335
  return;
70274
70336
  console.error("(dev) building dist/astrale.js for the session server…");
@@ -70284,7 +70346,7 @@ async function newerThan(dir, mtimeMs) {
70284
70346
  for (const item of entries) {
70285
70347
  if (!item.isFile())
70286
70348
  continue;
70287
- if (statSync(join12(item.parentPath, item.name)).mtimeMs > mtimeMs)
70349
+ if (statSync(join14(item.parentPath, item.name)).mtimeMs > mtimeMs)
70288
70350
  return true;
70289
70351
  }
70290
70352
  return false;
@@ -70293,11 +70355,18 @@ async function startSession(view, target, opts) {
70293
70355
  await ensureViewerAssets();
70294
70356
  const config2 = await readConfig();
70295
70357
  const kernelTarget = await resolveKernelTarget(opts, config2);
70358
+ const [instances, identities, runtime] = await Promise.all([
70359
+ readInstances(),
70360
+ readIdentities(),
70361
+ resolveServeRuntime()
70362
+ ]);
70363
+ return withViewPortAllocationLock(() => startSessionLocked(view, target, opts, kernelTarget, instances.active, identities.default, runtime));
70364
+ }
70365
+ async function startSessionLocked(view, target, opts, kernelTarget, activeInstance, defaultIdentity, runtime) {
70296
70366
  const port = await findFreePort(VIEW_PORT_BASE, VIEW_PORT_SPAN);
70297
70367
  if (port === null) {
70298
- fatal(new Error(`No free port in ${VIEW_PORT_BASE}-${VIEW_PORT_BASE + VIEW_PORT_SPAN - 1} — close sessions with \`astrale view --close --all\``));
70368
+ throw new Error(`No free port in ${VIEW_PORT_BASE}-${VIEW_PORT_BASE + VIEW_PORT_SPAN - 1} — close sessions with \`astrale view --close --all\``);
70299
70369
  }
70300
- const [instances, identities] = await Promise.all([readInstances(), readIdentities()]);
70301
70370
  const id = `v-${randomBytes(3).toString("hex")}`;
70302
70371
  const nonce = randomBytes(12).toString("hex");
70303
70372
  const record2 = {
@@ -70308,8 +70377,8 @@ async function startSession(view, target, opts) {
70308
70377
  pageUrl: `http://127.0.0.1:${port}/s/${nonce}/`,
70309
70378
  view,
70310
70379
  target,
70311
- instance: opts.instance ?? (opts.url ? opts.url : instances.active),
70312
- identity: opts.creds ? "(pre-signed creds)" : opts.as ?? identities.default,
70380
+ instance: opts.instance ?? (opts.url ? opts.url : activeInstance),
70381
+ identity: opts.creds ? "(pre-signed creds)" : opts.as ?? defaultIdentity,
70313
70382
  createdAt: new Date().toISOString()
70314
70383
  };
70315
70384
  const serveConfig = {
@@ -70331,7 +70400,6 @@ async function startSession(view, target, opts) {
70331
70400
  await mkdir10(VIEW_DIR, { recursive: true });
70332
70401
  await writeFile9(configPath(id), JSON.stringify(serveConfig, null, 2));
70333
70402
  const logFd = openSync(logPath(id), "a");
70334
- const runtime = await resolveServeRuntime();
70335
70403
  const child = spawnHandle(runtime.file, [...runtime.args, "__view-serve", "--config", configPath(id)], {
70336
70404
  detached: true,
70337
70405
  stdio: ["ignore", logFd, logFd]
@@ -70339,7 +70407,7 @@ async function startSession(view, target, opts) {
70339
70407
  child.unref();
70340
70408
  closeSync(logFd);
70341
70409
  if (!child.pid)
70342
- fatal(new Error("Failed to spawn the view session server"));
70410
+ throw new Error("Failed to spawn the view session server");
70343
70411
  const live = { ...record2, pid: child.pid };
70344
70412
  await saveRecord(live);
70345
70413
  const deadline = Date.now() + READY_TIMEOUT_MS;
@@ -70355,9 +70423,9 @@ async function startSession(view, target, opts) {
70355
70423
  }
70356
70424
  const tail = await readFile12(logPath(id), "utf8").catch(() => "");
70357
70425
  await closeSession(live);
70358
- return fatal(new Error(`View session server did not come up.${tail ? `
70426
+ throw new Error(`View session server did not come up.${tail ? `
70359
70427
  --- server log ---
70360
- ${tail.slice(-2000)}` : ""}`));
70428
+ ${tail.slice(-2000)}` : ""}`);
70361
70429
  }
70362
70430
  async function waitForPageState(record2) {
70363
70431
  const deadline = Date.now() + STATE_TIMEOUT_MS;
@@ -70502,6 +70570,7 @@ var init_view4 = __esm(() => {
70502
70570
  init_output();
70503
70571
  init_port();
70504
70572
  init_proc();
70573
+ init_port_allocation();
70505
70574
  init_resolve7();
70506
70575
  init_server();
70507
70576
  init_session2();
@@ -70886,28 +70955,28 @@ __export(exports_studio, {
70886
70955
  default: () => studio_default
70887
70956
  });
70888
70957
  import { existsSync as existsSync8, realpathSync } from "node:fs";
70889
- import { dirname as dirname10, join as join13, resolve } from "node:path";
70958
+ import { dirname as dirname10, join as join15, resolve } from "node:path";
70890
70959
  function resolveStudioDir() {
70891
70960
  const candidates = [];
70892
70961
  if (process.env.ASTRALE_STUDIO_DIR)
70893
70962
  candidates.push(process.env.ASTRALE_STUDIO_DIR);
70894
70963
  try {
70895
70964
  const entryDir = dirname10(realpathSync(process.argv[1] ?? ""));
70896
- candidates.push(join13(entryDir, "..", "studio"), join13(entryDir, "studio"));
70965
+ candidates.push(join15(entryDir, "..", "studio"), join15(entryDir, "studio"));
70897
70966
  } catch {}
70898
70967
  for (const c of candidates) {
70899
- if (existsSync8(join13(c, "server", "index.ts")))
70968
+ if (existsSync8(join15(c, "server", "index.ts")))
70900
70969
  return resolve(c);
70901
70970
  }
70902
70971
  throw new Error(`Domain Studio assets not found (looked in: ${candidates.join(", ") || "<none>"}). ` + `Reinstall the astrale CLI, or set ASTRALE_STUDIO_DIR to a studio checkout.`);
70903
70972
  }
70904
70973
  function isDevSource(studioDir) {
70905
- return existsSync8(join13(studioDir, "vite.config.ts")) && existsSync8(join13(studioDir, "client", "src"));
70974
+ return existsSync8(join15(studioDir, "vite.config.ts")) && existsSync8(join15(studioDir, "client", "src"));
70906
70975
  }
70907
70976
  function resolveViteBin(studioDir) {
70908
70977
  for (const c of [
70909
- join13(studioDir, "node_modules", ".bin", "vite"),
70910
- join13(studioDir, "..", "..", "node_modules", ".bin", "vite")
70978
+ join15(studioDir, "node_modules", ".bin", "vite"),
70979
+ join15(studioDir, "..", "..", "node_modules", ".bin", "vite")
70911
70980
  ]) {
70912
70981
  if (existsSync8(c))
70913
70982
  return c;
@@ -70982,6 +71051,11 @@ var init_studio = __esm(() => {
70982
71051
  },
70983
71052
  { flags: "--prod", description: "Serve the prebuilt client (the default)" },
70984
71053
  { flags: "--schema-dir <dir>", description: "Schema entry dir to scan for (default: schema)" },
71054
+ {
71055
+ flags: "--harness <name>",
71056
+ description: "Lock the agent harness for this Studio process",
71057
+ choices: ["claude", "codex"]
71058
+ },
70985
71059
  ...RAW_OUTPUT_OPTIONS
70986
71060
  ],
70987
71061
  afterHelpText: `
@@ -71005,6 +71079,10 @@ Behavior:
71005
71079
  By default the command just PRINTS the URL — it does not pop a browser (that's
71006
71080
  invasive when you already have a tab open). Pass --open to launch one.
71007
71081
 
71082
+ Agent harness: choose Claude Code or Codex per domain in Studio Settings. Pass
71083
+ --harness claude|codex to lock this process to one harness. Each harness uses
71084
+ its existing local authentication and keeps an independent resumable session.
71085
+
71008
71086
  The command stays attached and supervises its child processes; Ctrl-C tears
71009
71087
  them all down (a second Ctrl-C force-kills). With --json it prints a
71010
71088
  { url, port, mode, workspace } descriptor.
@@ -71013,6 +71091,7 @@ Examples:
71013
71091
  $ astrale studio # start the studio + print its URL (no browser)
71014
71092
  $ astrale studio --open # …and open it in a browser
71015
71093
  $ astrale studio ./my-domain
71094
+ $ astrale studio --harness codex
71016
71095
  $ astrale studio --port 4400
71017
71096
  $ astrale studio --dev # live-edit the studio itself (from source)
71018
71097
  `,
@@ -71115,12 +71194,13 @@ Examples:
71115
71194
  DOMAIN_STUDIO_DEV: "1",
71116
71195
  VITE_URL: `http://127.0.0.1:${vitePort}`,
71117
71196
  PORT: String(studioPort),
71118
- DOMAIN_STUDIO_HOST: "127.0.0.1"
71197
+ DOMAIN_STUDIO_HOST: "127.0.0.1",
71198
+ ...opts.harness ? { DOMAIN_STUDIO_HARNESS: opts.harness } : {}
71119
71199
  }
71120
71200
  }), "server");
71121
71201
  } else {
71122
- const dist = join13(studioDir, "client", "dist");
71123
- if (!existsSync8(join13(dist, "index.html"))) {
71202
+ const dist = join15(studioDir, "client", "dist");
71203
+ if (!existsSync8(join15(dist, "index.html"))) {
71124
71204
  throw new Error(`studio client not built at ${dist} — run: pnpm --filter @astrale-os/studio build`);
71125
71205
  }
71126
71206
  serverChild = supervise(spawnHandle("bun", ["server/index.ts", workspace, "--port", String(studioPort), "--no-open"], {
@@ -71130,7 +71210,8 @@ Examples:
71130
71210
  ...process.env,
71131
71211
  DOMAIN_STUDIO_DIST: dist,
71132
71212
  PORT: String(studioPort),
71133
- DOMAIN_STUDIO_HOST: "127.0.0.1"
71213
+ DOMAIN_STUDIO_HOST: "127.0.0.1",
71214
+ ...opts.harness ? { DOMAIN_STUDIO_HARNESS: opts.harness } : {}
71134
71215
  }
71135
71216
  }), "server");
71136
71217
  }
@@ -71242,7 +71323,7 @@ var init_list = __esm(() => {
71242
71323
  }));
71243
71324
  let managed = [];
71244
71325
  if (!opts.bookmarked) {
71245
- managed = await withSpinner("Fetching instances", !isMachine(opts), () => withAdminKernelClient(opts, async (ctx) => await ctx.client.call(adminInstanceMethod("list"), {})));
71326
+ managed = await withSpinner("Fetching instances", !isMachine(opts), () => listOwnedInstances(opts));
71246
71327
  }
71247
71328
  if (isMachine(opts)) {
71248
71329
  output({
@@ -71499,6 +71580,7 @@ __export(exports_status2, {
71499
71580
  var status_default2;
71500
71581
  var init_status2 = __esm(() => {
71501
71582
  init_source();
71583
+ init_errors3();
71502
71584
  init_client2();
71503
71585
  init_admin_target();
71504
71586
  init_log();
@@ -71510,20 +71592,26 @@ var init_status2 = __esm(() => {
71510
71592
  options: [...ADMIN_TARGET_OPTIONS],
71511
71593
  action: async (id, opts) => {
71512
71594
  try {
71513
- const result = await withSpinner(`Fetching instance ${id}`, !isMachine(opts), () => withAdminKernelClient(opts, async (ctx) => await ctx.client.call(adminInstanceMethod("info"), { id })));
71595
+ const result = await withSpinner(`Fetching instance ${id}`, !isMachine(opts), () => listOwnedInstances(opts).then((instances) => {
71596
+ const match = findOwnedInstance(instances, id);
71597
+ if (match)
71598
+ return match;
71599
+ throw new AstraleError("INSTANCE_NOT_FOUND", `No owned instance matches "${id}".`, "Run `astrale instance list` to see your instances.");
71600
+ }));
71514
71601
  if (isMachine(opts)) {
71515
71602
  output(result, opts);
71516
71603
  return;
71517
71604
  }
71518
71605
  console.log(`${source_default.bold(result.slug)} ${source_default.dim(result.url)}`);
71519
- if (result.state)
71520
- log.dim(` state: ${result.state}${result.phase ? ` (${result.phase})` : ""}`);
71606
+ log.dim(` state: ${result.state}${result.phase ? ` (${result.phase})` : ""}`);
71521
71607
  if (result.error)
71522
71608
  log.dim(` error: ${result.error}`);
71523
71609
  if (result.region)
71524
71610
  log.dim(` region: ${result.region}`);
71525
71611
  if (result.hostId)
71526
71612
  log.dim(` host: ${result.hostId}`);
71613
+ if (result.organizationId)
71614
+ log.dim(` organization: ${result.organizationId}`);
71527
71615
  if (result.createdAt)
71528
71616
  log.dim(` created: ${result.createdAt}`);
71529
71617
  } catch (e) {
@@ -71548,7 +71636,9 @@ async function resolveActiveForDisplay() {
71548
71636
  };
71549
71637
  }
71550
71638
  try {
71551
- const managed = await withAdminKernelClient({}, async (ctx) => await ctx.client.call(adminInstanceMethod("info"), { id: active.name }));
71639
+ const managed = findOwnedInstance(await listOwnedInstances({}), active.name);
71640
+ if (!managed)
71641
+ return { name: active.name };
71552
71642
  return {
71553
71643
  name: managed.slug,
71554
71644
  url: normalizeInstanceKernelUrl(managed.url),
@@ -71834,7 +71924,7 @@ async function installViaAdmin(target, opts) {
71834
71924
  const adminOpts = { ...opts, instance: undefined };
71835
71925
  try {
71836
71926
  await withAdminKernelClient(adminOpts, async (ctx) => {
71837
- const instances = await ctx.client.call(adminInstanceMethod("list"), {});
71927
+ const instances = await callOwnedInstances(ctx.client);
71838
71928
  const ref2 = await resolveDomainRef(ctx, target, interactive);
71839
71929
  const slug = await resolveTargetSlug(opts, target, interactive, instances);
71840
71930
  const match = instances.find((i) => i.slug === slug);
@@ -71842,6 +71932,7 @@ async function installViaAdmin(target, opts) {
71842
71932
  const known = instances.map((i) => i.slug).join(", ") || "(none)";
71843
71933
  throw new AstraleError("INSTANCE_NOT_MANAGED", `Instance "${slug}" is not admin-managed (managed: ${known}).`, `Install the url directly onto it instead: astrale domain install <url> --direct -i ${slug}`);
71844
71934
  }
71935
+ assertInstallTargetReady(match);
71845
71936
  const label = ref2.origin ?? ref2.url ?? "domain";
71846
71937
  const result = await withSpinner(`Installing ${label} on ${match.slug}`, !isMachine(opts), () => ctx.client.call(adminDomainMethod("install"), {
71847
71938
  instanceId: match.slug,
@@ -71858,9 +71949,15 @@ async function installViaAdmin(target, opts) {
71858
71949
  log.dim(` url: ${result.url}`);
71859
71950
  });
71860
71951
  } catch (e) {
71861
- fatal(e);
71952
+ fatal(e, opts);
71862
71953
  }
71863
71954
  }
71955
+ function assertInstallTargetReady(instance) {
71956
+ if (instance.state === "ready")
71957
+ return;
71958
+ const detail = instance.phase && instance.phase !== instance.state ? ` (${instance.phase})` : "";
71959
+ throw new AstraleError("INSTANCE_NOT_READY", `Instance "${instance.slug}" is ${instance.state}${detail}; domains cannot be installed yet.`, instance.error ?? `Run: astrale instance status ${instance.slug}`);
71960
+ }
71864
71961
  function domainRefFromTarget(target) {
71865
71962
  return isHttpUrl(target) ? { url: target } : { origin: target };
71866
71963
  }
@@ -72959,21 +73056,21 @@ var init_status4 = __esm(() => {
72959
73056
 
72960
73057
  // src/telemetry/store.ts
72961
73058
  import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
72962
- import { join as join14 } from "node:path";
73059
+ import { join as join16 } from "node:path";
72963
73060
  function sessionsRoot() {
72964
- return join14(createPaths().home, "sessions");
73061
+ return join16(createPaths().home, "sessions");
72965
73062
  }
72966
73063
  function sessionDir(id) {
72967
- return join14(sessionsRoot(), id);
73064
+ return join16(sessionsRoot(), id);
72968
73065
  }
72969
73066
  function eventsPath(id) {
72970
- return join14(sessionDir(id), "events.jsonl");
73067
+ return join16(sessionDir(id), "events.jsonl");
72971
73068
  }
72972
73069
  function metaPath(id) {
72973
- return join14(sessionDir(id), "meta.json");
73070
+ return join16(sessionDir(id), "meta.json");
72974
73071
  }
72975
73072
  function markerPath(id) {
72976
- return join14(sessionDir(id), ".analyzed");
73073
+ return join16(sessionDir(id), ".analyzed");
72977
73074
  }
72978
73075
  function readJsonSafe(path4) {
72979
73076
  try {
@@ -73051,12 +73148,12 @@ var init_list4 = __esm(() => {
73051
73148
  // src/telemetry/adapters/claude-code.ts
73052
73149
  import { existsSync as existsSync10, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
73053
73150
  import { homedir as homedir3 } from "node:os";
73054
- import { join as join15 } from "node:path";
73151
+ import { join as join17 } from "node:path";
73055
73152
  function mungeCwd(cwd) {
73056
73153
  return cwd.replace(/[^a-zA-Z0-9]/g, "-");
73057
73154
  }
73058
- function claudeCodeAdapter(base2 = join15(homedir3(), ".claude")) {
73059
- const projectsDir = join15(base2, "projects");
73155
+ function claudeCodeAdapter(base2 = join17(homedir3(), ".claude")) {
73156
+ const projectsDir = join17(base2, "projects");
73060
73157
  function detect2() {
73061
73158
  try {
73062
73159
  return existsSync10(projectsDir);
@@ -73080,7 +73177,7 @@ function claudeCodeAdapter(base2 = join15(homedir3(), ".claude")) {
73080
73177
  for (const dir of dirs) {
73081
73178
  if (dir !== munged && !dir.startsWith(prefix))
73082
73179
  continue;
73083
- const projectPath = join15(projectsDir, dir);
73180
+ const projectPath = join17(projectsDir, dir);
73084
73181
  let files;
73085
73182
  try {
73086
73183
  files = readdirSync3(projectPath);
@@ -73090,7 +73187,7 @@ function claudeCodeAdapter(base2 = join15(homedir3(), ".claude")) {
73090
73187
  for (const file2 of files) {
73091
73188
  if (!file2.endsWith(".jsonl"))
73092
73189
  continue;
73093
- const transcriptPath = join15(projectPath, file2);
73190
+ const transcriptPath = join17(projectPath, file2);
73094
73191
  try {
73095
73192
  const st = statSync3(transcriptPath);
73096
73193
  const mtimeMs = st.mtime.getTime();
@@ -73126,7 +73223,7 @@ var init_claude_code = __esm(() => {
73126
73223
  import { existsSync as existsSync11, openSync as openSync2, readdirSync as readdirSync4, readSync, statSync as statSync4 } from "node:fs";
73127
73224
  import { closeSync as closeSync2 } from "node:fs";
73128
73225
  import { homedir as homedir4 } from "node:os";
73129
- import { join as join16 } from "node:path";
73226
+ import { join as join18 } from "node:path";
73130
73227
  function readFirstLine(path4) {
73131
73228
  let fd = null;
73132
73229
  try {
@@ -73166,8 +73263,8 @@ function numericDirs(path4) {
73166
73263
  return [];
73167
73264
  }
73168
73265
  }
73169
- function codexAdapter(base2 = join16(homedir4(), ".codex")) {
73170
- const sessionsDir = join16(base2, "sessions");
73266
+ function codexAdapter(base2 = join18(homedir4(), ".codex")) {
73267
+ const sessionsDir = join18(base2, "sessions");
73171
73268
  function detect2() {
73172
73269
  try {
73173
73270
  return existsSync11(sessionsDir);
@@ -73182,12 +73279,12 @@ function codexAdapter(base2 = join16(homedir4(), ".codex")) {
73182
73279
  const endMs = window2.end.getTime();
73183
73280
  const lowerMs = startMs - DAY_MS;
73184
73281
  for (const yyyy of numericDirs(sessionsDir)) {
73185
- for (const mm of numericDirs(join16(sessionsDir, yyyy))) {
73186
- for (const dd of numericDirs(join16(sessionsDir, yyyy, mm))) {
73282
+ for (const mm of numericDirs(join18(sessionsDir, yyyy))) {
73283
+ for (const dd of numericDirs(join18(sessionsDir, yyyy, mm))) {
73187
73284
  const dayStart = Date.UTC(Number(yyyy), Number(mm) - 1, Number(dd));
73188
73285
  if (dayStart > endMs + DAY_MS || dayStart + DAY_MS <= lowerMs)
73189
73286
  continue;
73190
- scanDay(join16(sessionsDir, yyyy, mm, dd), root, startMs, endMs, sessions);
73287
+ scanDay(join18(sessionsDir, yyyy, mm, dd), root, startMs, endMs, sessions);
73191
73288
  }
73192
73289
  }
73193
73290
  }
@@ -73209,7 +73306,7 @@ function scanDay(dayPath, root, startMs, endMs, out) {
73209
73306
  for (const file2 of files) {
73210
73307
  if (!file2.startsWith("rollout-") || !file2.endsWith(".jsonl"))
73211
73308
  continue;
73212
- const transcriptPath = join16(dayPath, file2);
73309
+ const transcriptPath = join18(dayPath, file2);
73213
73310
  try {
73214
73311
  const st = statSync4(transcriptPath);
73215
73312
  const mtimeMs = st.mtime.getTime();
@@ -73356,7 +73453,7 @@ var init_gate = () => {};
73356
73453
  // src/telemetry/analyze.ts
73357
73454
  import { spawn as spawn3 } from "node:child_process";
73358
73455
  import { appendFileSync, writeFileSync as writeFileSync2 } from "node:fs";
73359
- import { join as join17 } from "node:path";
73456
+ import { join as join19 } from "node:path";
73360
73457
  function writeMarker(id, marker) {
73361
73458
  writeFileSync2(markerPath(id), JSON.stringify(marker, null, 2) + `
73362
73459
  `);
@@ -73452,7 +73549,7 @@ async function analyzeSession(id, opts = {}) {
73452
73549
  const guides = new Map(adapters.map((a) => [a.name, a.readingGuide]));
73453
73550
  const prompt = buildPrompt({ id, root, signals: signals2, guides, file: opts.file ?? false });
73454
73551
  const dir = sessionDir(id);
73455
- writeFileSync2(join17(dir, "analyzer-prompt.md"), prompt);
73552
+ writeFileSync2(join19(dir, "analyzer-prompt.md"), prompt);
73456
73553
  const outcome = await runClaude(prompt, dir, opts);
73457
73554
  const marker = {
73458
73555
  analyzedAt: new Date().toISOString(),
@@ -73460,7 +73557,7 @@ async function analyzeSession(id, opts = {}) {
73460
73557
  note: outcome.note
73461
73558
  };
73462
73559
  writeMarker(id, marker);
73463
- return { ...marker, reportPath: join17(dir, "report.md") };
73560
+ return { ...marker, reportPath: join19(dir, "report.md") };
73464
73561
  }
73465
73562
  function runClaude(prompt, cwd, opts) {
73466
73563
  return new Promise((resolve2) => {
@@ -73490,7 +73587,7 @@ function runClaude(prompt, cwd, opts) {
73490
73587
  child.on("close", (code) => {
73491
73588
  clearTimeout(timer);
73492
73589
  try {
73493
- appendFileSync(join17(cwd, "analyzer.log"), out + (err ? `
73590
+ appendFileSync(join19(cwd, "analyzer.log"), out + (err ? `
73494
73591
  --- stderr ---
73495
73592
  ${err}` : ""));
73496
73593
  } catch {}
@@ -73540,9 +73637,9 @@ var init_settings = __esm(() => {
73540
73637
 
73541
73638
  // src/telemetry/trigger.ts
73542
73639
  import { existsSync as existsSync12, mkdirSync as mkdirSync2, readFileSync as readFileSync6, rmSync as rmSync2, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
73543
- import { join as join18 } from "node:path";
73640
+ import { join as join20 } from "node:path";
73544
73641
  function lockPath() {
73545
- return join18(sessionsRoot(), ".analyzer.lock");
73642
+ return join20(sessionsRoot(), ".analyzer.lock");
73546
73643
  }
73547
73644
  function releaseLock() {
73548
73645
  try {
@@ -74301,12 +74398,12 @@ function redactArgv(argv) {
74301
74398
  init_store2();
74302
74399
  import { createHash as createHash2 } from "node:crypto";
74303
74400
  import { existsSync as existsSync13, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "node:fs";
74304
- import { dirname as dirname12, join as join19 } from "node:path";
74401
+ import { dirname as dirname12, join as join21 } from "node:path";
74305
74402
  var MAX_ID_LEN = 64;
74306
74403
  function findGitRoot(start) {
74307
74404
  let dir = start;
74308
74405
  for (;; ) {
74309
- if (existsSync13(join19(dir, ".git")))
74406
+ if (existsSync13(join21(dir, ".git")))
74310
74407
  return dir;
74311
74408
  const parent = dirname12(dir);
74312
74409
  if (parent === dir)
@@ -74407,7 +74504,7 @@ init_settings();
74407
74504
  init_store2();
74408
74505
  import { spawn as spawn4 } from "node:child_process";
74409
74506
  import { existsSync as existsSync14, mkdirSync as mkdirSync4, readFileSync as readFileSync7, rmSync as rmSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "node:fs";
74410
- import { join as join20 } from "node:path";
74507
+ import { join as join22 } from "node:path";
74411
74508
  var LOCK_STALE_MS2 = 30 * 60 * 1000;
74412
74509
  var GC_AGE_MS2 = 30 * 24 * 60 * 60 * 1000;
74413
74510
  var GC_MAX_PER_RUN = 20;
@@ -74426,7 +74523,7 @@ function gcOldSessions(sessions) {
74426
74523
  } catch {}
74427
74524
  }
74428
74525
  function lockPath2() {
74429
- return join20(sessionsRoot(), ".analyzer.lock");
74526
+ return join22(sessionsRoot(), ".analyzer.lock");
74430
74527
  }
74431
74528
  function claimLock() {
74432
74529
  const path4 = lockPath2();