@astrale-os/cli 1.0.0-beta.0 → 1.0.0-beta.2
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/README.md +7 -11
- package/dist/astrale.js +3769 -3549
- package/dist/public/connect-core.js +396 -392
- package/dist/public/keys/index.js +371 -378
- package/dist/public/paths/index.js +371 -378
- package/dist/types/lib/idp.d.ts +6 -0
- package/dist/types/lib/meta.d.ts +2 -2
- package/package.json +6 -4
- package/src/admin/binding.ts +168 -0
- package/src/admin/catalog/__tests__/client.test.ts +2 -2
- package/src/admin/catalog/client.ts +4 -9
- package/src/admin/instance/__tests__/client.test.ts +5 -3
- package/src/admin/instance/client.ts +9 -27
- package/src/commands/__tests__/auth-login.test.ts +54 -1
- package/src/commands/__tests__/call.test.ts +5 -1
- package/src/commands/__tests__/domain-install-operation.test.ts +23 -7
- package/src/commands/__tests__/domain-install-owned.test.ts +5 -1
- package/src/commands/__tests__/domain-list.test.ts +83 -4
- package/src/commands/__tests__/install-identity-override.test.ts +30 -19
- package/src/commands/__tests__/studio-descriptor.test.ts +22 -0
- package/src/commands/__tests__/update.test.ts +73 -0
- package/src/commands/domain/install.ts +11 -15
- package/src/commands/domain/list.ts +17 -12
- package/src/commands/studio.ts +20 -0
- package/src/commands/update.ts +39 -13
- package/src/connection/__tests__/errors.test.ts +16 -12
- package/src/connection/__tests__/exchange.test.ts +6 -5
- package/src/connection/exchange.ts +5 -4
- package/src/lib/__tests__/domain-publication.test.ts +144 -0
- package/src/lib/__tests__/idp-session.test.ts +22 -0
- package/src/lib/__tests__/idp.test.ts +2 -0
- package/src/lib/__tests__/instance.test.ts +6 -0
- package/src/lib/__tests__/studio-server-deps.test.ts +53 -16
- package/src/lib/__tests__/update.test.ts +12 -0
- package/src/lib/domain-publication.ts +102 -0
- package/src/lib/idp.ts +5 -1
- package/src/lib/instance.ts +6 -1
- package/src/lib/login-flow.ts +13 -3
- package/src/lib/meta.ts +2 -2
- package/src/lib/update.ts +4 -4
- package/src/program/__tests__/program.test.ts +1 -1
- package/studio/client/dist/assets/{elk-api-D2xgMJvi.js → elk-api-lwhFo7vB.js} +1 -1
- package/studio/client/dist/assets/index-B-c-smo9.js +8 -0
- package/studio/client/dist/assets/index-BckHuAWk.js +81 -0
- package/studio/client/dist/assets/index-C4aNQ6dz.css +1 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/package.json +3 -1
- package/studio/server/agent/bridge/stdio.ts +8 -1
- package/studio/server/agent/conversation.test.ts +10 -5
- package/studio/server/agent/conversation.ts +56 -8
- package/studio/server/agent/harness/codex/loadout.ts +18 -21
- package/studio/server/agent/harness/gateway/config.ts +20 -18
- package/studio/server/agent/harness/selection.ts +9 -1
- package/studio/server/agent/prompts/system.ts +3 -2
- package/studio/server/agent/prompts/turn.ts +13 -4
- package/studio/server/agent/run/completion.ts +2 -2
- package/studio/server/agent/run/preparation.ts +6 -5
- package/studio/server/agent/run/transcript.test.ts +35 -1
- package/studio/server/agent/run/transcript.ts +169 -3
- package/studio/server/agent/run/usage.test.ts +24 -0
- package/studio/server/agent/run/usage.ts +26 -1
- package/studio/server/api/canvas.ts +44 -0
- package/studio/server/api/comments.ts +85 -0
- package/studio/server/api/context.ts +61 -0
- package/studio/server/api/deployment.ts +39 -0
- package/studio/server/api/documents.ts +55 -0
- package/studio/server/api/domain.ts +59 -0
- package/studio/server/api/http.ts +45 -0
- package/studio/server/api/project.ts +57 -0
- package/studio/server/api/schema.ts +12 -0
- package/studio/server/api/views.ts +57 -0
- package/studio/server/api/workspace.ts +60 -0
- package/studio/server/api.test.ts +147 -0
- package/studio/server/api.ts +20 -422
- package/studio/server/cache.test.ts +111 -0
- package/studio/server/cache.ts +163 -9
- package/studio/server/cli-consumers.test.ts +494 -0
- package/studio/server/cli.test.ts +88 -0
- package/studio/server/cli.ts +240 -0
- package/studio/server/client-package.ts +25 -1
- package/studio/server/domain.test.ts +30 -2
- package/studio/server/domain.ts +14 -2
- package/studio/server/environment/dotenv-preview.test.ts +39 -0
- package/studio/server/environment/dotenv-preview.ts +27 -0
- package/studio/server/environment/files.test.ts +53 -0
- package/studio/server/{state/env.ts → environment/files.ts} +7 -48
- package/studio/server/{state → handoff}/copy.ts +21 -6
- package/studio/server/handoff/service.test.ts +22 -0
- package/studio/server/{state/handoff.ts → handoff/service.ts} +21 -14
- package/studio/server/instances/active.ts +72 -0
- package/studio/server/instances/deploy-record.test.ts +31 -0
- package/studio/server/instances/deploy-record.ts +37 -0
- package/studio/server/instances/deploy.ts +56 -0
- package/studio/server/{state/instance.test.ts → instances/probe.test.ts} +25 -21
- package/studio/server/instances/probe.ts +67 -0
- package/studio/server/instances/status.test.ts +12 -0
- package/studio/server/instances/status.ts +49 -0
- package/studio/server/introspect/anatomy/client-tree.ts +57 -0
- package/studio/server/introspect/anatomy/env-fields.ts +66 -0
- package/studio/server/introspect/anatomy/schema-definition.ts +53 -0
- package/studio/server/introspect/anatomy/source.ts +181 -0
- package/studio/server/introspect/anatomy/views/legacy.ts +232 -0
- package/studio/server/introspect/anatomy/views/routes.ts +105 -0
- package/studio/server/introspect/anatomy/views.ts +58 -0
- package/studio/server/introspect/anatomy-extras.test.ts +91 -4
- package/studio/server/introspect/anatomy-extras.ts +7 -726
- package/studio/server/introspect/anatomy.ts +46 -19
- package/studio/server/introspect/bundle.ts +15 -6
- package/studio/server/introspect/canonical-schema.test.ts +79 -0
- package/studio/server/introspect/canonical-schema.ts +80 -18
- package/studio/server/introspect/config-preview.test.ts +32 -0
- package/studio/server/introspect/config-preview.ts +119 -0
- package/studio/server/introspect/diff.test.ts +7 -11
- package/studio/server/introspect/diff.ts +28 -37
- package/studio/server/introspect/extractor.ts +8 -6
- package/studio/server/introspect/hash.ts +5 -2
- package/studio/server/introspect/overlay-tsmorph.ts +7 -1305
- package/studio/server/introspect/revision.test.ts +54 -0
- package/studio/server/introspect/revision.ts +49 -0
- package/studio/server/introspect/runtime.test.ts +63 -5
- package/studio/server/introspect/runtime.ts +49 -3
- package/studio/server/introspect/schema-ir-json.ts +181 -0
- package/studio/server/introspect/source-overlay/annotations.ts +14 -0
- package/studio/server/introspect/source-overlay/handlers.ts +681 -0
- package/studio/server/introspect/source-overlay/kernel-calls.ts +49 -0
- package/studio/server/introspect/source-overlay/project.ts +248 -0
- package/studio/server/introspect/source-overlay/spans.ts +360 -0
- package/studio/server/json.ts +46 -0
- package/studio/server/lifecycle.ts +5 -1
- package/studio/server/sse.test.ts +26 -0
- package/studio/server/sse.ts +4 -1
- package/studio/server/state/baseline.test.ts +212 -1
- package/studio/server/state/baseline.ts +145 -29
- package/studio/server/state/comments.test.ts +66 -1
- package/studio/server/state/comments.ts +173 -10
- package/studio/server/state/context.ts +34 -2
- package/studio/server/state/documents.ts +42 -1
- package/studio/server/state/integrations.ts +27 -1
- package/studio/server/state/layout.test.ts +34 -0
- package/studio/server/state/layout.ts +44 -17
- package/studio/server/state/settings.ts +29 -22
- package/studio/server/state/store.test.ts +122 -0
- package/studio/server/state/store.ts +66 -17
- package/studio/server/state/visibility.ts +22 -7
- package/studio/server/views/model.test.ts +57 -0
- package/studio/server/views/model.ts +66 -0
- package/studio/server/views/runtime.ts +40 -0
- package/studio/server/views/selection-repository.test.ts +31 -0
- package/studio/server/views/selection-repository.ts +60 -0
- package/studio/server/views/session.test.ts +73 -0
- package/studio/server/views/session.ts +166 -0
- package/studio/server/{state/views.test.ts → views/target.test.ts} +8 -159
- package/studio/server/views/target.ts +248 -0
- package/studio/server/watch.ts +1 -1
- package/studio/server/workspace/catalog.test.ts +17 -0
- package/studio/server/workspace/catalog.ts +43 -0
- package/studio/server/{state → workspace}/create.test.ts +1 -1
- package/studio/server/{state → workspace}/create.ts +1 -1
- package/studio/server/{state → workspace}/git.ts +1 -1
- package/studio/server/workspace/updates.ts +83 -0
- package/studio/server/workspace-state.ts +1 -1
- package/studio/shared/contracts/README.md +16 -0
- package/studio/shared/contracts/agent.ts +270 -0
- package/studio/shared/contracts/runtime.ts +50 -0
- package/studio/shared/contracts/schema.ts +462 -0
- package/studio/shared/contracts/surface.test.ts +319 -0
- package/studio/shared/contracts/workspace.ts +274 -0
- package/studio/shared/layout.test.ts +116 -0
- package/studio/shared/schema/identity.test.ts +58 -0
- package/studio/shared/schema/identity.ts +82 -0
- package/studio/shared/types.ts +11 -1099
- package/studio/tsconfig.json +1 -1
- package/viewer/dist/main.js +35 -35
- package/studio/client/dist/assets/index-BMdnsIJA.css +0 -1
- package/studio/client/dist/assets/index-D-vRV8w7.js +0 -8
- package/studio/client/dist/assets/index-LGSWRrk8.js +0 -81
- package/studio/server/state/catalog.ts +0 -117
- package/studio/server/state/instance.ts +0 -280
- package/studio/server/state/updates.ts +0 -63
- package/studio/server/state/views.ts +0 -535
|
@@ -2,26 +2,129 @@
|
|
|
2
2
|
* comments.ts — the comments / open-questions store. ALL persistence goes through
|
|
3
3
|
* store.ts (writes are allow-listed to <domain>/.domain-studio/). The on-disk file
|
|
4
4
|
* is `comments.json`, shaped { schemaVersion, comments: Comment[] }.
|
|
5
|
+
* `schemaVersion` is the historical annotate wire key for Studio's render
|
|
6
|
+
* fingerprint; it is not a canonical DSL schema revision.
|
|
5
7
|
*
|
|
6
8
|
* Comments are annotate-compatible: each thread entry is
|
|
7
9
|
* { id, role:'user'|'author', type:'text'|'choice', text, options?, answer? }
|
|
8
10
|
* and `kind` is derived from the FIRST thread entry's role
|
|
9
11
|
* (author-seeded ⇒ 'question', else 'comment').
|
|
10
12
|
*/
|
|
11
|
-
import type { Comment, CommentStore, ThreadEntry } from '../../shared/types'
|
|
13
|
+
import type { AnchorRef, Comment, CommentStore, ThreadEntry } from '../../shared/types'
|
|
12
14
|
|
|
15
|
+
import {
|
|
16
|
+
asBoolean,
|
|
17
|
+
asFiniteNumber,
|
|
18
|
+
asJsonRecord,
|
|
19
|
+
asString,
|
|
20
|
+
asStringArray,
|
|
21
|
+
parseJson,
|
|
22
|
+
} from '../json'
|
|
13
23
|
import { readJson, writeJson } from './store'
|
|
14
24
|
|
|
15
25
|
const FILE = 'comments.json'
|
|
16
26
|
|
|
27
|
+
function decodeAnchorRef(value: unknown): AnchorRef | undefined {
|
|
28
|
+
const record = asJsonRecord(value)
|
|
29
|
+
const ref = asString(record?.ref)
|
|
30
|
+
const kind = record?.kind
|
|
31
|
+
if (!ref || !['schema', 'section', 'file', 'free'].includes(String(kind))) return undefined
|
|
32
|
+
const file = asString(record?.file)
|
|
33
|
+
const startLine = asFiniteNumber(record?.startLine)
|
|
34
|
+
const endLine = asFiniteNumber(record?.endLine)
|
|
35
|
+
const label = asString(record?.label)
|
|
36
|
+
const x = asFiniteNumber(record?.x)
|
|
37
|
+
const y = asFiniteNumber(record?.y)
|
|
38
|
+
return {
|
|
39
|
+
ref,
|
|
40
|
+
kind: kind as AnchorRef['kind'],
|
|
41
|
+
...(file === undefined ? {} : { file }),
|
|
42
|
+
...(startLine === undefined ? {} : { startLine }),
|
|
43
|
+
...(endLine === undefined ? {} : { endLine }),
|
|
44
|
+
...(label === undefined ? {} : { label }),
|
|
45
|
+
...(x === undefined ? {} : { x }),
|
|
46
|
+
...(y === undefined ? {} : { y }),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function decodeThreadEntry(value: unknown): ThreadEntry | undefined {
|
|
51
|
+
const record = asJsonRecord(value)
|
|
52
|
+
if (!record) return undefined
|
|
53
|
+
const id = asString(record.id)
|
|
54
|
+
const role = record.role
|
|
55
|
+
const type = record.type
|
|
56
|
+
const text = asString(record.text)
|
|
57
|
+
if (
|
|
58
|
+
!id ||
|
|
59
|
+
(role !== 'user' && role !== 'author') ||
|
|
60
|
+
(type !== 'text' && type !== 'choice') ||
|
|
61
|
+
text === undefined
|
|
62
|
+
) {
|
|
63
|
+
return undefined
|
|
64
|
+
}
|
|
65
|
+
const options = asStringArray(record.options)
|
|
66
|
+
const answer = record.answer === null ? null : asString(record.answer)
|
|
67
|
+
return {
|
|
68
|
+
id,
|
|
69
|
+
role,
|
|
70
|
+
type,
|
|
71
|
+
text,
|
|
72
|
+
...(options === undefined ? {} : { options }),
|
|
73
|
+
...(answer === undefined ? {} : { answer }),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function decodeComment(value: unknown): Comment | undefined {
|
|
78
|
+
const record = asJsonRecord(value)
|
|
79
|
+
if (!record) return undefined
|
|
80
|
+
const id = asString(record.id)
|
|
81
|
+
if (!id) return undefined
|
|
82
|
+
const anchors = asStringArray(record.anchors) ?? []
|
|
83
|
+
const anchorRefs = Array.isArray(record.anchorRefs)
|
|
84
|
+
? record.anchorRefs.flatMap((anchor) => {
|
|
85
|
+
const decoded = decodeAnchorRef(anchor)
|
|
86
|
+
return decoded ? [decoded] : []
|
|
87
|
+
})
|
|
88
|
+
: []
|
|
89
|
+
const thread = Array.isArray(record.thread)
|
|
90
|
+
? record.thread.flatMap((entry) => {
|
|
91
|
+
const decoded = decodeThreadEntry(entry)
|
|
92
|
+
return decoded ? [decoded] : []
|
|
93
|
+
})
|
|
94
|
+
: []
|
|
95
|
+
const status = record.status === 'closed' ? 'closed' : 'open'
|
|
96
|
+
const closeNote = asString(record.closeNote)
|
|
97
|
+
const createdAt = asString(record.createdAt) ?? ''
|
|
98
|
+
const orphaned = asBoolean(record.orphaned)
|
|
99
|
+
return {
|
|
100
|
+
id,
|
|
101
|
+
anchors,
|
|
102
|
+
anchorRefs,
|
|
103
|
+
status,
|
|
104
|
+
thread,
|
|
105
|
+
createdAt,
|
|
106
|
+
kind: deriveKind(thread),
|
|
107
|
+
...(closeNote === undefined ? {} : { closeNote }),
|
|
108
|
+
...(orphaned === undefined ? {} : { orphaned }),
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function decodeCommentStore(value: unknown): CommentStore | undefined {
|
|
113
|
+
const record = asJsonRecord(value)
|
|
114
|
+
if (!record) return undefined
|
|
115
|
+
const comments = Array.isArray(record.comments)
|
|
116
|
+
? record.comments.flatMap((comment) => {
|
|
117
|
+
const decoded = decodeComment(comment)
|
|
118
|
+
return decoded ? [decoded] : []
|
|
119
|
+
})
|
|
120
|
+
: []
|
|
121
|
+
return { schemaVersion: asString(record.schemaVersion) ?? '', comments }
|
|
122
|
+
}
|
|
123
|
+
|
|
17
124
|
export function readComments(root: string): CommentStore {
|
|
18
125
|
// Allocate a fresh array for every missing store. A module-level EMPTY object
|
|
19
126
|
// would share its nested `comments` array across domains until each persisted.
|
|
20
|
-
|
|
21
|
-
return {
|
|
22
|
-
schemaVersion: store.schemaVersion ?? '',
|
|
23
|
-
comments: Array.isArray(store.comments) ? store.comments : [],
|
|
24
|
-
}
|
|
127
|
+
return readJson(root, FILE, decodeCommentStore, { schemaVersion: '', comments: [] })
|
|
25
128
|
}
|
|
26
129
|
|
|
27
130
|
function persist(root: string, store: CommentStore): void {
|
|
@@ -206,6 +309,63 @@ interface PastedStore {
|
|
|
206
309
|
comments?: PastedComment[]
|
|
207
310
|
}
|
|
208
311
|
|
|
312
|
+
function decodePastedEntry(value: unknown): PastedThreadEntry | undefined {
|
|
313
|
+
const record = asJsonRecord(value)
|
|
314
|
+
if (!record) return undefined
|
|
315
|
+
const role = record.role === 'user' || record.role === 'author' ? record.role : undefined
|
|
316
|
+
const type = record.type === 'text' || record.type === 'choice' ? record.type : undefined
|
|
317
|
+
const id = asString(record.id)
|
|
318
|
+
const text = asString(record.text)
|
|
319
|
+
const options = asStringArray(record.options)
|
|
320
|
+
const answer = record.answer === null ? null : asString(record.answer)
|
|
321
|
+
return {
|
|
322
|
+
...(id === undefined ? {} : { id }),
|
|
323
|
+
...(role === undefined ? {} : { role }),
|
|
324
|
+
...(type === undefined ? {} : { type }),
|
|
325
|
+
...(text === undefined ? {} : { text }),
|
|
326
|
+
...(options === undefined ? {} : { options }),
|
|
327
|
+
...(answer === undefined ? {} : { answer }),
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function decodePastedComment(value: unknown): PastedComment | undefined {
|
|
332
|
+
const record = asJsonRecord(value)
|
|
333
|
+
if (!record) return undefined
|
|
334
|
+
const id = asString(record.id)
|
|
335
|
+
const anchors = asStringArray(record.anchors)
|
|
336
|
+
const status = record.status === 'open' || record.status === 'closed' ? record.status : undefined
|
|
337
|
+
const closeNote = asString(record.closeNote)
|
|
338
|
+
const thread = Array.isArray(record.thread)
|
|
339
|
+
? record.thread.flatMap((entry) => {
|
|
340
|
+
const decoded = decodePastedEntry(entry)
|
|
341
|
+
return decoded ? [decoded] : []
|
|
342
|
+
})
|
|
343
|
+
: undefined
|
|
344
|
+
return {
|
|
345
|
+
...(id === undefined ? {} : { id }),
|
|
346
|
+
...(anchors === undefined ? {} : { anchors }),
|
|
347
|
+
...(status === undefined ? {} : { status }),
|
|
348
|
+
...(closeNote === undefined ? {} : { closeNote }),
|
|
349
|
+
...(thread === undefined ? {} : { thread }),
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function decodePastedStore(value: unknown): PastedStore | undefined {
|
|
354
|
+
const record = asJsonRecord(value)
|
|
355
|
+
if (!record) return undefined
|
|
356
|
+
const schemaVersion = asString(record.schemaVersion)
|
|
357
|
+
const comments = Array.isArray(record.comments)
|
|
358
|
+
? record.comments.flatMap((comment) => {
|
|
359
|
+
const decoded = decodePastedComment(comment)
|
|
360
|
+
return decoded ? [decoded] : []
|
|
361
|
+
})
|
|
362
|
+
: undefined
|
|
363
|
+
return {
|
|
364
|
+
...(schemaVersion === undefined ? {} : { schemaVersion }),
|
|
365
|
+
...(comments === undefined ? {} : { comments }),
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
209
369
|
/** Extract the LAST fenced ```json … ``` block from arbitrary prose. */
|
|
210
370
|
function extractLastJsonBlock(text: string): string | null {
|
|
211
371
|
const fence = /```json\s*\n?([\s\S]*?)```/gi
|
|
@@ -234,11 +394,11 @@ function normalizePastedEntry(e: PastedThreadEntry): ThreadEntry {
|
|
|
234
394
|
* - LAST ```json``` block is parsed.
|
|
235
395
|
* - For each pasted comment by id: if known locally, append NEW thread entries
|
|
236
396
|
* (dedupe by entry id) and apply status/closeNote; if unknown → unknownIds.
|
|
237
|
-
* - schemaMismatch when parsed.schemaVersion differs from
|
|
397
|
+
* - schemaMismatch when parsed.schemaVersion differs from the current render fingerprint.
|
|
238
398
|
*/
|
|
239
399
|
export function mergeReply(
|
|
240
400
|
root: string,
|
|
241
|
-
|
|
401
|
+
currentRenderFingerprint: string,
|
|
242
402
|
pastedText: string,
|
|
243
403
|
opts?: {
|
|
244
404
|
/** live loop: skip ONLY the texts already applied live this run, per comment id */
|
|
@@ -252,7 +412,8 @@ export function mergeReply(
|
|
|
252
412
|
if (block == null) {
|
|
253
413
|
throw new Error('no machine-state json block found in pasted text')
|
|
254
414
|
}
|
|
255
|
-
const parsed =
|
|
415
|
+
const parsed = decodePastedStore(parseJson(block))
|
|
416
|
+
if (!parsed) throw new Error('invalid machine-state json block')
|
|
256
417
|
const pastedComments = Array.isArray(parsed.comments) ? parsed.comments : []
|
|
257
418
|
|
|
258
419
|
const store = readComments(root)
|
|
@@ -309,7 +470,9 @@ export function mergeReply(
|
|
|
309
470
|
persist(root, store)
|
|
310
471
|
|
|
311
472
|
const pastedSchemaVersion = parsed.schemaVersion
|
|
312
|
-
const schemaMismatch = Boolean(
|
|
473
|
+
const schemaMismatch = Boolean(
|
|
474
|
+
pastedSchemaVersion && pastedSchemaVersion !== currentRenderFingerprint,
|
|
475
|
+
)
|
|
313
476
|
|
|
314
477
|
return {
|
|
315
478
|
merged,
|
|
@@ -7,17 +7,49 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { ContextItem, ContextStore } from '../../shared/types'
|
|
9
9
|
|
|
10
|
+
import { asBoolean, asJsonRecord, asString } from '../json'
|
|
10
11
|
import { readJson, writeJson, writeState } from './store'
|
|
11
12
|
|
|
12
13
|
const USER_PATH = 'context/user/index.json'
|
|
13
14
|
const AUTO_PATH = 'context/auto/index.json'
|
|
14
15
|
|
|
16
|
+
function decodeContextItem(value: unknown): ContextItem | undefined {
|
|
17
|
+
const record = asJsonRecord(value)
|
|
18
|
+
const id = asString(record?.id)
|
|
19
|
+
const bucket = record?.bucket === 'user' || record?.bucket === 'auto' ? record.bucket : undefined
|
|
20
|
+
const title = asString(record?.title)
|
|
21
|
+
const body = asString(record?.body)
|
|
22
|
+
const updatedAt = asString(record?.updatedAt)
|
|
23
|
+
if (!id || !bucket || title === undefined || body === undefined || !updatedAt) return undefined
|
|
24
|
+
const source = asString(record?.source)
|
|
25
|
+
const includeInHandoff = asBoolean(record?.includeInHandoff)
|
|
26
|
+
const freshness = asString(record?.freshness)
|
|
27
|
+
return {
|
|
28
|
+
id,
|
|
29
|
+
bucket,
|
|
30
|
+
title,
|
|
31
|
+
body,
|
|
32
|
+
updatedAt,
|
|
33
|
+
...(source === undefined ? {} : { source }),
|
|
34
|
+
...(includeInHandoff === undefined ? {} : { includeInHandoff }),
|
|
35
|
+
...(freshness === undefined ? {} : { freshness }),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function decodeContextItems(value: unknown): ContextItem[] | undefined {
|
|
40
|
+
if (!Array.isArray(value)) return undefined
|
|
41
|
+
return value.flatMap((item) => {
|
|
42
|
+
const decoded = decodeContextItem(item)
|
|
43
|
+
return decoded ? [decoded] : []
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
15
47
|
function readUser(root: string): ContextItem[] {
|
|
16
|
-
return readJson
|
|
48
|
+
return readJson(root, USER_PATH, decodeContextItems, [])
|
|
17
49
|
}
|
|
18
50
|
|
|
19
51
|
function readAuto(root: string): ContextItem[] {
|
|
20
|
-
return readJson
|
|
52
|
+
return readJson(root, AUTO_PATH, decodeContextItems, [])
|
|
21
53
|
}
|
|
22
54
|
|
|
23
55
|
function writeUser(root: string, items: ContextItem[]): void {
|
|
@@ -9,13 +9,54 @@ import { extname } from 'node:path'
|
|
|
9
9
|
|
|
10
10
|
import type { DocMeta } from '../../shared/types'
|
|
11
11
|
|
|
12
|
+
import { asFiniteNumber, asJsonRecord, asString } from '../json'
|
|
12
13
|
import { readJson, removeState, statePath, writeJson, writeStateBuffer } from './store'
|
|
13
14
|
|
|
14
15
|
const INDEX = 'context/documents/index.json'
|
|
15
16
|
const storedPath = (id: string, name: string) => `context/documents/${id}${extname(name)}`
|
|
16
17
|
|
|
18
|
+
function decodeDocument(value: unknown): DocMeta | undefined {
|
|
19
|
+
const record = asJsonRecord(value)
|
|
20
|
+
const id = asString(record?.id)
|
|
21
|
+
const name = asString(record?.name)
|
|
22
|
+
const type = asString(record?.type)
|
|
23
|
+
const size = asFiniteNumber(record?.size)
|
|
24
|
+
const addedAt = asString(record?.addedAt)
|
|
25
|
+
const stored = asString(record?.stored)
|
|
26
|
+
if (
|
|
27
|
+
!id ||
|
|
28
|
+
name === undefined ||
|
|
29
|
+
type === undefined ||
|
|
30
|
+
size === undefined ||
|
|
31
|
+
size < 0 ||
|
|
32
|
+
!addedAt ||
|
|
33
|
+
!stored?.startsWith('context/documents/') ||
|
|
34
|
+
stored.split('/').includes('..')
|
|
35
|
+
) {
|
|
36
|
+
return undefined
|
|
37
|
+
}
|
|
38
|
+
const updatedAt = asString(record?.updatedAt)
|
|
39
|
+
return {
|
|
40
|
+
id,
|
|
41
|
+
name,
|
|
42
|
+
type,
|
|
43
|
+
size,
|
|
44
|
+
addedAt,
|
|
45
|
+
stored,
|
|
46
|
+
...(updatedAt === undefined ? {} : { updatedAt }),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function decodeDocuments(value: unknown): DocMeta[] | undefined {
|
|
51
|
+
if (!Array.isArray(value)) return undefined
|
|
52
|
+
return value.flatMap((item) => {
|
|
53
|
+
const document = decodeDocument(item)
|
|
54
|
+
return document ? [document] : []
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
17
58
|
export function listDocuments(root: string): DocMeta[] {
|
|
18
|
-
return readJson
|
|
59
|
+
return readJson(root, INDEX, decodeDocuments, [])
|
|
19
60
|
}
|
|
20
61
|
|
|
21
62
|
export function addDocument(root: string, name: string, type: string, data: Uint8Array): DocMeta {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { Integration, IntegrationsState } from '../../shared/types'
|
|
8
8
|
|
|
9
|
+
import { asJsonRecord, asString } from '../json'
|
|
9
10
|
import { readJson, writeJson } from './store'
|
|
10
11
|
|
|
11
12
|
const PATH = 'integrations.json'
|
|
@@ -14,8 +15,33 @@ interface IntegrationsFile {
|
|
|
14
15
|
integrations: Integration[]
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
function decodeIntegration(value: unknown): Integration | undefined {
|
|
19
|
+
const record = asJsonRecord(value)
|
|
20
|
+
const id = asString(record?.id)
|
|
21
|
+
const name = asString(record?.name)
|
|
22
|
+
const kind = asString(record?.kind)
|
|
23
|
+
const status = asString(record?.status)
|
|
24
|
+
if (id === undefined || name === undefined || kind === undefined || status === undefined)
|
|
25
|
+
return undefined
|
|
26
|
+
const notes = asString(record?.notes)
|
|
27
|
+
return { id, name, kind, status, ...(notes === undefined ? {} : { notes }) }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function decodeIntegrationsFile(value: unknown): IntegrationsFile | undefined {
|
|
31
|
+
const record = asJsonRecord(value)
|
|
32
|
+
if (!record) return undefined
|
|
33
|
+
return {
|
|
34
|
+
integrations: Array.isArray(record.integrations)
|
|
35
|
+
? record.integrations.flatMap((item) => {
|
|
36
|
+
const integration = decodeIntegration(item)
|
|
37
|
+
return integration ? [integration] : []
|
|
38
|
+
})
|
|
39
|
+
: [],
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
17
43
|
function read(root: string): Integration[] {
|
|
18
|
-
return readJson
|
|
44
|
+
return readJson(root, PATH, decodeIntegrationsFile, { integrations: [] }).integrations
|
|
19
45
|
}
|
|
20
46
|
|
|
21
47
|
function write(root: string, integrations: Integration[]): void {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { afterEach, expect, test } from 'bun:test'
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { asJsonRecord } from '../json'
|
|
7
|
+
import { readLayout, saveLayout } from './layout'
|
|
8
|
+
import { readJson, writeJson } from './store'
|
|
9
|
+
|
|
10
|
+
const roots: string[] = []
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
while (roots.length > 0) rmSync(roots.pop()!, { recursive: true, force: true })
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('reads the old layout hash key but writes only renderFingerprint', () => {
|
|
17
|
+
const root = mkdtempSync(join(tmpdir(), 'studio-layout-'))
|
|
18
|
+
roots.push(root)
|
|
19
|
+
writeJson(root, 'layout.json', {
|
|
20
|
+
schemaHash: 'sha-legacy',
|
|
21
|
+
positions: { 'class.Note': { x: 1, y: 2 } },
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
expect(readLayout(root)).toEqual({
|
|
25
|
+
renderFingerprint: 'sha-legacy',
|
|
26
|
+
positions: { 'class.Note': { x: 1, y: 2 } },
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
saveLayout(root, { 'class.Note': { x: 3, y: 4 } }, 'sha-current')
|
|
30
|
+
expect(readJson(root, 'layout.json', asJsonRecord, {})).toEqual({
|
|
31
|
+
renderFingerprint: 'sha-current',
|
|
32
|
+
positions: { 'class.Note': { x: 3, y: 4 } },
|
|
33
|
+
})
|
|
34
|
+
})
|
|
@@ -2,36 +2,63 @@
|
|
|
2
2
|
* layout.ts — persisted manual graph layout per domain. Positions are keyed by
|
|
3
3
|
* node id (e.g. `class.Monitor`), so they survive schema changes: removed nodes'
|
|
4
4
|
* positions are harmless, new nodes fall back to auto-layout. Stamped with the
|
|
5
|
-
*
|
|
5
|
+
* render fingerprint for reference. Persisted via the allow-listed store
|
|
6
|
+
* (layout.json).
|
|
6
7
|
*/
|
|
8
|
+
import type { LayoutState, NodePosition } from '../../shared/types'
|
|
9
|
+
|
|
10
|
+
import { asFiniteNumber, asJsonRecord, asString } from '../json'
|
|
7
11
|
import { readJson, removeState, writeJson } from './store'
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
x: number
|
|
11
|
-
y: number
|
|
12
|
-
/** persisted size — only expanded module containers carry one. */
|
|
13
|
-
w?: number
|
|
14
|
-
h?: number
|
|
15
|
-
}
|
|
13
|
+
const FILE = 'layout.json'
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
interface StoredLayout extends LayoutState {
|
|
18
16
|
schemaHash?: string
|
|
19
|
-
/** node id → manual position (only nodes the user has moved) */
|
|
20
|
-
positions: Record<string, NodePosition>
|
|
21
17
|
}
|
|
22
18
|
|
|
23
|
-
|
|
19
|
+
function decodePosition(value: unknown): NodePosition | undefined {
|
|
20
|
+
const record = asJsonRecord(value)
|
|
21
|
+
const x = asFiniteNumber(record?.x)
|
|
22
|
+
const y = asFiniteNumber(record?.y)
|
|
23
|
+
if (x === undefined || y === undefined) return undefined
|
|
24
|
+
const w = asFiniteNumber(record?.w)
|
|
25
|
+
const h = asFiniteNumber(record?.h)
|
|
26
|
+
return { x, y, ...(w === undefined ? {} : { w }), ...(h === undefined ? {} : { h }) }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function decodeStoredLayout(value: unknown): StoredLayout | undefined {
|
|
30
|
+
const record = asJsonRecord(value)
|
|
31
|
+
if (!record) return undefined
|
|
32
|
+
const positions: Record<string, NodePosition> = {}
|
|
33
|
+
for (const [id, candidate] of Object.entries(asJsonRecord(record.positions) ?? {})) {
|
|
34
|
+
const position = decodePosition(candidate)
|
|
35
|
+
if (position) positions[id] = position
|
|
36
|
+
}
|
|
37
|
+
const renderFingerprint = asString(record.renderFingerprint)
|
|
38
|
+
const schemaHash = asString(record.schemaHash)
|
|
39
|
+
return {
|
|
40
|
+
positions,
|
|
41
|
+
...(renderFingerprint === undefined ? {} : { renderFingerprint }),
|
|
42
|
+
...(schemaHash === undefined ? {} : { schemaHash }),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
24
45
|
|
|
25
46
|
export function readLayout(root: string): LayoutState {
|
|
26
|
-
|
|
47
|
+
const stored = readJson(root, FILE, decodeStoredLayout, { positions: {} })
|
|
48
|
+
return {
|
|
49
|
+
...((stored.renderFingerprint ?? stored.schemaHash)
|
|
50
|
+
? { renderFingerprint: stored.renderFingerprint ?? stored.schemaHash }
|
|
51
|
+
: {}),
|
|
52
|
+
positions: stored.positions ?? {},
|
|
53
|
+
}
|
|
27
54
|
}
|
|
28
55
|
|
|
29
56
|
export function saveLayout(
|
|
30
57
|
root: string,
|
|
31
58
|
positions: Record<string, NodePosition>,
|
|
32
|
-
|
|
59
|
+
renderFingerprint?: string,
|
|
33
60
|
): LayoutState {
|
|
34
|
-
const next: LayoutState = {
|
|
61
|
+
const next: LayoutState = { renderFingerprint, positions }
|
|
35
62
|
writeJson(root, FILE, next)
|
|
36
63
|
return next
|
|
37
64
|
}
|
|
@@ -39,11 +66,11 @@ export function saveLayout(
|
|
|
39
66
|
export function setNodePositions(
|
|
40
67
|
root: string,
|
|
41
68
|
updates: Record<string, NodePosition>,
|
|
42
|
-
|
|
69
|
+
renderFingerprint?: string,
|
|
43
70
|
): LayoutState {
|
|
44
71
|
const cur = readLayout(root)
|
|
45
72
|
const next: LayoutState = {
|
|
46
|
-
|
|
73
|
+
renderFingerprint: renderFingerprint ?? cur.renderFingerprint,
|
|
47
74
|
positions: { ...cur.positions, ...updates },
|
|
48
75
|
}
|
|
49
76
|
writeJson(root, FILE, next)
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type NumericStudioSetting,
|
|
11
11
|
} from '../../shared/settings-values'
|
|
12
12
|
import { AGENT_ACCESS_LEVELS, AGENT_EFFORT_LEVELS, type StudioSettings } from '../../shared/types'
|
|
13
|
+
import { asJsonRecord, asString } from '../json'
|
|
13
14
|
import { readJson, writeJson } from './store'
|
|
14
15
|
|
|
15
16
|
const PATH = 'settings.json'
|
|
@@ -25,37 +26,43 @@ export const DEFAULT_SETTINGS: StudioSettings = {
|
|
|
25
26
|
viewProbeTimeoutMs: 8000,
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
function normalizeSettings(input:
|
|
29
|
-
const
|
|
29
|
+
function normalizeSettings(input: unknown): Partial<StudioSettings> {
|
|
30
|
+
const record = asJsonRecord(input)
|
|
31
|
+
if (!record) return {}
|
|
32
|
+
const out: Partial<StudioSettings> = {}
|
|
30
33
|
for (const key of Object.keys(STUDIO_NUMERIC_LIMITS) as NumericStudioSetting[]) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (value === null) delete out[key]
|
|
34
|
-
else out[key] = value
|
|
34
|
+
const value = parseStudioNumericSetting(key, record[key])
|
|
35
|
+
if (value !== null) out[key] = value
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
if (out.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
37
|
+
|
|
38
|
+
const effort = AGENT_EFFORT_LEVELS.find((candidate) => candidate === record.agentEffort)
|
|
39
|
+
if (effort) out.agentEffort = effort
|
|
40
|
+
const access = AGENT_ACCESS_LEVELS.find((candidate) => candidate === record.agentAccess)
|
|
41
|
+
if (access) out.agentAccess = access
|
|
42
|
+
|
|
43
|
+
const integrationsDir = asString(record.integrationsDir)
|
|
44
|
+
if (integrationsDir !== undefined) out.integrationsDir = integrationsDir
|
|
45
|
+
|
|
46
|
+
const models = asJsonRecord(record.agentModels)
|
|
47
|
+
if (models) {
|
|
48
|
+
out.agentModels = Object.fromEntries(
|
|
49
|
+
Object.entries(models)
|
|
50
|
+
.filter((entry): entry is [string, string] => typeof entry[1] === 'string')
|
|
51
|
+
.map(([harness, model]) => [harness.trim().toLowerCase(), model.trim()])
|
|
52
|
+
.filter(([harness, model]) => !!harness && !!model),
|
|
53
|
+
)
|
|
51
54
|
}
|
|
52
55
|
return out
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
function decodeSettings(value: unknown): Partial<StudioSettings> | undefined {
|
|
59
|
+
return asJsonRecord(value) ? normalizeSettings(value) : undefined
|
|
60
|
+
}
|
|
61
|
+
|
|
55
62
|
export function readSettings(root: string): StudioSettings {
|
|
56
63
|
return {
|
|
57
64
|
...DEFAULT_SETTINGS,
|
|
58
|
-
...
|
|
65
|
+
...readJson(root, PATH, decodeSettings, {}),
|
|
59
66
|
}
|
|
60
67
|
}
|
|
61
68
|
|