@astrale-os/cli 0.4.0-alpha.13 → 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 (121) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +2 -2
  3. package/THIRD-PARTY-NOTICES.md +27 -0
  4. package/dist/astrale.js +24396 -9632
  5. package/package.json +24 -22
  6. package/src/command.ts +2 -0
  7. package/src/commands/__tests__/admin-instance.test.ts +3 -2
  8. package/src/commands/__tests__/domain-list.test.ts +6 -2
  9. package/src/commands/__tests__/help-contract.test.ts +27 -14
  10. package/src/commands/__tests__/install-identity-override.test.ts +2 -2
  11. package/src/commands/__tests__/ls.test.ts +1 -1
  12. package/src/commands/__tests__/read-commands.test.ts +201 -0
  13. package/src/commands/__tests__/view.test.ts +100 -0
  14. package/src/commands/call.ts +27 -44
  15. package/src/commands/describe.ts +57 -58
  16. package/src/commands/domain/install.ts +9 -9
  17. package/src/commands/domain/list.ts +2 -2
  18. package/src/commands/domain/publish.ts +3 -3
  19. package/src/commands/get.ts +48 -23
  20. package/src/commands/identity/register.ts +27 -33
  21. package/src/commands/instance/active.ts +2 -2
  22. package/src/commands/instance/create.ts +1 -1
  23. package/src/commands/instance/delete.ts +3 -3
  24. package/src/commands/instance/list.ts +8 -3
  25. package/src/commands/instance/status.ts +4 -3
  26. package/src/commands/instance/use.ts +2 -2
  27. package/src/commands/logs.ts +8 -8
  28. package/src/commands/ls.ts +77 -55
  29. package/src/commands/mutate.ts +191 -0
  30. package/src/commands/query.ts +307 -20
  31. package/src/commands/session/analyze.ts +50 -0
  32. package/src/commands/session/list.ts +36 -0
  33. package/src/commands/token.ts +4 -7
  34. package/src/commands/view-serve.ts +26 -0
  35. package/src/commands/view.ts +614 -0
  36. package/src/connect-core.test.ts +42 -0
  37. package/src/connect-core.ts +53 -0
  38. package/src/kernel/__tests__/auth.test.ts +1 -0
  39. package/src/kernel/__tests__/expand.test.ts +123 -0
  40. package/src/kernel/client.ts +33 -37
  41. package/src/kernel/expand.ts +55 -61
  42. package/src/kernel/graph.ts +96 -0
  43. package/src/kernel/index.ts +18 -3
  44. package/src/kernel/options.ts +1 -1
  45. package/src/kernel/run.ts +0 -13
  46. package/src/lib/__tests__/instance-target.test.ts +34 -0
  47. package/src/lib/__tests__/table.test.ts +1 -1
  48. package/src/lib/__tests__/view-open-intent.test.ts +308 -0
  49. package/src/lib/__tests__/view-snapshot.test.ts +77 -0
  50. package/src/lib/admin-domain.ts +8 -4
  51. package/src/lib/admin-instance.ts +5 -1
  52. package/src/lib/config.ts +1 -0
  53. package/src/lib/domain-identity.ts +1 -1
  54. package/src/lib/instance-target.ts +5 -0
  55. package/src/lib/instance.ts +11 -0
  56. package/src/lib/log.ts +12 -2
  57. package/src/lib/login-flow.ts +41 -4
  58. package/src/lib/provision-instance.ts +2 -2
  59. package/src/lib/self.ts +1 -3
  60. package/src/lib/view/open-intent.ts +97 -0
  61. package/src/lib/view/resolve.ts +104 -0
  62. package/src/lib/view/server.ts +294 -0
  63. package/src/lib/view/session.ts +123 -0
  64. package/src/lib/view/snapshot.ts +101 -0
  65. package/src/program.ts +16 -3
  66. package/src/registry.ts +1 -1
  67. package/src/setup/render.ts +1 -3
  68. package/src/setup/steps/instance.ts +2 -2
  69. package/src/telemetry/__tests__/gate.test.ts +63 -0
  70. package/src/telemetry/__tests__/recorder.test.ts +110 -0
  71. package/src/telemetry/__tests__/redact.test.ts +79 -0
  72. package/src/telemetry/__tests__/session.test.ts +166 -0
  73. package/src/telemetry/__tests__/trigger.test.ts +49 -0
  74. package/src/telemetry/adapters/__tests__/claude-code.test.ts +89 -0
  75. package/src/telemetry/adapters/__tests__/codex.test.ts +138 -0
  76. package/src/telemetry/adapters/__tests__/index.test.ts +88 -0
  77. package/src/telemetry/adapters/claude-code.ts +90 -0
  78. package/src/telemetry/adapters/codex.ts +160 -0
  79. package/src/telemetry/adapters/index.ts +45 -0
  80. package/src/telemetry/adapters/types.ts +21 -0
  81. package/src/telemetry/analyze.ts +227 -0
  82. package/src/telemetry/gate.ts +79 -0
  83. package/src/telemetry/recorder.ts +57 -0
  84. package/src/telemetry/redact.ts +53 -0
  85. package/src/telemetry/session.ts +88 -0
  86. package/src/telemetry/settings.ts +26 -0
  87. package/src/telemetry/store.ts +91 -0
  88. package/src/telemetry/trigger.ts +119 -0
  89. package/src/telemetry/types.ts +64 -0
  90. package/studio/client/dist/assets/index-CyN5G8IA.js +109 -0
  91. package/studio/client/dist/assets/index-DKKMHBBC.css +1 -0
  92. package/studio/client/dist/index.html +2 -2
  93. package/studio/server/agent/ask.ts +7 -2
  94. package/studio/server/agent/claude.ts +15 -3
  95. package/studio/server/agent/runner.ts +4 -1
  96. package/studio/server/agent/session-id.ts +13 -0
  97. package/studio/server/api.ts +50 -32
  98. package/studio/server/cache.ts +17 -6
  99. package/studio/server/client-package.test.ts +147 -0
  100. package/studio/server/client-package.ts +242 -0
  101. package/studio/server/index.ts +13 -0
  102. package/studio/server/introspect/anatomy-extras.test.ts +91 -0
  103. package/studio/server/introspect/anatomy-extras.ts +48 -49
  104. package/studio/server/introspect/anatomy.ts +7 -7
  105. package/studio/server/introspect/overlay-tsmorph.test.ts +104 -0
  106. package/studio/server/introspect/overlay-tsmorph.ts +230 -72
  107. package/studio/server/state/harness-gateway.ts +12 -3
  108. package/studio/server/state/harness-token.ts +0 -0
  109. package/studio/server/state/views.test.ts +90 -0
  110. package/studio/server/state/views.ts +396 -99
  111. package/studio/server/state/visibility.ts +10 -6
  112. package/studio/server/view-dev-server.test.ts +111 -0
  113. package/studio/server/view-dev-server.ts +372 -0
  114. package/studio/shared/types.ts +57 -10
  115. package/viewer/dist/index.html +93 -0
  116. package/viewer/dist/main.js +71 -0
  117. package/src/kernel/__tests__/remote-routing.test.ts +0 -70
  118. package/src/kernel/remote-routing.ts +0 -88
  119. package/studio/client/dist/assets/index-DOwzZAEK.css +0 -1
  120. package/studio/client/dist/assets/index-wtU0Zxhy.js +0 -183
  121. package/studio/tsconfig.json +0 -23
