@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
@@ -28,7 +28,7 @@ function newProject(): Project {
28
28
  function tryAddFile(project: Project, file: string): SourceFile | undefined {
29
29
  try {
30
30
  if (!existsSync(file)) return undefined
31
- return project.addSourceFileAtPath(file)
31
+ return project.getSourceFile(file) ?? project.addSourceFileAtPath(file)
32
32
  } catch {
33
33
  return undefined
34
34
  }
@@ -163,28 +163,129 @@ interface ConfigResolution {
163
163
  unlinkedReason?: 'no-config'
164
164
  }
165
165
 
166
- function resolveConfigObject(configNode: Node): ConfigResolution {
167
- // Case A: a call expression, e.g. `todo('Owner.name')`. Follow the helper to
168
- // its returned object literal (the local `todo` factory in evaluation).
169
- if (Node.isCallExpression(configNode)) {
170
- const obj = followFactoryToObject(configNode)
166
+ function resolveConfigObject(configNode: Node, seen = new Set<string>()): ConfigResolution {
167
+ const node = unwrapExpression(configNode)
168
+ const key = `${node.getSourceFile().getFilePath()}:${node.getStart()}`
169
+ if (seen.has(key)) return { implemented: false, unlinkedReason: 'no-config' }
170
+ seen.add(key)
171
+
172
+ if (Node.isCallExpression(node)) {
173
+ // Curried SDK helpers carry the handler config in their final argument:
174
+ // `remoteMethod()(schema, 'Issue', 'delete', { authorize, execute })`.
175
+ const lastArg = node.getArguments().at(-1)
176
+ if (lastArg) {
177
+ const candidate = unwrapExpression(lastArg)
178
+ if (Node.isObjectLiteralExpression(candidate) || Node.isIdentifier(candidate)) {
179
+ return resolveConfigObject(candidate, seen)
180
+ }
181
+ }
182
+
183
+ // Local factories such as `todo('Owner.name')` return the config object themselves.
184
+ const obj = followFactoryToObject(node)
171
185
  if (obj) return classifyObjectLiteral(obj)
172
186
  return { implemented: false, unlinkedReason: 'no-config' }
173
187
  }
174
- if (Node.isObjectLiteralExpression(configNode)) {
175
- return classifyObjectLiteral(configNode)
188
+ if (Node.isObjectLiteralExpression(node)) {
189
+ return classifyObjectLiteral(node)
176
190
  }
177
- // An identifier referencing a config defined elsewhere try to resolve it.
178
- if (Node.isIdentifier(configNode)) {
179
- const decl = firstValueDeclaration(configNode)
180
- if (decl && Node.isVariableDeclaration(decl)) {
181
- const init = decl.getInitializer()
182
- if (init) return resolveConfigObject(init)
183
- }
191
+ // Handler maps normally reference a local or imported `remoteMethod` value.
192
+ if (Node.isIdentifier(node)) {
193
+ const value = valueOfIdentifier(node)
194
+ if (value) return resolveConfigObject(value, seen)
184
195
  }
185
196
  return { implemented: false, unlinkedReason: 'no-config' }
186
197
  }
187
198
 
199
+ /** Remove syntax-only wrappers around a runtime value. */
200
+ function unwrapExpression(node: Node): Node {
201
+ let current = node
202
+ while (
203
+ Node.isParenthesizedExpression(current) ||
204
+ Node.isAsExpression(current) ||
205
+ Node.isSatisfiesExpression(current) ||
206
+ Node.isTypeAssertion(current)
207
+ ) {
208
+ current = current.getExpression()
209
+ }
210
+ return current
211
+ }
212
+
213
+ /** Resolve a local or relatively imported identifier to its runtime value. */
214
+ function valueOfIdentifier(id: Node): Node | undefined {
215
+ if (!Node.isIdentifier(id)) return undefined
216
+ const decl = firstValueDeclaration(id)
217
+ if (decl && Node.isVariableDeclaration(decl)) return decl.getInitializer()
218
+ return resolveImportedValue(id)
219
+ }
220
+
221
+ /** Follow a named relative import to the exported value that defines it. */
222
+ function resolveImportedValue(id: Node): Node | undefined {
223
+ if (!Node.isIdentifier(id)) return undefined
224
+ const localName = id.getText()
225
+ const sourceFile = id.getSourceFile()
226
+
227
+ for (const imp of sourceFile.getImportDeclarations()) {
228
+ for (const named of imp.getNamedImports()) {
229
+ const local = named.getAliasNode()?.getText() ?? named.getName()
230
+ if (local !== localName) continue
231
+ const target = resolveModuleFile(sourceFile, imp.getModuleSpecifierValue())
232
+ if (!target) return undefined
233
+ return findExportedValue(id.getProject(), target, named.getName(), new Set<string>())
234
+ }
235
+ }
236
+ return undefined
237
+ }
238
+
239
+ /** Find an exported variable/function value, following local and barrel exports. */
240
+ function findExportedValue(
241
+ project: Project,
242
+ file: string,
243
+ exportName: string,
244
+ seen: Set<string>,
245
+ ): Node | undefined {
246
+ const key = `${file}:${exportName}`
247
+ if (seen.has(key)) return undefined
248
+ seen.add(key)
249
+ const sf = tryAddFile(project, file)
250
+ if (!sf) return undefined
251
+
252
+ const localValue = (name: string, exportedOnly: boolean): Node | undefined => {
253
+ const variable = sf
254
+ .getVariableDeclarations()
255
+ .find(
256
+ (candidate) => candidate.getName() === name && (!exportedOnly || candidate.isExported()),
257
+ )
258
+ if (variable) return variable.getInitializer()
259
+ return sf
260
+ .getFunctions()
261
+ .find(
262
+ (candidate) => candidate.getName() === name && (!exportedOnly || candidate.isExported()),
263
+ )
264
+ }
265
+
266
+ const direct = localValue(exportName, true)
267
+ if (direct) return direct
268
+
269
+ for (const ex of sf.getExportDeclarations()) {
270
+ const modSpec = ex.getModuleSpecifierValue()
271
+ const target = modSpec ? resolveModuleFile(sf, modSpec) : undefined
272
+ const named = ex.getNamedExports()
273
+ if (named.length > 0) {
274
+ for (const specifier of named) {
275
+ const exposed = specifier.getAliasNode()?.getText() ?? specifier.getName()
276
+ if (exposed !== exportName) continue
277
+ const original = specifier.getName()
278
+ if (target) return findExportedValue(project, target, original, seen)
279
+ return localValue(original, false)
280
+ }
281
+ } else if (target) {
282
+ const found = findExportedValue(project, target, exportName, seen)
283
+ if (found) return found
284
+ }
285
+ }
286
+ return undefined
287
+ }
288
+
188
289
  /** Given `todo('x')`, find the factory's `return { … }` object literal. */
189
290
  function followFactoryToObject(call: CallExpression): Node | undefined {
190
291
  const expr = call.getExpression()
@@ -287,11 +388,8 @@ function functionBodyOf(node: Node): Node | null | undefined {
287
388
  if (Node.isArrowFunction(node) || Node.isFunctionExpression(node)) fn = node
288
389
  else if (Node.isMethodDeclaration(node)) fn = node
289
390
  else if (Node.isIdentifier(node)) {
290
- const decl = firstValueDeclaration(node)
291
- if (decl && Node.isVariableDeclaration(decl)) {
292
- const init = decl.getInitializer()
293
- if (init) return functionBodyOf(init)
294
- }
391
+ const value = valueOfIdentifier(node)
392
+ if (value) return functionBodyOf(value)
295
393
  return undefined
296
394
  }
297
395
  if (!fn) return undefined
@@ -434,9 +532,11 @@ function resolveModuleFile(from: SourceFile, spec: string): string | undefined {
434
532
  if (!spec.startsWith('.')) return undefined
435
533
  const baseDir = dirname(from.getFilePath())
436
534
  const base = resolvePath(baseDir, spec)
535
+ const sourceBase = base.replace(/\.(?:[cm]?js|jsx)$/, '')
437
536
  const candidates = [
438
- base.endsWith('.ts') ? base : `${base}.ts`,
439
- base.endsWith('.tsx') ? base : `${base}.tsx`,
537
+ base,
538
+ sourceBase.endsWith('.ts') ? sourceBase : `${sourceBase}.ts`,
539
+ sourceBase.endsWith('.tsx') ? sourceBase : `${sourceBase}.tsx`,
440
540
  `${base}/index.ts`,
441
541
  `${base}/index.tsx`,
442
542
  ]
@@ -0,0 +1,90 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+
3
+ import type { RememberedViewTarget, ViewTargetCandidate } from '../../shared/types'
4
+
5
+ import {
6
+ conciseCliFailure,
7
+ localViewUrl,
8
+ reconcileRememberedTarget,
9
+ targetFromRow,
10
+ targetQuery,
11
+ } from './views'
12
+
13
+ describe('managed local view URLs', () => {
14
+ test('mounts each view beneath the Studio-assigned server origin', () => {
15
+ expect(localViewUrl('http://127.0.0.1:5173', '/ui/issues')).toBe(
16
+ 'http://127.0.0.1:5173/ui/issues',
17
+ )
18
+ expect(localViewUrl('http://127.0.0.1:61001/ignored', 'ui/issue')).toBe(
19
+ 'http://127.0.0.1:61001/ui/issue',
20
+ )
21
+ })
22
+ })
23
+
24
+ test('reduces CLI stack output to the actionable kernel failure', () => {
25
+ expect(
26
+ conciseCliFailure(
27
+ `275 | if (code === KERNEL_ERROR_CODES.INVALID_REQUEST)\n276 |\nPermissionDeniedError: Permission denied: READ on /:issues.astrale.ai:view.issue\n details: {\n method: \"View:resolve\"\n}\n at mapServerError (errors.ts:280:61)\nBun v1.3.14`,
28
+ ),
29
+ ).toBe('PermissionDeniedError: Permission denied: READ on /:issues.astrale.ai:view.issue')
30
+ })
31
+
32
+ describe('target candidates', () => {
33
+ const remembered: RememberedViewTarget = {
34
+ id: 'gone-id',
35
+ className: 'Issue',
36
+ classOrigin: 'issues.astrale.ai',
37
+ label: 'Deleted issue',
38
+ }
39
+ const candidate: ViewTargetCandidate = {
40
+ id: 'live-id',
41
+ ref: '@live-id',
42
+ className: 'Issue',
43
+ classOrigin: 'issues.astrale.ai',
44
+ label: 'Live issue',
45
+ }
46
+
47
+ test('turns a missing remembered node into an explicit stale selection', () => {
48
+ expect(reconcileRememberedTarget(remembered, [candidate])).toEqual({
49
+ selected: null,
50
+ stale: remembered,
51
+ })
52
+ expect(reconcileRememberedTarget({ ...remembered, id: 'live-id' }, [candidate])).toEqual({
53
+ selected: candidate,
54
+ stale: null,
55
+ })
56
+ })
57
+
58
+ test('builds friendly candidates from generic qualified node properties', () => {
59
+ expect(
60
+ targetFromRow(
61
+ {
62
+ id: 'iss-1',
63
+ props: {
64
+ 'kernel.astrale.ai:interface.Named.property.name': 'Broken authorization badge',
65
+ 'kernel.astrale.ai:interface.Descriptable.property.description': 'Studio regression',
66
+ 'kernel.astrale.ai:interface.Statused.property.status': 'open',
67
+ },
68
+ },
69
+ 'Issue',
70
+ 'issues.astrale.ai',
71
+ ),
72
+ ).toEqual({
73
+ id: 'iss-1',
74
+ ref: '@iss-1',
75
+ className: 'Issue',
76
+ classOrigin: 'issues.astrale.ai',
77
+ label: 'Broken authorization badge',
78
+ description: 'Studio regression',
79
+ status: 'open',
80
+ })
81
+ })
82
+
83
+ test('queries exact class instances through instance_of', () => {
84
+ const query = targetQuery('Issue', 'issues.astrale.ai')
85
+ expect(query).toContain('(n:Node)-[:instance_of]->(c:Class)-[:of_domain]->(d:Domain)')
86
+ expect(query).toContain('"Issue"')
87
+ expect(query).toContain('"issues.astrale.ai"')
88
+ expect(query).toContain('LIMIT 201')
89
+ })
90
+ })