@astrale-os/cli 0.7.0-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 (105) hide show
  1. package/README.md +3 -2
  2. package/dist/astrale.js +118 -95
  3. package/package.json +2 -3
  4. package/src/commands/studio.ts +13 -0
  5. package/src/lib/__tests__/view-assets.test.ts +114 -0
  6. package/src/lib/instance.ts +0 -11
  7. package/src/lib/login-flow.ts +4 -41
  8. package/src/lib/view/assets.ts +64 -0
  9. package/src/lib/view/server.ts +4 -37
  10. package/studio/client/dist/assets/index-BaqEuIQJ.js +109 -0
  11. package/studio/client/dist/assets/index-jRdExahh.css +1 -0
  12. package/studio/client/dist/index.html +2 -2
  13. package/studio/server/agent/ask.test.ts +74 -0
  14. package/studio/server/agent/ask.ts +20 -22
  15. package/studio/server/agent/bridge/client.test.ts +49 -0
  16. package/studio/server/agent/bridge/client.ts +37 -0
  17. package/studio/server/agent/bridge/grant.test.ts +62 -0
  18. package/studio/server/agent/bridge/grant.ts +84 -0
  19. package/studio/server/agent/bridge/routes.test.ts +151 -0
  20. package/studio/server/agent/bridge/routes.ts +148 -0
  21. package/studio/server/agent/{bridge-mcp.ts → bridge/stdio.ts} +27 -21
  22. package/studio/server/agent/conversation.test.ts +84 -0
  23. package/studio/server/agent/conversation.ts +110 -0
  24. package/studio/server/agent/{types.ts → harness/adapter.ts} +63 -12
  25. package/studio/server/agent/harness/claude/adapter.test.ts +338 -0
  26. package/studio/server/agent/harness/claude/adapter.ts +71 -0
  27. package/studio/server/agent/harness/claude/ask.ts +114 -0
  28. package/studio/server/agent/harness/claude/capabilities.ts +27 -0
  29. package/studio/server/agent/harness/claude/command.ts +63 -0
  30. package/studio/server/agent/harness/claude/events.ts +215 -0
  31. package/studio/server/agent/harness/claude/loadout.ts +132 -0
  32. package/studio/server/agent/harness/claude/mcp.test.ts +36 -0
  33. package/studio/server/agent/harness/claude/mcp.ts +41 -0
  34. package/studio/server/agent/harness/claude/skills.ts +43 -0
  35. package/studio/server/agent/harness/codex/adapter.test.ts +269 -0
  36. package/studio/server/agent/harness/codex/adapter.ts +116 -0
  37. package/studio/server/agent/harness/codex/ask.test.ts +176 -0
  38. package/studio/server/agent/harness/codex/ask.ts +181 -0
  39. package/studio/server/agent/harness/codex/command.test.ts +69 -0
  40. package/studio/server/agent/harness/codex/command.ts +43 -0
  41. package/studio/server/agent/harness/codex/events.test.ts +94 -0
  42. package/studio/server/agent/harness/codex/events.ts +132 -0
  43. package/studio/server/agent/harness/codex/exec.ts +113 -0
  44. package/studio/server/agent/harness/codex/loadout.ts +63 -0
  45. package/studio/server/agent/harness/codex/mcp.test.ts +32 -0
  46. package/studio/server/agent/harness/codex/mcp.ts +27 -0
  47. package/studio/server/agent/harness/codex/models.test.ts +167 -0
  48. package/studio/server/agent/harness/codex/models.ts +211 -0
  49. package/studio/server/agent/harness/codex/skills.ts +33 -0
  50. package/studio/server/agent/harness/gateway/config.test.ts +98 -0
  51. package/studio/server/{state/harness-gateway.ts → agent/harness/gateway/config.ts} +30 -8
  52. package/studio/server/agent/harness/gateway/token.test.ts +133 -0
  53. package/studio/server/agent/harness/gateway/token.ts +166 -0
  54. package/studio/server/agent/harness/mock/adapter.ts +190 -0
  55. package/studio/server/agent/harness/mock/domain-edit.ts +41 -0
  56. package/studio/server/agent/harness/process.test.ts +46 -0
  57. package/studio/server/agent/harness/process.ts +104 -0
  58. package/studio/server/agent/harness/registry.ts +37 -0
  59. package/studio/server/agent/harness/selection.test.ts +75 -0
  60. package/studio/server/agent/harness/selection.ts +77 -0
  61. package/studio/server/agent/harness/skills.test.ts +95 -0
  62. package/studio/server/agent/harness/skills.ts +100 -0
  63. package/studio/server/agent/layout.test.ts +95 -0
  64. package/studio/server/agent/notify.ts +12 -0
  65. package/studio/server/agent/{schema-map.ts → prompts/anchors.ts} +2 -2
  66. package/studio/server/agent/prompts/ask.ts +34 -0
  67. package/studio/server/agent/{prompt.ts → prompts/system.ts} +3 -107
  68. package/studio/server/agent/prompts/turn.ts +57 -0
  69. package/studio/server/agent/routes.test.ts +176 -0
  70. package/studio/server/agent/routes.ts +214 -0
  71. package/studio/server/agent/run/completion.ts +193 -0
  72. package/studio/server/agent/run/coordinator.test.ts +337 -0
  73. package/studio/server/agent/run/coordinator.ts +104 -0
  74. package/studio/server/agent/run/live-state.ts +73 -0
  75. package/studio/server/agent/run/preparation.ts +162 -0
  76. package/studio/server/agent/run/transcript.test.ts +47 -0
  77. package/studio/server/agent/run/transcript.ts +30 -0
  78. package/studio/server/agent/run/usage.test.ts +46 -0
  79. package/studio/server/{state → agent/run}/usage.ts +4 -4
  80. package/studio/server/agent/stream.ts +37 -0
  81. package/studio/server/agent/{session-id.ts → telemetry.ts} +1 -1
  82. package/studio/server/api-agent-loadout.test.ts +69 -0
  83. package/studio/server/api.ts +3 -155
  84. package/studio/server/index.ts +6 -5
  85. package/studio/server/state/comments.test.ts +29 -0
  86. package/studio/server/state/comments.ts +3 -3
  87. package/studio/server/state/settings.test.ts +78 -0
  88. package/studio/server/state/settings.ts +28 -1
  89. package/studio/shared/agent-effort.test.ts +12 -0
  90. package/studio/shared/agent-effort.ts +15 -0
  91. package/studio/shared/agent-models.test.ts +9 -0
  92. package/studio/shared/agent-models.ts +14 -0
  93. package/studio/shared/settings-values.test.ts +18 -0
  94. package/studio/shared/settings-values.ts +20 -0
  95. package/studio/shared/types.ts +58 -7
  96. package/src/connect-core.test.ts +0 -42
  97. package/src/connect-core.ts +0 -53
  98. package/studio/client/dist/assets/index-DAC1a9vW.js +0 -109
  99. package/studio/client/dist/assets/index-huaFafBC.css +0 -1
  100. package/studio/server/agent/bridge.ts +0 -188
  101. package/studio/server/agent/claude.ts +0 -678
  102. package/studio/server/agent/mock.ts +0 -186
  103. package/studio/server/agent/registry.ts +0 -29
  104. package/studio/server/agent/runner.ts +0 -487
  105. package/studio/server/state/harness-token.ts +0 -0