@@ -1,11 +1,10 @@
1
- import { K } from '@astrale-os/kernel-core'
2
1
  import { importJWK, SignJWT, type JWK } from 'jose'
3
2
  import { readFile } from 'node:fs/promises'
4
3
 
5
4
  import type { CommandDefinition } from '../../command'
6
- import type { KernelCommandOpts } from '../../kernel'
5
+ import type { ClientContext, KernelCommandOpts } from '../../kernel'
7
6
 
8
- import { runKernelCommand } from '../../kernel'
7
+ import { bindGraph, runKernelCommand } from '../../kernel'
9
8
  import { KERNEL_PASSTHROUGH_OPTIONS } from '../../kernel/options'
10
9
  import { getIdentity, setRegistration } from '../../lib/identity'
11
10
  import { getActive } from '../../lib/instance'
@@ -13,7 +12,6 @@ import { fileExists, keypairPaths } from '../../lib/keys'
13
12
  import { fatal, log } from '../../lib/log'
14
13
  import { output } from '../../lib/output'
15
14
 
16
- type CreateNodeResult = { id: string; path: string }
17
15
  type RegisterIdentityResult = { iss: string; sub: string }
18
16
 
19
17
  type RegisterOpts = KernelCommandOpts & {
@@ -27,37 +25,32 @@ async function readJwk(path: string): Promise<JWK> {
27
25
  return JSON.parse(raw) as JWK
28
26
  }
29
27
 
30
- type ChildNode = { class?: string; path?: string }
31
-
32
28
  /**
33
29
  * Discover the identity-bearer class on the target instance: find the
34
- * installed distribution domain (whatever its origin `dist.astrale.ai` in
35
- * prod, `dist.localhost` locally) and address its `User` class semantically.
30
+ * installed domain that owns a `User` class (workspace on managed instances)
31
+ * and address it semantically.
36
32
  * The bearer class must exist before a non-root identity can be registered;
37
33
  * failing here names the real problem instead of surfacing as a confusing
38
- * permission error on a hardcoded prod origin.
34
+ * permission error on a hardcoded origin.
39
35
  */
40
- async function resolveUserClassPath(ctx: {
41
- client: { call(path: string, params: unknown): Promise<unknown> }
42
- }): Promise<string> {
43
- const children = (await ctx.client.call('/::listChildren', {})) as ChildNode[]
44
- const domains = children
45
- .filter((c) => typeof c.class === 'string' && c.class.endsWith(':class.Domain'))
46
- .map((c) => (c.path ?? '').replace(/^\//, ''))
36
+ async function resolveUserClassPath(ctx: ClientContext): Promise<string> {
37
+ const graph = bindGraph(ctx)
38
+ // Root's direct children are the installed Domain nodes.
39
+ const result = await graph.children('/')
40
+ const domains = result.nodes
41
+ .filter((n) => n.class.className === 'Domain')
42
+ .map((n) => n.path.raw.replace(/^\//, ''))
47
43
  .filter(Boolean)
48
44
  for (const origin of domains) {
49
- try {
50
- // The class materializes as a `class.User` Folder under the domain mount;
51
- // a resolvable read means the domain declares it.
52
- await ctx.client.call(`/${origin}/class.User::get`, {})
45
+ // The class materializes as a `class.User` Folder under the domain mount;
46
+ // a resolving node means the domain declares it (null when absent).
47
+ if (await graph.get(`/${origin}/class.User`)) {
53
48
  return `/:${origin}:class.User`
54
- } catch {
55
- // This domain has no User class — try the next one.
56
49
  }
57
50
  }
58
51
  throw new Error(
59
52
  'No installed domain declares a `User` class on this instance — registering a ' +
60
- 'non-root identity needs an identity-bearer class (install the distribution ' +
53
+ 'non-root identity needs an identity-bearer class (install workspace or another ' +
61
54
  'domain, or pass --class <classPath> explicitly).',
62
55
  )
63
56
  }
@@ -85,7 +78,7 @@ export default {
85
78
  {
86
79
  flags: '--class <classPath>',
87
80
  description:
88
- 'Class path of the identity node to create (default: /:dist.<origin>:class.User when distribution is installed)',
81
+ 'Class path of the identity node to create (default: the installed domain that declares class.User)',
89
82
  },
90
83
  {
91
84
  flags: '--path <nodePath>',
@@ -127,23 +120,24 @@ export default {
127
120
 
128
121
  const classPath = opts.class ?? (await resolveUserClassPath(ctx))
129
122
 
130
- // A bare `astrale identity create <name>` carries no profile, but the
131
- // bearer class may require one (distribution's User wants
132
- // firstName/lastName) — default both to the identity name so a dev
123
+ // A bare `astrale identity create <name>` carries no profile, but a
124
+ // User bearer class may require one. Default both names so a dev
133
125
  // registration works out of the box; `--props` overrides.
134
126
  const extraProps = opts.props ? (JSON.parse(opts.props) as Record<string, unknown>) : {}
135
127
  const userDefaults = classPath.endsWith(':class.User')
136
128
  ? { firstName: name, lastName: name }
137
129
  : {}
138
130
 
139
- const node = (await ctx.client.call(K.Node.createNode.path.method.raw, {
140
- class: classPath,
141
- path: nodePath,
142
- props: { 'Statused.status': 'creating', ...userDefaults, ...extraProps },
143
- })) as CreateNodeResult
131
+ // One-arm create through function.mutate; the minted node id comes
132
+ // back in createdNodes (keyed by the `at` path).
133
+ const nodeId = await bindGraph(ctx).createNode(classPath, nodePath, {
134
+ 'Statused.status': 'creating',
135
+ ...userDefaults,
136
+ ...extraProps,
137
+ })
144
138
 
145
139
  const bootstrap = await mintBootstrapJwt(privateJwk)
146
- const result = (await ctx.client.call(`@${node.id}::registerIdentity`, {
140
+ const result = (await ctx.client.call(`@${nodeId}::registerIdentity`, {
147
141
  signingKey: {
148
142
  publicKey: { jwk: publicJwk },
149
143
  credential: bootstrap,
@@ -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 []
@@ -1,9 +1,9 @@
1
1
  import type { JournalEntry, JournalFilter } from '@astrale-os/kernel-core'
2
2
 
3
3
  /**
4
- * `astrale logs` — tail the kernel event journal (Root.journal) for the target
4
+ * `astrale logs` — tail the kernel event journal (function.journal) for the target
5
5
  * instance. Defaults to the kernel journal syscall
6
- * `/kernel.astrale.ai/class.Root/journal`; `--service <name>` switches to the
6
+ * `/kernel.astrale.ai/functions/journal`; `--service <name>` switches to the
7
7
  * per-instance `services` domain log buffer (the historical behavior).
8
8
  *
9
9
  * Target the instance with `-i <instance>` (inherited from withKernelOptions).
@@ -18,13 +18,13 @@ import { runKernelCommand, withKernelClient } from '../kernel'
18
18
  import { fatal, withSpinner } from '../lib/log'
19
19
  import { isMachine, output, presentList } from '../lib/output'
20
20
 
21
- const ROOT_JOURNAL_PATH = '/kernel.astrale.ai/class.Root/journal'
21
+ const JOURNAL_FN_PATH = '/kernel.astrale.ai/functions/journal'
22
22
  const DEFAULT_SERVICES_ORIGIN = 'services.astrale.ai'
23
23
  const DEFAULT_LIMIT = 200
24
24
  const FOLLOW_INTERVAL_MS = 2000
25
25
  // The journal-read syscall journals its own ops; hide them by default so polling
26
26
  // doesn't self-pollute the view. `--all` shows them.
27
- const SELF_READ_PREFIX = 'op:class.Root.method.journal:'
27
+ const SELF_READ_PREFIX = 'op:function.journal:'
28
28
 
29
29
  // The published @astrale-os/kernel-core@0.5.0 JournalFilter has no `cursor` yet
30
30
  // (added on the kernel branch). Type against the augmented shape so the
@@ -48,7 +48,7 @@ type LogsOpts = KernelCommandOpts & {
48
48
  }
49
49
 
50
50
  /**
51
- * Kernel journal page. The Root.journal syscall returns a BARE `JournalEntry[]`;
51
+ * Kernel journal page. The function.journal syscall returns a BARE `JournalEntry[]`;
52
52
  * the client derives the next cursor from the max `seq`. We still model a
53
53
  * `nextCursor` field so the TTY footer / incremental tail share one shape, and
54
54
  * so a future paged kernel response degrades gracefully.
@@ -58,7 +58,7 @@ type EventsPage = {
58
58
  nextCursor?: number | null
59
59
  }
60
60
 
61
- // ── Root.journal (default) ───────────────────────────────────
61
+ // ── function.journal (default) ───────────────────────────────────
62
62
 
63
63
  /** The highest `seq` across entries, or null when empty. */
64
64
  function maxSeq(entries: JournalEntry[]): number | null {
@@ -177,7 +177,7 @@ function eventsProjection(entries: JournalEntry[], showTiming = false): ListProj
177
177
  }
178
178
 
179
179
  async function fetchEventsPage(ctx: ClientContext, opts: LogsOpts): Promise<EventsPage> {
180
- const raw = await ctx.client.call(ROOT_JOURNAL_PATH, buildEventsParams(opts))
180
+ const raw = await ctx.client.call(JOURNAL_FN_PATH, buildEventsParams(opts))
181
181
  const page = normalizePage(raw)
182
182
  // Strip the journal's own read ops by default (but keep nextCursor past them).
183
183
  const entries = opts.all
@@ -313,7 +313,7 @@ export default {
313
313
  },
314
314
  ],
315
315
  afterHelpText: `
316
- Default: tails the kernel event journal via ${ROOT_JOURNAL_PATH} on the target
316
+ Default: tails the kernel event journal via ${JOURNAL_FN_PATH} on the target
317
317
  instance (-i <instance>). Topics use ':'-segmented globs ('*' one segment,
318
318
  '**' zero-or-more). Machine output (--json / pipe) emits the JournalEntry[]
319
319
  array; a TTY shows a SEQ/TIME/TOPIC/LATENCY/PRINCIPAL table (LATENCY is the
@@ -1,14 +1,17 @@
1
+ import { rawOf } from '@astrale-os/kernel-client/graph'
1
2
  import chalk from 'chalk'
2
3
 
3
4
  import type { CommandDefinition } from '../command'
4
- import type { KernelCommandOpts, ClientContext, SelfExpansionMeta } from '../kernel'
5
+ import type { GraphNode, KernelCommandOpts, SelfExpansionMeta } from '../kernel'
5
6
  import type { ListProjection } from '../lib/output'
6
7
 
7
8
  import {
9
+ bindGraph,
10
+ childrenCursor,
8
11
  expandSelfInPath,
9
- extractItems,
10
12
  formatKernelError,
11
13
  runKernelCommand,
14
+ splitRoot,
12
15
  withKernelClient,
13
16
  withSelfHint,
14
17
  } from '../kernel'
@@ -23,25 +26,19 @@ type LsOpts = KernelCommandOpts & {
23
26
  filter?: string
24
27
  }
25
28
 
26
- /** A child node as returned by `::listChildren`. */
27
- type Item = {
28
- id?: string
29
- class?: string
30
- path?: string
31
- props?: Record<string, unknown>
32
- __labels?: string[]
33
- }
29
+ /** A child node row the shared `function.get` node shape. */
30
+ type Item = GraphNode
34
31
 
35
32
  // ── Display projection ──────────────────────────────────────
36
33
 
37
- /** `/dist.astrale.ai` → `dist.astrale.ai`; `/` stays `/`. */
34
+ /** `/example.astrale.ai` → `example.astrale.ai`; `/` stays `/`. */
38
35
  export function basename(path?: string): string {
39
36
  if (!path || path === '/') return path ?? ''
40
37
  return path.slice(path.lastIndexOf('/') + 1)
41
38
  }
42
39
 
43
40
  /** `/:kernel.astrale.ai:class.Domain` → `Domain`; falls back to the most specific label. */
44
- export function classNameOf(item: Item): string {
41
+ export function classNameOf(item: { class?: string; __labels?: string[] }): string {
45
42
  const tail = item.class?.split(/[/:.]/).pop()
46
43
  return tail || item.__labels?.[item.__labels.length - 1] || '?'
47
44
  }
@@ -92,14 +89,36 @@ export async function lsCommand(path: string, opts: LsOpts): Promise<void> {
92
89
  await runKernelCommand({
93
90
  opts,
94
91
  label: `Children of ${expandedPath}`,
95
- fn: (ctx) => withSelfHint(() => ctx.client.call(`${expandedPath}::listChildren`, {}), meta),
96
- format: (result, fmtOpts) => {
97
- const items = applyFilter(extractItems<Item>(result), opts.filter)
92
+ // Flat listing = ONE function.get depth:1; the root is dropped, the rest are
93
+ // the direct children (replaces the removed `::listChildren` syscall).
94
+ fn: async (ctx) => {
95
+ const result = await withSelfHint(
96
+ () => bindGraph(ctx).query((q) => q.from(expandedPath).children()),
97
+ meta,
98
+ )
99
+ return {
100
+ children: splitRoot(result.wire.nodes, expandedPath).children,
101
+ next: childrenCursor(result.wire),
102
+ }
103
+ },
104
+ format: ({ children, next }, fmtOpts) => {
105
+ const items = applyFilter(children, opts.filter)
98
106
  presentList(
99
107
  items,
100
108
  { ...fmtOpts, quiet: opts.quiet, count: opts.count, long: opts.long },
101
109
  lsProjection,
102
110
  )
111
+ if (next && !isMachine(fmtOpts) && !opts.quiet && !opts.count) {
112
+ process.stdout.write(
113
+ chalk.dim(
114
+ ' more children truncated - page with `query ' +
115
+ expandedPath +
116
+ ' --depth 1 --children \'{"cursor":"' +
117
+ next +
118
+ '"}\'`\n',
119
+ ),
120
+ )
121
+ }
103
122
  },
104
123
  })
105
124
  }
@@ -107,9 +126,8 @@ export async function lsCommand(path: string, opts: LsOpts): Promise<void> {
107
126
  // ── Recursive tree ──────────────────────────────────────────
108
127
 
109
128
  const MAX_DEPTH = 5
110
- const MAX_NODES = 200
111
129
 
112
- type TreeNode = Item & { children?: TreeNode[] }
130
+ type TreeNode = Item & { children: TreeNode[] }
113
131
 
114
132
  async function recursiveLs(
115
133
  path: string,
@@ -121,8 +139,13 @@ async function recursiveLs(
121
139
 
122
140
  try {
123
141
  await withKernelClient(opts, async (ctx) => {
124
- const counter = { count: 0 }
125
- const tree = await withSelfHint(() => buildTree(ctx, path, 0, counter), meta)
142
+ // The recursive walk is now ONE function.get to the depth cap; the tree is
143
+ // reassembled client-side from the flat node page (was an N+1 buildTree).
144
+ const result = await withSelfHint(
145
+ () => bindGraph(ctx).query((q) => q.from(path).descend(MAX_DEPTH)),
146
+ meta,
147
+ )
148
+ const tree = buildTree(result.wire.nodes, path)
126
149
  spin?.succeed(`Tree of ${path}`)
127
150
  if (!machine) console.log('')
128
151
 
@@ -141,32 +164,30 @@ async function recursiveLs(
141
164
  }
142
165
  }
143
166
 
144
- async function buildTree(
145
- ctx: ClientContext,
146
- path: string,
147
- depth: number,
148
- counter: { count: number },
149
- ): Promise<TreeNode[]> {
150
- if (depth >= MAX_DEPTH || counter.count >= MAX_NODES) return []
151
- try {
152
- const result = await ctx.client.call(`${path}::listChildren`, {})
153
- const items = extractItems<Item>(result)
154
-
155
- const nodes: TreeNode[] = []
156
- for (const item of items) {
157
- if (counter.count >= MAX_NODES) break
158
- counter.count++
159
- const node: TreeNode = { ...item }
160
- // Descend by the child's absolute path (the kernel returns `path`, not `slug`).
161
- if (item.path && item.path !== '/') {
162
- node.children = await buildTree(ctx, item.path, depth + 1, counter)
163
- }
164
- nodes.push(node)
165
- }
166
- return nodes
167
- } catch {
168
- return []
167
+ /** Parent absolute path of an absolute node path (`/a/b` → `/a`; `/a` → `/`). */
168
+ function parentPathOf(path: string): string {
169
+ const i = path.lastIndexOf('/')
170
+ return i <= 0 ? '/' : path.slice(0, i)
171
+ }
172
+
173
+ /** Reassemble the flat `function.get` node page into a tree rooted at `rootPath`. */
174
+ function buildTree(nodes: readonly GraphNode[], rootPath: string): TreeNode[] {
175
+ const rootRaw = rawOf(rootPath)
176
+ const byPath = new Map<string, TreeNode>()
177
+ for (const n of nodes) {
178
+ const raw = rawOf(n.path)
179
+ if (raw === rootRaw) continue
180
+ byPath.set(raw, { ...n, children: [] })
181
+ }
182
+
183
+ const roots: TreeNode[] = []
184
+ for (const [raw, node] of byPath) {
185
+ const parent = parentPathOf(raw)
186
+ const parentNode = parent === rootRaw ? undefined : byPath.get(parent)
187
+ if (parentNode) parentNode.children.push(node)
188
+ else roots.push(node)
169
189
  }
190
+ return roots
170
191
  }
171
192
 
172
193
  function printTree(nodes: TreeNode[], prefix: string): void {
@@ -179,7 +200,7 @@ function printTree(nodes: TreeNode[], prefix: string): void {
179
200
  const name = basename(node.path) || node.id || '?'
180
201
  console.log(`${prefix}${connector}${chalk.cyan(name)} ${chalk.dim(classNameOf(node))}`)
181
202
 
182
- if (node.children && node.children.length > 0) {
203
+ if (node.children.length > 0) {
183
204
  printTree(node.children, prefix + childPrefix)
184
205
  }
185
206
  }
@@ -188,7 +209,7 @@ function printTree(nodes: TreeNode[], prefix: string): void {
188
209
  function printTreeQuiet(nodes: TreeNode[]): void {
189
210
  for (const node of nodes) {
190
211
  process.stdout.write(itemPath(node) + '\n')
191
- if (node.children) printTreeQuiet(node.children)
212
+ if (node.children.length > 0) printTreeQuiet(node.children)
192
213
  }
193
214
  }
194
215
 
@@ -197,13 +218,14 @@ export default {
197
218
  description: 'List children of a node',
198
219
  afterHelpText: `
199
220
  Behavior:
200
- Default output is a NAME/KIND/ID table on a TTY, JSON when piped. --filter
201
- matches a node KIND or label: Folder, Method, Domain. At a domain's tree
202
- position the children are Folder nodes (class.X), not Class so --filter
203
- Class returns nothing; use --filter Folder, or descend into class.<X> and
204
- --filter Method. -R tree view is TTY-only (raw/JSON emits the nested tree).
205
- -q prints one absolute path per line (pipeable). Note: ls /<domain> may
206
- report NOT_FOUND even when it exists use describe, or ls one of its children.
221
+ Default output is a NAME/KIND/ID table on a TTY, JSON when piped. One
222
+ function.get depth:1 fetches the direct children; -R fetches the whole
223
+ subtree (to depth ${MAX_DEPTH}) in a SINGLE call and renders it as a tree.
224
+ --filter matches a child KIND or label (post-filter): Folder, Function,
225
+ Domain. At a domain's tree position the children are Folder nodes
226
+ (class.X), not Class so --filter Class returns nothing; use --filter
227
+ Folder, or descend into class.<X>. -q prints one absolute path per line
228
+ (pipeable).
207
229
 
208
230
  Examples:
209
231
  $ astrale ls /
@@ -220,10 +242,10 @@ Examples:
220
242
  { flags: '--count', description: 'Print only the number of children' },
221
243
  {
222
244
  flags: '--filter <kind>',
223
- description: 'Filter children by kind or label (e.g., Folder, Method, Domain)',
245
+ description: 'Filter children by kind or label (e.g., Folder, Function, Domain)',
224
246
  },
225
247
  ],
226
248
  action: async (path, opts) => {
227
- await lsCommand((path as string | undefined) ?? '/', opts as Parameters<typeof lsCommand>[1])
249
+ await lsCommand((path as string | undefined) ?? '/', opts as LsOpts)
228
250
  },
229
251
  } satisfies CommandDefinition