@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
@@ -1,30 +1,35 @@
1
1
  import chalk from 'chalk'
2
2
 
3
+ import type { OwnedInstanceInfo } from '../../lib/admin-instance'
3
4
  import type { SetupContext, SetupStep } from '../types'
4
5
 
5
- import { withAdminKernelClient } from '../../kernel/client'
6
- import { adminInstanceMethod, type InstanceInfo } from '../../lib/admin-instance'
6
+ import { AstraleError } from '../../errors'
7
+ import { listOwnedInstances } from '../../kernel/client'
7
8
  import { normalizeInstanceKernelUrl, setActive, upsertManagedBookmark } from '../../lib/instance'
8
9
  import { readLocalStatus } from '../../lib/local-status'
9
10
  import { log, withSpinner } from '../../lib/log'
10
11
  import { renderInstanceHero } from '../../lib/panel'
11
12
  import { confirmDefaultYes, promptText, selectFrom } from '../../lib/prompt'
12
- import { provisionInstance } from '../../lib/provision-instance'
13
+ import { provisionInstance, type ProvisionResult } from '../../lib/provision-instance'
13
14
  import { guiOrigin, slugError } from '../util'
14
15
 
15
- /** All admin-managed instances; degrades to [] when the admin kernel is unreachable. */
16
- async function fetchManaged(ctx: SetupContext): Promise<InstanceInfo[]> {
17
- try {
18
- return await withSpinner('Checking for existing instances', !ctx.machine, () =>
19
- withAdminKernelClient(
20
- ctx.opts,
21
- async (client) =>
22
- (await client.client.call(adminInstanceMethod('list'), {})) as InstanceInfo[],
23
- ),
24
- )
25
- } catch {
26
- return []
27
- }
16
+ export type InstanceSetupDependencies = {
17
+ fetchOwned: (ctx: SetupContext) => Promise<OwnedInstanceInfo[]>
18
+ adopt: (info: OwnedInstanceInfo) => Promise<void>
19
+ selectReady: (instances: OwnedInstanceInfo[]) => Promise<OwnedInstanceInfo | null>
20
+ confirmCreate: () => Promise<boolean>
21
+ promptSlug: () => Promise<string | undefined>
22
+ provision: (slug: string) => Promise<ProvisionResult>
23
+ }
24
+
25
+ export type OwnedInstanceAdoptionDependencies = {
26
+ upsert: (
27
+ key: string,
28
+ slug: string,
29
+ url: string,
30
+ organizationId?: string,
31
+ ) => Promise<{ repointedFrom?: string }>
32
+ activate: (slug: string) => Promise<unknown>
28
33
  }
29
34
 
30
35
  /** Print the click-inviting hero for a freshly-active instance. */
@@ -34,9 +39,15 @@ function hero(slug: string, kernelUrl: string): void {
34
39
  console.log('')
35
40
  }
36
41
 
37
- async function adopt(info: InstanceInfo): Promise<void> {
38
- const { repointedFrom } = await upsertManagedBookmark(info.slug, info.slug, info.url)
39
- await setActive(info.slug)
42
+ export async function adoptOwnedInstance(
43
+ info: OwnedInstanceInfo,
44
+ deps: OwnedInstanceAdoptionDependencies = {
45
+ upsert: upsertManagedBookmark,
46
+ activate: setActive,
47
+ },
48
+ ): Promise<void> {
49
+ const { repointedFrom } = await deps.upsert(info.slug, info.slug, info.url, info.organizationId)
50
+ await deps.activate(info.slug)
40
51
  if (repointedFrom) {
41
52
  log.warn(
42
53
  `Bookmark "${info.slug}" repointed: ${repointedFrom} → ${normalizeInstanceKernelUrl(info.url)}`,
@@ -46,10 +57,118 @@ async function adopt(info: InstanceInfo): Promise<void> {
46
57
  hero(info.slug, info.url)
47
58
  }
48
59
 
60
+ function defaultDependencies(ctx: SetupContext): InstanceSetupDependencies {
61
+ return {
62
+ fetchOwned: (setupCtx) =>
63
+ withSpinner('Checking for existing instances', !setupCtx.machine, () =>
64
+ listOwnedInstances(setupCtx.opts),
65
+ ),
66
+ adopt: adoptOwnedInstance,
67
+ selectReady: (instances) =>
68
+ selectFrom(
69
+ 'No active instance. Pick one:',
70
+ instances.map((info) => ({
71
+ label: `${info.slug} ${chalk.dim(guiOrigin(info.url))}`,
72
+ value: info,
73
+ })),
74
+ ),
75
+ confirmCreate: () => confirmDefaultYes('No instances yet. Provision your first one now?'),
76
+ promptSlug: () =>
77
+ ctx.slug
78
+ ? Promise.resolve(ctx.slug)
79
+ : promptText('Pick a slug for your instance', { validate: slugError }),
80
+ provision: (slug) => provisionInstance(slug, ctx.opts),
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Reconcile owner-scoped admin instances when no local active bookmark exists.
86
+ * A failed ownership lookup is never treated as an empty account: setup stops
87
+ * before creation so a transient auth/network failure cannot create a duplicate.
88
+ */
89
+ export async function ensureOwnedInstance(
90
+ ctx: SetupContext,
91
+ deps: InstanceSetupDependencies = defaultDependencies(ctx),
92
+ ): Promise<'fixed' | 'skipped'> {
93
+ let owned: OwnedInstanceInfo[]
94
+ try {
95
+ owned = await deps.fetchOwned(ctx)
96
+ } catch (cause) {
97
+ const detail = cause instanceof Error ? cause.message : String(cause)
98
+ throw new AstraleError(
99
+ 'INSTANCE_DISCOVERY_FAILED',
100
+ `Could not check your Astrale instances: ${detail}`,
101
+ 'No instance was created. Check `astrale admin status`, then rerun `astrale setup`.',
102
+ )
103
+ }
104
+
105
+ const ready = owned.filter((info) => info.state === 'ready')
106
+ if (ready.length === 1) {
107
+ await deps.adopt(ready[0]!)
108
+ return 'fixed'
109
+ }
110
+
111
+ if (ready.length > 1) {
112
+ const choice = await deps.selectReady(ready)
113
+ if (choice === null) {
114
+ log.dim(' Skipped — no active instance set.')
115
+ return 'skipped'
116
+ }
117
+ await deps.adopt(choice)
118
+ return 'fixed'
119
+ }
120
+
121
+ if (owned.length > 0) {
122
+ reportNotReady(owned)
123
+ return 'skipped'
124
+ }
125
+
126
+ if (!(await deps.confirmCreate())) {
127
+ log.dim(' Skipped — create one later: astrale instance create <slug>')
128
+ return 'skipped'
129
+ }
130
+
131
+ const slug = await deps.promptSlug()
132
+ if (!slug) {
133
+ log.dim(' Skipped — no slug given.')
134
+ return 'skipped'
135
+ }
136
+
137
+ const { created, selectionError } = await deps.provision(slug)
138
+ if (selectionError) {
139
+ const detail = selectionError instanceof Error ? selectionError.message : String(selectionError)
140
+ throw new AstraleError(
141
+ 'INSTANCE_SELECTION_FAILED',
142
+ `Instance "${slug}" was provisioned, but the CLI could not select it: ${detail}`,
143
+ `Fix local CLI storage, then run \`astrale instance use ${slug}\`.`,
144
+ )
145
+ }
146
+ hero(slug, created.url)
147
+ return 'fixed'
148
+ }
149
+
150
+ function reportNotReady(instances: OwnedInstanceInfo[]): void {
151
+ log.warn(
152
+ `${instances.length === 1 ? 'Your instance is' : 'Your instances are'} not ready; setup will not create another.`,
153
+ )
154
+ for (const info of instances) {
155
+ const phase = info.phase && info.phase !== info.state ? ` (${info.phase})` : ''
156
+ log.dim(` ${info.slug}: ${info.state}${phase} · astrale instance status ${info.slug}`)
157
+ if (info.error) log.dim(` ${info.error}`)
158
+ }
159
+ if (instances.some((info) => info.state === 'failed')) {
160
+ log.dim(
161
+ ' Inspect the failure, then deliberately delete/recreate it with `astrale instance` commands.',
162
+ )
163
+ } else {
164
+ log.dim(' Wait for provisioning to finish, then rerun `astrale setup`.')
165
+ }
166
+ }
167
+
49
168
  /**
50
169
  * Step 3 — an active instance, the heart of the flow. If the user already has
51
- * managed instances, offer to adopt one or create another; otherwise offer to
52
- * provision their first. Either way ends on the instance hero.
170
+ * owned instances, adopt the sole ready one or ask among several; only a
171
+ * confirmed empty owner list may fall through to first-instance provisioning.
53
172
  */
54
173
  export const instanceStep: SetupStep = {
55
174
  id: 'instance',
@@ -75,45 +194,6 @@ export const instanceStep: SetupStep = {
75
194
  return 'unchanged'
76
195
  }
77
196
 
78
- const managed = await fetchManaged(ctx)
79
-
80
- // Pick an existing instance or fall through to creating a new one.
81
- if (managed.length > 0) {
82
- const choice = await selectFrom<InstanceInfo | 'create'>(
83
- 'No active instance. Pick one or create a new instance:',
84
- [
85
- ...managed.map((info) => ({
86
- label: `${info.slug} ${chalk.dim(guiOrigin(info.url))}`,
87
- value: info as InstanceInfo | 'create',
88
- })),
89
- { label: chalk.cyan('➕ Create a new instance'), value: 'create' as const },
90
- ],
91
- )
92
- if (choice === null) {
93
- log.dim(' Skipped — no active instance set.')
94
- return 'skipped'
95
- }
96
- if (choice !== 'create') {
97
- await adopt(choice)
98
- return 'fixed'
99
- }
100
- } else {
101
- const yes = await confirmDefaultYes('No instances yet. Provision your first one now?')
102
- if (!yes) {
103
- log.dim(' Skipped — create one later: astrale instance create <slug>')
104
- return 'skipped'
105
- }
106
- }
107
-
108
- const slug =
109
- ctx.slug ?? (await promptText('Pick a slug for your instance', { validate: slugError }))
110
- if (!slug) {
111
- log.dim(' Skipped — no slug given.')
112
- return 'skipped'
113
- }
114
-
115
- const { created } = await provisionInstance(slug, ctx.opts)
116
- hero(slug, created.url)
117
- return 'fixed'
197
+ return ensureOwnedInstance(ctx)
118
198
  },
119
199
  }