@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.
- package/LICENSE +202 -0
- package/README.md +2 -2
- package/THIRD-PARTY-NOTICES.md +27 -0
- package/dist/astrale.js +24396 -9632
- package/package.json +24 -22
- package/src/command.ts +2 -0
- package/src/commands/__tests__/admin-instance.test.ts +3 -2
- package/src/commands/__tests__/domain-list.test.ts +6 -2
- package/src/commands/__tests__/help-contract.test.ts +27 -14
- package/src/commands/__tests__/install-identity-override.test.ts +2 -2
- package/src/commands/__tests__/ls.test.ts +1 -1
- package/src/commands/__tests__/read-commands.test.ts +201 -0
- package/src/commands/__tests__/view.test.ts +100 -0
- package/src/commands/call.ts +27 -44
- package/src/commands/describe.ts +57 -58
- package/src/commands/domain/install.ts +9 -9
- package/src/commands/domain/list.ts +2 -2
- package/src/commands/domain/publish.ts +3 -3
- package/src/commands/get.ts +48 -23
- package/src/commands/identity/register.ts +27 -33
- package/src/commands/instance/active.ts +2 -2
- package/src/commands/instance/create.ts +1 -1
- package/src/commands/instance/delete.ts +3 -3
- package/src/commands/instance/list.ts +8 -3
- package/src/commands/instance/status.ts +4 -3
- package/src/commands/instance/use.ts +2 -2
- package/src/commands/logs.ts +8 -8
- package/src/commands/ls.ts +77 -55
- package/src/commands/mutate.ts +191 -0
- package/src/commands/query.ts +307 -20
- package/src/commands/session/analyze.ts +50 -0
- package/src/commands/session/list.ts +36 -0
- package/src/commands/token.ts +4 -7
- package/src/commands/view-serve.ts +26 -0
- package/src/commands/view.ts +614 -0
- package/src/connect-core.test.ts +42 -0
- package/src/connect-core.ts +53 -0
- package/src/kernel/__tests__/auth.test.ts +1 -0
- package/src/kernel/__tests__/expand.test.ts +123 -0
- package/src/kernel/client.ts +33 -37
- package/src/kernel/expand.ts +55 -61
- package/src/kernel/graph.ts +96 -0
- package/src/kernel/index.ts +18 -3
- package/src/kernel/options.ts +1 -1
- package/src/kernel/run.ts +0 -13
- package/src/lib/__tests__/instance-target.test.ts +34 -0
- package/src/lib/__tests__/table.test.ts +1 -1
- package/src/lib/__tests__/view-open-intent.test.ts +308 -0
- package/src/lib/__tests__/view-snapshot.test.ts +77 -0
- package/src/lib/admin-domain.ts +8 -4
- package/src/lib/admin-instance.ts +5 -1
- package/src/lib/config.ts +1 -0
- package/src/lib/domain-identity.ts +1 -1
- package/src/lib/instance-target.ts +5 -0
- package/src/lib/instance.ts +11 -0
- package/src/lib/log.ts +12 -2
- package/src/lib/login-flow.ts +41 -4
- package/src/lib/provision-instance.ts +2 -2
- package/src/lib/self.ts +1 -3
- package/src/lib/view/open-intent.ts +97 -0
- package/src/lib/view/resolve.ts +104 -0
- package/src/lib/view/server.ts +294 -0
- package/src/lib/view/session.ts +123 -0
- package/src/lib/view/snapshot.ts +101 -0
- package/src/program.ts +16 -3
- package/src/registry.ts +1 -1
- package/src/setup/render.ts +1 -3
- package/src/setup/steps/instance.ts +2 -2
- package/src/telemetry/__tests__/gate.test.ts +63 -0
- package/src/telemetry/__tests__/recorder.test.ts +110 -0
- package/src/telemetry/__tests__/redact.test.ts +79 -0
- package/src/telemetry/__tests__/session.test.ts +166 -0
- package/src/telemetry/__tests__/trigger.test.ts +49 -0
- package/src/telemetry/adapters/__tests__/claude-code.test.ts +89 -0
- package/src/telemetry/adapters/__tests__/codex.test.ts +138 -0
- package/src/telemetry/adapters/__tests__/index.test.ts +88 -0
- package/src/telemetry/adapters/claude-code.ts +90 -0
- package/src/telemetry/adapters/codex.ts +160 -0
- package/src/telemetry/adapters/index.ts +45 -0
- package/src/telemetry/adapters/types.ts +21 -0
- package/src/telemetry/analyze.ts +227 -0
- package/src/telemetry/gate.ts +79 -0
- package/src/telemetry/recorder.ts +57 -0
- package/src/telemetry/redact.ts +53 -0
- package/src/telemetry/session.ts +88 -0
- package/src/telemetry/settings.ts +26 -0
- package/src/telemetry/store.ts +91 -0
- package/src/telemetry/trigger.ts +119 -0
- package/src/telemetry/types.ts +64 -0
- package/studio/client/dist/assets/index-CyN5G8IA.js +109 -0
- package/studio/client/dist/assets/index-DKKMHBBC.css +1 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/server/agent/ask.ts +7 -2
- package/studio/server/agent/claude.ts +15 -3
- package/studio/server/agent/runner.ts +4 -1
- package/studio/server/agent/session-id.ts +13 -0
- package/studio/server/api.ts +50 -32
- package/studio/server/cache.ts +17 -6
- package/studio/server/client-package.test.ts +147 -0
- package/studio/server/client-package.ts +242 -0
- package/studio/server/index.ts +13 -0
- package/studio/server/introspect/anatomy-extras.test.ts +91 -0
- package/studio/server/introspect/anatomy-extras.ts +48 -49
- package/studio/server/introspect/anatomy.ts +7 -7
- package/studio/server/introspect/overlay-tsmorph.test.ts +104 -0
- package/studio/server/introspect/overlay-tsmorph.ts +230 -72
- package/studio/server/state/harness-gateway.ts +12 -3
- package/studio/server/state/harness-token.ts +0 -0
- package/studio/server/state/views.test.ts +90 -0
- package/studio/server/state/views.ts +396 -99
- package/studio/server/state/visibility.ts +10 -6
- package/studio/server/view-dev-server.test.ts +111 -0
- package/studio/server/view-dev-server.ts +372 -0
- package/studio/shared/types.ts +57 -10
- package/viewer/dist/index.html +93 -0
- package/viewer/dist/main.js +71 -0
- package/src/kernel/__tests__/remote-routing.test.ts +0 -70
- package/src/kernel/remote-routing.ts +0 -88
- package/studio/client/dist/assets/index-DOwzZAEK.css +0 -1
- package/studio/client/dist/assets/index-wtU0Zxhy.js +0 -183
- package/studio/tsconfig.json +0 -23
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { PatchInput } from '@astrale-os/kernel-client/graph'
|
|
2
|
+
|
|
3
|
+
import { patchDataSchema } from '@astrale-os/kernel-core'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
import { readFile } from 'node:fs/promises'
|
|
6
|
+
|
|
7
|
+
import type { CommandDefinition } from '../command'
|
|
8
|
+
import type { KernelCommandOpts } from '../kernel'
|
|
9
|
+
import type { MutationResultWire } from '../kernel'
|
|
10
|
+
|
|
11
|
+
import { bindGraph, runKernelCommand } from '../kernel'
|
|
12
|
+
import { log } from '../lib/log'
|
|
13
|
+
import { output } from '../lib/output'
|
|
14
|
+
import { renderTable } from '../lib/table'
|
|
15
|
+
|
|
16
|
+
type MutateOpts = KernelCommandOpts & { data?: string; file?: string; dry?: boolean }
|
|
17
|
+
|
|
18
|
+
export async function mutateCommand(opts: MutateOpts): Promise<void> {
|
|
19
|
+
let raw: unknown
|
|
20
|
+
try {
|
|
21
|
+
raw = await readPatch(opts)
|
|
22
|
+
} catch (e) {
|
|
23
|
+
log.error(e instanceof Error ? e.message : 'Invalid patch')
|
|
24
|
+
process.exit(1)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// --dry: validate the patch locally against the kernel's own schema and print
|
|
29
|
+
// the normalized form (every arm defaulted to []). No kernel round-trip.
|
|
30
|
+
if (opts.dry) {
|
|
31
|
+
const parsed = patchDataSchema.safeParse(raw)
|
|
32
|
+
if (!parsed.success) {
|
|
33
|
+
log.error('Patch failed local validation:')
|
|
34
|
+
for (const issue of parsed.error.issues) {
|
|
35
|
+
log.dim(` ${issue.path.join('.') || '(root)'}: ${issue.message}`)
|
|
36
|
+
}
|
|
37
|
+
process.exit(1)
|
|
38
|
+
}
|
|
39
|
+
output(parsed.data, opts)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await runKernelCommand<MutationResultWire>({
|
|
44
|
+
opts,
|
|
45
|
+
label: 'Mutate',
|
|
46
|
+
fn: (ctx) => bindGraph(ctx).mutate(raw as PatchInput),
|
|
47
|
+
format: (result, fmtOpts, isRaw) => {
|
|
48
|
+
if (isRaw) {
|
|
49
|
+
output(result, fmtOpts)
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
printResult(result)
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Patch source ladder (highest wins): --data > --file > stdin. */
|
|
58
|
+
async function readPatch(opts: MutateOpts): Promise<unknown> {
|
|
59
|
+
if (opts.data) {
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(opts.data)
|
|
62
|
+
} catch {
|
|
63
|
+
throw new Error(`Invalid JSON in --data: ${opts.data}`)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (opts.file) {
|
|
67
|
+
let text: string
|
|
68
|
+
try {
|
|
69
|
+
text = await readFile(opts.file, 'utf-8')
|
|
70
|
+
} catch (e) {
|
|
71
|
+
throw new Error(`Cannot read --file ${opts.file}: ${e instanceof Error ? e.message : e}`)
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(text)
|
|
75
|
+
} catch {
|
|
76
|
+
throw new Error(`Invalid JSON in ${opts.file}`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const stdin = await readStdin()
|
|
80
|
+
if (stdin) {
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(stdin)
|
|
83
|
+
} catch {
|
|
84
|
+
throw new Error('Invalid JSON from stdin')
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
throw new Error(
|
|
88
|
+
'No patch provided — pass --data <json>, --file <path>, or pipe a PatchData JSON on stdin',
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function readStdin(): Promise<string | null> {
|
|
93
|
+
if (process.stdin.isTTY) return null
|
|
94
|
+
const chunks: Buffer[] = []
|
|
95
|
+
for await (const chunk of process.stdin) {
|
|
96
|
+
chunks.push(chunk as Buffer)
|
|
97
|
+
}
|
|
98
|
+
const text = Buffer.concat(chunks).toString('utf-8').trim()
|
|
99
|
+
return text || null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function printResult(result: MutationResultWire): void {
|
|
103
|
+
const nodeRows = Object.entries(result.createdNodes ?? {})
|
|
104
|
+
const edgeRows = Object.entries(result.createdEdges ?? {})
|
|
105
|
+
|
|
106
|
+
if (nodeRows.length === 0 && edgeRows.length === 0) {
|
|
107
|
+
console.log(chalk.dim(' applied — no nodes or edges minted (updates/deletes only)'))
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (nodeRows.length > 0) {
|
|
112
|
+
console.log(` ${chalk.bold('Created nodes:')}`)
|
|
113
|
+
console.log(
|
|
114
|
+
renderTable(
|
|
115
|
+
nodeRows.map(([at, id]) => ({ at, id })),
|
|
116
|
+
{
|
|
117
|
+
columns: [
|
|
118
|
+
{ key: 'at', header: 'AT', color: chalk.cyan },
|
|
119
|
+
{ key: 'id', header: 'ID', color: chalk.dim },
|
|
120
|
+
],
|
|
121
|
+
showHeader: true,
|
|
122
|
+
},
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
console.log('')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (edgeRows.length > 0) {
|
|
129
|
+
console.log(` ${chalk.bold('Created edges:')}`)
|
|
130
|
+
console.log(
|
|
131
|
+
renderTable(
|
|
132
|
+
edgeRows.map(([tuple, id]) => ({ tuple, id })),
|
|
133
|
+
{
|
|
134
|
+
columns: [
|
|
135
|
+
{ key: 'tuple', header: 'CLASS|SOURCE|SLUG|TARGET', color: chalk.cyan },
|
|
136
|
+
{ key: 'id', header: 'ID', color: chalk.dim },
|
|
137
|
+
],
|
|
138
|
+
showHeader: true,
|
|
139
|
+
},
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
console.log('')
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export default {
|
|
147
|
+
name: 'mutate',
|
|
148
|
+
description: 'Apply a batch graph write (create/update/delete nodes & edges) via function.mutate',
|
|
149
|
+
afterHelpText: `
|
|
150
|
+
Behavior:
|
|
151
|
+
Sends a PatchData patch through the kernel's function.mutate door — a
|
|
152
|
+
single all-or-nothing write. Patch source (highest wins): --data > --file
|
|
153
|
+
> stdin. Prints the minted id maps (createdNodes: at→id, createdEdges:
|
|
154
|
+
class|source|slug|target → id); --json emits the raw MutationResult.
|
|
155
|
+
|
|
156
|
+
Authorization is per-arm: a create needs USE on the class and EDIT on the
|
|
157
|
+
parent, an update/delete needs EDIT on the target. A denied arm fails the
|
|
158
|
+
whole patch. --dry validates the patch locally (kernel patchDataSchema) and
|
|
159
|
+
prints the normalized form without touching the kernel.
|
|
160
|
+
|
|
161
|
+
PatchData shape:
|
|
162
|
+
{
|
|
163
|
+
"nodes": {
|
|
164
|
+
"create": [{ "class": "/:d:class.X", "at": "/d/x", "props": {} }],
|
|
165
|
+
"update": [{ "class": "/:d:class.X", "path": "/d/x", "props": {} }],
|
|
166
|
+
"delete": [{ "class": "/:d:class.X", "path": "/d/x" }]
|
|
167
|
+
},
|
|
168
|
+
"edges": {
|
|
169
|
+
"create": [{ "class": "/:d:class.e", "source": "/a", "target": "/b", "props": {} }],
|
|
170
|
+
"delete": [{ "class": "/:d:class.e", "source": "/a", "target": "/b" }]
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
Examples:
|
|
175
|
+
$ astrale mutate --data '{"nodes":{"create":[{"class":"/:blog.acme.com:class.Author","at":"/blog.acme.com/authors/ada","props":{}}]}}'
|
|
176
|
+
$ astrale mutate --file patch.json
|
|
177
|
+
$ echo '{"nodes":{"delete":[{"class":"/:d:class.X","path":"/d/x"}]}}' | astrale mutate
|
|
178
|
+
$ astrale mutate --file patch.json --dry
|
|
179
|
+
`,
|
|
180
|
+
options: [
|
|
181
|
+
{ flags: '-d, --data <json>', description: 'PatchData as a JSON string' },
|
|
182
|
+
{ flags: '-f, --file <path>', description: 'Read PatchData JSON from a file' },
|
|
183
|
+
{
|
|
184
|
+
flags: '--dry',
|
|
185
|
+
description: 'Validate locally (no kernel call) and print the normalized patch',
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
action: async (opts) => {
|
|
189
|
+
await mutateCommand(opts as MutateOpts)
|
|
190
|
+
},
|
|
191
|
+
} satisfies CommandDefinition
|
package/src/commands/query.ts
CHANGED
|
@@ -1,32 +1,319 @@
|
|
|
1
|
-
import { K } from '@astrale-os/kernel-core'
|
|
1
|
+
import { getInputSchema, K } from '@astrale-os/kernel-core'
|
|
2
|
+
import chalk from 'chalk'
|
|
2
3
|
|
|
3
4
|
import type { CommandDefinition } from '../command'
|
|
4
|
-
import type { KernelCommandOpts } from '../kernel'
|
|
5
|
+
import type { GetResultWire, KernelCommandOpts, QueryASTInput, SelfExpansionMeta } from '../kernel'
|
|
5
6
|
|
|
6
|
-
import { runKernelCommand } from '../kernel'
|
|
7
|
+
import { bindGraph, expandSelfInPath, runKernelCommand, withSelfHint } from '../kernel'
|
|
8
|
+
import { log } from '../lib/log'
|
|
9
|
+
import { isMachine, output } from '../lib/output'
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
type QueryOpts = KernelCommandOpts & {
|
|
12
|
+
depth?: string
|
|
13
|
+
children?: string
|
|
14
|
+
edges?: string
|
|
15
|
+
ast?: string
|
|
16
|
+
cypher?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type QueryMode =
|
|
20
|
+
| { kind: 'cypher'; cypher: string }
|
|
21
|
+
| { kind: 'ast'; ast: QueryASTInput }
|
|
22
|
+
| { kind: 'roots'; roots: string[]; meta: SelfExpansionMeta | undefined; query: BuiltQuery }
|
|
23
|
+
|
|
24
|
+
export async function queryCommand(paths: string[], opts: QueryOpts): Promise<void> {
|
|
25
|
+
let mode: QueryMode
|
|
26
|
+
try {
|
|
27
|
+
mode = await parseMode(paths, opts)
|
|
28
|
+
} catch (e) {
|
|
29
|
+
log.error(e instanceof Error ? e.message : 'Invalid arguments')
|
|
30
|
+
process.exit(1)
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (mode.kind === 'cypher') {
|
|
35
|
+
await runKernelCommand({
|
|
36
|
+
opts,
|
|
37
|
+
label: 'Query',
|
|
38
|
+
fn: (ctx) => ctx.client.call(K.$.f('query').path.domain.raw, { cypher: mode.cypher }),
|
|
39
|
+
format: (result, fmtOpts) => output(result, fmtOpts),
|
|
40
|
+
})
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await runKernelCommand<GetResultWire>({
|
|
10
45
|
opts,
|
|
11
|
-
label: 'Query',
|
|
12
|
-
fn: (ctx) =>
|
|
46
|
+
label: mode.kind === 'roots' ? 'Query ' + mode.roots.join(' ') : 'Query',
|
|
47
|
+
fn: async (ctx) => {
|
|
48
|
+
const read = () => bindGraph(ctx).query(mode.kind === 'roots' ? mode.query.ast : mode.ast)
|
|
49
|
+
const result = mode.kind === 'roots' ? await withSelfHint(read, mode.meta) : await read()
|
|
50
|
+
return result.wire
|
|
51
|
+
},
|
|
52
|
+
format: (result, fmtOpts) => {
|
|
53
|
+
output(result, fmtOpts)
|
|
54
|
+
if (result.next && !isMachine(fmtOpts)) printCursorFooter(result.next)
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function parseMode(paths: string[], opts: QueryOpts): Promise<QueryMode> {
|
|
60
|
+
const rootsInput = paths ?? []
|
|
61
|
+
const hasRoots = rootsInput.length > 0
|
|
62
|
+
const hasAst = opts.ast !== undefined
|
|
63
|
+
const hasCypher = opts.cypher !== undefined
|
|
64
|
+
const hasSelectors =
|
|
65
|
+
opts.depth !== undefined || opts.children !== undefined || opts.edges !== undefined
|
|
66
|
+
|
|
67
|
+
if (hasCypher) {
|
|
68
|
+
if (hasAst || hasRoots || hasSelectors) {
|
|
69
|
+
throw new Error('--cypher cannot be used with roots, --ast, --depth, --children, or --edges')
|
|
70
|
+
}
|
|
71
|
+
return { kind: 'cypher', cypher: opts.cypher as string }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (hasAst) {
|
|
75
|
+
if (hasRoots || hasSelectors) {
|
|
76
|
+
throw new Error('--ast cannot be used with positional roots or --depth/--children/--edges')
|
|
77
|
+
}
|
|
78
|
+
return { kind: 'ast', ast: parseAst(opts.ast as string) }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!hasRoots) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
'Usage: astrale query <paths...> [--depth <n>] [--children <json>] [--edges <json>] | --ast <json> | --cypher <query>',
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const { roots, meta } = await expandRoots(rootsInput, opts)
|
|
88
|
+
return { kind: 'roots', roots, meta, query: buildQuery(roots, opts) }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseAst(raw: string): QueryASTInput {
|
|
92
|
+
let parsed: unknown
|
|
93
|
+
try {
|
|
94
|
+
parsed = JSON.parse(raw)
|
|
95
|
+
} catch {
|
|
96
|
+
throw new Error('--ast must be JSON: ' + raw)
|
|
97
|
+
}
|
|
98
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
99
|
+
throw new Error('--ast must be a JSON object')
|
|
100
|
+
}
|
|
101
|
+
return parsed as QueryASTInput
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function expandRoots(
|
|
105
|
+
paths: string[],
|
|
106
|
+
opts: QueryOpts,
|
|
107
|
+
): Promise<{ roots: string[]; meta: SelfExpansionMeta | undefined }> {
|
|
108
|
+
const roots: string[] = []
|
|
109
|
+
let meta: SelfExpansionMeta | undefined
|
|
110
|
+
for (const p of paths) {
|
|
111
|
+
const expanded = await expandSelfInPath(p, opts)
|
|
112
|
+
roots.push(expanded.path)
|
|
113
|
+
if (!meta && expanded.meta) meta = expanded.meta
|
|
114
|
+
}
|
|
115
|
+
return { roots, meta }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
type QueryDir = 'in' | 'out' | 'both'
|
|
119
|
+
type QueryOrder = { by: string; dir: 'asc' | 'desc' }
|
|
120
|
+
type ChildrenSelector = { classes?: string[]; limit?: number; cursor?: string; order?: QueryOrder }
|
|
121
|
+
type EdgeSelector = {
|
|
122
|
+
as?: string
|
|
123
|
+
classes?: string[]
|
|
124
|
+
direction?: QueryDir
|
|
125
|
+
limit?: number
|
|
126
|
+
cursor?: string
|
|
127
|
+
order?: QueryOrder
|
|
128
|
+
}
|
|
129
|
+
type BuiltQuery = { ast: QueryASTInput; depth: number; hasEdges: boolean }
|
|
130
|
+
|
|
131
|
+
function buildQuery(roots: string[], opts: QueryOpts): BuiltQuery {
|
|
132
|
+
const depth = opts.depth !== undefined ? parseRange('--depth', opts.depth, 0, 5) : 0
|
|
133
|
+
const children =
|
|
134
|
+
opts.children !== undefined
|
|
135
|
+
? parseSelector<ChildrenSelector>('--children', opts.children, getInputSchema.shape.children)
|
|
136
|
+
: undefined
|
|
137
|
+
const edges =
|
|
138
|
+
opts.edges !== undefined
|
|
139
|
+
? parseSelector<EdgeSelector | EdgeSelector[]>(
|
|
140
|
+
'--edges',
|
|
141
|
+
opts.edges,
|
|
142
|
+
getInputSchema.shape.edges,
|
|
143
|
+
)
|
|
144
|
+
: undefined
|
|
145
|
+
|
|
146
|
+
const steps: NonNullable<QueryASTInput['steps']> = []
|
|
147
|
+
if (depth > 0) steps.push({ expand: childExpand(depth, children) })
|
|
148
|
+
edgeSelectors(edges).forEach((selector, index) => {
|
|
149
|
+
steps.push({ expand: edgeExpand(selector, index) })
|
|
13
150
|
})
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
ast: { version: 1, from: roots, ...(steps.length > 0 ? { steps } : {}) },
|
|
154
|
+
depth,
|
|
155
|
+
hasEdges: edges !== undefined,
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function childExpand(depth: number, children: ChildrenSelector | undefined) {
|
|
160
|
+
return {
|
|
161
|
+
edge: 'has_parent',
|
|
162
|
+
dir: 'in' as const,
|
|
163
|
+
depth,
|
|
164
|
+
...(children?.classes !== undefined ? { filter: { class: children.classes } } : {}),
|
|
165
|
+
...pageFields(children),
|
|
166
|
+
...(children?.order !== undefined ? { order: children.order } : {}),
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function edgeExpand(selector: EdgeSelector, index: number) {
|
|
171
|
+
return {
|
|
172
|
+
...(selector.classes !== undefined ? { edge: selector.classes } : {}),
|
|
173
|
+
dir: selector.direction ?? 'both',
|
|
174
|
+
as: selector.as ?? 'e' + index,
|
|
175
|
+
...pageFields(selector),
|
|
176
|
+
...(selector.order !== undefined ? { order: selector.order } : {}),
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function pageFields(selector: { limit?: number; cursor?: string } | undefined) {
|
|
181
|
+
if (selector?.limit === undefined && selector?.cursor === undefined) return {}
|
|
182
|
+
return {
|
|
183
|
+
page: {
|
|
184
|
+
...(selector.limit !== undefined ? { limit: selector.limit } : {}),
|
|
185
|
+
...(selector.cursor !== undefined ? { cursor: selector.cursor } : {}),
|
|
186
|
+
},
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function edgeSelectors(edges: EdgeSelector | EdgeSelector[] | undefined): EdgeSelector[] {
|
|
191
|
+
if (edges === undefined) return []
|
|
192
|
+
return Array.isArray(edges) ? edges : [edges]
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function parseRange(flag: string, raw: string, min: number, max: number): number {
|
|
196
|
+
const n = Number(raw)
|
|
197
|
+
if (!Number.isInteger(n) || n < min || n > max) {
|
|
198
|
+
throw new Error(flag + ' needs an integer in [' + min + ', ' + max + '], got "' + raw + '"')
|
|
199
|
+
}
|
|
200
|
+
return n
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
type SelectorIssue = { readonly path: readonly PropertyKey[]; readonly message: string }
|
|
204
|
+
type SelectorSchema = {
|
|
205
|
+
safeParse(
|
|
206
|
+
value: unknown,
|
|
207
|
+
): { success: true } | { success: false; error: { issues: readonly SelectorIssue[] } }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function parseSelector<T>(flag: string, raw: string, schema: SelectorSchema): T {
|
|
211
|
+
let parsed: unknown
|
|
212
|
+
try {
|
|
213
|
+
parsed = JSON.parse(raw)
|
|
214
|
+
} catch {
|
|
215
|
+
throw new Error(flag + ' must be JSON: ' + raw)
|
|
216
|
+
}
|
|
217
|
+
if (parsed === null || typeof parsed !== 'object') {
|
|
218
|
+
throw new Error(
|
|
219
|
+
flag + ' must be a JSON selector object' + (flag === '--edges' ? ' or array' : ''),
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
const check = schema.safeParse(parsed)
|
|
223
|
+
if (!check.success) {
|
|
224
|
+
const detail = check.error.issues
|
|
225
|
+
.map((i) => (i.path.join('.') || '(root)') + ': ' + i.message)
|
|
226
|
+
.join('; ')
|
|
227
|
+
throw new Error(flag + ' invalid selector: ' + detail)
|
|
228
|
+
}
|
|
229
|
+
return parsed as T
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function printCursorFooter(next: NonNullable<GetResultWire['next']>): void {
|
|
233
|
+
const entries = Object.entries(next)
|
|
234
|
+
if (entries.length === 1) {
|
|
235
|
+
const cursors = entries[0]?.[1]
|
|
236
|
+
if (cursors?.children) {
|
|
237
|
+
process.stdout.write(
|
|
238
|
+
chalk.dim(' more: --children \'{"cursor":"' + cursors.children + '"}\'\n'),
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
for (const [alias, cursor] of Object.entries(cursors?.edges ?? {})) {
|
|
242
|
+
process.stdout.write(
|
|
243
|
+
chalk.dim(
|
|
244
|
+
' more edges[' +
|
|
245
|
+
alias +
|
|
246
|
+
']: --edges \'{"as":"' +
|
|
247
|
+
alias +
|
|
248
|
+
'","cursor":"' +
|
|
249
|
+
cursor +
|
|
250
|
+
'"}\'\n',
|
|
251
|
+
),
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
process.stdout.write(
|
|
257
|
+
chalk.dim(' more results - per-root cursors in .next (page each root by --cursor)\n'),
|
|
258
|
+
)
|
|
14
259
|
}
|
|
15
260
|
|
|
16
261
|
export default {
|
|
17
262
|
name: 'query',
|
|
18
|
-
description: 'Run a
|
|
19
|
-
afterHelpText:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
263
|
+
description: 'Run a structured graph read',
|
|
264
|
+
afterHelpText: [
|
|
265
|
+
'',
|
|
266
|
+
'Behavior:',
|
|
267
|
+
' Structured read door for the query AST. Positional roots build a v1 AST',
|
|
268
|
+
' with optional child and edge expansion, then lower through function.get',
|
|
269
|
+
' today. A true query syscall may back this command later.',
|
|
270
|
+
'',
|
|
271
|
+
' Output is always the full GraphData envelope { nodes, edges, aliases }',
|
|
272
|
+
' with .roots and .next when returned. On a TTY, cursor footers are printed',
|
|
273
|
+
' when .next has more pages.',
|
|
274
|
+
'',
|
|
275
|
+
' --children takes { classes?, limit?, cursor?, order? } and shapes the',
|
|
276
|
+
' depth-1 children page (needs --depth >= 1 to bite). --edges takes an edge',
|
|
277
|
+
' selector, or a JSON array of selectors.',
|
|
278
|
+
'',
|
|
279
|
+
' --ast (experimental) accepts a raw QueryASTInput JSON object. The AST shape',
|
|
280
|
+
' is not a stable contract yet — prefer the flags above. @self is not expanded',
|
|
281
|
+
' inside --ast JSON.',
|
|
282
|
+
'',
|
|
283
|
+
' --cypher is a read-only escape hatch. The kernel rejects write keywords',
|
|
284
|
+
' such as CREATE, DELETE, SET, MERGE, REMOVE, and DETACH.',
|
|
285
|
+
'',
|
|
286
|
+
'Examples:',
|
|
287
|
+
' $ astrale query / --depth 1',
|
|
288
|
+
' $ astrale query /a /b --edges \'{"direction":"both"}\'',
|
|
289
|
+
' $ astrale query /kernel.astrale.ai --depth 2 --children \'{"classes":["/:kernel.astrale.ai:class.Folder"]}\'',
|
|
290
|
+
" $ astrale query --cypher 'MATCH (n) RETURN count(n) AS total'",
|
|
291
|
+
'',
|
|
292
|
+
].join('\n'),
|
|
293
|
+
arguments: [
|
|
294
|
+
{
|
|
295
|
+
name: 'paths...',
|
|
296
|
+
description: 'One or more root paths (/domain/Class) or IDs (@nodeId)',
|
|
297
|
+
required: false,
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
options: [
|
|
301
|
+
{ flags: '--depth <n>', description: 'Subtree depth to fetch (0-5, default 0)' },
|
|
302
|
+
{
|
|
303
|
+
flags: '--children <json>',
|
|
304
|
+
description: 'Children selector { classes?, limit?, cursor?, order? } (needs --depth >= 1)',
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
flags: '--edges <json>',
|
|
308
|
+
description: 'Edge selector (or JSON array of selectors) to include',
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
flags: '--ast <json>',
|
|
312
|
+
description: 'Raw QueryASTInput JSON object (experimental, unstable shape)',
|
|
313
|
+
},
|
|
314
|
+
{ flags: '--cypher <query>', description: 'Read-only Cypher escape hatch' },
|
|
315
|
+
],
|
|
316
|
+
action: async (paths, opts) => {
|
|
317
|
+
await queryCommand(Array.isArray(paths) ? paths : [], opts as QueryOpts)
|
|
31
318
|
},
|
|
32
319
|
} satisfies CommandDefinition
|
|
@@ -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
|
package/src/commands/token.ts
CHANGED
|
@@ -2,12 +2,11 @@ import type { CommandDefinition } from '../command'
|
|
|
2
2
|
import type { KernelCommandOpts } from '../kernel'
|
|
3
3
|
|
|
4
4
|
import { runKernelCommand } from '../kernel'
|
|
5
|
-
import { mintDelegationPath } from '../kernel/remote-routing'
|
|
6
5
|
import { log } from '../lib/log'
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* `astrale token` — mint a fresh delegation token for the active instance
|
|
10
|
-
* + active identity
|
|
9
|
+
* + active identity through the bound AuthApi.
|
|
11
10
|
*/
|
|
12
11
|
export type TokenOpts = KernelCommandOpts & {
|
|
13
12
|
audience?: string
|
|
@@ -26,12 +25,10 @@ export async function tokenCommand(opts: TokenOpts): Promise<void> {
|
|
|
26
25
|
const audience = opts.audience ?? ''
|
|
27
26
|
const parsedTtl = Number(opts.ttl)
|
|
28
27
|
const ttl = Number.isFinite(parsedTtl) && parsedTtl > 0 ? parsedTtl : 3600
|
|
29
|
-
const
|
|
30
|
-
const result = (await ctx.client.call(mintPath, {
|
|
28
|
+
const result = await ctx.client.as(ctx.credential).auth.delegate({
|
|
31
29
|
audience,
|
|
32
|
-
delegation: { kind: 'identity', self: true },
|
|
33
30
|
ttl,
|
|
34
|
-
})
|
|
31
|
+
})
|
|
35
32
|
return result
|
|
36
33
|
},
|
|
37
34
|
format: (token, fmtOpts, isRaw) => {
|
|
@@ -63,7 +60,7 @@ What this token is FOR — worker-direct HTTP calls:
|
|
|
63
60
|
use 'astrale call' (which signs per-call) instead.
|
|
64
61
|
|
|
65
62
|
Examples:
|
|
66
|
-
$ export TOKEN=$(astrale token --audience
|
|
63
|
+
$ export TOKEN=$(astrale token --audience shell.astrale.ai --raw)
|
|
67
64
|
$ astrale token --audience worker.example.com --for alice -i staging
|
|
68
65
|
`,
|
|
69
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
|