@@ -0,0 +1,114 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test'
2
+ import { mkdtemp, mkdir, readFile, realpath, rm, symlink, writeFile } from 'node:fs/promises'
3
+ import { tmpdir } from 'node:os'
4
+ import { dirname, join, relative } from 'node:path'
5
+ import { pathToFileURL } from 'node:url'
6
+
7
+ import { ensureViewerAssets, viewerDistDir } from '../view/assets'
8
+
9
+ const temporaryDirectories: string[] = []
10
+
11
+ afterEach(async () => {
12
+ await Promise.all(
13
+ temporaryDirectories
14
+ .splice(0)
15
+ .map((directory) => rm(directory, { recursive: true, force: true })),
16
+ )
17
+ })
18
+
19
+ describe('viewer asset resolution', () => {
20
+ test('skips partial candidates and prefers complete package-owned assets', async () => {
21
+ const root = await mkdtemp(join(tmpdir(), 'astrale-view-candidates-'))
22
+ temporaryDirectories.push(root)
23
+ const prefix = join(root, '.npm-global')
24
+ const packageRoot = join(prefix, 'lib', 'node_modules', '@astrale-os', 'cli')
25
+ const entry = join(prefix, 'bin', 'astrale')
26
+ const bundle = join(packageRoot, 'dist', 'astrale.js')
27
+ const published = join(packageRoot, 'viewer', 'dist')
28
+ const legacy = join(prefix, 'viewer', 'dist')
29
+
30
+ await mkdir(dirname(bundle), { recursive: true })
31
+ await mkdir(published, { recursive: true })
32
+ await mkdir(legacy, { recursive: true })
33
+ await writeFile(bundle, '')
34
+ await writeFile(join(published, 'main.js'), '')
35
+ await writeFile(join(legacy, 'main.js'), '')
36
+ await writeFile(join(legacy, 'index.html'), '')
37
+
38
+ expect(viewerDistDir(pathToFileURL(bundle).href, entry)).toBe(legacy)
39
+
40
+ await writeFile(join(published, 'index.html'), '')
41
+ expect(viewerDistDir(pathToFileURL(bundle).href, entry)).toBe(published)
42
+ })
43
+
44
+ test('builds missing viewer assets in a clean source checkout', async () => {
45
+ const root = await mkdtemp(join(tmpdir(), 'astrale-view-source-'))
46
+ temporaryDirectories.push(root)
47
+ const module = join(root, 'src', 'lib', 'view', 'assets.ts')
48
+ const source = join(root, 'viewer')
49
+ const dist = join(source, 'dist')
50
+
51
+ await mkdir(dirname(module), { recursive: true })
52
+ await mkdir(source, { recursive: true })
53
+ await writeFile(module, '')
54
+ await writeFile(join(source, 'main.ts'), 'document.body.textContent = "viewer ready"\n')
55
+ await writeFile(join(source, 'index.html'), '<!doctype html><body></body>\n')
56
+
57
+ expect(await ensureViewerAssets(pathToFileURL(module).href, join(root, 'bin', 'astrale'))).toBe(
58
+ dist,
59
+ )
60
+ expect(await readFile(join(dist, 'index.html'), 'utf8')).toContain('<body>')
61
+ expect(await readFile(join(dist, 'main.js'), 'utf8')).toContain('viewer ready')
62
+ })
63
+
64
+ test('uses the bundled module location when invoked through a global bin symlink', async () => {
65
+ const root = await mkdtemp(join(tmpdir(), 'astrale-view-bundle-'))
66
+ temporaryDirectories.push(root)
67
+ const prefix = join(root, '.npm-global')
68
+ const packageRoot = join(prefix, 'lib', 'node_modules', '@astrale-os', 'cli')
69
+ const entry = join(prefix, 'bin', 'astrale')
70
+ const sourceEntry = join(root, 'entry.ts')
71
+ const viewer = join(packageRoot, 'viewer', 'dist')
72
+ const assetsPath = join(import.meta.dirname, '..', 'view', 'assets.ts')
73
+ const relativeAssetsPath = relative(
74
+ await realpath(dirname(sourceEntry)),
75
+ await realpath(assetsPath),
76
+ )
77
+ const assetsModule = relativeAssetsPath.startsWith('.')
78
+ ? relativeAssetsPath
79
+ : `./${relativeAssetsPath}`
80
+
81
+ await mkdir(join(prefix, 'bin'), { recursive: true })
82
+ await mkdir(viewer, { recursive: true })
83
+ await writeFile(join(viewer, 'main.js'), '')
84
+ await writeFile(join(viewer, 'index.html'), '')
85
+ await writeFile(
86
+ sourceEntry,
87
+ `import { viewerDistDir } from ${JSON.stringify(assetsModule)}\nconsole.log(viewerDistDir())\n`,
88
+ )
89
+ const build = await Bun.build({
90
+ entrypoints: [sourceEntry],
91
+ outdir: join(packageRoot, 'dist'),
92
+ target: 'node',
93
+ format: 'esm',
94
+ })
95
+ expect(build.success).toBe(true)
96
+ await symlink(
97
+ join('..', 'lib', 'node_modules', '@astrale-os', 'cli', 'dist', 'entry.js'),
98
+ entry,
99
+ )
100
+
101
+ const node = Bun.which('node')
102
+ expect(node).not.toBeNull()
103
+ const process = Bun.spawn([node!, entry], { stdout: 'pipe', stderr: 'pipe' })
104
+ const [stdout, stderr, exitCode] = await Promise.all([
105
+ new Response(process.stdout).text(),
106
+ new Response(process.stderr).text(),
107
+ process.exited,
108
+ ])
109
+
110
+ expect(exitCode).toBe(0)
111
+ expect(stderr).toBe('')
112
+ expect(stdout.trim()).toBe(await realpath(viewer))
113
+ })
114
+ })
@@ -113,17 +113,6 @@ export function sanitizeStore(store: InstanceStore): { store: InstanceStore; cha
113
113
 
114
114
  let instancesMemo: InstanceStore | null = null
115
115
 
116
- /**
117
- * Drop the in-process bookmark cache so the next `readInstances` re-reads disk.
118
- *
119
- * A long-lived host (e.g. `@astrale-os/connect-host`) would otherwise serve a
120
- * stale bookmark list after the user runs `astrale instance bookmark`. The CLI
121
- * itself is one-shot, so this is a no-op for command usage.
122
- */
123
- export function resetInstancesMemo(): void {
124
- instancesMemo = null
125
- }
126
-
127
116
  export async function readInstances(
128
117
  _config?: AstraleConfig,
129
118
  opts: { persist?: boolean } = {},
@@ -29,27 +29,6 @@ import { log } from './log'
29
29
  * behavior; presentation (the device URL is logged here; success lines are the
30
30
  * caller's) does not.
31
31
  */
32
- /**
33
- * Interactive device-flow progress. Emitted once, when the device authorization
34
- * has been requested and the user must approve it in a browser. The caller
35
- * decides how to present it (the CLI logs it; the desktop opens the URL and
36
- * pushes it to the renderer). Structurally identical to connect-host's wire
37
- * `AuthVerificationEvent`, but declared here so the CLI carries no dependency on
38
- * connect-host (which depends on the CLI, not the reverse).
39
- */
40
- export type DeviceVerification = {
41
- /** The base verification URL (fallback to the complete one when absent). */
42
- verificationUri: string
43
- /** Pre-filled verification URL (opens straight to the approval page). */
44
- verificationUriComplete?: string
45
- /** The short user code to confirm — a reassurance fallback when the URL is pre-filled. */
46
- userCode?: string
47
- /** Seconds until the device code expires. */
48
- expiresIn?: number
49
- /** Human-readable instruction from the IdP. */
50
- message?: string
51
- }
52
-
53
32
  export type LoginFlowOpts = {
54
33
  idp?: string
55
34
  name?: string
@@ -63,12 +42,6 @@ export type LoginFlowOpts = {
63
42
  codeVerifier?: string
64
43
  /** Switch the default identity to the one we just logged in (default true). */
65
44
  use?: boolean
66
- /**
67
- * When set, receives the device-flow verification details and REPLACES the
68
- * default `log.info` presentation (a headless/GUI caller drives the UI). The
69
- * `auth login` command passes nothing, so its terminal output is unchanged.
70
- */
71
- onVerification?: (e: DeviceVerification) => void
72
45
  }
73
46
 
74
47
  export type LoginResult = {
@@ -175,20 +148,10 @@ async function obtainToken(
175
148
  scope,
176
149
  audience: opts.audience,
177
150
  })
178
- if (opts.onVerification) {
179
- opts.onVerification({
180
- verificationUri: device.verification_uri ?? device.verification_uri_complete ?? '',
181
- verificationUriComplete: device.verification_uri_complete,
182
- userCode: device.user_code,
183
- expiresIn: device.expires_in,
184
- message: device.message,
185
- })
186
- } else {
187
- if (device.verification_uri_complete) log.info(`Open: ${device.verification_uri_complete}`)
188
- else if (device.verification_uri) log.info(`Open: ${device.verification_uri}`)
189
- if (device.user_code) log.info(`Code: ${device.user_code}`)
190
- if (device.message) log.dim(` ${device.message}`)
191
- }
151
+ if (device.verification_uri_complete) log.info(`Open: ${device.verification_uri_complete}`)
152
+ else if (device.verification_uri) log.info(`Open: ${device.verification_uri}`)
153
+ if (device.user_code) log.info(`Code: ${device.user_code}`)
154
+ if (device.message) log.dim(` ${device.message}`)
192
155
 
193
156
  return pollDeviceToken({
194
157
  idp,
@@ -0,0 +1,64 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { copyFile } from 'node:fs/promises'
3
+ import { dirname, join } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ /**
7
+ * The prebuilt host-page bundle, shipped next to the CLI module
8
+ * (`<pkg>/viewer/dist`). The module URL is authoritative because npm global
9
+ * installs expose the CLI through a bin symlink outside the package root.
10
+ */
11
+ export function viewerDistDir(moduleUrl = import.meta.url, entry = process.argv[1] ?? '.'): string {
12
+ const override = process.env.ASTRALE_VIEWER_DIR
13
+ if (override) return override
14
+
15
+ const moduleDirectory = dirname(fileURLToPath(moduleUrl))
16
+ const published = join(moduleDirectory, '..', 'viewer', 'dist')
17
+ const source = join(moduleDirectory, '..', '..', '..', 'viewer', 'dist')
18
+ const legacy = join(dirname(entry), '..', 'viewer', 'dist')
19
+ const complete = [published, source, legacy].find(hasViewerBundle)
20
+ if (complete) return complete
21
+
22
+ // A source checkout may intentionally omit generated dist assets. Keep the
23
+ // source-derived destination so ensureViewerAssets can build it on demand.
24
+ if (hasViewerSource(join(source, '..'))) return source
25
+
26
+ // Published installs cannot rebuild: preserve the package-relative path in
27
+ // the diagnostic instead of pointing at the npm prefix beside the bin link.
28
+ return published
29
+ }
30
+
31
+ /** Ensure the host bundle exists; on a dev checkout, build it with Bun. */
32
+ export async function ensureViewerAssets(
33
+ moduleUrl = import.meta.url,
34
+ entry = process.argv[1] ?? '.',
35
+ ): Promise<string> {
36
+ const dist = viewerDistDir(moduleUrl, entry)
37
+ if (hasViewerBundle(dist)) return dist
38
+ const srcDir = join(dist, '..')
39
+ const bun = (
40
+ globalThis as { Bun?: { build: (o: object) => Promise<{ success: boolean; logs: unknown[] }> } }
41
+ ).Bun
42
+ if (bun && hasViewerSource(srcDir)) {
43
+ const result = await bun.build({
44
+ entrypoints: [join(srcDir, 'main.ts')],
45
+ outdir: dist,
46
+ target: 'browser',
47
+ minify: false,
48
+ })
49
+ if (!result.success) throw new Error(`viewer build failed: ${result.logs.join('\n')}`)
50
+ await copyFile(join(srcDir, 'index.html'), join(dist, 'index.html'))
51
+ return dist
52
+ }
53
+ throw new Error(
54
+ `viewer bundle missing at ${dist} — reinstall the CLI (or run \`bun scripts/build.ts\` in a dev checkout)`,
55
+ )
56
+ }
57
+
58
+ function hasViewerBundle(directory: string): boolean {
59
+ return existsSync(join(directory, 'main.js')) && existsSync(join(directory, 'index.html'))
60
+ }
61
+
62
+ function hasViewerSource(directory: string): boolean {
63
+ return existsSync(join(directory, 'main.ts')) && existsSync(join(directory, 'index.html'))
64
+ }
@@ -1,17 +1,19 @@
1
1
  import type { ReadableStream as WebReadableStream } from 'node:stream/web'
2
2
 
3
- import { existsSync } from 'node:fs'
4
3
  import { readFile } from 'node:fs/promises'
5
4
  import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'
6
- import { dirname, join } from 'node:path'
5
+ import { join } from 'node:path'
7
6
  import { Readable } from 'node:stream'
8
7
 
9
8
  import type { ViewServeConfig } from './session'
10
9
 
11
10
  import { withKernelClient } from '../../kernel'
12
11
  import { fetchWithCaFile } from '../../kernel/ca-fetch'
12
+ import { viewerDistDir } from './assets'
13
13
  import { removeSessionFiles } from './session'
14
14
 
15
+ export { ensureViewerAssets, viewerDistDir } from './assets'
16
+
15
17
  /**
16
18
  * The view-session server: serves the host page, hands it config and fresh
17
19
  * credentials, receives its lifecycle reports, and proxies the view's kernel
@@ -197,41 +199,6 @@ export function startViewServer(config: ViewServeConfig): Server {
197
199
  return server
198
200
  }
199
201
 
200
- /**
201
- * The prebuilt host-page bundle, shipped next to the CLI entry
202
- * (`<pkg>/viewer/dist`). Dev runs (bun, no dist) build it on demand.
203
- */
204
- export function viewerDistDir(): string {
205
- const override = process.env.ASTRALE_VIEWER_DIR
206
- if (override) return override
207
- return join(dirname(process.argv[1] ?? '.'), '..', 'viewer', 'dist')
208
- }
209
-
210
- /** Ensure the host bundle exists; on a dev checkout, build it with Bun. */
211
- export async function ensureViewerAssets(): Promise<string> {
212
- const dist = viewerDistDir()
213
- if (existsSync(join(dist, 'main.js')) && existsSync(join(dist, 'index.html'))) return dist
214
- const srcDir = join(dist, '..')
215
- const bun = (
216
- globalThis as { Bun?: { build: (o: object) => Promise<{ success: boolean; logs: unknown[] }> } }
217
- ).Bun
218
- if (bun && existsSync(join(srcDir, 'main.ts'))) {
219
- const result = await bun.build({
220
- entrypoints: [join(srcDir, 'main.ts')],
221
- outdir: dist,
222
- target: 'browser',
223
- minify: false,
224
- })
225
- if (!result.success) throw new Error(`viewer build failed: ${result.logs.join('\n')}`)
226
- const { copyFile } = await import('node:fs/promises')
227
- await copyFile(join(srcDir, 'index.html'), join(dist, 'index.html'))
228
- return dist
229
- }
230
- throw new Error(
231
- `viewer bundle missing at ${dist} — reinstall the CLI (or run \`bun scripts/build.ts\` in a dev checkout)`,
232
- )
233
- }
234
-
235
202
  function jwtExpiry(token: string): number | null {
236
203
  try {
237
204
  const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString('utf8')) as {