@astrale-os/cli 0.4.0-alpha.13
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/.check-workspace.cjs +40 -0
- package/README.md +151 -0
- package/dist/astrale.js +59749 -0
- package/package.json +90 -0
- package/src/command.ts +40 -0
- package/src/commands/__tests__/admin-instance.test.ts +73 -0
- package/src/commands/__tests__/auth-login.test.ts +178 -0
- package/src/commands/__tests__/auth-token.test.ts +223 -0
- package/src/commands/__tests__/call.test.ts +72 -0
- package/src/commands/__tests__/domain-list.test.ts +74 -0
- package/src/commands/__tests__/help-contract.test.ts +136 -0
- package/src/commands/__tests__/install-identity-override.test.ts +65 -0
- package/src/commands/__tests__/instance-bookmark.test.ts +101 -0
- package/src/commands/__tests__/instance-create-hosts.test.ts +29 -0
- package/src/commands/__tests__/instance-list-rows.test.ts +63 -0
- package/src/commands/__tests__/logs.test.ts +117 -0
- package/src/commands/__tests__/ls.test.ts +25 -0
- package/src/commands/__tests__/setup-plan.test.ts +61 -0
- package/src/commands/admin/status.ts +61 -0
- package/src/commands/admin/use.ts +77 -0
- package/src/commands/auth/login.ts +82 -0
- package/src/commands/auth/logout.ts +50 -0
- package/src/commands/auth/status.ts +86 -0
- package/src/commands/auth/token.ts +162 -0
- package/src/commands/browser.ts +207 -0
- package/src/commands/call.ts +300 -0
- package/src/commands/describe.ts +182 -0
- package/src/commands/domain/install.ts +420 -0
- package/src/commands/domain/list.ts +154 -0
- package/src/commands/domain/publish.ts +155 -0
- package/src/commands/get.ts +60 -0
- package/src/commands/identity/create.ts +26 -0
- package/src/commands/identity/delete.ts +18 -0
- package/src/commands/identity/export.ts +66 -0
- package/src/commands/identity/import.ts +101 -0
- package/src/commands/identity/list.ts +56 -0
- package/src/commands/identity/register.ts +170 -0
- package/src/commands/identity/sync.ts +32 -0
- package/src/commands/identity/unsync.ts +24 -0
- package/src/commands/identity/use.ts +18 -0
- package/src/commands/identity/whoami.ts +34 -0
- package/src/commands/idp/add.ts +150 -0
- package/src/commands/idp/list.ts +57 -0
- package/src/commands/idp/refresh.ts +38 -0
- package/src/commands/idp/remove.ts +36 -0
- package/src/commands/idp/show.ts +29 -0
- package/src/commands/instance/active.ts +64 -0
- package/src/commands/instance/bookmark.ts +72 -0
- package/src/commands/instance/create.ts +69 -0
- package/src/commands/instance/delete.ts +72 -0
- package/src/commands/instance/forget.ts +26 -0
- package/src/commands/instance/list.ts +149 -0
- package/src/commands/instance/status.ts +42 -0
- package/src/commands/instance/use.ts +210 -0
- package/src/commands/logs.ts +347 -0
- package/src/commands/ls.ts +229 -0
- package/src/commands/query.ts +32 -0
- package/src/commands/setup.ts +54 -0
- package/src/commands/status.ts +60 -0
- package/src/commands/studio.ts +401 -0
- package/src/commands/token.ts +77 -0
- package/src/commands/update.ts +267 -0
- package/src/commands/use.ts +87 -0
- package/src/errors.ts +65 -0
- package/src/kernel/__tests__/auth.test.ts +77 -0
- package/src/kernel/__tests__/errors.test.ts +43 -0
- package/src/kernel/__tests__/remote-routing.test.ts +70 -0
- package/src/kernel/auth.ts +234 -0
- package/src/kernel/ca-fetch.ts +119 -0
- package/src/kernel/client.ts +191 -0
- package/src/kernel/errors.ts +280 -0
- package/src/kernel/expand.ts +217 -0
- package/src/kernel/index.ts +14 -0
- package/src/kernel/options.ts +22 -0
- package/src/kernel/remote-routing.ts +88 -0
- package/src/kernel/run.ts +63 -0
- package/src/kernel/types.ts +14 -0
- package/src/lib/__tests__/admin-target.test.ts +112 -0
- package/src/lib/__tests__/binary.test.ts +56 -0
- package/src/lib/__tests__/command-dx.test.ts +58 -0
- package/src/lib/__tests__/concurrency.test.ts +62 -0
- package/src/lib/__tests__/config.test.ts +53 -0
- package/src/lib/__tests__/design.test.ts +99 -0
- package/src/lib/__tests__/domain-identity.test.ts +60 -0
- package/src/lib/__tests__/format.test.ts +22 -0
- package/src/lib/__tests__/fs-atomic.test.ts +104 -0
- package/src/lib/__tests__/identity.test.ts +79 -0
- package/src/lib/__tests__/idp-session.driver.ts +53 -0
- package/src/lib/__tests__/idp-session.test.ts +357 -0
- package/src/lib/__tests__/idp.test.ts +385 -0
- package/src/lib/__tests__/instance-candidates.test.ts +73 -0
- package/src/lib/__tests__/instance-target.test.ts +183 -0
- package/src/lib/__tests__/instance.test.ts +136 -0
- package/src/lib/__tests__/keys.test.ts +129 -0
- package/src/lib/__tests__/local-status.test.ts +202 -0
- package/src/lib/__tests__/output.test.ts +150 -0
- package/src/lib/__tests__/panel.test.ts +40 -0
- package/src/lib/__tests__/port.test.ts +44 -0
- package/src/lib/__tests__/prompt.test.ts +25 -0
- package/src/lib/__tests__/sdk-deps.test.ts +68 -0
- package/src/lib/__tests__/self.test.ts +272 -0
- package/src/lib/__tests__/studio-server-deps.test.ts +74 -0
- package/src/lib/__tests__/table.test.ts +53 -0
- package/src/lib/__tests__/update.test.ts +246 -0
- package/src/lib/__tests__/use-target.test.ts +56 -0
- package/src/lib/__tests__/validation.test.ts +34 -0
- package/src/lib/admin-domain.ts +25 -0
- package/src/lib/admin-instance.ts +26 -0
- package/src/lib/admin-target.ts +217 -0
- package/src/lib/binary.ts +131 -0
- package/src/lib/browser.ts +150 -0
- package/src/lib/command-dx.ts +161 -0
- package/src/lib/concurrency.ts +31 -0
- package/src/lib/config.ts +45 -0
- package/src/lib/domain-identity.ts +49 -0
- package/src/lib/env.ts +49 -0
- package/src/lib/format.ts +4 -0
- package/src/lib/fs-atomic.ts +126 -0
- package/src/lib/identity.ts +256 -0
- package/src/lib/idp-session.ts +134 -0
- package/src/lib/idp.ts +876 -0
- package/src/lib/instance-candidates.ts +49 -0
- package/src/lib/instance-target.ts +182 -0
- package/src/lib/instance.ts +395 -0
- package/src/lib/keys.ts +294 -0
- package/src/lib/local-status.ts +152 -0
- package/src/lib/log.ts +116 -0
- package/src/lib/login-flow.ts +164 -0
- package/src/lib/meta.ts +86 -0
- package/src/lib/output.ts +222 -0
- package/src/lib/panel.ts +61 -0
- package/src/lib/paths.ts +11 -0
- package/src/lib/port.ts +41 -0
- package/src/lib/proc.ts +82 -0
- package/src/lib/prompt.ts +136 -0
- package/src/lib/provision-instance.ts +170 -0
- package/src/lib/sdk-deps.ts +104 -0
- package/src/lib/self.ts +166 -0
- package/src/lib/skills.ts +171 -0
- package/src/lib/table.ts +62 -0
- package/src/lib/update.ts +315 -0
- package/src/lib/use-target.ts +24 -0
- package/src/lib/validation.ts +59 -0
- package/src/program.ts +200 -0
- package/src/registry.ts +59 -0
- package/src/setup/__tests__/util.test.ts +29 -0
- package/src/setup/engine.ts +83 -0
- package/src/setup/render.ts +109 -0
- package/src/setup/steps/admin.ts +78 -0
- package/src/setup/steps/agent-browser.ts +81 -0
- package/src/setup/steps/auth.ts +54 -0
- package/src/setup/steps/domain.ts +68 -0
- package/src/setup/steps/index.ts +18 -0
- package/src/setup/steps/instance.ts +119 -0
- package/src/setup/steps/skills-bridge.ts +70 -0
- package/src/setup/steps/skills.ts +59 -0
- package/src/setup/types.ts +61 -0
- package/src/setup/util.ts +34 -0
- package/src/test-utils.ts +18 -0
- package/studio/client/dist/assets/index-DOwzZAEK.css +1 -0
- package/studio/client/dist/assets/index-wtU0Zxhy.js +183 -0
- package/studio/client/dist/index.html +13 -0
- package/studio/package.json +62 -0
- package/studio/server/agent/ask.ts +68 -0
- package/studio/server/agent/bridge-mcp.ts +182 -0
- package/studio/server/agent/bridge.ts +188 -0
- package/studio/server/agent/claude.ts +666 -0
- package/studio/server/agent/mock.ts +186 -0
- package/studio/server/agent/prompt.ts +202 -0
- package/studio/server/agent/registry.ts +29 -0
- package/studio/server/agent/runner.ts +484 -0
- package/studio/server/agent/schema-map.ts +112 -0
- package/studio/server/agent/types.ts +120 -0
- package/studio/server/api.ts +574 -0
- package/studio/server/cache.ts +138 -0
- package/studio/server/detect.ts +81 -0
- package/studio/server/domain.ts +70 -0
- package/studio/server/index.ts +136 -0
- package/studio/server/introspect/anatomy-extras.ts +398 -0
- package/studio/server/introspect/anatomy.ts +108 -0
- package/studio/server/introspect/bundle.ts +57 -0
- package/studio/server/introspect/core-extractor.ts +119 -0
- package/studio/server/introspect/core.ts +44 -0
- package/studio/server/introspect/diff.ts +133 -0
- package/studio/server/introspect/extractor.ts +102 -0
- package/studio/server/introspect/hash.ts +21 -0
- package/studio/server/introspect/overlay-tsmorph.ts +874 -0
- package/studio/server/introspect/overlay.ts +57 -0
- package/studio/server/introspect/runtime.ts +99 -0
- package/studio/server/introspect/schema-refs.ts +46 -0
- package/studio/server/lifecycle.ts +38 -0
- package/studio/server/sse.ts +57 -0
- package/studio/server/state/baseline.ts +211 -0
- package/studio/server/state/catalog.ts +117 -0
- package/studio/server/state/comments.ts +321 -0
- package/studio/server/state/context.ts +167 -0
- package/studio/server/state/copy.ts +156 -0
- package/studio/server/state/create.ts +156 -0
- package/studio/server/state/documents.ts +70 -0
- package/studio/server/state/env.ts +161 -0
- package/studio/server/state/git.ts +75 -0
- package/studio/server/state/handoff.ts +55 -0
- package/studio/server/state/harness-gateway.ts +181 -0
- package/studio/server/state/harness-token.ts +0 -0
- package/studio/server/state/instance.ts +244 -0
- package/studio/server/state/integrations.ts +55 -0
- package/studio/server/state/layout.ts +55 -0
- package/studio/server/state/settings.ts +39 -0
- package/studio/server/state/store.ts +97 -0
- package/studio/server/state/updates.ts +63 -0
- package/studio/server/state/usage.ts +37 -0
- package/studio/server/state/views.ts +138 -0
- package/studio/server/state/visibility.ts +33 -0
- package/studio/server/watch.ts +81 -0
- package/studio/server/workspace-state.ts +26 -0
- package/studio/server/workspace-watch.ts +101 -0
- package/studio/shared/types.ts +873 -0
- package/studio/tsconfig.json +23 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* comments.ts — the comments / open-questions store. ALL persistence goes through
|
|
3
|
+
* store.ts (writes are allow-listed to <domain>/.domain-studio/). The on-disk file
|
|
4
|
+
* is `comments.json`, shaped { schemaVersion, comments: Comment[] }.
|
|
5
|
+
*
|
|
6
|
+
* Comments are annotate-compatible: each thread entry is
|
|
7
|
+
* { id, role:'user'|'author', type:'text'|'choice', text, options?, answer? }
|
|
8
|
+
* and `kind` is derived from the FIRST thread entry's role
|
|
9
|
+
* (author-seeded ⇒ 'question', else 'comment').
|
|
10
|
+
*/
|
|
11
|
+
import type { Comment, CommentStore, ThreadEntry } from '../../shared/types'
|
|
12
|
+
|
|
13
|
+
import { readJson, writeJson } from './store'
|
|
14
|
+
|
|
15
|
+
const FILE = 'comments.json'
|
|
16
|
+
|
|
17
|
+
const EMPTY: CommentStore = { schemaVersion: '', comments: [] }
|
|
18
|
+
|
|
19
|
+
export function readComments(root: string): CommentStore {
|
|
20
|
+
const store = readJson<CommentStore>(root, FILE, { ...EMPTY })
|
|
21
|
+
return {
|
|
22
|
+
schemaVersion: store.schemaVersion ?? '',
|
|
23
|
+
comments: Array.isArray(store.comments) ? store.comments : [],
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function persist(root: string, store: CommentStore): void {
|
|
28
|
+
writeJson(root, FILE, store)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Derive `kind` from the first thread entry's role. */
|
|
32
|
+
function deriveKind(thread: ThreadEntry[]): Comment['kind'] {
|
|
33
|
+
return thread[0]?.role === 'author' ? 'question' : 'comment'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeEntry(input: {
|
|
37
|
+
role: ThreadEntry['role']
|
|
38
|
+
type?: ThreadEntry['type']
|
|
39
|
+
text: string
|
|
40
|
+
options?: string[]
|
|
41
|
+
answer?: string | null
|
|
42
|
+
}): ThreadEntry {
|
|
43
|
+
const entry: ThreadEntry = {
|
|
44
|
+
id: crypto.randomUUID(),
|
|
45
|
+
role: input.role,
|
|
46
|
+
type: input.type ?? 'text',
|
|
47
|
+
text: input.text,
|
|
48
|
+
}
|
|
49
|
+
if (input.options !== undefined) entry.options = input.options
|
|
50
|
+
if (input.answer !== undefined) entry.answer = input.answer
|
|
51
|
+
return entry
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function upsertComment(
|
|
55
|
+
root: string,
|
|
56
|
+
input: {
|
|
57
|
+
id?: string
|
|
58
|
+
anchors: string[]
|
|
59
|
+
anchorRefs: import('../../shared/types').AnchorRef[]
|
|
60
|
+
text?: string
|
|
61
|
+
firstRole?: 'user' | 'author'
|
|
62
|
+
type?: 'text' | 'choice'
|
|
63
|
+
options?: string[]
|
|
64
|
+
schemaVersion?: string
|
|
65
|
+
},
|
|
66
|
+
): Comment {
|
|
67
|
+
const store = readComments(root)
|
|
68
|
+
if (input.schemaVersion !== undefined) store.schemaVersion = input.schemaVersion
|
|
69
|
+
|
|
70
|
+
// If id provided and exists → append a thread entry instead of creating.
|
|
71
|
+
if (input.id) {
|
|
72
|
+
const existing = store.comments.find((c) => c.id === input.id)
|
|
73
|
+
if (existing) {
|
|
74
|
+
const entry = makeEntry({
|
|
75
|
+
role: input.firstRole ?? 'user',
|
|
76
|
+
type: input.type,
|
|
77
|
+
text: input.text ?? '',
|
|
78
|
+
options: input.options,
|
|
79
|
+
})
|
|
80
|
+
existing.thread.push(entry)
|
|
81
|
+
existing.kind = deriveKind(existing.thread)
|
|
82
|
+
persist(root, store)
|
|
83
|
+
return existing
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const role = input.firstRole ?? 'user'
|
|
88
|
+
const firstEntry = makeEntry({
|
|
89
|
+
role,
|
|
90
|
+
type: input.type,
|
|
91
|
+
text: input.text ?? '',
|
|
92
|
+
options: input.options,
|
|
93
|
+
})
|
|
94
|
+
const comment: Comment = {
|
|
95
|
+
id: input.id ?? crypto.randomUUID(),
|
|
96
|
+
anchors: input.anchors,
|
|
97
|
+
anchorRefs: input.anchorRefs,
|
|
98
|
+
status: 'open',
|
|
99
|
+
thread: [firstEntry],
|
|
100
|
+
createdAt: new Date().toISOString(),
|
|
101
|
+
kind: role === 'author' ? 'question' : 'comment',
|
|
102
|
+
}
|
|
103
|
+
store.comments.push(comment)
|
|
104
|
+
persist(root, store)
|
|
105
|
+
return comment
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function addThreadEntry(
|
|
109
|
+
root: string,
|
|
110
|
+
id: string,
|
|
111
|
+
entry: {
|
|
112
|
+
role: 'user' | 'author'
|
|
113
|
+
type?: 'text' | 'choice'
|
|
114
|
+
text: string
|
|
115
|
+
options?: string[]
|
|
116
|
+
answer?: string | null
|
|
117
|
+
},
|
|
118
|
+
): Comment | null {
|
|
119
|
+
const store = readComments(root)
|
|
120
|
+
const comment = store.comments.find((c) => c.id === id)
|
|
121
|
+
if (!comment) return null
|
|
122
|
+
comment.thread.push(makeEntry(entry))
|
|
123
|
+
comment.kind = deriveKind(comment.thread)
|
|
124
|
+
persist(root, store)
|
|
125
|
+
return comment
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Edit the text of a single thread entry in place (the studio is single-user). */
|
|
129
|
+
export function editThreadEntry(
|
|
130
|
+
root: string,
|
|
131
|
+
id: string,
|
|
132
|
+
entryId: string,
|
|
133
|
+
text: string,
|
|
134
|
+
): Comment | null {
|
|
135
|
+
const store = readComments(root)
|
|
136
|
+
const comment = store.comments.find((c) => c.id === id)
|
|
137
|
+
if (!comment) return null
|
|
138
|
+
const entry = comment.thread.find((e) => e.id === entryId)
|
|
139
|
+
if (!entry) return null
|
|
140
|
+
entry.text = text
|
|
141
|
+
persist(root, store)
|
|
142
|
+
return comment
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function setStatus(
|
|
146
|
+
root: string,
|
|
147
|
+
id: string,
|
|
148
|
+
status: 'open' | 'closed',
|
|
149
|
+
closeNote?: string,
|
|
150
|
+
): Comment | null {
|
|
151
|
+
const store = readComments(root)
|
|
152
|
+
const comment = store.comments.find((c) => c.id === id)
|
|
153
|
+
if (!comment) return null
|
|
154
|
+
comment.status = status
|
|
155
|
+
if (closeNote !== undefined) comment.closeNote = closeNote
|
|
156
|
+
comment.kind = deriveKind(comment.thread)
|
|
157
|
+
persist(root, store)
|
|
158
|
+
return comment
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function deleteComment(root: string, id: string): boolean {
|
|
162
|
+
const store = readComments(root)
|
|
163
|
+
const idx = store.comments.findIndex((c) => c.id === id)
|
|
164
|
+
if (idx === -1) return false
|
|
165
|
+
store.comments.splice(idx, 1)
|
|
166
|
+
persist(root, store)
|
|
167
|
+
return true
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Mark a comment orphaned when NONE of its anchorRefs[].ref is present in
|
|
172
|
+
* `validRefs`; clear the flag otherwise. Persists and returns the store.
|
|
173
|
+
*/
|
|
174
|
+
export function markOrphans(root: string, validRefs: string[]): CommentStore {
|
|
175
|
+
const valid = new Set(validRefs)
|
|
176
|
+
const store = readComments(root)
|
|
177
|
+
for (const comment of store.comments) {
|
|
178
|
+
const refs = comment.anchorRefs ?? []
|
|
179
|
+
const anyValid = refs.some((r) => valid.has(r.ref))
|
|
180
|
+
comment.orphaned = refs.length > 0 ? !anyValid : true
|
|
181
|
+
comment.kind = deriveKind(comment.thread)
|
|
182
|
+
}
|
|
183
|
+
persist(root, store)
|
|
184
|
+
return store
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
interface PastedThreadEntry {
|
|
188
|
+
id?: string
|
|
189
|
+
role?: 'user' | 'author'
|
|
190
|
+
type?: 'text' | 'choice'
|
|
191
|
+
text?: string
|
|
192
|
+
options?: string[]
|
|
193
|
+
answer?: string | null
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
interface PastedComment {
|
|
197
|
+
id?: string
|
|
198
|
+
anchors?: string[]
|
|
199
|
+
status?: 'open' | 'closed'
|
|
200
|
+
closeNote?: string
|
|
201
|
+
thread?: PastedThreadEntry[]
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
interface PastedStore {
|
|
205
|
+
schemaVersion?: string
|
|
206
|
+
comments?: PastedComment[]
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Extract the LAST fenced ```json … ``` block from arbitrary prose. */
|
|
210
|
+
function extractLastJsonBlock(text: string): string | null {
|
|
211
|
+
const fence = /```json\s*\n?([\s\S]*?)```/gi
|
|
212
|
+
let match: RegExpExecArray | null
|
|
213
|
+
let last: string | null = null
|
|
214
|
+
while ((match = fence.exec(text)) !== null) {
|
|
215
|
+
last = match[1]
|
|
216
|
+
}
|
|
217
|
+
return last == null ? null : last.trim()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function normalizePastedEntry(e: PastedThreadEntry): ThreadEntry {
|
|
221
|
+
const entry: ThreadEntry = {
|
|
222
|
+
id: e.id && e.id.length > 0 ? e.id : crypto.randomUUID(),
|
|
223
|
+
role: e.role === 'author' ? 'author' : 'user',
|
|
224
|
+
type: e.type === 'choice' ? 'choice' : 'text',
|
|
225
|
+
text: e.text ?? '',
|
|
226
|
+
}
|
|
227
|
+
if (e.options !== undefined) entry.options = e.options
|
|
228
|
+
if (e.answer !== undefined) entry.answer = e.answer
|
|
229
|
+
return entry
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Merge an agent-pasted machine-state reply (annotate shape) into the store.
|
|
234
|
+
* - LAST ```json``` block is parsed.
|
|
235
|
+
* - For each pasted comment by id: if known locally, append NEW thread entries
|
|
236
|
+
* (dedupe by entry id) and apply status/closeNote; if unknown → unknownIds.
|
|
237
|
+
* - schemaMismatch when parsed.schemaVersion differs from currentSchemaHash.
|
|
238
|
+
*/
|
|
239
|
+
export function mergeReply(
|
|
240
|
+
root: string,
|
|
241
|
+
currentSchemaHash: string,
|
|
242
|
+
pastedText: string,
|
|
243
|
+
opts?: {
|
|
244
|
+
/** live loop: skip ONLY the texts already applied live this run, per comment id */
|
|
245
|
+
skipByComment?: Map<string, Set<string>>
|
|
246
|
+
/** manual paste: dedupe author replies against the whole thread (a re-paste with
|
|
247
|
+
* regenerated ids would otherwise duplicate) — there are no live replies to scope to */
|
|
248
|
+
dedupeAuthorText?: boolean
|
|
249
|
+
},
|
|
250
|
+
): import('../../shared/types').MergeResult {
|
|
251
|
+
const block = extractLastJsonBlock(pastedText)
|
|
252
|
+
if (block == null) {
|
|
253
|
+
throw new Error('no machine-state json block found in pasted text')
|
|
254
|
+
}
|
|
255
|
+
const parsed = JSON.parse(block) as PastedStore
|
|
256
|
+
const pastedComments = Array.isArray(parsed.comments) ? parsed.comments : []
|
|
257
|
+
|
|
258
|
+
const store = readComments(root)
|
|
259
|
+
let merged = 0
|
|
260
|
+
let closed = 0
|
|
261
|
+
const unknownIds: string[] = []
|
|
262
|
+
|
|
263
|
+
for (const pc of pastedComments) {
|
|
264
|
+
if (!pc.id) {
|
|
265
|
+
unknownIds.push('')
|
|
266
|
+
continue
|
|
267
|
+
}
|
|
268
|
+
const local = store.comments.find((c) => c.id === pc.id)
|
|
269
|
+
if (!local) {
|
|
270
|
+
unknownIds.push(pc.id)
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const seen = new Set(local.thread.map((t) => t.id))
|
|
275
|
+
// Skip ONLY the author texts already applied LIVE this run (the agent regenerates
|
|
276
|
+
// ids, so id-dedupe alone wouldn't catch a bridge reply echoed in the block). Scope
|
|
277
|
+
// it to this run — NOT the whole thread history — so a legitimately-repeated reply
|
|
278
|
+
// across turns ("Done." twice) is never dropped.
|
|
279
|
+
const liveTexts = opts?.skipByComment?.get(pc.id)
|
|
280
|
+
const authorTexts = opts?.dedupeAuthorText
|
|
281
|
+
? new Set(local.thread.filter((t) => t.role === 'author').map((t) => t.text.trim()))
|
|
282
|
+
: null
|
|
283
|
+
let appended = 0
|
|
284
|
+
for (const pe of pc.thread ?? []) {
|
|
285
|
+
const entry = normalizePastedEntry(pe)
|
|
286
|
+
if (seen.has(entry.id)) continue
|
|
287
|
+
if (
|
|
288
|
+
entry.role === 'author' &&
|
|
289
|
+
(liveTexts?.has(entry.text.trim()) || authorTexts?.has(entry.text.trim()))
|
|
290
|
+
)
|
|
291
|
+
continue
|
|
292
|
+
seen.add(entry.id)
|
|
293
|
+
if (entry.role === 'author') authorTexts?.add(entry.text.trim())
|
|
294
|
+
local.thread.push(entry)
|
|
295
|
+
appended += 1
|
|
296
|
+
}
|
|
297
|
+
if (appended > 0) merged += 1
|
|
298
|
+
|
|
299
|
+
// Only ever close from a merge — never REOPEN a thread the user closed mid-run.
|
|
300
|
+
if (pc.status === 'closed' && local.status !== 'closed') {
|
|
301
|
+
local.status = 'closed'
|
|
302
|
+
closed += 1
|
|
303
|
+
}
|
|
304
|
+
if (pc.closeNote !== undefined) local.closeNote = pc.closeNote
|
|
305
|
+
|
|
306
|
+
local.kind = deriveKind(local.thread)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
persist(root, store)
|
|
310
|
+
|
|
311
|
+
const pastedSchemaVersion = parsed.schemaVersion
|
|
312
|
+
const schemaMismatch = Boolean(pastedSchemaVersion && pastedSchemaVersion !== currentSchemaHash)
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
merged,
|
|
316
|
+
closed,
|
|
317
|
+
unknownIds,
|
|
318
|
+
schemaMismatch,
|
|
319
|
+
pastedSchemaVersion,
|
|
320
|
+
}
|
|
321
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context.ts — persists studio context items split into two buckets:
|
|
3
|
+
* - user items at context/user/index.json (ContextItem[])
|
|
4
|
+
* - auto items at context/auto/index.json (ContextItem[])
|
|
5
|
+
* Auto items are also mirrored to readable .md files under context/auto/ so an
|
|
6
|
+
* external agent can read them from disk. ALL writes go through store.ts.
|
|
7
|
+
*/
|
|
8
|
+
import type { ContextItem, ContextStore } from '../../shared/types'
|
|
9
|
+
|
|
10
|
+
import { readJson, writeJson, writeState } from './store'
|
|
11
|
+
|
|
12
|
+
const USER_PATH = 'context/user/index.json'
|
|
13
|
+
const AUTO_PATH = 'context/auto/index.json'
|
|
14
|
+
|
|
15
|
+
function readUser(root: string): ContextItem[] {
|
|
16
|
+
return readJson<ContextItem[]>(root, USER_PATH, [])
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readAuto(root: string): ContextItem[] {
|
|
20
|
+
return readJson<ContextItem[]>(root, AUTO_PATH, [])
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function writeUser(root: string, items: ContextItem[]): void {
|
|
24
|
+
writeJson(root, USER_PATH, items)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function writeAuto(root: string, items: ContextItem[]): void {
|
|
28
|
+
writeJson(root, AUTO_PATH, items)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function now(): string {
|
|
32
|
+
return new Date().toISOString()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function readContext(root: string): ContextStore {
|
|
36
|
+
return { user: readUser(root), auto: readAuto(root) }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function addUserContext(
|
|
40
|
+
root: string,
|
|
41
|
+
input: { title: string; body: string; source?: string },
|
|
42
|
+
): ContextItem {
|
|
43
|
+
const item: ContextItem = {
|
|
44
|
+
id: crypto.randomUUID(),
|
|
45
|
+
bucket: 'user',
|
|
46
|
+
title: input.title,
|
|
47
|
+
body: input.body,
|
|
48
|
+
source: input.source,
|
|
49
|
+
updatedAt: now(),
|
|
50
|
+
}
|
|
51
|
+
const items = readUser(root)
|
|
52
|
+
items.push(item)
|
|
53
|
+
writeUser(root, items)
|
|
54
|
+
return item
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function updateContext(
|
|
58
|
+
root: string,
|
|
59
|
+
id: string,
|
|
60
|
+
patch: { title?: string; body?: string },
|
|
61
|
+
): ContextItem | null {
|
|
62
|
+
const user = readUser(root)
|
|
63
|
+
const ui = user.findIndex((it) => it.id === id)
|
|
64
|
+
if (ui !== -1) {
|
|
65
|
+
const updated: ContextItem = {
|
|
66
|
+
...user[ui],
|
|
67
|
+
...(patch.title !== undefined ? { title: patch.title } : {}),
|
|
68
|
+
...(patch.body !== undefined ? { body: patch.body } : {}),
|
|
69
|
+
updatedAt: now(),
|
|
70
|
+
}
|
|
71
|
+
user[ui] = updated
|
|
72
|
+
writeUser(root, user)
|
|
73
|
+
return updated
|
|
74
|
+
}
|
|
75
|
+
const auto = readAuto(root)
|
|
76
|
+
const ai = auto.findIndex((it) => it.id === id)
|
|
77
|
+
if (ai !== -1) {
|
|
78
|
+
const updated: ContextItem = {
|
|
79
|
+
...auto[ai],
|
|
80
|
+
...(patch.title !== undefined ? { title: patch.title } : {}),
|
|
81
|
+
...(patch.body !== undefined ? { body: patch.body } : {}),
|
|
82
|
+
updatedAt: now(),
|
|
83
|
+
}
|
|
84
|
+
auto[ai] = updated
|
|
85
|
+
writeAuto(root, auto)
|
|
86
|
+
return updated
|
|
87
|
+
}
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function deleteContext(root: string, id: string): boolean {
|
|
92
|
+
const user = readUser(root)
|
|
93
|
+
const userKept = user.filter((it) => it.id !== id)
|
|
94
|
+
if (userKept.length !== user.length) {
|
|
95
|
+
writeUser(root, userKept)
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
const auto = readAuto(root)
|
|
99
|
+
const autoKept = auto.filter((it) => it.id !== id)
|
|
100
|
+
if (autoKept.length !== auto.length) {
|
|
101
|
+
writeAuto(root, autoKept)
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function setAutoInclude(root: string, id: string, include: boolean): ContextItem | null {
|
|
108
|
+
const auto = readAuto(root)
|
|
109
|
+
const idx = auto.findIndex((it) => it.id === id)
|
|
110
|
+
if (idx === -1) return null
|
|
111
|
+
const updated: ContextItem = {
|
|
112
|
+
...auto[idx],
|
|
113
|
+
includeInHandoff: include,
|
|
114
|
+
updatedAt: now(),
|
|
115
|
+
}
|
|
116
|
+
auto[idx] = updated
|
|
117
|
+
writeAuto(root, auto)
|
|
118
|
+
return updated
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface AutoSpec {
|
|
122
|
+
title: string
|
|
123
|
+
body: string | undefined
|
|
124
|
+
file: string
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function materializeAuto(
|
|
128
|
+
root: string,
|
|
129
|
+
digests: { changes?: string; schemaChange?: string; comments?: string },
|
|
130
|
+
): void {
|
|
131
|
+
const specs: AutoSpec[] = [
|
|
132
|
+
{ title: 'Current changes', body: digests.changes, file: 'changes.md' },
|
|
133
|
+
{ title: 'Schema changes', body: digests.schemaChange, file: 'schema-change.md' },
|
|
134
|
+
{ title: 'Open questions digest', body: digests.comments, file: 'comments-digest.md' },
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
const auto = readAuto(root)
|
|
138
|
+
const ts = now()
|
|
139
|
+
|
|
140
|
+
for (const spec of specs) {
|
|
141
|
+
if (spec.body === undefined) continue
|
|
142
|
+
const existing = auto.find((it) => it.title === spec.title)
|
|
143
|
+
if (existing) {
|
|
144
|
+
existing.body = spec.body
|
|
145
|
+
existing.bucket = 'auto'
|
|
146
|
+
existing.source = 'auto-computed'
|
|
147
|
+
existing.freshness = ts
|
|
148
|
+
existing.updatedAt = ts
|
|
149
|
+
// preserve prior include choice
|
|
150
|
+
if (existing.includeInHandoff === undefined) existing.includeInHandoff = false
|
|
151
|
+
} else {
|
|
152
|
+
auto.push({
|
|
153
|
+
id: crypto.randomUUID(),
|
|
154
|
+
bucket: 'auto',
|
|
155
|
+
title: spec.title,
|
|
156
|
+
body: spec.body,
|
|
157
|
+
source: 'auto-computed',
|
|
158
|
+
updatedAt: ts,
|
|
159
|
+
freshness: ts,
|
|
160
|
+
includeInHandoff: false,
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
writeState(root, `context/auto/${spec.file}`, spec.body)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
writeAuto(root, auto)
|
|
167
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* copy.ts — the copy-payload builder. A PURE function (no IO): it turns studio
|
|
3
|
+
* state into a single Markdown handoff payload that an author agent can paste
|
|
4
|
+
* into its own session. The payload ends with a fenced ```json``` machine-state
|
|
5
|
+
* block in the annotate shape so the author can reply by appending
|
|
6
|
+
* {role:'author'} thread entries and merging back by id.
|
|
7
|
+
*/
|
|
8
|
+
import type { Comment, ContextItem, DocMeta, ThreadEntry } from '../../shared/types'
|
|
9
|
+
|
|
10
|
+
interface CopyParts {
|
|
11
|
+
origin: string
|
|
12
|
+
root: string
|
|
13
|
+
schemaHash: string
|
|
14
|
+
openComments: Comment[]
|
|
15
|
+
userContext: ContextItem[]
|
|
16
|
+
autoContext: ContextItem[]
|
|
17
|
+
documents?: DocMeta[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function primaryAnchor(comment: Comment): { ref: string; file?: string } | null {
|
|
21
|
+
const ref = comment.anchorRefs?.[0]
|
|
22
|
+
if (!ref) return null
|
|
23
|
+
return { ref: ref.ref, file: ref.file }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function latestThreadEntry(comment: Comment): ThreadEntry | null {
|
|
27
|
+
const thread = comment.thread ?? []
|
|
28
|
+
return thread.length > 0 ? thread[thread.length - 1] : null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatBytes(n: number): string {
|
|
32
|
+
if (!Number.isFinite(n) || n < 0) return 'unknown size'
|
|
33
|
+
if (n < 1024) return `${n} B`
|
|
34
|
+
if (n < 1024 * 1024) return `${Math.round(n / 1024)} KB`
|
|
35
|
+
return `${(n / 1024 / 1024).toFixed(1)} MB`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** annotate machine-state projection of a comment: {id, anchors, status, thread}. */
|
|
39
|
+
function toMachineState(comment: Comment): {
|
|
40
|
+
id: string
|
|
41
|
+
anchors: string[]
|
|
42
|
+
status: 'open' | 'closed'
|
|
43
|
+
thread: ThreadEntry[]
|
|
44
|
+
} {
|
|
45
|
+
return {
|
|
46
|
+
id: comment.id,
|
|
47
|
+
anchors: comment.anchors ?? [],
|
|
48
|
+
status: comment.status,
|
|
49
|
+
thread: (comment.thread ?? []).map((t) => {
|
|
50
|
+
const entry: ThreadEntry = {
|
|
51
|
+
id: t.id,
|
|
52
|
+
role: t.role,
|
|
53
|
+
type: t.type,
|
|
54
|
+
text: t.text,
|
|
55
|
+
}
|
|
56
|
+
if (t.options !== undefined) entry.options = t.options
|
|
57
|
+
if (t.answer !== undefined) entry.answer = t.answer
|
|
58
|
+
return entry
|
|
59
|
+
}),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function buildCopyMarkdown(parts: CopyParts): string {
|
|
64
|
+
const { origin, root, schemaHash, openComments, userContext, autoContext, documents = [] } = parts
|
|
65
|
+
const lines: string[] = []
|
|
66
|
+
|
|
67
|
+
// ── Header ──
|
|
68
|
+
lines.push(`# Domain handoff — ${origin}`)
|
|
69
|
+
lines.push('')
|
|
70
|
+
lines.push(`- **Origin:** \`${origin}\``)
|
|
71
|
+
lines.push(`- **Repo root:** \`${root}\``)
|
|
72
|
+
lines.push(`- **Schema hash:** \`${schemaHash}\``)
|
|
73
|
+
lines.push('')
|
|
74
|
+
lines.push(
|
|
75
|
+
'Load the **astrale-domain** skill, use the context/threads below, ' +
|
|
76
|
+
'read any listed context documents if relevant, edit code on disk, and reply by appending one `{role:"author"}` entry per open thread ' +
|
|
77
|
+
'(merge by `id`). Resolve a thread by adding a closing note plus `"status":"closed"`; ' +
|
|
78
|
+
'a thread whose last entry is not yours will be resent.',
|
|
79
|
+
)
|
|
80
|
+
lines.push('')
|
|
81
|
+
|
|
82
|
+
// ── Context documents (paths only; contents stay on disk for the agent to read on demand) ──
|
|
83
|
+
if (documents.length > 0) {
|
|
84
|
+
lines.push('## Context documents')
|
|
85
|
+
lines.push('')
|
|
86
|
+
for (const doc of documents) {
|
|
87
|
+
const type = doc.type || 'application/octet-stream'
|
|
88
|
+
lines.push(
|
|
89
|
+
`- \`.domain-studio/${doc.stored}\` — ${doc.name} (${type}, ${formatBytes(doc.size)})`,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
lines.push('')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Saved context notes ──
|
|
96
|
+
if (userContext.length > 0) {
|
|
97
|
+
lines.push('## Saved context notes')
|
|
98
|
+
lines.push('')
|
|
99
|
+
for (const item of userContext) {
|
|
100
|
+
lines.push(`### ${item.title}`)
|
|
101
|
+
lines.push('')
|
|
102
|
+
lines.push(item.body)
|
|
103
|
+
lines.push('')
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Context (auto) — optional, caller already filtered ──
|
|
108
|
+
if (autoContext.length > 0) {
|
|
109
|
+
lines.push('## Auto context')
|
|
110
|
+
lines.push('')
|
|
111
|
+
for (const item of autoContext) {
|
|
112
|
+
lines.push(`### ${item.title}`)
|
|
113
|
+
lines.push('')
|
|
114
|
+
lines.push(item.body)
|
|
115
|
+
lines.push('')
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Open comments & questions ──
|
|
120
|
+
lines.push('## Open comments & questions')
|
|
121
|
+
lines.push('')
|
|
122
|
+
if (openComments.length === 0) {
|
|
123
|
+
lines.push('_none_')
|
|
124
|
+
lines.push('')
|
|
125
|
+
} else {
|
|
126
|
+
openComments.forEach((comment, i) => {
|
|
127
|
+
const anchor = primaryAnchor(comment)
|
|
128
|
+
const latest = latestThreadEntry(comment)
|
|
129
|
+
const head = `${i + 1}. **${comment.kind}**`
|
|
130
|
+
const where = anchor ? ` — \`${anchor.ref}\`${anchor.file ? ` (${anchor.file})` : ''}` : ''
|
|
131
|
+
lines.push(`${head}${where}`)
|
|
132
|
+
if (latest) {
|
|
133
|
+
lines.push(` - ${latest.role}: ${latest.text}`)
|
|
134
|
+
if (latest.type === 'choice') {
|
|
135
|
+
if (latest.options && latest.options.length > 0) {
|
|
136
|
+
lines.push(` - options: ${latest.options.join(', ')}`)
|
|
137
|
+
}
|
|
138
|
+
lines.push(` - answer: ${latest.answer ?? '(unanswered)'}`)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
lines.push('')
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Trailing machine-state block ──
|
|
146
|
+
const machineState = {
|
|
147
|
+
schemaVersion: schemaHash,
|
|
148
|
+
comments: openComments.map(toMachineState),
|
|
149
|
+
}
|
|
150
|
+
lines.push('```json')
|
|
151
|
+
lines.push(JSON.stringify(machineState, null, 2))
|
|
152
|
+
lines.push('```')
|
|
153
|
+
lines.push('')
|
|
154
|
+
|
|
155
|
+
return lines.join('\n')
|
|
156
|
+
}
|