@astrale-os/cli 0.5.0-alpha.0 → 0.6.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 (96) hide show
  1. package/dist/astrale.js +12962 -487
  2. package/package.json +7 -3
  3. package/src/command.ts +2 -0
  4. package/src/commands/__tests__/admin-instance.test.ts +3 -2
  5. package/src/commands/__tests__/domain-list.test.ts +6 -2
  6. package/src/commands/__tests__/install-identity-override.test.ts +1 -1
  7. package/src/commands/__tests__/view.test.ts +100 -0
  8. package/src/commands/call.ts +1 -1
  9. package/src/commands/domain/install.ts +5 -5
  10. package/src/commands/domain/list.ts +2 -2
  11. package/src/commands/domain/publish.ts +3 -3
  12. package/src/commands/instance/active.ts +2 -2
  13. package/src/commands/instance/create.ts +1 -1
  14. package/src/commands/instance/delete.ts +3 -3
  15. package/src/commands/instance/list.ts +8 -3
  16. package/src/commands/instance/status.ts +4 -3
  17. package/src/commands/instance/use.ts +2 -2
  18. package/src/commands/query.ts +27 -8
  19. package/src/commands/session/analyze.ts +50 -0
  20. package/src/commands/session/list.ts +36 -0
  21. package/src/commands/token.ts +1 -1
  22. package/src/commands/view-serve.ts +26 -0
  23. package/src/commands/view.ts +614 -0
  24. package/src/connect-core.test.ts +42 -0
  25. package/src/connect-core.ts +53 -0
  26. package/src/kernel/__tests__/auth.test.ts +1 -0
  27. package/src/kernel/client.ts +30 -24
  28. package/src/kernel/expand.ts +3 -2
  29. package/src/kernel/index.ts +7 -1
  30. package/src/kernel/options.ts +1 -1
  31. package/src/lib/__tests__/instance-target.test.ts +34 -0
  32. package/src/lib/__tests__/view-open-intent.test.ts +308 -0
  33. package/src/lib/__tests__/view-snapshot.test.ts +77 -0
  34. package/src/lib/admin-domain.ts +8 -4
  35. package/src/lib/admin-instance.ts +5 -1
  36. package/src/lib/config.ts +1 -0
  37. package/src/lib/instance-target.ts +5 -0
  38. package/src/lib/instance.ts +11 -0
  39. package/src/lib/log.ts +12 -2
  40. package/src/lib/login-flow.ts +41 -4
  41. package/src/lib/provision-instance.ts +2 -2
  42. package/src/lib/view/open-intent.ts +97 -0
  43. package/src/lib/view/resolve.ts +104 -0
  44. package/src/lib/view/server.ts +294 -0
  45. package/src/lib/view/session.ts +123 -0
  46. package/src/lib/view/snapshot.ts +101 -0
  47. package/src/program.ts +12 -1
  48. package/src/registry.ts +1 -1
  49. package/src/setup/steps/instance.ts +2 -2
  50. package/src/telemetry/__tests__/gate.test.ts +63 -0
  51. package/src/telemetry/__tests__/recorder.test.ts +110 -0
  52. package/src/telemetry/__tests__/redact.test.ts +79 -0
  53. package/src/telemetry/__tests__/session.test.ts +166 -0
  54. package/src/telemetry/__tests__/trigger.test.ts +49 -0
  55. package/src/telemetry/adapters/__tests__/claude-code.test.ts +89 -0
  56. package/src/telemetry/adapters/__tests__/codex.test.ts +138 -0
  57. package/src/telemetry/adapters/__tests__/index.test.ts +88 -0
  58. package/src/telemetry/adapters/claude-code.ts +90 -0
  59. package/src/telemetry/adapters/codex.ts +160 -0
  60. package/src/telemetry/adapters/index.ts +45 -0
  61. package/src/telemetry/adapters/types.ts +21 -0
  62. package/src/telemetry/analyze.ts +227 -0
  63. package/src/telemetry/gate.ts +79 -0
  64. package/src/telemetry/recorder.ts +57 -0
  65. package/src/telemetry/redact.ts +53 -0
  66. package/src/telemetry/session.ts +88 -0
  67. package/src/telemetry/settings.ts +26 -0
  68. package/src/telemetry/store.ts +91 -0
  69. package/src/telemetry/trigger.ts +119 -0
  70. package/src/telemetry/types.ts +64 -0
  71. package/studio/client/dist/assets/index-CyN5G8IA.js +109 -0
  72. package/studio/client/dist/assets/index-DKKMHBBC.css +1 -0
  73. package/studio/client/dist/index.html +2 -2
  74. package/studio/server/agent/ask.ts +2 -1
  75. package/studio/server/agent/runner.ts +4 -1
  76. package/studio/server/agent/session-id.ts +13 -0
  77. package/studio/server/api.ts +43 -25
  78. package/studio/server/cache.ts +17 -6
  79. package/studio/server/client-package.test.ts +147 -0
  80. package/studio/server/client-package.ts +242 -0
  81. package/studio/server/index.ts +13 -0
  82. package/studio/server/introspect/anatomy-extras.test.ts +91 -0
  83. package/studio/server/introspect/anatomy-extras.ts +48 -49
  84. package/studio/server/introspect/anatomy.ts +7 -7
  85. package/studio/server/introspect/overlay-tsmorph.test.ts +104 -0
  86. package/studio/server/introspect/overlay-tsmorph.ts +122 -22
  87. package/studio/server/state/views.test.ts +90 -0
  88. package/studio/server/state/views.ts +396 -99
  89. package/studio/server/state/visibility.ts +5 -5
  90. package/studio/server/view-dev-server.test.ts +111 -0
  91. package/studio/server/view-dev-server.ts +372 -0
  92. package/studio/shared/types.ts +57 -10
  93. package/viewer/dist/index.html +93 -0
  94. package/viewer/dist/main.js +71 -0
  95. package/studio/client/dist/assets/index-BcejyJpa.css +0 -1
  96. package/studio/client/dist/assets/index-Cqz3Oy_B.js +0 -179
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/cli",
3
- "version": "0.5.0-alpha.0",
3
+ "version": "0.6.0-alpha.0",
4
4
  "description": "Astrale CLI — connect to existing Astrale kernels",
