@rool-dev/extension 0.3.5
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 +458 -0
- package/dist/cli/build-pipeline.d.ts +18 -0
- package/dist/cli/build-pipeline.d.ts.map +1 -0
- package/dist/cli/build-pipeline.js +160 -0
- package/dist/cli/build.d.ts +9 -0
- package/dist/cli/build.d.ts.map +1 -0
- package/dist/cli/build.js +17 -0
- package/dist/cli/dev.d.ts +10 -0
- package/dist/cli/dev.d.ts.map +1 -0
- package/dist/cli/dev.js +257 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +34 -0
- package/dist/cli/init.d.ts +8 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +113 -0
- package/dist/cli/publish.d.ts +9 -0
- package/dist/cli/publish.d.ts.map +1 -0
- package/dist/cli/publish.js +65 -0
- package/dist/cli/vite-utils.d.ts +23 -0
- package/dist/cli/vite-utils.d.ts.map +1 -0
- package/dist/cli/vite-utils.js +105 -0
- package/dist/client.d.ts +139 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +360 -0
- package/dist/dev/AppGrid.svelte +246 -0
- package/dist/dev/AppGrid.svelte.d.ts +14 -0
- package/dist/dev/AppGrid.svelte.d.ts.map +1 -0
- package/dist/dev/DevHostController.d.ts +85 -0
- package/dist/dev/DevHostController.d.ts.map +1 -0
- package/dist/dev/DevHostController.js +429 -0
- package/dist/dev/HostShell.svelte +119 -0
- package/dist/dev/HostShell.svelte.d.ts +11 -0
- package/dist/dev/HostShell.svelte.d.ts.map +1 -0
- package/dist/dev/Sidebar.svelte +290 -0
- package/dist/dev/Sidebar.svelte.d.ts +22 -0
- package/dist/dev/Sidebar.svelte.d.ts.map +1 -0
- package/dist/dev/TabBar.svelte +83 -0
- package/dist/dev/TabBar.svelte.d.ts +14 -0
- package/dist/dev/TabBar.svelte.d.ts.map +1 -0
- package/dist/dev/app.css +1 -0
- package/dist/dev/host-shell.d.ts +8 -0
- package/dist/dev/host-shell.d.ts.map +1 -0
- package/dist/dev/host-shell.js +15282 -0
- package/dist/dev/host-shell.js.map +1 -0
- package/dist/dev/vite-env.d.ts +4 -0
- package/dist/host.d.ts +55 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +203 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/manifest.d.ts +40 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +11 -0
- package/dist/protocol.d.ts +48 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +14 -0
- package/dist/reactive.svelte.d.ts +150 -0
- package/dist/reactive.svelte.d.ts.map +1 -0
- package/dist/reactive.svelte.js +362 -0
- package/dist/types.d.ts +139 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/package.json +79 -0
package/README.md
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
# Rool Extension
|
|
2
|
+
|
|
3
|
+
An extension is a feature package that adds capabilities to a Rool Space. Extensions are Svelte 5 components hosted in sandboxed iframes, communicating with the host via a postMessage bridge. Each extension gets a reactive channel as its interface to the Space's objects, schema, AI, and real-time events.
|
|
4
|
+
|
|
5
|
+
Developers build extensions to create custom experiences on top of a Space — productivity tools, dashboards, data views, games, or anything else. Multiple extensions can be installed into the same Space, letting users and teams assemble an AI-powered interface that fits exactly how they work.
|
|
6
|
+
|
|
7
|
+
An extension project is just two files:
|
|
8
|
+
|
|
9
|
+
- **`App.svelte`** — Your UI component (receives a reactive channel as a prop)
|
|
10
|
+
- **`manifest.json`** — Manifest with id, name, icon, visibility, and collection access
|
|
11
|
+
|
|
12
|
+
Everything else (Vite config, entry point, HTML, Tailwind CSS) is provided by the CLI.
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx rool-extension init my-extension
|
|
18
|
+
cd my-extension
|
|
19
|
+
pnpm install
|
|
20
|
+
npx rool-extension dev
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This opens a dev host at `/__rool-host/` that loads your extension in a sandboxed iframe, connected to a real Rool Space.
|
|
24
|
+
|
|
25
|
+
## Manifest
|
|
26
|
+
|
|
27
|
+
`manifest.json` declares your extension's identity and collection access:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"id": "my-extension",
|
|
32
|
+
"name": "My Extension",
|
|
33
|
+
"public": false,
|
|
34
|
+
"icon": "icon.png",
|
|
35
|
+
"description": "What this extension does",
|
|
36
|
+
"collections": {
|
|
37
|
+
"write": {
|
|
38
|
+
"task": [
|
|
39
|
+
{ "name": "title", "type": { "kind": "string" } },
|
|
40
|
+
{ "name": "done", "type": { "kind": "boolean" } }
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"read": "*"
|
|
44
|
+
},
|
|
45
|
+
"systemInstruction": "Optional system instruction for the AI"
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
| Field | Required | Description |
|
|
50
|
+
|-------|----------|-------------|
|
|
51
|
+
| `id` | Yes | Unique identifier (lowercase, hyphens) |
|
|
52
|
+
| `name` | Yes | Display name |
|
|
53
|
+
| `public` | Yes | Whether the extension is listed in the public extension directory |
|
|
54
|
+
| `icon` | No | Path to an icon image file relative to the project root (e.g. `"icon.png"`) |
|
|
55
|
+
| `description` | No | Short description |
|
|
56
|
+
| `collections` | Yes | Collection access declarations — can be `{}` (see below) |
|
|
57
|
+
| `systemInstruction` | No | Default system instruction for the AI channel |
|
|
58
|
+
|
|
59
|
+
### Collection Access
|
|
60
|
+
|
|
61
|
+
The `collections` field declares what collections the extension works with, grouped by access level:
|
|
62
|
+
|
|
63
|
+
- **`write`** — Collections the extension can create, update, and delete objects in. An object with field definitions creates the collection in the space. `"*"` grants write access to all collections.
|
|
64
|
+
- **`read`** — Collections the extension can read from. An object with field definitions declares the expected shape. `"*"` grants read access to all collections.
|
|
65
|
+
|
|
66
|
+
`write` implies `read` — no need to list a collection under both.
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
// Extension with its own collections + read access to everything else
|
|
70
|
+
"collections": {
|
|
71
|
+
"write": {
|
|
72
|
+
"card": [
|
|
73
|
+
{ "name": "front", "type": { "kind": "string" } },
|
|
74
|
+
{ "name": "back", "type": { "kind": "string" } }
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
"read": "*"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Full access to all collections (chat, SQL interface, etc.)
|
|
81
|
+
"collections": {
|
|
82
|
+
"write": "*"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Read-only access to all collections
|
|
86
|
+
"collections": {
|
|
87
|
+
"read": "*"
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Extension Component
|
|
92
|
+
|
|
93
|
+
App.svelte receives a single prop — a `ReactiveChannel`:
|
|
94
|
+
|
|
95
|
+
```svelte
|
|
96
|
+
<script lang="ts">
|
|
97
|
+
import type { ReactiveChannel } from '@rool-dev/extension';
|
|
98
|
+
|
|
99
|
+
interface Props {
|
|
100
|
+
channel: ReactiveChannel;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let { channel }: Props = $props();
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<div>
|
|
107
|
+
<p>Connected to: {channel.spaceName}</p>
|
|
108
|
+
<p>Objects: {channel.objectIds.length}</p>
|
|
109
|
+
<button onclick={() => channel.prompt('Hello')}>Send</button>
|
|
110
|
+
</div>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The component can import other `.svelte` components and `.ts` files — standard Svelte/TypeScript conventions apply. Tailwind CSS v4 is available out of the box. Add an `app.css` file to include custom styles.
|
|
114
|
+
|
|
115
|
+
### Example: Task List
|
|
116
|
+
|
|
117
|
+
A complete extension that lets users add tasks, mark them done, and ask the AI to generate tasks from a description. The `watch` primitive keeps the list in sync with the Space in real-time.
|
|
118
|
+
|
|
119
|
+
```svelte
|
|
120
|
+
<script lang="ts">
|
|
121
|
+
import type { ReactiveChannel } from '@rool-dev/extension';
|
|
122
|
+
|
|
123
|
+
interface Props { channel: ReactiveChannel }
|
|
124
|
+
let { channel }: Props = $props();
|
|
125
|
+
|
|
126
|
+
const tasks = channel.watch({ collection: 'task' });
|
|
127
|
+
|
|
128
|
+
let input = $state('');
|
|
129
|
+
|
|
130
|
+
async function addTask() {
|
|
131
|
+
if (!input.trim()) return;
|
|
132
|
+
await channel.createObject({ data: { title: input, done: false } });
|
|
133
|
+
input = '';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function generate() {
|
|
137
|
+
if (!input.trim()) return;
|
|
138
|
+
await channel.prompt(`Create tasks for: ${input}`);
|
|
139
|
+
input = '';
|
|
140
|
+
}
|
|
141
|
+
</script>
|
|
142
|
+
|
|
143
|
+
<div class="flex gap-2 mb-4">
|
|
144
|
+
<input bind:value={input} placeholder="New task or describe what you need…"
|
|
145
|
+
class="flex-1 border rounded px-2 py-1" onkeydown={(e) => e.key === 'Enter' && addTask()} />
|
|
146
|
+
<button onclick={addTask} class="px-3 py-1 bg-blue-600 text-white rounded">Add</button>
|
|
147
|
+
<button onclick={generate} class="px-3 py-1 bg-violet-600 text-white rounded">AI Generate</button>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
{#each tasks.objects as task (task.id)}
|
|
151
|
+
<label class="flex items-center gap-2 py-1">
|
|
152
|
+
<input type="checkbox" checked={task.done}
|
|
153
|
+
onchange={() => channel.updateObject(task.id, { data: { done: !task.done } })} />
|
|
154
|
+
<span class:line-through={task.done}>{task.title}</span>
|
|
155
|
+
</label>
|
|
156
|
+
{/each}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
This example covers the main patterns you'll use in most extensions: `watch` for a live query, `createObject` for direct mutations, `updateObject` for edits, and `prompt` to let the AI create or modify objects on the user's behalf.
|
|
160
|
+
|
|
161
|
+
## ReactiveChannel
|
|
162
|
+
|
|
163
|
+
The channel is the extension's interface to the host Space — objects, schema, AI, metadata, undo/redo, and real-time events.
|
|
164
|
+
|
|
165
|
+
### Reactive State
|
|
166
|
+
|
|
167
|
+
These are Svelte 5 `$state` properties — use them directly in templates or `$effect` blocks:
|
|
168
|
+
|
|
169
|
+
| Property | Type | Description |
|
|
170
|
+
|----------|------|-------------|
|
|
171
|
+
| `interactions` | `Interaction[]` | Channel interaction history (auto-updates) |
|
|
172
|
+
| `objectIds` | `string[]` | All object IDs in the space (auto-updates on create/delete) |
|
|
173
|
+
| `collections` | `string[]` | Collection names from the schema (auto-updates) |
|
|
174
|
+
| `conversations` | `ConversationInfo[]` | Conversations in this channel (auto-updates on create/delete/rename) |
|
|
175
|
+
|
|
176
|
+
### Properties
|
|
177
|
+
|
|
178
|
+
| Property | Type | Description |
|
|
179
|
+
|----------|------|-------------|
|
|
180
|
+
| `channelId` | `string` | Channel ID |
|
|
181
|
+
| `spaceId` | `string` | Space ID |
|
|
182
|
+
| `spaceName` | `string` | Space name |
|
|
183
|
+
| `role` | `RoolUserRole` | User's role (`owner`, `admin`, `editor`, `viewer`) |
|
|
184
|
+
| `linkAccess` | `LinkAccess` | URL sharing level |
|
|
185
|
+
| `userId` | `string` | Current user's ID |
|
|
186
|
+
| `isReadOnly` | `boolean` | True if viewer role |
|
|
187
|
+
|
|
188
|
+
### Object Operations
|
|
189
|
+
|
|
190
|
+
Objects are plain key/value records. `id` is reserved; everything else is application-defined. References between objects are data fields whose values are object IDs.
|
|
191
|
+
|
|
192
|
+
| Method | Description |
|
|
193
|
+
|--------|-------------|
|
|
194
|
+
| `getObject(id)` | Get object data, or undefined if not found |
|
|
195
|
+
| `findObjects(options)` | Find objects using filters and/or natural language (see below) |
|
|
196
|
+
| `getObjectIds(options?)` | Get all object IDs. Options: `{ limit?, order? }` |
|
|
197
|
+
| `createObject(options)` | Create a new object. Returns `{ object, message }` |
|
|
198
|
+
| `updateObject(id, options)` | Update an existing object. Returns `{ object, message }` |
|
|
199
|
+
| `deleteObjects(ids)` | Delete objects by ID |
|
|
200
|
+
| `stat(id)` | Get audit info (modifiedAt, modifiedBy) from local cache |
|
|
201
|
+
|
|
202
|
+
#### createObject / updateObject
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// Create with literal data
|
|
206
|
+
await channel.createObject({ data: { title: 'Hello', status: 'draft' } })
|
|
207
|
+
|
|
208
|
+
// Use {{placeholders}} for AI-generated content
|
|
209
|
+
await channel.createObject({ data: { headline: '{{catchy headline about coffee}}' } })
|
|
210
|
+
|
|
211
|
+
// Update fields directly
|
|
212
|
+
await channel.updateObject(id, { data: { status: 'published' } })
|
|
213
|
+
|
|
214
|
+
// Update via AI instruction
|
|
215
|
+
await channel.updateObject(id, { prompt: 'Make it shorter and more casual' })
|
|
216
|
+
|
|
217
|
+
// Delete a field by setting it to null
|
|
218
|
+
await channel.updateObject(id, { data: { subtitle: null } })
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Placeholders are resolved by the AI during the mutation and replaced with concrete values. The `{{...}}` syntax is never stored.
|
|
222
|
+
|
|
223
|
+
**createObject options:** `data` (required, include `id` for a custom ID), `ephemeral`.
|
|
224
|
+
**updateObject options:** `data`, `prompt`, `ephemeral`.
|
|
225
|
+
|
|
226
|
+
#### findObjects
|
|
227
|
+
|
|
228
|
+
- **`where` only** — exact-match filtering, no AI, no credits
|
|
229
|
+
- **`collection` only** — filter by collection name, no AI, no credits
|
|
230
|
+
- **`prompt` only** — AI-powered semantic query over all objects
|
|
231
|
+
- **`where` + `prompt`** — `where` narrows the set first, then AI queries within it
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
await channel.findObjects({ collection: 'note' })
|
|
235
|
+
await channel.findObjects({ where: { status: 'active' } })
|
|
236
|
+
await channel.findObjects({ collection: 'note', where: { status: 'active' } })
|
|
237
|
+
await channel.findObjects({ prompt: 'notes about climate solutions' })
|
|
238
|
+
await channel.findObjects({ collection: 'note', prompt: 'most urgent', limit: 5 })
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Options: `where`, `collection`, `prompt`, `limit`, `objectIds`, `order` (`'asc'` | `'desc'`), `ephemeral`.
|
|
242
|
+
|
|
243
|
+
#### Hidden Fields
|
|
244
|
+
|
|
245
|
+
Fields starting with `_` (e.g., `_ui`) are hidden from AI and ignored by the schema. Use them for UI state, positions, or other data the AI shouldn't see:
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
await channel.createObject({ data: { title: 'Note', _ui: { x: 100, y: 200 } } })
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### AI
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
const { message, objects } = await channel.prompt('Create three tasks');
|
|
255
|
+
const { message } = await channel.prompt('Summarize', { readOnly: true, effort: 'QUICK' });
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
| Option | Description |
|
|
259
|
+
|--------|-------------|
|
|
260
|
+
| `objectIds` | Focus on specific objects |
|
|
261
|
+
| `responseSchema` | Request structured JSON response |
|
|
262
|
+
| `effort` | `'QUICK'`, `'STANDARD'`, `'REASONING'`, or `'RESEARCH'` |
|
|
263
|
+
| `ephemeral` | Don't record in interaction history |
|
|
264
|
+
| `readOnly` | Disable mutation tools |
|
|
265
|
+
| `attachments` | Files to attach (`File`, `Blob`, or `{ data, contentType }`) |
|
|
266
|
+
|
|
267
|
+
The AI automatically receives interaction history, recently modified objects, and any objects passed via `objectIds` as context.
|
|
268
|
+
|
|
269
|
+
### Schema
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
channel.getSchema()
|
|
273
|
+
await channel.createCollection('task', [
|
|
274
|
+
{ name: 'title', type: { kind: 'string' } },
|
|
275
|
+
{ name: 'done', type: { kind: 'boolean' } },
|
|
276
|
+
])
|
|
277
|
+
await channel.alterCollection('task', [...updatedFields])
|
|
278
|
+
await channel.dropCollection('task')
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Undo/Redo
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
await channel.checkpoint('Before delete')
|
|
285
|
+
await channel.deleteObjects([id])
|
|
286
|
+
await channel.undo() // restores deleted object
|
|
287
|
+
await channel.redo() // deletes again
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Metadata
|
|
291
|
+
|
|
292
|
+
Arbitrary key-value storage on the Space (not visible to AI):
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
channel.setMetadata('viewport', { zoom: 1.5 })
|
|
296
|
+
channel.getMetadata('viewport')
|
|
297
|
+
channel.getAllMetadata()
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Interaction History & Conversations
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
channel.getInteractions()
|
|
304
|
+
channel.getSystemInstruction()
|
|
305
|
+
await channel.setSystemInstruction('Respond in haiku')
|
|
306
|
+
|
|
307
|
+
// List all conversations in this channel
|
|
308
|
+
channel.getConversations()
|
|
309
|
+
|
|
310
|
+
// Delete or rename a conversation
|
|
311
|
+
await channel.deleteConversation('old-thread')
|
|
312
|
+
await channel.renameConversation('Research')
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Conversation Handles
|
|
316
|
+
|
|
317
|
+
For extensions that need multiple independent interaction threads (e.g., chat with multiple threads), use `channel.conversation()` to get a reactive handle scoped to a specific conversation:
|
|
318
|
+
|
|
319
|
+
```svelte
|
|
320
|
+
<script>
|
|
321
|
+
const thread = channel.conversation('thread-42');
|
|
322
|
+
</script>
|
|
323
|
+
|
|
324
|
+
<!-- thread.interactions is reactive $state — auto-updates via SSE -->
|
|
325
|
+
{#each thread.interactions as interaction}
|
|
326
|
+
<div>{interaction.output}</div>
|
|
327
|
+
{/each}
|
|
328
|
+
|
|
329
|
+
<button onclick={() => thread.prompt('Hello')}>Send</button>
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
// Reactive state
|
|
334
|
+
thread.interactions // $state<Interaction[]> — auto-updates
|
|
335
|
+
|
|
336
|
+
// All conversation-scoped methods
|
|
337
|
+
await thread.prompt('Hello');
|
|
338
|
+
await thread.createObject({ data: { text: 'Hello' } });
|
|
339
|
+
await thread.setSystemInstruction('Respond in haiku');
|
|
340
|
+
await thread.rename('Research Thread');
|
|
341
|
+
|
|
342
|
+
// Cleanup
|
|
343
|
+
thread.close(); // Stop listening for updates
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Conversations are auto-created on first interaction — no explicit create step needed. All conversations share one bridge connection. The 50-message cap applies per conversation.
|
|
347
|
+
|
|
348
|
+
### Events
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
channel.on('objectCreated', ({ objectId, object, source }) => { ... })
|
|
352
|
+
channel.on('objectUpdated', ({ objectId, object, source }) => { ... })
|
|
353
|
+
channel.on('objectDeleted', ({ objectId, source }) => { ... })
|
|
354
|
+
channel.on('metadataUpdated', ({ metadata, source }) => { ... })
|
|
355
|
+
channel.on('channelUpdated', ({ channelId, source }) => { ... })
|
|
356
|
+
channel.on('conversationUpdated', ({ conversationId, channelId, source }) => { ... })
|
|
357
|
+
channel.on('reset', ({ source }) => { ... })
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
`source` is `'local_user'`, `'remote_user'`, `'remote_agent'`, or `'system'`.
|
|
361
|
+
|
|
362
|
+
### Reactive Primitives
|
|
363
|
+
|
|
364
|
+
#### `channel.watch(options)`
|
|
365
|
+
|
|
366
|
+
Auto-updating filtered object list:
|
|
367
|
+
|
|
368
|
+
```svelte
|
|
369
|
+
<script>
|
|
370
|
+
const tasks = channel.watch({ collection: 'task' });
|
|
371
|
+
</script>
|
|
372
|
+
|
|
373
|
+
{#each tasks.objects as task}
|
|
374
|
+
<div>{task.title}</div>
|
|
375
|
+
{/each}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
| State | Description |
|
|
379
|
+
|-------|-------------|
|
|
380
|
+
| `watch.objects` | `$state<RoolObject[]>` — matching objects |
|
|
381
|
+
| `watch.loading` | `$state<boolean>` — loading state |
|
|
382
|
+
|
|
383
|
+
Methods: `watch.refresh()`, `watch.close()`.
|
|
384
|
+
|
|
385
|
+
#### `channel.object(id)`
|
|
386
|
+
|
|
387
|
+
Single reactive object subscription:
|
|
388
|
+
|
|
389
|
+
```svelte
|
|
390
|
+
<script>
|
|
391
|
+
const item = channel.object('abc123');
|
|
392
|
+
</script>
|
|
393
|
+
|
|
394
|
+
{#if item.data}
|
|
395
|
+
<div>{item.data.title}</div>
|
|
396
|
+
{/if}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
| State | Description |
|
|
400
|
+
|-------|-------------|
|
|
401
|
+
| `object.data` | `$state<RoolObject | undefined>` — object data |
|
|
402
|
+
| `object.loading` | `$state<boolean>` — loading state |
|
|
403
|
+
|
|
404
|
+
Methods: `object.refresh()`, `object.close()`.
|
|
405
|
+
|
|
406
|
+
## Hosting
|
|
407
|
+
|
|
408
|
+
Extensions run in a sandboxed iframe (`allow-scripts allow-same-origin`). The host creates the iframe, establishes a postMessage bridge, and proxies all channel operations to a real Rool Space. The extension never authenticates directly — the host handles auth and forwards operations.
|
|
409
|
+
|
|
410
|
+
The bridge protocol:
|
|
411
|
+
1. Extension sends `rool:ready`
|
|
412
|
+
2. Host responds with `rool:init` (channel metadata, schema, space info)
|
|
413
|
+
3. Extension calls channel methods → `rool:request` → host executes → `rool:response`
|
|
414
|
+
4. Host pushes real-time events → `rool:event` → extension updates reactive state
|
|
415
|
+
|
|
416
|
+
## CLI Commands
|
|
417
|
+
|
|
418
|
+
| Command | Description |
|
|
419
|
+
|---------|-------------|
|
|
420
|
+
| `rool-extension init [name]` | Scaffold a new extension project |
|
|
421
|
+
| `rool-extension dev` | Start the dev server with host shell |
|
|
422
|
+
| `rool-extension build` | Build the extension |
|
|
423
|
+
| `rool-extension publish` | Build and publish the extension |
|
|
424
|
+
|
|
425
|
+
## Exported Types
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
import type {
|
|
429
|
+
ReactiveChannel,
|
|
430
|
+
ReactiveConversationHandle,
|
|
431
|
+
ReactiveObject,
|
|
432
|
+
ReactiveWatch,
|
|
433
|
+
WatchOptions,
|
|
434
|
+
RoolObject,
|
|
435
|
+
RoolObjectStat,
|
|
436
|
+
SpaceSchema,
|
|
437
|
+
CollectionDef,
|
|
438
|
+
FieldDef,
|
|
439
|
+
FieldType,
|
|
440
|
+
Interaction,
|
|
441
|
+
InteractionStatus,
|
|
442
|
+
ConversationInfo,
|
|
443
|
+
ToolCall,
|
|
444
|
+
PromptOptions,
|
|
445
|
+
PromptEffort,
|
|
446
|
+
FindObjectsOptions,
|
|
447
|
+
CreateObjectOptions,
|
|
448
|
+
UpdateObjectOptions,
|
|
449
|
+
ChangeSource,
|
|
450
|
+
RoolUserRole,
|
|
451
|
+
LinkAccess,
|
|
452
|
+
ChannelEvents,
|
|
453
|
+
} from '@rool-dev/extension';
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
## License
|
|
457
|
+
|
|
458
|
+
MIT - see [LICENSE](../../LICENSE) for details.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extension build pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Runs a Vite production build for a Rool extension project, producing a ready-to-deploy
|
|
5
|
+
* dist/ directory with index.html, compiled assets, and manifest.json.
|
|
6
|
+
*/
|
|
7
|
+
import type { Manifest } from '../manifest.js';
|
|
8
|
+
/**
|
|
9
|
+
* Run a Vite production build for a Rool extension.
|
|
10
|
+
* Produces dist/ with compiled assets, index.html, and manifest.json.
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildExtension(cwd: string, manifest: Manifest): Promise<{
|
|
13
|
+
outDir: string;
|
|
14
|
+
totalSize: number;
|
|
15
|
+
}>;
|
|
16
|
+
/** Zip the project directory into a Buffer, excluding node_modules and .git. */
|
|
17
|
+
export declare function zipProject(projectDir: string): Promise<Buffer>;
|
|
18
|
+
//# sourceMappingURL=build-pipeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-pipeline.d.ts","sourceRoot":"","sources":["../../src/cli/build-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AA8D/C;;;GAGG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CA6EpH;AAQD,gFAAgF;AAChF,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAY9D"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extension build pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Runs a Vite production build for a Rool extension project, producing a ready-to-deploy
|
|
5
|
+
* dist/ directory with index.html, compiled assets, and manifest.json.
|
|
6
|
+
*/
|
|
7
|
+
import { build } from 'vite';
|
|
8
|
+
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
9
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
10
|
+
import { existsSync, readdirSync, statSync, copyFileSync, writeFileSync } from 'fs';
|
|
11
|
+
import { resolve, dirname } from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
import archiver from 'archiver';
|
|
14
|
+
import { getSvelteAliases } from './vite-utils.js';
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Paths
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Vite build plugin (production version of the dev virtual entry)
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
function roolExtensionBuildPlugin(root, tailwindCssPath) {
|
|
23
|
+
const VIRTUAL_ENTRY = 'virtual:rool-extension-entry';
|
|
24
|
+
const RESOLVED_ENTRY = '\0' + VIRTUAL_ENTRY;
|
|
25
|
+
const VIRTUAL_CSS = 'virtual:rool-extension-tailwind.css';
|
|
26
|
+
const RESOLVED_CSS = '\0' + VIRTUAL_CSS;
|
|
27
|
+
const appPath = resolve(root, 'App.svelte');
|
|
28
|
+
const cssPath = resolve(root, 'app.css');
|
|
29
|
+
const hasCss = existsSync(cssPath);
|
|
30
|
+
return {
|
|
31
|
+
name: 'rool-extension-build',
|
|
32
|
+
resolveId(id) {
|
|
33
|
+
if (id === VIRTUAL_ENTRY)
|
|
34
|
+
return RESOLVED_ENTRY;
|
|
35
|
+
if (id === VIRTUAL_CSS)
|
|
36
|
+
return RESOLVED_CSS;
|
|
37
|
+
return undefined;
|
|
38
|
+
},
|
|
39
|
+
load(id) {
|
|
40
|
+
if (id === RESOLVED_CSS)
|
|
41
|
+
return `@import "${tailwindCssPath}";`;
|
|
42
|
+
if (id !== RESOLVED_ENTRY)
|
|
43
|
+
return;
|
|
44
|
+
return [
|
|
45
|
+
`import { initExtension } from '@rool-dev/extension';`,
|
|
46
|
+
`import { mount } from 'svelte';`,
|
|
47
|
+
`import App from '${appPath}';`,
|
|
48
|
+
`import '${VIRTUAL_CSS}';`,
|
|
49
|
+
hasCss ? `import '${cssPath}';` : ``,
|
|
50
|
+
``,
|
|
51
|
+
`async function main() {`,
|
|
52
|
+
` const channel = await initExtension();`,
|
|
53
|
+
` mount(App, {`,
|
|
54
|
+
` target: document.getElementById('app'),`,
|
|
55
|
+
` props: { channel },`,
|
|
56
|
+
` });`,
|
|
57
|
+
`}`,
|
|
58
|
+
``,
|
|
59
|
+
`main().catch((err) => {`,
|
|
60
|
+
` document.getElementById('app').innerHTML =`,
|
|
61
|
+
` '<div style="padding:2rem;color:red"><h2>Failed to initialize extension</h2><p>' + err.message + '</p></div>';`,
|
|
62
|
+
`});`,
|
|
63
|
+
].filter(Boolean).join('\n');
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Build
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
/**
|
|
71
|
+
* Run a Vite production build for a Rool extension.
|
|
72
|
+
* Produces dist/ with compiled assets, index.html, and manifest.json.
|
|
73
|
+
*/
|
|
74
|
+
export async function buildExtension(cwd, manifest) {
|
|
75
|
+
const tailwindPkgDir = dirname(fileURLToPath(import.meta.resolve('tailwindcss/package.json')));
|
|
76
|
+
const tailwindCssPath = resolve(tailwindPkgDir, 'index.css');
|
|
77
|
+
const extensionPkgPath = resolve(__dirname, '..');
|
|
78
|
+
const outDir = resolve(cwd, 'dist');
|
|
79
|
+
await build({
|
|
80
|
+
configFile: false,
|
|
81
|
+
root: cwd,
|
|
82
|
+
build: {
|
|
83
|
+
outDir,
|
|
84
|
+
emptyOutDir: true,
|
|
85
|
+
rolldownOptions: {
|
|
86
|
+
input: 'virtual:rool-extension-entry',
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
resolve: {
|
|
90
|
+
alias: [
|
|
91
|
+
{ find: '@rool-dev/extension', replacement: extensionPkgPath },
|
|
92
|
+
{ find: /^tailwindcss$/, replacement: tailwindCssPath },
|
|
93
|
+
...getSvelteAliases(),
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
plugins: [
|
|
97
|
+
tailwindcss(),
|
|
98
|
+
svelte(),
|
|
99
|
+
roolExtensionBuildPlugin(cwd, tailwindCssPath),
|
|
100
|
+
],
|
|
101
|
+
logLevel: 'warn',
|
|
102
|
+
});
|
|
103
|
+
// Copy manifest.json into dist
|
|
104
|
+
copyFileSync(resolve(cwd, 'manifest.json'), resolve(outDir, 'manifest.json'));
|
|
105
|
+
// Copy icon file into dist if specified
|
|
106
|
+
if (manifest.icon) {
|
|
107
|
+
const iconSrc = resolve(cwd, manifest.icon);
|
|
108
|
+
if (existsSync(iconSrc)) {
|
|
109
|
+
copyFileSync(iconSrc, resolve(outDir, manifest.icon));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Write index.html (Vite build doesn't generate one from virtual entry)
|
|
113
|
+
const assets = readdirSync(resolve(outDir, 'assets')).filter(f => f.endsWith('.js') || f.endsWith('.css'));
|
|
114
|
+
const jsFiles = assets.filter(f => f.endsWith('.js'));
|
|
115
|
+
const cssFiles = assets.filter(f => f.endsWith('.css'));
|
|
116
|
+
const indexHtml = `<!DOCTYPE html>
|
|
117
|
+
<html lang="en" style="height:100%">
|
|
118
|
+
<head>
|
|
119
|
+
<meta charset="UTF-8">
|
|
120
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
121
|
+
<title>${manifest.name}</title>
|
|
122
|
+
${cssFiles.map(f => ` <link rel="stylesheet" href="/assets/${f}">`).join('\n')}
|
|
123
|
+
</head>
|
|
124
|
+
<body style="height:100%;margin:0">
|
|
125
|
+
<div id="app" style="height:100%"></div>
|
|
126
|
+
${jsFiles.map(f => ` <script type="module" src="/assets/${f}"></script>`).join('\n')}
|
|
127
|
+
</body>
|
|
128
|
+
</html>`;
|
|
129
|
+
writeFileSync(resolve(outDir, 'index.html'), indexHtml);
|
|
130
|
+
// Calculate total size
|
|
131
|
+
let totalSize = 0;
|
|
132
|
+
function walkDir(dir) {
|
|
133
|
+
for (const entry of readdirSync(dir)) {
|
|
134
|
+
const full = resolve(dir, entry);
|
|
135
|
+
const stat = statSync(full);
|
|
136
|
+
if (stat.isDirectory())
|
|
137
|
+
walkDir(full);
|
|
138
|
+
else
|
|
139
|
+
totalSize += stat.size;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
walkDir(outDir);
|
|
143
|
+
return { outDir, totalSize };
|
|
144
|
+
}
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Zip
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
const ZIP_EXCLUDE = ['node_modules/**', '.git/**'];
|
|
149
|
+
/** Zip the project directory into a Buffer, excluding node_modules and .git. */
|
|
150
|
+
export function zipProject(projectDir) {
|
|
151
|
+
return new Promise((resolve, reject) => {
|
|
152
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
153
|
+
const chunks = [];
|
|
154
|
+
archive.on('data', (chunk) => chunks.push(chunk));
|
|
155
|
+
archive.on('end', () => resolve(Buffer.concat(chunks)));
|
|
156
|
+
archive.on('error', reject);
|
|
157
|
+
archive.glob('**/*', { cwd: projectDir, ignore: ZIP_EXCLUDE, dot: false });
|
|
158
|
+
archive.finalize();
|
|
159
|
+
});
|
|
160
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/cli/build.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,wBAAsB,KAAK,kBAQ1B"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rool-extension build
|
|
3
|
+
*
|
|
4
|
+
* Builds the extension with Vite without publishing.
|
|
5
|
+
*
|
|
6
|
+
* Usage: npx rool-extension build
|
|
7
|
+
*/
|
|
8
|
+
import { readManifestOrExit, formatBytes } from './vite-utils.js';
|
|
9
|
+
import { buildExtension } from './build-pipeline.js';
|
|
10
|
+
export async function build() {
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
const manifest = readManifestOrExit(cwd);
|
|
13
|
+
console.log(`\n Building ${manifest.name}...\n`);
|
|
14
|
+
const { totalSize } = await buildExtension(cwd, manifest);
|
|
15
|
+
console.log(`\n Build complete — ${formatBytes(totalSize)}`);
|
|
16
|
+
console.log(` Output: dist/\n`);
|
|
17
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rool-extension dev
|
|
3
|
+
*
|
|
4
|
+
* Starts the extension's Vite dev server with the dev host shell injected.
|
|
5
|
+
* The host shell is served at /__rool-host/ and the extension at /.
|
|
6
|
+
*
|
|
7
|
+
* Usage: npx rool-extension dev
|
|
8
|
+
*/
|
|
9
|
+
export declare function dev(): Promise<void>;
|
|
10
|
+
//# sourceMappingURL=dev.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/cli/dev.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAuNH,wBAAsB,GAAG,kBAuDxB"}
|