@abraca/orchestrator 2.3.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/README.md +237 -0
- package/dist/abracadabra-orchestrator.cjs +2709 -0
- package/dist/abracadabra-orchestrator.cjs.map +1 -0
- package/dist/abracadabra-orchestrator.esm.js +2665 -0
- package/dist/abracadabra-orchestrator.esm.js.map +1 -0
- package/dist/index.d.ts +261 -0
- package/package.json +39 -0
- package/src/actions/awareness.ts +101 -0
- package/src/actions/chat.ts +30 -0
- package/src/actions/connect.ts +16 -0
- package/src/actions/content.ts +46 -0
- package/src/actions/cursor.ts +30 -0
- package/src/actions/document.ts +120 -0
- package/src/actions/flow.ts +46 -0
- package/src/actions/index.ts +197 -0
- package/src/actions/navigate.ts +15 -0
- package/src/actions/type.ts +113 -0
- package/src/actor-connection.ts +246 -0
- package/src/converters/markdownToYjs.ts +932 -0
- package/src/converters/types.ts +179 -0
- package/src/crypto.ts +71 -0
- package/src/define.ts +147 -0
- package/src/easing.ts +15 -0
- package/src/index.ts +105 -0
- package/src/orchestrator.ts +153 -0
- package/src/timeline-runner.ts +84 -0
- package/src/types.ts +245 -0
- package/src/utils.ts +50 -0
- package/src/yjs-utils.ts +238 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
export interface UserMetaField {
|
|
2
|
+
id: string
|
|
3
|
+
type: string
|
|
4
|
+
label?: string
|
|
5
|
+
key?: string
|
|
6
|
+
latKey?: string
|
|
7
|
+
lngKey?: string
|
|
8
|
+
startKey?: string
|
|
9
|
+
endKey?: string
|
|
10
|
+
allDayKey?: string
|
|
11
|
+
presets?: string[]
|
|
12
|
+
options?: string[]
|
|
13
|
+
min?: number
|
|
14
|
+
max?: number
|
|
15
|
+
step?: number
|
|
16
|
+
unit?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PageMeta extends Record<string, unknown> {
|
|
20
|
+
// Universal display
|
|
21
|
+
color?: string
|
|
22
|
+
icon?: string
|
|
23
|
+
subtitle?: string
|
|
24
|
+
note?: string
|
|
25
|
+
|
|
26
|
+
// Datetime
|
|
27
|
+
datetimeStart?: string
|
|
28
|
+
datetimeEnd?: string
|
|
29
|
+
allDay?: boolean
|
|
30
|
+
dateStart?: string
|
|
31
|
+
dateEnd?: string
|
|
32
|
+
timeStart?: string
|
|
33
|
+
timeEnd?: string
|
|
34
|
+
|
|
35
|
+
// Task/status
|
|
36
|
+
checked?: boolean
|
|
37
|
+
priority?: number
|
|
38
|
+
status?: string
|
|
39
|
+
taskProgress?: number
|
|
40
|
+
rating?: number
|
|
41
|
+
tags?: string[]
|
|
42
|
+
members?: { id: string; label: string }[]
|
|
43
|
+
|
|
44
|
+
// Contact/value
|
|
45
|
+
url?: string
|
|
46
|
+
email?: string
|
|
47
|
+
phone?: string
|
|
48
|
+
number?: number
|
|
49
|
+
unit?: string
|
|
50
|
+
|
|
51
|
+
// Cover image
|
|
52
|
+
coverUploadId?: string
|
|
53
|
+
coverDocId?: string
|
|
54
|
+
coverMimeType?: string
|
|
55
|
+
|
|
56
|
+
// Geo/Map
|
|
57
|
+
geoType?: 'marker' | 'line' | 'measure'
|
|
58
|
+
geoLat?: number
|
|
59
|
+
geoLng?: number
|
|
60
|
+
geoDescription?: string
|
|
61
|
+
|
|
62
|
+
// Whiteboard
|
|
63
|
+
wbX?: number
|
|
64
|
+
wbY?: number
|
|
65
|
+
wbW?: number
|
|
66
|
+
wbH?: number
|
|
67
|
+
wbBg?: string
|
|
68
|
+
|
|
69
|
+
// Dashboard
|
|
70
|
+
deskX?: number
|
|
71
|
+
deskY?: number
|
|
72
|
+
deskZ?: number
|
|
73
|
+
deskMode?: string
|
|
74
|
+
|
|
75
|
+
// Mindmap
|
|
76
|
+
mmX?: number
|
|
77
|
+
mmY?: number
|
|
78
|
+
|
|
79
|
+
// Graph
|
|
80
|
+
graphX?: number
|
|
81
|
+
graphY?: number
|
|
82
|
+
graphPinned?: boolean
|
|
83
|
+
showRefEdges?: boolean
|
|
84
|
+
|
|
85
|
+
// Spatial (3D)
|
|
86
|
+
spShape?: string
|
|
87
|
+
spColor?: string
|
|
88
|
+
spOpacity?: number
|
|
89
|
+
spX?: number
|
|
90
|
+
spY?: number
|
|
91
|
+
spZ?: number
|
|
92
|
+
spRX?: number
|
|
93
|
+
spRY?: number
|
|
94
|
+
spRZ?: number
|
|
95
|
+
spSX?: number
|
|
96
|
+
spSY?: number
|
|
97
|
+
spSZ?: number
|
|
98
|
+
spModelUploadId?: string
|
|
99
|
+
spModelDocId?: string
|
|
100
|
+
|
|
101
|
+
// Slides
|
|
102
|
+
slidesTransition?: string
|
|
103
|
+
slidesTheme?: string
|
|
104
|
+
|
|
105
|
+
// Media metadata (extracted on import)
|
|
106
|
+
mediaDuration?: number
|
|
107
|
+
mediaWidth?: number
|
|
108
|
+
mediaHeight?: number
|
|
109
|
+
mediaCamera?: string
|
|
110
|
+
mediaLens?: string
|
|
111
|
+
mediaIso?: number
|
|
112
|
+
mediaFocalLength?: number
|
|
113
|
+
mediaAperture?: number
|
|
114
|
+
mediaShutterSpeed?: string
|
|
115
|
+
mediaArtist?: string
|
|
116
|
+
mediaAlbum?: string
|
|
117
|
+
mediaGenre?: string
|
|
118
|
+
mediaYear?: number
|
|
119
|
+
dateTaken?: string
|
|
120
|
+
|
|
121
|
+
// Sheets cell formatting
|
|
122
|
+
bold?: boolean
|
|
123
|
+
italic?: boolean
|
|
124
|
+
textColor?: string
|
|
125
|
+
bgColor?: string
|
|
126
|
+
align?: string
|
|
127
|
+
formula?: string
|
|
128
|
+
|
|
129
|
+
// Renderer config (set on the page doc itself, not children)
|
|
130
|
+
kanbanColumnWidth?: string
|
|
131
|
+
galleryColumns?: number
|
|
132
|
+
galleryAspect?: string
|
|
133
|
+
galleryCardStyle?: string
|
|
134
|
+
galleryShowLabels?: boolean
|
|
135
|
+
gallerySortBy?: string
|
|
136
|
+
calendarView?: string
|
|
137
|
+
calendarWeekStart?: string
|
|
138
|
+
calendarShowWeekNumbers?: boolean
|
|
139
|
+
tableMode?: string
|
|
140
|
+
tableSortDir?: string
|
|
141
|
+
tableColumns?: any[]
|
|
142
|
+
tableColumnWidths?: Record<string, number>
|
|
143
|
+
tableColumnOrder?: string[]
|
|
144
|
+
timelineZoom?: string
|
|
145
|
+
checklistFilter?: string
|
|
146
|
+
checklistSort?: string
|
|
147
|
+
mapShowLabels?: boolean
|
|
148
|
+
spatialGridVisible?: boolean
|
|
149
|
+
chartType?: string
|
|
150
|
+
chartMetric?: string
|
|
151
|
+
chartColorScheme?: string
|
|
152
|
+
chartLimit?: number
|
|
153
|
+
chartShowLegend?: boolean
|
|
154
|
+
chartShowValues?: boolean
|
|
155
|
+
mediaRepeat?: string
|
|
156
|
+
mediaShuffle?: boolean
|
|
157
|
+
sheetsDefaultColWidth?: number
|
|
158
|
+
sheetsDefaultRowHeight?: number
|
|
159
|
+
sheetsShowGridlines?: boolean
|
|
160
|
+
sheetsColumnWidths?: Record<string, number>
|
|
161
|
+
sheetsRowHeights?: Record<string, number>
|
|
162
|
+
sheetsFreezeRows?: number
|
|
163
|
+
sheetsFreezeCols?: number
|
|
164
|
+
|
|
165
|
+
// Internal
|
|
166
|
+
_metaFields?: UserMetaField[]
|
|
167
|
+
_metaInitialized?: boolean
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface TreeEntry {
|
|
171
|
+
id: string
|
|
172
|
+
label: string
|
|
173
|
+
parentId: string | null
|
|
174
|
+
order: number
|
|
175
|
+
type?: string
|
|
176
|
+
meta?: PageMeta
|
|
177
|
+
createdAt?: number
|
|
178
|
+
updatedAt?: number
|
|
179
|
+
}
|
package/src/crypto.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ed25519 key generation, persistence, and challenge signing for actor auth.
|
|
3
|
+
* Copied from packages/mcp/src/crypto.ts
|
|
4
|
+
*/
|
|
5
|
+
import * as ed from '@noble/ed25519'
|
|
6
|
+
import { sha512 } from '@noble/hashes/sha2.js'
|
|
7
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
8
|
+
|
|
9
|
+
// @noble/ed25519 v3 hash hook
|
|
10
|
+
ed.hashes.sha512 = sha512
|
|
11
|
+
ed.hashes.sha512Async = (m: Uint8Array) => Promise.resolve(sha512(m))
|
|
12
|
+
import { existsSync } from 'node:fs'
|
|
13
|
+
import { dirname } from 'node:path'
|
|
14
|
+
|
|
15
|
+
function toBase64url(bytes: Uint8Array): string {
|
|
16
|
+
return Buffer.from(bytes).toString('base64url')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function fromBase64url(b64: string): Uint8Array {
|
|
20
|
+
return new Uint8Array(Buffer.from(b64, 'base64url'))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AgentKeypair {
|
|
24
|
+
privateKey: Uint8Array
|
|
25
|
+
publicKeyB64: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Load an existing Ed25519 keypair from disk, or generate and persist a new one.
|
|
30
|
+
*/
|
|
31
|
+
export async function loadOrCreateKeypair(keyPath: string): Promise<AgentKeypair> {
|
|
32
|
+
if (existsSync(keyPath)) {
|
|
33
|
+
const seed = await readFile(keyPath)
|
|
34
|
+
if (seed.length !== 32) {
|
|
35
|
+
throw new Error(`Invalid key file at ${keyPath}: expected 32 bytes, got ${seed.length}`)
|
|
36
|
+
}
|
|
37
|
+
const privateKey = new Uint8Array(seed)
|
|
38
|
+
const publicKey = ed.getPublicKey(privateKey)
|
|
39
|
+
return { privateKey, publicKeyB64: toBase64url(publicKey) }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const privateKey = ed.utils.randomSecretKey()
|
|
43
|
+
const publicKey = ed.getPublicKey(privateKey)
|
|
44
|
+
|
|
45
|
+
const dir = dirname(keyPath)
|
|
46
|
+
if (!existsSync(dir)) {
|
|
47
|
+
await mkdir(dir, { recursive: true, mode: 0o700 })
|
|
48
|
+
}
|
|
49
|
+
await writeFile(keyPath, Buffer.from(privateKey), { mode: 0o600 })
|
|
50
|
+
|
|
51
|
+
console.error(`[orchestrator] Generated new keypair at ${keyPath}`)
|
|
52
|
+
return { privateKey, publicKeyB64: toBase64url(publicKey) }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Generate a deterministic Ed25519 keypair from a seed string (e.g. actor name).
|
|
57
|
+
* The same seed always produces the same keypair.
|
|
58
|
+
*/
|
|
59
|
+
export function deterministicKeypair(seed: string): AgentKeypair {
|
|
60
|
+
const hash = sha512(new TextEncoder().encode(seed + ':orchestrator-salt'))
|
|
61
|
+
const privateKey = hash.slice(0, 32)
|
|
62
|
+
const publicKey = ed.getPublicKey(privateKey)
|
|
63
|
+
return { privateKey, publicKeyB64: toBase64url(publicKey) }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Sign a base64url challenge with the agent's private key; returns base64url signature. */
|
|
67
|
+
export function signChallenge(challengeB64: string, privateKey: Uint8Array): string {
|
|
68
|
+
const challenge = fromBase64url(challengeB64)
|
|
69
|
+
const sig = ed.sign(challenge, privateKey)
|
|
70
|
+
return toBase64url(sig)
|
|
71
|
+
}
|
package/src/define.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Script definition helpers — typed factory functions for building scenes.
|
|
3
|
+
*/
|
|
4
|
+
import type {
|
|
5
|
+
Scene,
|
|
6
|
+
ActorDef,
|
|
7
|
+
ConnectAction,
|
|
8
|
+
DisconnectAction,
|
|
9
|
+
NavigateAction,
|
|
10
|
+
TypeAction,
|
|
11
|
+
TypeDeleteAction,
|
|
12
|
+
SelectAction,
|
|
13
|
+
MoveCursorAction,
|
|
14
|
+
SetStatusAction,
|
|
15
|
+
SetAwarenessAction,
|
|
16
|
+
ClearAwarenessAction,
|
|
17
|
+
CreateDocumentAction,
|
|
18
|
+
MoveDocumentAction,
|
|
19
|
+
RenameDocumentAction,
|
|
20
|
+
WriteContentAction,
|
|
21
|
+
DeleteContentAction,
|
|
22
|
+
SetMetaAction,
|
|
23
|
+
PointerMoveAction,
|
|
24
|
+
ScrollToAction,
|
|
25
|
+
KanbanHoverAction,
|
|
26
|
+
KanbanDragAction,
|
|
27
|
+
SendChatAction,
|
|
28
|
+
WaitAction,
|
|
29
|
+
ParallelAction,
|
|
30
|
+
SequenceAction,
|
|
31
|
+
RepeatAction,
|
|
32
|
+
TimelineEntry,
|
|
33
|
+
EasingName,
|
|
34
|
+
} from './types.ts'
|
|
35
|
+
|
|
36
|
+
/** Define a complete scene. */
|
|
37
|
+
export function defineScene(scene: Scene): Scene {
|
|
38
|
+
return scene
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Define an actor. */
|
|
42
|
+
export function actor(name: string, opts: Omit<ActorDef, 'name'>): ActorDef {
|
|
43
|
+
return { name, ...opts }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Factory functions for all action types. */
|
|
47
|
+
export const actions = {
|
|
48
|
+
connect(): ConnectAction {
|
|
49
|
+
return { type: 'connect' }
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
disconnect(): DisconnectAction {
|
|
53
|
+
return { type: 'disconnect' }
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
navigate(docId: string): NavigateAction {
|
|
57
|
+
return { type: 'navigate', docId }
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
type(docId: string, text: string, opts?: { speed?: number; variance?: number; position?: number }): TypeAction {
|
|
61
|
+
return { type: 'type', docId, text, ...opts }
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
typeDelete(docId: string, count: number, opts?: { speed?: number; variance?: number; position?: number }): TypeDeleteAction {
|
|
65
|
+
return { type: 'typeDelete', docId, count, ...opts }
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
select(docId: string, anchor: number, head: number): SelectAction {
|
|
69
|
+
return { type: 'select', docId, anchor, head }
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
moveCursor(docId: string, from: number, to: number, duration: number, easing?: EasingName): MoveCursorAction {
|
|
73
|
+
return { type: 'moveCursor', docId, from, to, duration, easing }
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
setStatus(status: string | null): SetStatusAction {
|
|
77
|
+
return { type: 'setStatus', status }
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
setAwareness(fields: Record<string, unknown>, docId?: string): SetAwarenessAction {
|
|
81
|
+
return { type: 'setAwareness', docId, fields }
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
clearAwareness(fields: string[], docId?: string): ClearAwarenessAction {
|
|
85
|
+
return { type: 'clearAwareness', docId, fields }
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
createDocument(parentId: string, label: string, opts?: { docType?: string; meta?: Record<string, unknown>; assignId?: string }): CreateDocumentAction {
|
|
89
|
+
return { type: 'createDocument', parentId, label, ...opts }
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
moveDocument(docId: string, newParentId: string, order?: number): MoveDocumentAction {
|
|
93
|
+
return { type: 'moveDocument', docId, newParentId, order }
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
renameDocument(docId: string, label: string): RenameDocumentAction {
|
|
97
|
+
return { type: 'renameDocument', docId, label }
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
writeContent(docId: string, markdown: string): WriteContentAction {
|
|
101
|
+
return { type: 'writeContent', docId, markdown }
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
deleteContent(docId: string, from: number, length: number): DeleteContentAction {
|
|
105
|
+
return { type: 'deleteContent', docId, from, length }
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
setMeta(docId: string, meta: Record<string, unknown>): SetMetaAction {
|
|
109
|
+
return { type: 'setMeta', docId, meta }
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
pointerMove(docId: string, from: { x: number; y: number }, to: { x: number; y: number }, duration: number, easing?: EasingName): PointerMoveAction {
|
|
113
|
+
return { type: 'pointerMove', docId, from, to, duration, easing }
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
scrollTo(docId: string, position: number): ScrollToAction {
|
|
117
|
+
return { type: 'scrollTo', docId, position }
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
kanbanHover(docId: string, cardId: string | null): KanbanHoverAction {
|
|
121
|
+
return { type: 'kanbanHover', docId, cardId }
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
kanbanDrag(docId: string, cardId: string, toColumnId: string, duration: number): KanbanDragAction {
|
|
125
|
+
return { type: 'kanbanDrag', docId, cardId, toColumnId, duration }
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
sendChat(channel: string, message: string): SendChatAction {
|
|
129
|
+
return { type: 'sendChat', channel, message }
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
wait(duration: number): WaitAction {
|
|
133
|
+
return { type: 'wait', duration }
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
parallel(entries: TimelineEntry[]): ParallelAction {
|
|
137
|
+
return { type: 'parallel', actions: entries }
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
sequence(entries: TimelineEntry[]): SequenceAction {
|
|
141
|
+
return { type: 'sequence', actions: entries }
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
repeat(times: number, entries: TimelineEntry[]): RepeatAction {
|
|
145
|
+
return { type: 'repeat', times, actions: entries }
|
|
146
|
+
},
|
|
147
|
+
}
|
package/src/easing.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Easing functions for animated actions (cursor movement, pointer, etc.)
|
|
3
|
+
*/
|
|
4
|
+
import type { EasingName, EasingFn } from './types.ts'
|
|
5
|
+
|
|
6
|
+
export const easings: Record<EasingName, EasingFn> = {
|
|
7
|
+
linear: (t) => t,
|
|
8
|
+
easeIn: (t) => t * t,
|
|
9
|
+
easeOut: (t) => t * (2 - t),
|
|
10
|
+
easeInOut: (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getEasing(name?: EasingName): EasingFn {
|
|
14
|
+
return easings[name ?? 'easeInOut']
|
|
15
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Abracadabra Orchestrator — CouShell commercial director.
|
|
4
|
+
*
|
|
5
|
+
* Usage (after pnpm build:packages):
|
|
6
|
+
* node --experimental-transform-types \
|
|
7
|
+
* packages/orchestrator/dist/abracadabra-orchestrator.esm.js ./scripts/demo.ts
|
|
8
|
+
*
|
|
9
|
+
* Dry run:
|
|
10
|
+
* node --experimental-transform-types \
|
|
11
|
+
* packages/orchestrator/dist/abracadabra-orchestrator.esm.js --dry-run ./scripts/demo.ts
|
|
12
|
+
*/
|
|
13
|
+
import { Orchestrator } from './orchestrator.ts'
|
|
14
|
+
|
|
15
|
+
// Re-export public API for script files
|
|
16
|
+
export { defineScene, actor, actions } from './define.ts'
|
|
17
|
+
export { Orchestrator } from './orchestrator.ts'
|
|
18
|
+
export type {
|
|
19
|
+
Scene,
|
|
20
|
+
ActorDef,
|
|
21
|
+
ServerConfig,
|
|
22
|
+
Action,
|
|
23
|
+
TimelineEntry,
|
|
24
|
+
ConnectAction,
|
|
25
|
+
DisconnectAction,
|
|
26
|
+
NavigateAction,
|
|
27
|
+
TypeAction,
|
|
28
|
+
TypeDeleteAction,
|
|
29
|
+
SelectAction,
|
|
30
|
+
MoveCursorAction,
|
|
31
|
+
SetStatusAction,
|
|
32
|
+
SetAwarenessAction,
|
|
33
|
+
ClearAwarenessAction,
|
|
34
|
+
CreateDocumentAction,
|
|
35
|
+
MoveDocumentAction,
|
|
36
|
+
RenameDocumentAction,
|
|
37
|
+
WriteContentAction,
|
|
38
|
+
DeleteContentAction,
|
|
39
|
+
SetMetaAction,
|
|
40
|
+
PointerMoveAction,
|
|
41
|
+
ScrollToAction,
|
|
42
|
+
KanbanHoverAction,
|
|
43
|
+
KanbanDragAction,
|
|
44
|
+
SendChatAction,
|
|
45
|
+
RepeatAction,
|
|
46
|
+
WaitAction,
|
|
47
|
+
ParallelAction,
|
|
48
|
+
SequenceAction,
|
|
49
|
+
EasingName,
|
|
50
|
+
} from './types.ts'
|
|
51
|
+
|
|
52
|
+
async function main() {
|
|
53
|
+
const args = process.argv.slice(2)
|
|
54
|
+
const dryRun = args.includes('--dry-run')
|
|
55
|
+
const scriptPath = args.find(a => !a.startsWith('--'))
|
|
56
|
+
|
|
57
|
+
if (!scriptPath) {
|
|
58
|
+
console.error('Usage: abracadabra-orchestrator [--dry-run] <script.ts>')
|
|
59
|
+
console.error('')
|
|
60
|
+
console.error(' The script file should export a Scene object (use defineScene()).')
|
|
61
|
+
console.error('')
|
|
62
|
+
console.error('Options:')
|
|
63
|
+
console.error(' --dry-run Validate the scene without connecting to the server')
|
|
64
|
+
console.error('')
|
|
65
|
+
console.error('Example:')
|
|
66
|
+
console.error(' node --conditions=source --experimental-transform-types \\')
|
|
67
|
+
console.error(' packages/orchestrator/src/index.ts ./scripts/demo.ts')
|
|
68
|
+
process.exit(1)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const orchestrator = new Orchestrator()
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
await orchestrator.load(scriptPath)
|
|
75
|
+
orchestrator.prepare()
|
|
76
|
+
|
|
77
|
+
if (dryRun) {
|
|
78
|
+
orchestrator.dryRun()
|
|
79
|
+
} else {
|
|
80
|
+
await orchestrator.run()
|
|
81
|
+
}
|
|
82
|
+
} catch (err: any) {
|
|
83
|
+
console.error(`[orchestrator] Fatal: ${err.message}`)
|
|
84
|
+
process.exit(1)
|
|
85
|
+
} finally {
|
|
86
|
+
if (!dryRun) {
|
|
87
|
+
await orchestrator.cleanup()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Only run CLI when executed directly (not imported as a library)
|
|
93
|
+
// Check if argv[1] points to a file within this package's directory
|
|
94
|
+
import { fileURLToPath } from 'node:url'
|
|
95
|
+
import { dirname, resolve } from 'node:path'
|
|
96
|
+
|
|
97
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
98
|
+
const __dirname = dirname(__filename)
|
|
99
|
+
|
|
100
|
+
if (process.argv[1]) {
|
|
101
|
+
const resolved = resolve(process.argv[1])
|
|
102
|
+
if (resolved.startsWith(__dirname) || resolved.includes('abracadabra-orchestrator')) {
|
|
103
|
+
main()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrator — top-level class that loads a scene, manages actors, and runs the timeline.
|
|
3
|
+
*/
|
|
4
|
+
import { resolve } from 'node:path'
|
|
5
|
+
import { pathToFileURL } from 'node:url'
|
|
6
|
+
import type { Scene } from './types.ts'
|
|
7
|
+
import { ActorConnection } from './actor-connection.ts'
|
|
8
|
+
import { TimelineRunner, type ErrorStrategy } from './timeline-runner.ts'
|
|
9
|
+
import { createExecutor } from './actions/index.ts'
|
|
10
|
+
import { log } from './utils.ts'
|
|
11
|
+
|
|
12
|
+
export class Orchestrator {
|
|
13
|
+
private scene: Scene | null = null
|
|
14
|
+
private actors = new Map<string, ActorConnection>()
|
|
15
|
+
private vars = new Map<string, string>()
|
|
16
|
+
|
|
17
|
+
/** Load a scene from a TypeScript file. */
|
|
18
|
+
async load(scriptPath: string): Promise<void> {
|
|
19
|
+
const absPath = resolve(scriptPath)
|
|
20
|
+
const fileUrl = pathToFileURL(absPath).href
|
|
21
|
+
const mod = await import(fileUrl)
|
|
22
|
+
const scene: Scene = mod.default ?? mod
|
|
23
|
+
|
|
24
|
+
if (!scene.server?.url) {
|
|
25
|
+
throw new Error('Scene must have server.url')
|
|
26
|
+
}
|
|
27
|
+
if (!scene.actors?.length) {
|
|
28
|
+
throw new Error('Scene must have at least one actor')
|
|
29
|
+
}
|
|
30
|
+
if (!scene.timeline?.length) {
|
|
31
|
+
throw new Error('Scene must have at least one timeline entry')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.scene = scene
|
|
35
|
+
log(`Loaded scene: ${scene.actors.length} actors, ${scene.timeline.length} timeline entries`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Prepare actor connections (does not connect yet — that's a timeline action). */
|
|
39
|
+
prepare(): void {
|
|
40
|
+
if (!this.scene) throw new Error('No scene loaded')
|
|
41
|
+
|
|
42
|
+
if (this.scene.vars) {
|
|
43
|
+
for (const [k, v] of Object.entries(this.scene.vars)) {
|
|
44
|
+
this.vars.set(k, v)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const actorDef of this.scene.actors) {
|
|
49
|
+
if (this.actors.has(actorDef.name)) {
|
|
50
|
+
throw new Error(`Duplicate actor name: ${actorDef.name}`)
|
|
51
|
+
}
|
|
52
|
+
const conn = new ActorConnection(actorDef, this.scene.server)
|
|
53
|
+
this.actors.set(actorDef.name, conn)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
log(`Prepared ${this.actors.size} actors: ${[...this.actors.keys()].join(', ')}`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Validate the scene without connecting. Logs the timeline structure. */
|
|
60
|
+
dryRun(): void {
|
|
61
|
+
if (!this.scene) throw new Error('No scene loaded')
|
|
62
|
+
|
|
63
|
+
const actorNames = new Set(this.scene.actors.map(a => a.name))
|
|
64
|
+
const errors: string[] = []
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < this.scene.timeline.length; i++) {
|
|
67
|
+
const entry = this.scene.timeline[i]!
|
|
68
|
+
const prefix = `timeline[${i}]`
|
|
69
|
+
|
|
70
|
+
if (entry.actor && !actorNames.has(entry.actor)) {
|
|
71
|
+
errors.push(`${prefix}: unknown actor "${entry.actor}"`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const needsActor = !['wait', 'parallel', 'sequence', 'repeat'].includes(entry.action.type)
|
|
75
|
+
if (needsActor && !entry.actor) {
|
|
76
|
+
errors.push(`${prefix}: ${entry.action.type} requires an actor`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (errors.length) {
|
|
81
|
+
log(`Dry run found ${errors.length} issue(s):`)
|
|
82
|
+
for (const e of errors) log(` - ${e}`)
|
|
83
|
+
} else {
|
|
84
|
+
log('Dry run: no issues found')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Print timeline summary
|
|
88
|
+
const sorted = [...this.scene.timeline].sort((a, b) => (a.at ?? 0) - (b.at ?? 0))
|
|
89
|
+
log('Timeline:')
|
|
90
|
+
for (const entry of sorted) {
|
|
91
|
+
const ts = `${((entry.at ?? 0) / 1000).toFixed(1)}s`
|
|
92
|
+
const actorLabel = entry.actor ?? '—'
|
|
93
|
+
log(` [${ts}] ${actorLabel}: ${entry.action.type}`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (this.scene.duration) {
|
|
97
|
+
log(`Scene duration: ${(this.scene.duration / 1000).toFixed(1)}s`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Run the timeline. */
|
|
102
|
+
async run(): Promise<void> {
|
|
103
|
+
if (!this.scene) throw new Error('No scene loaded')
|
|
104
|
+
|
|
105
|
+
// Run onStart hook
|
|
106
|
+
if (this.scene.onStart) {
|
|
107
|
+
log('Running onStart hook...')
|
|
108
|
+
await this.scene.onStart()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const executor = createExecutor({
|
|
112
|
+
actors: this.actors,
|
|
113
|
+
serverConfig: this.scene.server,
|
|
114
|
+
vars: this.vars,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const runner = new TimelineRunner()
|
|
118
|
+
log('Starting timeline...')
|
|
119
|
+
const startTime = Date.now()
|
|
120
|
+
|
|
121
|
+
if (this.scene.duration) {
|
|
122
|
+
// Race timeline against duration timeout
|
|
123
|
+
const timelinePromise = runner.run(this.scene.timeline, executor)
|
|
124
|
+
const timeoutPromise = new Promise<void>(resolve => {
|
|
125
|
+
setTimeout(() => {
|
|
126
|
+
log(`Scene duration reached (${this.scene!.duration}ms), stopping...`)
|
|
127
|
+
resolve()
|
|
128
|
+
}, this.scene.duration)
|
|
129
|
+
})
|
|
130
|
+
await Promise.race([timelinePromise, timeoutPromise])
|
|
131
|
+
} else {
|
|
132
|
+
await runner.run(this.scene.timeline, executor)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1)
|
|
136
|
+
log(`Timeline complete in ${elapsed}s`)
|
|
137
|
+
|
|
138
|
+
// Run onEnd hook
|
|
139
|
+
if (this.scene.onEnd) {
|
|
140
|
+
log('Running onEnd hook...')
|
|
141
|
+
await this.scene.onEnd()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Disconnect all actors gracefully. */
|
|
146
|
+
async cleanup(): Promise<void> {
|
|
147
|
+
const disconnects = [...this.actors.values()].map(a => a.disconnect().catch(() => {}))
|
|
148
|
+
await Promise.all(disconnects)
|
|
149
|
+
this.actors.clear()
|
|
150
|
+
this.vars.clear()
|
|
151
|
+
log('All actors disconnected')
|
|
152
|
+
}
|
|
153
|
+
}
|