5
5
  "keywords": [
6
6
  "astrale",
@@ -24,12 +24,14 @@
24
24
  "studio/shared",
25
25
  "studio/client/dist",
26
26
  "studio/package.json",
27
+ "viewer/dist",
27
28
  "THIRD-PARTY-NOTICES.md"
28
29
  ],
29
30
  "type": "module",
30
31
  "exports": {
31
32
  "./keys": "./src/lib/keys.ts",
32
- "./paths": "./src/lib/paths.ts"
33
+ "./paths": "./src/lib/paths.ts",
34
+ "./connect-core": "./src/connect-core.ts"
33
35
  },
34
36
  "publishConfig": {
35
37
  "access": "public",
@@ -50,6 +52,7 @@
50
52
  },
51
53
  "devDependencies": {
52
54
  "@astrale-os/ox": ">=0.1.0 <1.0.0",
55
+ "@astrale-os/shell": ">=0.3.5 <1.0.0",
53
56
  "@astrale/commitlint-config": "npm:@jsr/astrale__commitlint-config@~2.0.1",
54
57
  "@commitlint/cli": "~20.3.1",
55
58
  "@commitlint/config-conventional": "~20.3.1",
@@ -58,6 +61,7 @@
58
61
  "@typescript/native-preview": "latest",
59
62
  "husky": "~9.1.7",
60
63
  "lint-staged": "~16.2.7",
64
+ "msgpackr": "^1.12.1",
61
65
  "oxfmt": "0.52.0",
62
66
  "oxlint": "latest",
63
67
  "typescript": "~6.0.1-rc"
@@ -77,7 +81,7 @@
77
81
  "scripts": {
78
82
  "preinstall": "node .check-workspace.cjs",
79
83
  "build": "bun scripts/build.ts",
80
- "typecheck": "tsgo --noEmit",
84
+ "typecheck": "tsgo --noEmit && tsgo --noEmit -p viewer",
81
85
  "lint": "oxlint .",
82
86
  "lint:fix": "oxlint --fix .",
83
87
  "format": "pnpm exec oxfmt --write .",
package/src/command.ts CHANGED
@@ -16,6 +16,8 @@ export type CommandDefinition = {
16
16
  description: string
17
17
  /** One-line summary shown in the parent's command list (Commander `.summary()`). */
18
18
  summary?: string
19
+ /** Hide from help listings (internal entries, e.g. `__view-serve`). */
20
+ hidden?: boolean
19
21
  aliases?: string[]
20
22
  arguments?: CommandArgument[]
21
23
  options?: CommandOption[]
@@ -3,12 +3,13 @@ import { describe, expect, test } from 'bun:test'
3
3
  import type { IdentityStore } from '../../lib/identity'
4
4
 
5
5
  import { AuthError } from '../../errors'
6
- import { ADMIN_INSTANCE } from '../../lib/admin-instance'
6
+ import { ADMIN_INSTANCE, adminInstanceMethod } from '../../lib/admin-instance'
7
7
  import { assertAlphaCreateIdentity } from '../instance/create'
8
8
 
9
9
  describe('admin-backed instance commands', () => {
10
10
  test('target the merged Instance class', () => {
11
- expect(ADMIN_INSTANCE).toBe('/admin.astrale.ai/class.Instance')
11
+ expect(ADMIN_INSTANCE).toBe('/:admin.astrale.ai:class.Instance')
12
+ expect(adminInstanceMethod('list')).toBe('/:admin.astrale.ai:class.Instance:list')
12
13
  })
13
14
 
14
15
  test('instance create preflight points fresh installs at WorkOS login', () => {
@@ -1,7 +1,6 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
 
3
- import type { DomainInfo } from '../../lib/admin-domain'
4
-
3
+ import { ADMIN_DOMAIN, adminDomainMethod, type DomainInfo } from '../../lib/admin-domain'
5
4
  import { byDefaultThenName, domainProjection, type DomainRow } from '../domain/list'
6
5
 
7
6
  const strip = (s: string): string => s.replace(/\[[0-9;]*m/g, '')
@@ -18,6 +17,11 @@ function entry(over: Partial<DomainInfo>): DomainInfo {
18
17
  }
19
18
 
20
19
  describe('domain list — ordering', () => {
20
+ test('uses the semantic static method path', () => {
21
+ expect(ADMIN_DOMAIN).toBe('/:admin.astrale.ai:class.DomainEntry')
22
+ expect(adminDomainMethod('list')).toBe('/:admin.astrale.ai:class.DomainEntry:list')
23
+ })
24
+
21
25
  test('install-by-default sorts first, then alphabetical by origin', () => {
22
26
  const rows = [
23
27
  entry({ origin: 'zeta.astrale.ai' }),
@@ -23,7 +23,7 @@ describe('identity-override detection', () => {
23
23
 
24
24
  test('origin differing from the serving host is an override', () => {
25
25
  // The spec §5 attack shape: a fork on workers.dev claiming a well-known origin.
26
- expect(isIdentityOverride('workspace.astrale.ai', 'crm.workers.dev')).toBe(true)
26
+ expect(isIdentityOverride('shell.astrale.ai', 'crm.workers.dev')).toBe(true)
27
27
  // The scaffold default also aliases until the placeholder origin is edited.
28
28
  expect(isIdentityOverride('hldom.example.dev', 'hldom-example-dev.acme.workers.dev')).toBe(true)
29
29
  })
@@ -0,0 +1,100 @@
1
+ import { beforeEach, describe, expect, mock, test } from 'bun:test'
2
+
3
+ const getMock = mock(async (_path: string) => ({ id: 'issue-id' }))
4
+ const clientCallMock = mock(async (): Promise<unknown> => {
5
+ throw new Error('View:resolve must not run for a bare --view-url')
6
+ })
7
+ const abMock = mock(async (_args: string[]) => ({
8
+ ok: true,
9
+ data: { snapshot: '- button "Ready" [ref=e1]' },
10
+ error: null,
11
+ }))
12
+
13
+ mock.module('../../kernel', () => ({
14
+ bindGraph: () => ({ get: getMock }),
15
+ expandSelfInPath: async (path: string) => ({ path }),
16
+ resolveKernelTarget: mock(),
17
+ withKernelClient: async (
18
+ _opts: unknown,
19
+ run: (ctx: { client: { call: typeof clientCallMock } }) => Promise<unknown>,
20
+ ) => run({ client: { call: clientCallMock } }),
21
+ }))
22
+
23
+ mock.module('../../lib/browser', () => ({
24
+ ab: abMock,
25
+ AGENT_BROWSER_REPO: 'vercel-labs/agent-browser',
26
+ BROWSER_DIR: '/tmp/astrale-browser',
27
+ findAgentBrowser: mock(async () => '/usr/bin/agent-browser'),
28
+ }))
29
+
30
+ beforeEach(() => {
31
+ getMock.mockClear()
32
+ clientCallMock.mockClear()
33
+ abMock.mockClear()
34
+ })
35
+
36
+ describe('view session resolution', () => {
37
+ test('uses a bare --view-url target only as shell context', async () => {
38
+ const { resolveSession } = await import('../view')
39
+ const targetPath = '/users/user-1/issue-1'
40
+
41
+ const result = await resolveSession(undefined, {
42
+ viewUrl: 'http://127.0.0.1:5173/ui/issues',
43
+ handshake: 'shell',
44
+ target: targetPath,
45
+ })
46
+
47
+ expect(getMock).toHaveBeenCalledWith(targetPath)
48
+ expect(clientCallMock).not.toHaveBeenCalled()
49
+ expect(result).toEqual({
50
+ view: {
51
+ url: 'http://127.0.0.1:5173/ui/issues',
52
+ functionId: 'dev-view',
53
+ handshake: 'shell',
54
+ name: 'dev',
55
+ },
56
+ target: { id: 'issue-id', path: targetPath },
57
+ candidates: [],
58
+ })
59
+ })
60
+
61
+ test('lists every candidate without requiring a view selection', async () => {
62
+ const { resolveSession } = await import('../view')
63
+ const targetPath = '/domains/ai-gateway.astrale.ai/core/gemini-3-5-flash'
64
+ const candidates = [
65
+ {
66
+ id: 'chat-id',
67
+ path: '/domains/ai-gateway.astrale.ai/views/chat',
68
+ url: 'https://ai-gateway.astrale.ai/ui/chat',
69
+ origin: 'class' as const,
70
+ },
71
+ {
72
+ id: 'model-id',
73
+ path: '/domains/ai-gateway.astrale.ai/views/model',
74
+ url: 'https://ai-gateway.astrale.ai/ui/model',
75
+ origin: 'class' as const,
76
+ },
77
+ ]
78
+ clientCallMock.mockImplementationOnce(async () => candidates)
79
+
80
+ const result = await resolveSession(targetPath, { list: true })
81
+
82
+ expect(result).toEqual({
83
+ target: { id: 'issue-id', path: targetPath },
84
+ candidates,
85
+ })
86
+ })
87
+ })
88
+
89
+ describe('view capture timing', () => {
90
+ test('settles the view before taking a screenshot', async () => {
91
+ const { runSnapshotExtras } = await import('../view')
92
+
93
+ await runSnapshotExtras({ screenshot: '/tmp/view.png' })
94
+
95
+ const commands = abMock.mock.calls.map(([args]) => args[0])
96
+ expect(commands.at(-1)).toBe('screenshot')
97
+ expect(commands.slice(0, -1).every((command) => command === 'snapshot')).toBe(true)
98
+ expect(commands.length).toBeGreaterThan(2)
99
+ })
100
+ })
@@ -261,7 +261,7 @@ Examples:
261
261
  $ astrale call /:host.astrale.ai:class.KernelInstance:list
262
262
  $ astrale call /:blog.acme.com:class.Author:list limit=10
263
263
  $ astrale call '@self::deactivate'
264
- $ astrale call /:workspace.astrale.ai:class.App:available --json
264
+ $ astrale call /:shell.astrale.ai:class.Application:available --json
265
265
  `,
266
266
  arguments: [
267
267
  {
@@ -7,8 +7,8 @@ import type { KernelCommandOpts } from '../../kernel'
7
7
  import { AstraleError } from '../../errors'
8
8
  import { runKernelCommand } from '../../kernel'
9
9
  import { withAdminKernelClient } from '../../kernel/client'
10
- import { ADMIN_DOMAIN, type DomainInfo } from '../../lib/admin-domain'
11
- import { ADMIN_INSTANCE, type InstanceInfo } from '../../lib/admin-instance'
10
+ import { adminDomainMethod, type DomainInfo } from '../../lib/admin-domain'
11
+ import { adminInstanceMethod, type InstanceInfo } from '../../lib/admin-instance'
12
12
  import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
13
13
  import { getActive } from '../../lib/instance'
14
14
  import { fatal, log, withSpinner } from '../../lib/log'
@@ -125,7 +125,7 @@ async function installViaAdmin(target: string | undefined, opts: InstallOpts): P
125
125
 
126
126
  try {
127
127
  await withAdminKernelClient(adminOpts, async (ctx) => {
128
- const instances = (await ctx.client.call(`${ADMIN_INSTANCE}/list`, {})) as InstanceInfo[]
128
+ const instances = (await ctx.client.call(adminInstanceMethod('list'), {})) as InstanceInfo[]
129
129
 
130
130
  const ref = await resolveDomainRef(ctx, target, interactive)
131
131
  const slug = await resolveTargetSlug(opts, target, interactive, instances)
@@ -145,7 +145,7 @@ async function installViaAdmin(target: string | undefined, opts: InstallOpts): P
145
145
  `Installing ${label} on ${match.slug}`,
146
146
  !isMachine(opts),
147
147
  () =>
148
- ctx.client.call(`${ADMIN_DOMAIN}/install`, {
148
+ ctx.client.call(adminDomainMethod('install'), {
149
149
  instanceId: match.slug,
150
150
  ...ref,
151
151
  }) as Promise<DomainInstallResult>,
@@ -190,7 +190,7 @@ async function resolveDomainRef(
190
190
  if (target) return domainRefFromTarget(target)
191
191
  if (!interactive) throw new AstraleError('MISSING_ARG', 'No domain given.')
192
192
 
193
- const catalog = (await ctx.client.call(`${ADMIN_DOMAIN}/list`, {})) as DomainInfo[]
193
+ const catalog = (await ctx.client.call(adminDomainMethod('list'), {})) as DomainInfo[]
194
194
  const installable = catalog.filter((d) => d.url)
195
195
  if (installable.length === 0) {
196
196
  throw new AstraleError(
@@ -5,7 +5,7 @@ import type { KernelCommandOpts } from '../../kernel'
5
5
  import type { ListProjection, RawOutputOpts } from '../../lib/output'
6
6
 
7
7
  import { withAdminKernelClient } from '../../kernel/client'
8
- import { ADMIN_DOMAIN, type DomainInfo } from '../../lib/admin-domain'
8
+ import { adminDomainMethod, type DomainInfo } from '../../lib/admin-domain'
9
9
  import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
10
10
  import { fatal, withSpinner } from '../../lib/log'
11
11
  import { isMachine, presentList } from '../../lib/output'
@@ -102,7 +102,7 @@ Examples:
102
102
  async (): Promise<DomainRow[]> => {
103
103
  const list = await withAdminKernelClient(
104
104
  opts,
105
- async (ctx) => (await ctx.client.call(`${ADMIN_DOMAIN}/list`, {})) as DomainInfo[],
105
+ async (ctx) => (await ctx.client.call(adminDomainMethod('list'), {})) as DomainInfo[],
106
106
  )
107
107
  const filtered = opts.defaultOnly ? list.filter((d) => d.installByDefault) : list
108
108
  filtered.sort(byDefaultThenName)
@@ -4,7 +4,7 @@ import type { CommandDefinition } from '../../command'
4
4
  import type { KernelCommandOpts } from '../../kernel'
5
5
 
6
6
  import { withAdminKernelClient } from '../../kernel/client'
7
- import { ADMIN_DOMAIN, type DomainInfo } from '../../lib/admin-domain'
7
+ import { adminDomainMethod, type DomainInfo } from '../../lib/admin-domain'
8
8
  import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
9
9
  import { fatal, withSpinner } from '../../lib/log'
10
10
  import { isMachine, output } from '../../lib/output'
@@ -115,7 +115,7 @@ Examples:
115
115
  // silently bumping `updatedAt`. `info` throws when absent → treat as
116
116
  // a fresh publish.
117
117
  const existing = (await ctx.client
118
- .call(`${ADMIN_DOMAIN}/info`, { origin })
118
+ .call(adminDomainMethod('info'), { origin })
119
119
  .catch(() => null)) as DomainInfo | null
120
120
  if (
121
121
  existing &&
@@ -127,7 +127,7 @@ Examples:
127
127
  ) {
128
128
  return { entry: existing, changed: false as const, isNew: false }
129
129
  }
130
- const entry = (await ctx.client.call(`${ADMIN_DOMAIN}/publish`, {
130
+ const entry = (await ctx.client.call(adminDomainMethod('publish'), {
131
131
  origin,
132
132
  name,
133
133
  url: publicUrl,
@@ -3,7 +3,7 @@ import chalk from 'chalk'
3
3
  import type { CommandDefinition } from '../../command'
4
4
 
5
5
  import { withAdminKernelClient } from '../../kernel/client'
6
- import { ADMIN_INSTANCE, type InstanceInfo } from '../../lib/admin-instance'
6
+ import { adminInstanceMethod, type InstanceInfo } from '../../lib/admin-instance'
7
7
  import { getActive, normalizeInstanceKernelUrl } from '../../lib/instance'
8
8
  import { log } from '../../lib/log'
9
9
  import { RAW_OUTPUT_OPTIONS, isMachine, output, type RawOutputOpts } from '../../lib/output'
@@ -51,7 +51,7 @@ async function resolveActiveForDisplay(): Promise<{
51
51
  const managed = await withAdminKernelClient(
52
52
  {},
53
53
  async (ctx) =>
54
- (await ctx.client.call(`${ADMIN_INSTANCE}/info`, { id: active.name })) as InstanceInfo,
54
+ (await ctx.client.call(adminInstanceMethod('info'), { id: active.name })) as InstanceInfo,
55
55
  )
56
56
  return {
57
57
  name: managed.slug,
@@ -63,7 +63,7 @@ Examples:
63
63
  return
64
64
  }
65
65
  } catch (e) {
66
- fatal(e)
66
+ fatal(e, opts)
67
67
  }
68
68
  },
69
69
  } satisfies CommandDefinition
@@ -2,7 +2,7 @@ import type { CommandDefinition } from '../../command'
2
2
  import type { KernelCommandOpts } from '../../kernel'
3
3
 
4
4
  import { withAdminKernelClient } from '../../kernel/client'
5
- import { ADMIN_INSTANCE, type InstanceInfo } from '../../lib/admin-instance'
5
+ import { adminInstanceMethod, type InstanceInfo } from '../../lib/admin-instance'
6
6
  import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
7
7
  import { clearActive, readInstances, removeInstance, resolveInstanceKey } from '../../lib/instance'
8
8
  import { fatal, log, withSpinner } from '../../lib/log'
@@ -40,7 +40,7 @@ Behavior:
40
40
  withAdminKernelClient(
41
41
  opts,
42
42
  async (ctx) =>
43
- (await ctx.client.call(`${ADMIN_INSTANCE}/delete`, { id })) as InstanceInfo,
43
+ (await ctx.client.call(adminInstanceMethod('delete'), { id })) as InstanceInfo,
44
44
  ),
45
45
  { success: (deleted) => `Deleted instance: ${deleted.slug}` },
46
46
  )
@@ -66,7 +66,7 @@ Behavior:
66
66
  log.dim(` hint: it's a local bookmark — drop it with: astrale instance forget ${id}`)
67
67
  process.exit(1)
68
68
  }
69
- fatal(e)
69
+ fatal(e, opts)
70
70
  }
71
71
  },
72
72
  } satisfies CommandDefinition
@@ -5,7 +5,11 @@ import type { KernelCommandOpts } from '../../kernel'
5
5
  import type { Column } from '../../lib/output'
6
6
 
7
7
  import { withAdminKernelClient } from '../../kernel/client'
8
- import { ADMIN_INSTANCE, formatInstanceLocation, type InstanceInfo } from '../../lib/admin-instance'
8
+ import {
9
+ adminInstanceMethod,
10
+ formatInstanceLocation,
11
+ type InstanceInfo,
12
+ } from '../../lib/admin-instance'
9
13
  import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
10
14
  import { normalizeInstanceKernelUrl, readInstances } from '../../lib/instance'
11
15
  import { fatal, log, withSpinner } from '../../lib/log'
@@ -60,7 +64,8 @@ export default {
60
64
  managed = await withSpinner('Fetching instances', !isMachine(opts), () =>
61
65
  withAdminKernelClient(
62
66
  opts,
63
- async (ctx) => (await ctx.client.call(`${ADMIN_INSTANCE}/list`, {})) as InstanceInfo[],
67
+ async (ctx) =>
68
+ (await ctx.client.call(adminInstanceMethod('list'), {})) as InstanceInfo[],
64
69
  ),
65
70
  )
66
71
  }
@@ -87,7 +92,7 @@ export default {
87
92
  }
88
93
  process.stdout.write(renderTable(rows, { columns: COLUMNS }) + '\n')
89
94
  } catch (e) {
90
- fatal(e)
95
+ fatal(e, opts)
91
96
  }
92
97
  },
93
98
  } satisfies CommandDefinition
@@ -4,7 +4,7 @@ import type { CommandDefinition } from '../../command'
4
4
  import type { KernelCommandOpts } from '../../kernel'
5
5
 
6
6
  import { withAdminKernelClient } from '../../kernel/client'
7
- import { ADMIN_INSTANCE, type InstanceInfo } from '../../lib/admin-instance'
7
+ import { adminInstanceMethod, type InstanceInfo } from '../../lib/admin-instance'
8
8
  import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
9
9
  import { fatal, log, withSpinner } from '../../lib/log'
10
10
  import { isMachine, output, type RawOutputOpts } from '../../lib/output'
@@ -21,7 +21,8 @@ export default {
21
21
  const result = await withSpinner(`Fetching instance ${id}`, !isMachine(opts), () =>
22
22
  withAdminKernelClient(
23
23
  opts,
24
- async (ctx) => (await ctx.client.call(`${ADMIN_INSTANCE}/info`, { id })) as InstanceInfo,
24
+ async (ctx) =>
25
+ (await ctx.client.call(adminInstanceMethod('info'), { id })) as InstanceInfo,
25
26
  ),
26
27
  )
27
28
  if (isMachine(opts)) {
@@ -36,7 +37,7 @@ export default {
36
37
  if (result.hostId) log.dim(` host: ${result.hostId}`)
37
38
  if (result.createdAt) log.dim(` created: ${result.createdAt}`)
38
39
  } catch (e) {
39
- fatal(e)
40
+ fatal(e, opts)
40
41
  }
41
42
  },
42
43
  } satisfies CommandDefinition
@@ -2,7 +2,7 @@ import type { CommandDefinition } from '../../command'
2
2
 
3
3
  import { AstraleError } from '../../errors'
4
4
  import { withAdminKernelClient } from '../../kernel/client'
5
- import { ADMIN_INSTANCE, type InstanceInfo } from '../../lib/admin-instance'
5
+ import { adminInstanceMethod, type InstanceInfo } from '../../lib/admin-instance'
6
6
  import { ADMIN_TARGET_OPTIONS } from '../../lib/admin-target'
7
7
  import { getDefault, setDefault } from '../../lib/identity'
8
8
  import {
@@ -142,7 +142,7 @@ async function fetchManagedInstances(name: string, opts: UseOpts): Promise<Insta
142
142
  try {
143
143
  return await withAdminKernelClient(
144
144
  opts,
145
- async (ctx) => (await ctx.client.call(`${ADMIN_INSTANCE}/list`, {})) as InstanceInfo[],
145
+ async (ctx) => (await ctx.client.call(adminInstanceMethod('list'), {})) as InstanceInfo[],
146
146
  )
147
147
  } catch {
148
148
  return []
@@ -46,8 +46,7 @@ export async function queryCommand(paths: string[], opts: QueryOpts): Promise<vo
46
46
  label: mode.kind === 'roots' ? 'Query ' + mode.roots.join(' ') : 'Query',
47
47
  fn: async (ctx) => {
48
48
  const read = () => bindGraph(ctx).query(mode.kind === 'roots' ? mode.query.ast : mode.ast)
49
- const result =
50
- mode.kind === 'roots' ? await withSelfHint(read, mode.meta) : await read()
49
+ const result = mode.kind === 'roots' ? await withSelfHint(read, mode.meta) : await read()
51
50
  return result.wire
52
51
  },
53
52
  format: (result, fmtOpts) => {
@@ -62,7 +61,8 @@ async function parseMode(paths: string[], opts: QueryOpts): Promise<QueryMode> {
62
61
  const hasRoots = rootsInput.length > 0
63
62
  const hasAst = opts.ast !== undefined
64
63
  const hasCypher = opts.cypher !== undefined
65
- const hasSelectors = opts.depth !== undefined || opts.children !== undefined || opts.edges !== undefined
64
+ const hasSelectors =
65
+ opts.depth !== undefined || opts.children !== undefined || opts.edges !== undefined
66
66
 
67
67
  if (hasCypher) {
68
68
  if (hasAst || hasRoots || hasSelectors) {
@@ -215,7 +215,9 @@ function parseSelector<T>(flag: string, raw: string, schema: SelectorSchema): T
215
215
  throw new Error(flag + ' must be JSON: ' + raw)
216
216
  }
217
217
  if (parsed === null || typeof parsed !== 'object') {
218
- throw new Error(flag + ' must be a JSON selector object' + (flag === '--edges' ? ' or array' : ''))
218
+ throw new Error(
219
+ flag + ' must be a JSON selector object' + (flag === '--edges' ? ' or array' : ''),
220
+ )
219
221
  }
220
222
  const check = schema.safeParse(parsed)
221
223
  if (!check.success) {
@@ -232,11 +234,21 @@ function printCursorFooter(next: NonNullable<GetResultWire['next']>): void {
232
234
  if (entries.length === 1) {
233
235
  const cursors = entries[0]?.[1]
234
236
  if (cursors?.children) {
235
- process.stdout.write(chalk.dim(' more: --children \'{"cursor":"' + cursors.children + '"}\'\n'))
237
+ process.stdout.write(
238
+ chalk.dim(' more: --children \'{"cursor":"' + cursors.children + '"}\'\n'),
239
+ )
236
240
  }
237
241
  for (const [alias, cursor] of Object.entries(cursors?.edges ?? {})) {
238
242
  process.stdout.write(
239
- chalk.dim(' more edges[' + alias + ']: --edges \'{"as":"' + alias + '","cursor":"' + cursor + '"}\'\n'),
243
+ chalk.dim(
244
+ ' more edges[' +
245
+ alias +
246
+ ']: --edges \'{"as":"' +
247
+ alias +
248
+ '","cursor":"' +
249
+ cursor +
250
+ '"}\'\n',
251
+ ),
240
252
  )
241
253
  }
242
254
  return
@@ -279,7 +291,11 @@ export default {
279
291
  '',
280
292
  ].join('\n'),
281
293
  arguments: [
282
- { name: 'paths...', description: 'One or more root paths (/domain/Class) or IDs (@nodeId)', required: false },
294
+ {
295
+ name: 'paths...',
296
+ description: 'One or more root paths (/domain/Class) or IDs (@nodeId)',
297
+ required: false,
298
+ },
283
299
  ],
284
300
  options: [
285
301
  { flags: '--depth <n>', description: 'Subtree depth to fetch (0-5, default 0)' },
@@ -291,7 +307,10 @@ export default {
291
307
  flags: '--edges <json>',
292
308
  description: 'Edge selector (or JSON array of selectors) to include',
293
309
  },
294
- { flags: '--ast <json>', description: 'Raw QueryASTInput JSON object (experimental, unstable shape)' },
310
+ {
311
+ flags: '--ast <json>',
312
+ description: 'Raw QueryASTInput JSON object (experimental, unstable shape)',
313
+ },
295
314
  { flags: '--cypher <query>', description: 'Read-only Cypher escape hatch' },
296
315
  ],
297
316
  action: async (paths, opts) => {
@@ -0,0 +1,50 @@
1
+ import chalk from 'chalk'
2
+
3
+ import type { CommandDefinition } from '../../command'
4
+
5
+ import { log } from '../../lib/log'
6
+ import { analyzeSession } from '../../telemetry/analyze'
7
+ import { listSessions } from '../../telemetry/store'
8
+ import { restampLock, releaseLock } from '../../telemetry/trigger'
9
+
10
+ export default {
11
+ name: 'analyze',
12
+ description: 'Analyze a recorded session for DX frictions (dry-run: writes report.md)',
13
+ arguments: [{ name: 'id', description: 'Session id (default: most recent closed, unanalyzed)' }],
14
+ options: [
15
+ { flags: '--file', description: 'File cleared findings as issues on the admin tracker' },
16
+ { flags: '--model <model>', description: 'Model for the analyzer pass' },
17
+ { flags: '--force', description: 'Re-analyze even if already analyzed' },
18
+ { flags: '--auto', description: 'Triggered mode: quiet, used by the opportunistic trigger' },
19
+ ],
20
+ action: async (
21
+ id: string | undefined,
22
+ opts: { file?: boolean; model?: string; force?: boolean; auto?: boolean },
23
+ ) => {
24
+ if (opts.auto) restampLock()
25
+ try {
26
+ let target = id
27
+ if (!target) {
28
+ const candidate = listSessions().find((s) => s.closed && (opts.force || !s.analyzed))
29
+ if (!candidate) {
30
+ if (!opts.auto) log.dim('Nothing to analyze — no closed, unanalyzed session.')
31
+ return
32
+ }
33
+ target = candidate.id
34
+ }
35
+ if (!opts.auto) log.step(`analyzing session ${target} …`)
36
+ const outcome = await analyzeSession(target, opts)
37
+ if (opts.auto) return
38
+ if (outcome.outcome === 'skipped-quiet') {
39
+ log.dim(`quiet session (${outcome.note}) — nothing to analyze`)
40
+ } else if (outcome.outcome === 'error') {
41
+ log.error(`analyzer failed: ${outcome.note ?? 'unknown'}`)
42
+ } else {
43
+ log.success(`${outcome.outcome} (${outcome.note ?? ''})`)
44
+ if (outcome.reportPath) console.log(chalk.dim(` ${outcome.reportPath}`))
45
+ }
46
+ } finally {
47
+ if (opts.auto) releaseLock()
48
+ }
49
+ },
50
+ } satisfies CommandDefinition
@@ -0,0 +1,36 @@
1
+ import chalk from 'chalk'
2
+
3
+ import type { CommandDefinition } from '../../command'
4
+
5
+ import { log } from '../../lib/log'
6
+ import { isMachine, output, RAW_OUTPUT_OPTIONS } from '../../lib/output'
7
+ import { listSessions } from '../../telemetry/store'
8
+
9
+ export default {
10
+ name: 'list',
11
+ description: 'List locally recorded work sessions and their analysis state',
12
+ options: [...RAW_OUTPUT_OPTIONS],
13
+ action: async (opts: { raw?: boolean; json?: boolean }) => {
14
+ const sessions = listSessions()
15
+ if (isMachine(opts)) {
16
+ output(sessions, opts)
17
+ return
18
+ }
19
+ if (sessions.length === 0) {
20
+ log.dim('No sessions recorded yet. Run any astrale command in a workspace to start one.')
21
+ return
22
+ }
23
+ for (const s of sessions) {
24
+ const state = s.analyzed
25
+ ? s.analyzed.outcome === 'error'
26
+ ? chalk.red(s.analyzed.outcome)
27
+ : chalk.green(s.analyzed.outcome)
28
+ : s.closed
29
+ ? chalk.yellow('closed, unanalyzed')
30
+ : chalk.cyan('open')
31
+ const when = s.lastEventAt ? s.lastEventAt.toISOString().slice(0, 16) : 'no events'
32
+ console.log(`${chalk.bold(s.id)} ${state}`)
33
+ console.log(chalk.dim(` ${s.meta?.root ?? '(unknown root)'} — last activity ${when}`))
34
+ }
35
+ },
36
+ } satisfies CommandDefinition
@@ -60,7 +60,7 @@ What this token is FOR — worker-direct HTTP calls:
60
60
  use 'astrale call' (which signs per-call) instead.
61
61
 
62
62
  Examples:
63
- $ export TOKEN=$(astrale token --audience workspace.astrale.ai --raw)
63
+ $ export TOKEN=$(astrale token --audience shell.astrale.ai --raw)
64
64
  $ astrale token --audience worker.example.com --for alice -i staging
65
65
  `,
66
66
  options: [
@@ -0,0 +1,26 @@
1
+ import { readFile } from 'node:fs/promises'
2
+
3
+ import type { CommandDefinition } from '../command'
4
+ import type { ViewServeConfig } from '../lib/view/session'
5
+
6
+ import { startViewServer } from '../lib/view/server'
7
+
8
+ /**
9
+ * Internal (hidden) entry for the detached view-session server: `astrale view`
10
+ * re-invokes the CLI with `__view-serve --config <file>` so the server runs
11
+ * under the same runtime and codebase as the CLI itself.
12
+ */
13
+ export default {
14
+ name: '__view-serve',
15
+ hidden: true,
16
+ description: 'Internal: run a view-session server from a config file',
17
+ options: [{ flags: '--config <path>', description: 'ViewServeConfig JSON path' }],
18
+ action: async (opts: { config?: string }) => {
19
+ if (!opts.config) throw new Error('--config is required')
20
+ const config = JSON.parse(await readFile(opts.config, 'utf8')) as ViewServeConfig
21
+ startViewServer(config)
22
+ console.log(`view session ${config.session.id} listening on ${config.session.pageUrl}`)
23
+ // Keep the process alive; startViewServer owns shutdown (signals + idle).
24
+ await new Promise(() => {})
25
+ },
26
+ } satisfies CommandDefinition