@rool-dev/sdk 0.8.2-dev.02b70e5 → 0.8.2-dev.d82ea25
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 +465 -1005
- package/dist/channel.d.ts +93 -248
- package/dist/channel.d.ts.map +1 -1
- package/dist/channel.js +410 -577
- package/dist/channel.js.map +1 -1
- package/dist/client.d.ts +14 -46
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +31 -124
- package/dist/client.js.map +1 -1
- package/dist/graphql.d.ts +11 -36
- package/dist/graphql.d.ts.map +1 -1
- package/dist/graphql.js +72 -311
- package/dist/graphql.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/path.d.ts +6 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +47 -0
- package/dist/path.js.map +1 -0
- package/dist/reroute.d.ts +22 -0
- package/dist/reroute.d.ts.map +1 -0
- package/dist/reroute.js +61 -0
- package/dist/reroute.js.map +1 -0
- package/dist/rest.d.ts +27 -0
- package/dist/rest.d.ts.map +1 -0
- package/dist/rest.js +78 -0
- package/dist/rest.js.map +1 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +25 -10
- package/dist/router.js.map +1 -1
- package/dist/space.d.ts +23 -16
- package/dist/space.d.ts.map +1 -1
- package/dist/space.js +111 -78
- package/dist/space.js.map +1 -1
- package/dist/subscription.d.ts.map +1 -1
- package/dist/subscription.js +47 -40
- package/dist/subscription.js.map +1 -1
- package/dist/types.d.ts +85 -224
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -4
- package/dist/types.js.map +1 -1
- package/dist/webdav.d.ts +176 -0
- package/dist/webdav.d.ts.map +1 -0
- package/dist/webdav.js +495 -0
- package/dist/webdav.js.map +1 -0
- package/package.json +2 -1
- package/dist/apps.d.ts +0 -30
- package/dist/apps.d.ts.map +0 -1
- package/dist/apps.js +0 -81
- package/dist/apps.js.map +0 -1
- package/dist/media.d.ts +0 -76
- package/dist/media.d.ts.map +0 -1
- package/dist/media.js +0 -249
- package/dist/media.js.map +0 -1
package/dist/channel.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { EventEmitter } from './event-emitter.js';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { WebDAVError } from './webdav.js';
|
|
3
|
+
import { isObjectPath, machinePath, machineUri } from './path.js';
|
|
4
|
+
// 6-character alphanumeric ID — used for object names, interactionIds, conversationIds, etc.
|
|
5
|
+
const ENTITY_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
6
|
+
const GET_OBJECTS_CHUNK_SIZE = 500;
|
|
4
7
|
export function generateEntityId() {
|
|
5
8
|
let result = '';
|
|
6
9
|
for (let i = 0; i < 6; i++) {
|
|
7
|
-
result +=
|
|
10
|
+
result += ENTITY_CHARS[Math.floor(Math.random() * ENTITY_CHARS.length)];
|
|
8
11
|
}
|
|
9
12
|
return result;
|
|
10
13
|
}
|
|
@@ -40,8 +43,118 @@ function findDefaultLeaf(interactions) {
|
|
|
40
43
|
}
|
|
41
44
|
return best?.id;
|
|
42
45
|
}
|
|
43
|
-
|
|
44
|
-
const
|
|
46
|
+
function objectPath(input) {
|
|
47
|
+
const path = machinePath(input);
|
|
48
|
+
if (!isObjectPath(path)) {
|
|
49
|
+
throw new Error(`Object path must be /space/<collection>/<name>.json without dotfiles: ${input}`);
|
|
50
|
+
}
|
|
51
|
+
return path;
|
|
52
|
+
}
|
|
53
|
+
function collectionPath(name) {
|
|
54
|
+
return machinePath(`/space/${name}`);
|
|
55
|
+
}
|
|
56
|
+
function schemaPath(name) {
|
|
57
|
+
return `${collectionPath(name)}/.schema.json`;
|
|
58
|
+
}
|
|
59
|
+
function objectFromBody(path, body) {
|
|
60
|
+
return { path, body };
|
|
61
|
+
}
|
|
62
|
+
function jsonObject(value, label) {
|
|
63
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
64
|
+
throw new Error(`${label} must be a JSON object`);
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
function patchBody(current, patch) {
|
|
69
|
+
const next = { ...current };
|
|
70
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
71
|
+
if (value === null || value === undefined)
|
|
72
|
+
delete next[key];
|
|
73
|
+
else
|
|
74
|
+
next[key] = value;
|
|
75
|
+
}
|
|
76
|
+
return next;
|
|
77
|
+
}
|
|
78
|
+
function collectionDef(input, options) {
|
|
79
|
+
const base = Array.isArray(input)
|
|
80
|
+
? { fields: input }
|
|
81
|
+
: { fields: input.fields, schemaOrgType: input.schemaOrgType };
|
|
82
|
+
const schemaOrgType = options?.schemaOrgType ?? base.schemaOrgType;
|
|
83
|
+
return schemaOrgType ? { fields: base.fields, schemaOrgType } : { fields: base.fields };
|
|
84
|
+
}
|
|
85
|
+
function attachmentBody(file) {
|
|
86
|
+
if (isFile(file)) {
|
|
87
|
+
return {
|
|
88
|
+
filename: safeAttachmentFilename(file.name, file.type),
|
|
89
|
+
contentType: file.type || 'application/octet-stream',
|
|
90
|
+
body: file,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (isBlob(file)) {
|
|
94
|
+
const contentType = file.type || 'application/octet-stream';
|
|
95
|
+
return {
|
|
96
|
+
filename: safeAttachmentFilename('attachment', contentType),
|
|
97
|
+
contentType,
|
|
98
|
+
body: file,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
filename: safeAttachmentFilename(file.filename ?? 'attachment', file.contentType),
|
|
103
|
+
contentType: file.contentType,
|
|
104
|
+
body: base64Body(file.data),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function isFile(value) {
|
|
108
|
+
return typeof File !== 'undefined' && value instanceof File;
|
|
109
|
+
}
|
|
110
|
+
function isBlob(value) {
|
|
111
|
+
return typeof Blob !== 'undefined' && value instanceof Blob;
|
|
112
|
+
}
|
|
113
|
+
function safeAttachmentFilename(name, contentType) {
|
|
114
|
+
const fallback = `attachment.${extensionForContentType(contentType)}`;
|
|
115
|
+
const leaf = name.split(/[/\\]/).pop() || fallback;
|
|
116
|
+
const cleaned = leaf.replace(/[\x00-\x1f\x7f]/g, '').replace(/\s+/g, '_');
|
|
117
|
+
return cleaned.replace(/[^A-Za-z0-9._-]/g, '_').replace(/^\.+$/, '') || fallback;
|
|
118
|
+
}
|
|
119
|
+
function extensionForContentType(contentType) {
|
|
120
|
+
if (contentType === 'image/png')
|
|
121
|
+
return 'png';
|
|
122
|
+
if (contentType === 'image/jpeg')
|
|
123
|
+
return 'jpg';
|
|
124
|
+
if (contentType === 'image/gif')
|
|
125
|
+
return 'gif';
|
|
126
|
+
if (contentType === 'image/webp')
|
|
127
|
+
return 'webp';
|
|
128
|
+
if (contentType === 'image/svg+xml')
|
|
129
|
+
return 'svg';
|
|
130
|
+
if (contentType === 'application/pdf')
|
|
131
|
+
return 'pdf';
|
|
132
|
+
if (contentType === 'text/markdown')
|
|
133
|
+
return 'md';
|
|
134
|
+
if (contentType === 'text/plain')
|
|
135
|
+
return 'txt';
|
|
136
|
+
if (contentType === 'text/csv')
|
|
137
|
+
return 'csv';
|
|
138
|
+
if (contentType === 'text/html')
|
|
139
|
+
return 'html';
|
|
140
|
+
if (contentType === 'application/json')
|
|
141
|
+
return 'json';
|
|
142
|
+
if (contentType === 'application/xml')
|
|
143
|
+
return 'xml';
|
|
144
|
+
return 'bin';
|
|
145
|
+
}
|
|
146
|
+
function base64Body(data) {
|
|
147
|
+
const clean = data.includes(',') ? data.slice(data.indexOf(',') + 1) : data;
|
|
148
|
+
if (typeof Buffer !== 'undefined') {
|
|
149
|
+
const buffer = Buffer.from(clean, 'base64');
|
|
150
|
+
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
151
|
+
}
|
|
152
|
+
const binary = atob(clean);
|
|
153
|
+
const bytes = new Uint8Array(binary.length);
|
|
154
|
+
for (let i = 0; i < binary.length; i++)
|
|
155
|
+
bytes[i] = binary.charCodeAt(i);
|
|
156
|
+
return bytes.buffer;
|
|
157
|
+
}
|
|
45
158
|
/**
|
|
46
159
|
* A channel is a space + channelId pair.
|
|
47
160
|
*
|
|
@@ -49,16 +162,10 @@ const OBJECT_COLLECT_TIMEOUT = 30000;
|
|
|
49
162
|
* at open time and cannot be changed. To use a different channel,
|
|
50
163
|
* open a second one.
|
|
51
164
|
*
|
|
52
|
-
* Objects are
|
|
53
|
-
* and the channel's own history are cached
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* Features:
|
|
57
|
-
* - High-level object operations
|
|
58
|
-
* - Built-in undo/redo with checkpoints
|
|
59
|
-
* - Metadata management
|
|
60
|
-
* - Event emission for state changes
|
|
61
|
-
* - Real-time updates via space-specific subscription
|
|
165
|
+
* Objects are addressed by machine path (`/space/.../*.json`).
|
|
166
|
+
* Only schema, metadata, object stats, and the channel's own history are cached
|
|
167
|
+
* locally. Object bodies are fetched on demand. Object/file reactivity is
|
|
168
|
+
* exposed at the space level via WebDAV sync notifications.
|
|
62
169
|
*/
|
|
63
170
|
export class RoolChannel extends EventEmitter {
|
|
64
171
|
_id;
|
|
@@ -70,24 +177,17 @@ export class RoolChannel extends EventEmitter {
|
|
|
70
177
|
_conversationId;
|
|
71
178
|
_closed = false;
|
|
72
179
|
graphqlClient;
|
|
73
|
-
|
|
180
|
+
restClient;
|
|
181
|
+
webdav;
|
|
74
182
|
onCloseCallback;
|
|
75
183
|
logger;
|
|
76
|
-
// Local cache for bounded data (schema, metadata, own channel, object
|
|
184
|
+
// Local cache for bounded data (schema, metadata, own channel, object stats)
|
|
77
185
|
_meta;
|
|
78
186
|
_schema;
|
|
79
187
|
_channel;
|
|
80
|
-
_objectIds;
|
|
81
188
|
_objectStats;
|
|
82
189
|
// Active leaf per conversation (client-side tree cursor)
|
|
83
190
|
_activeLeaves = new Map();
|
|
84
|
-
// Object collection: tracks pending local mutations for dedup
|
|
85
|
-
// Maps objectId → optimistic object data (for create/update) or null (for delete)
|
|
86
|
-
_pendingMutations = new Map();
|
|
87
|
-
// Resolvers waiting for object data from SSE events
|
|
88
|
-
_objectResolvers = new Map();
|
|
89
|
-
// Buffer for object data that arrived before a collector was registered
|
|
90
|
-
_objectBuffer = new Map();
|
|
91
191
|
constructor(config) {
|
|
92
192
|
super();
|
|
93
193
|
this._id = config.id;
|
|
@@ -99,14 +199,14 @@ export class RoolChannel extends EventEmitter {
|
|
|
99
199
|
this._channelId = config.channelId;
|
|
100
200
|
this._conversationId = 'default';
|
|
101
201
|
this.graphqlClient = config.graphqlClient;
|
|
102
|
-
this.
|
|
202
|
+
this.restClient = config.restClient;
|
|
203
|
+
this.webdav = config.webdav;
|
|
103
204
|
this.logger = config.logger;
|
|
104
205
|
this.onCloseCallback = config.onClose;
|
|
105
206
|
// Initialize local cache from server data
|
|
106
207
|
this._meta = config.meta;
|
|
107
208
|
this._schema = config.schema;
|
|
108
|
-
this._channel = config.channel;
|
|
109
|
-
this._objectIds = config.objectIds;
|
|
209
|
+
this._channel = config.channel ?? undefined;
|
|
110
210
|
this._objectStats = new Map(Object.entries(config.objectStats));
|
|
111
211
|
}
|
|
112
212
|
/**
|
|
@@ -127,16 +227,12 @@ export class RoolChannel extends EventEmitter {
|
|
|
127
227
|
return;
|
|
128
228
|
this._meta = data.meta;
|
|
129
229
|
this._schema = data.schema;
|
|
130
|
-
this._objectIds = data.objectIds;
|
|
131
230
|
this._objectStats = new Map(Object.entries(data.objectStats));
|
|
132
231
|
if (data.channel)
|
|
133
232
|
this._channel = data.channel;
|
|
134
233
|
this._activeLeaves.clear();
|
|
135
234
|
this.emit('reset', { source: 'system' });
|
|
136
235
|
}
|
|
137
|
-
// ===========================================================================
|
|
138
|
-
// Properties
|
|
139
|
-
// ===========================================================================
|
|
140
236
|
get id() {
|
|
141
237
|
return this._id;
|
|
142
238
|
}
|
|
@@ -176,27 +272,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
176
272
|
get isReadOnly() {
|
|
177
273
|
return this._role === 'viewer';
|
|
178
274
|
}
|
|
179
|
-
/**
|
|
180
|
-
* Get the extension URL if this channel was created via installExtension, or null.
|
|
181
|
-
*/
|
|
182
|
-
get extensionUrl() {
|
|
183
|
-
return this._channel?.extensionUrl ?? null;
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Get the extension ID if this channel has an installed extension, or null.
|
|
187
|
-
*/
|
|
188
|
-
get extensionId() {
|
|
189
|
-
return this._channel?.extensionId ?? null;
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Get the extension manifest if this channel has an installed extension, or null.
|
|
193
|
-
*/
|
|
194
|
-
get manifest() {
|
|
195
|
-
return this._channel?.manifest ?? null;
|
|
196
|
-
}
|
|
197
|
-
// ===========================================================================
|
|
198
|
-
// Channel History Access
|
|
199
|
-
// ===========================================================================
|
|
200
275
|
/**
|
|
201
276
|
* Get the active branch of the current conversation as a flat array (root → leaf).
|
|
202
277
|
* Walks from the active leaf up through parentId pointers.
|
|
@@ -298,22 +373,12 @@ export class RoolChannel extends EventEmitter {
|
|
|
298
373
|
});
|
|
299
374
|
}
|
|
300
375
|
}
|
|
301
|
-
// ===========================================================================
|
|
302
|
-
// Conversations
|
|
303
|
-
// ===========================================================================
|
|
304
376
|
/**
|
|
305
377
|
* Get a handle for a specific conversation within this channel.
|
|
306
|
-
* The handle scopes AI and mutation operations to that conversation's
|
|
307
|
-
* interaction history, while sharing the channel's single SSE connection.
|
|
308
|
-
*
|
|
309
|
-
* Conversations are auto-created on first interaction — no explicit create needed.
|
|
310
378
|
*/
|
|
311
379
|
conversation(conversationId) {
|
|
312
380
|
return new ConversationHandle(this, conversationId);
|
|
313
381
|
}
|
|
314
|
-
// ===========================================================================
|
|
315
|
-
// Channel Lifecycle
|
|
316
|
-
// ===========================================================================
|
|
317
382
|
/**
|
|
318
383
|
* Close this channel and clean up resources.
|
|
319
384
|
* Stops real-time subscription and unregisters from client.
|
|
@@ -321,267 +386,225 @@ export class RoolChannel extends EventEmitter {
|
|
|
321
386
|
close() {
|
|
322
387
|
this._closed = true;
|
|
323
388
|
this.onCloseCallback();
|
|
324
|
-
// Clean up pending object collectors
|
|
325
|
-
this._objectResolvers.clear();
|
|
326
|
-
this._objectBuffer.clear();
|
|
327
|
-
this._pendingMutations.clear();
|
|
328
389
|
this.removeAllListeners();
|
|
329
390
|
}
|
|
330
|
-
// ===========================================================================
|
|
331
|
-
// Undo / Redo (Server-managed checkpoints)
|
|
332
|
-
// ===========================================================================
|
|
333
391
|
/**
|
|
334
392
|
* Create a checkpoint of the current space state.
|
|
335
|
-
* Checkpoints are space-wide and shared across channels and users.
|
|
336
|
-
* @returns The checkpoint ID
|
|
337
393
|
*/
|
|
338
394
|
async checkpoint(label = 'Change') {
|
|
339
395
|
const result = await this.graphqlClient.checkpoint(this._id, label, this._channelId);
|
|
340
396
|
return result.checkpointId;
|
|
341
397
|
}
|
|
342
|
-
/**
|
|
343
|
-
* Check if undo is available for this space.
|
|
344
|
-
*/
|
|
398
|
+
/** Check if undo is available for this space. */
|
|
345
399
|
async canUndo() {
|
|
346
400
|
const status = await this.graphqlClient.checkpointStatus(this._id, this._channelId);
|
|
347
401
|
return status.canUndo;
|
|
348
402
|
}
|
|
349
|
-
/**
|
|
350
|
-
* Check if redo is available for this space.
|
|
351
|
-
*/
|
|
403
|
+
/** Check if redo is available for this space. */
|
|
352
404
|
async canRedo() {
|
|
353
405
|
const status = await this.graphqlClient.checkpointStatus(this._id, this._channelId);
|
|
354
406
|
return status.canRedo;
|
|
355
407
|
}
|
|
356
|
-
/**
|
|
357
|
-
* Restore the space to the most recent checkpoint.
|
|
358
|
-
* @returns true if undo was performed
|
|
359
|
-
*/
|
|
408
|
+
/** Restore the space to the most recent checkpoint. */
|
|
360
409
|
async undo() {
|
|
361
410
|
const result = await this.graphqlClient.undo(this._id, this._channelId);
|
|
362
411
|
return result.success;
|
|
363
412
|
}
|
|
364
|
-
/**
|
|
365
|
-
* Reapply the most recently undone checkpoint.
|
|
366
|
-
* Affects the entire space.
|
|
367
|
-
* @returns true if redo was performed
|
|
368
|
-
*/
|
|
413
|
+
/** Reapply the most recently undone checkpoint. */
|
|
369
414
|
async redo() {
|
|
370
415
|
const result = await this.graphqlClient.redo(this._id, this._channelId);
|
|
371
416
|
return result.success;
|
|
372
417
|
}
|
|
373
|
-
/**
|
|
374
|
-
* Clear the space's checkpoint history.
|
|
375
|
-
*/
|
|
418
|
+
/** Clear the space's checkpoint history. */
|
|
376
419
|
async clearHistory() {
|
|
377
420
|
await this.graphqlClient.clearCheckpointHistory(this._id, this._channelId);
|
|
378
421
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
return this.graphqlClient.getObject(this._id, objectId);
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Get an object's stat (audit information).
|
|
391
|
-
* Returns modification timestamp and author, or undefined if object not found.
|
|
392
|
-
*/
|
|
393
|
-
stat(objectId) {
|
|
394
|
-
return this._objectStats.get(objectId);
|
|
395
|
-
}
|
|
396
|
-
/**
|
|
397
|
-
* Find objects using structured filters and/or natural language.
|
|
398
|
-
*
|
|
399
|
-
* `where` provides exact-match filtering — values must match literally (no placeholders or operators).
|
|
400
|
-
* `prompt` enables AI-powered semantic queries. When both are provided, `where` and `objectIds`
|
|
401
|
-
* constrain the data set before the AI sees it.
|
|
402
|
-
*
|
|
403
|
-
* @param options.where - Exact-match field filter (e.g. `{ type: 'article' }`). Constrains which objects the AI can see when combined with `prompt`.
|
|
404
|
-
* @param options.prompt - Natural language query. Triggers AI evaluation (uses credits).
|
|
405
|
-
* @param options.limit - Maximum number of results to return (applies to structured filtering only; the AI controls its own result size).
|
|
406
|
-
* @param options.objectIds - Scope search to specific object IDs. Constrains the candidate set in both structured and AI queries.
|
|
407
|
-
* @param options.order - Sort order by modifiedAt: `'asc'` or `'desc'` (default: `'desc'`). Only applies to structured filtering (no `prompt`).
|
|
408
|
-
* @param options.ephemeral - If true, the query won't be recorded in interaction history.
|
|
409
|
-
* @returns The matching objects and a descriptive message.
|
|
410
|
-
*/
|
|
411
|
-
async findObjects(options) {
|
|
412
|
-
return this._findObjectsImpl(options, this._conversationId);
|
|
422
|
+
davHeaders(conversationId, interactionId) {
|
|
423
|
+
const headers = new Headers({
|
|
424
|
+
'X-Rool-Channel-Id': this._channelId,
|
|
425
|
+
'X-Rool-Conversation-Id': conversationId,
|
|
426
|
+
});
|
|
427
|
+
if (interactionId)
|
|
428
|
+
headers.set('X-Rool-Interaction-Id', interactionId);
|
|
429
|
+
return headers;
|
|
413
430
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
431
|
+
async readObject(path) {
|
|
432
|
+
const canonical = objectPath(path);
|
|
433
|
+
try {
|
|
434
|
+
const response = await this.webdav.get(canonical);
|
|
435
|
+
const body = jsonObject(await response.json(), `Object ${canonical}`);
|
|
436
|
+
return { object: objectFromBody(canonical, body), etag: response.headers.get('ETag') };
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
if (error instanceof WebDAVError && error.status === 404)
|
|
440
|
+
return undefined;
|
|
441
|
+
if (error instanceof SyntaxError)
|
|
442
|
+
throw new Error(`Object ${canonical} did not contain valid JSON`);
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
417
445
|
}
|
|
418
|
-
/**
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
446
|
+
/** Get an object JSON file by machine path. Fetches from the server on each call. */
|
|
447
|
+
async getObject(path) {
|
|
448
|
+
return (await this.readObject(path))?.object;
|
|
449
|
+
}
|
|
450
|
+
/** Get object JSON files by machine path in bulk. Duplicate paths are fetched once. */
|
|
451
|
+
async getObjects(paths) {
|
|
452
|
+
const canonical = [];
|
|
453
|
+
const seen = new Set();
|
|
454
|
+
for (const path of paths) {
|
|
455
|
+
const normalized = objectPath(path);
|
|
456
|
+
if (seen.has(normalized))
|
|
457
|
+
continue;
|
|
458
|
+
seen.add(normalized);
|
|
459
|
+
canonical.push(normalized);
|
|
428
460
|
}
|
|
429
|
-
|
|
430
|
-
|
|
461
|
+
const result = { objects: [], missing: [] };
|
|
462
|
+
for (let i = 0; i < canonical.length; i += GET_OBJECTS_CHUNK_SIZE) {
|
|
463
|
+
const chunk = canonical.slice(i, i + GET_OBJECTS_CHUNK_SIZE);
|
|
464
|
+
const partial = await this.restClient.getObjects(this._id, chunk);
|
|
465
|
+
result.objects.push(...partial.objects);
|
|
466
|
+
result.missing.push(...partial.missing);
|
|
431
467
|
}
|
|
432
|
-
return
|
|
468
|
+
return result;
|
|
433
469
|
}
|
|
434
|
-
/**
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
return this._createObjectImpl(options, this._conversationId);
|
|
470
|
+
/** Get an object's cached audit information. */
|
|
471
|
+
stat(path) {
|
|
472
|
+
return this._objectStats.get(objectPath(path));
|
|
473
|
+
}
|
|
474
|
+
/** Create or replace an object JSON file at an exact machine path. */
|
|
475
|
+
async putObject(path, body) {
|
|
476
|
+
return this._putObjectImpl(path, body, this._conversationId);
|
|
442
477
|
}
|
|
443
478
|
/** @internal */
|
|
444
|
-
async
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
const objectId = typeof data.id === 'string' ? data.id : generateEntityId();
|
|
448
|
-
// Validate ID format: alphanumeric, hyphens, underscores only
|
|
449
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(objectId)) {
|
|
450
|
-
throw new Error(`Invalid object ID "${objectId}". IDs must contain only alphanumeric characters, hyphens, and underscores.`);
|
|
451
|
-
}
|
|
452
|
-
const dataWithId = { ...data, id: objectId };
|
|
453
|
-
// Emit optimistic event and track for dedup
|
|
454
|
-
this._pendingMutations.set(objectId, dataWithId);
|
|
455
|
-
this.emit('objectCreated', { objectId, object: dataWithId, source: 'local_user' });
|
|
479
|
+
async _putObjectImpl(path, body, conversationId) {
|
|
480
|
+
const canonical = objectPath(path);
|
|
481
|
+
const optimistic = objectFromBody(canonical, body);
|
|
456
482
|
try {
|
|
457
|
-
// Await mutation — server processes AI placeholders before responding.
|
|
458
|
-
// SSE events arrive during the await and are buffered via _deliverObject.
|
|
459
483
|
const interactionId = generateEntityId();
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
484
|
+
await this.webdav.put(canonical, JSON.stringify(body), {
|
|
485
|
+
contentType: 'application/json',
|
|
486
|
+
headers: this.davHeaders(conversationId, interactionId),
|
|
487
|
+
});
|
|
488
|
+
const fresh = await this.getObject(canonical) ?? optimistic;
|
|
489
|
+
return { object: fresh, message: `Put ${canonical}` };
|
|
464
490
|
}
|
|
465
491
|
catch (error) {
|
|
466
|
-
this.logger.error('[RoolChannel] Failed to
|
|
467
|
-
this._pendingMutations.delete(objectId);
|
|
468
|
-
this._cancelCollector(objectId);
|
|
469
|
-
// Emit reset so UI can recover from the optimistic event
|
|
492
|
+
this.logger.error('[RoolChannel] Failed to put object:', error);
|
|
470
493
|
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
471
|
-
this.emit('reset', { source: 'system' });
|
|
472
494
|
throw error;
|
|
473
495
|
}
|
|
474
496
|
}
|
|
475
|
-
/**
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
* @param options.data - Fields to add or update. Pass null or undefined to delete a field. Use {{placeholder}} for AI-generated content. Fields prefixed with _ are hidden from AI.
|
|
479
|
-
* @param options.prompt - AI prompt for content editing (optional).
|
|
480
|
-
* @param options.ephemeral - If true, the operation won't be recorded in interaction history.
|
|
481
|
-
* @returns The updated object (with AI-filled content) and message
|
|
482
|
-
*/
|
|
483
|
-
async updateObject(objectId, options) {
|
|
484
|
-
return this._updateObjectImpl(objectId, options, this._conversationId);
|
|
497
|
+
/** Patch an existing object. Null or undefined deletes a field. */
|
|
498
|
+
async patchObject(path, options) {
|
|
499
|
+
return this._patchObjectImpl(path, options, this._conversationId);
|
|
485
500
|
}
|
|
486
501
|
/** @internal */
|
|
487
|
-
async
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
}
|
|
502
|
+
async _patchObjectImpl(path, options, conversationId) {
|
|
503
|
+
const canonical = objectPath(path);
|
|
504
|
+
const data = options.data ?? {};
|
|
505
|
+
const current = await this.readObject(canonical);
|
|
506
|
+
if (!current)
|
|
507
|
+
throw new Error(`Object ${canonical} not found`);
|
|
508
|
+
const body = patchBody(current.object.body, data);
|
|
509
|
+
const optimistic = objectFromBody(canonical, body);
|
|
510
|
+
try {
|
|
511
|
+
const interactionId = generateEntityId();
|
|
512
|
+
await this.webdav.put(canonical, JSON.stringify(body), {
|
|
513
|
+
contentType: 'application/json',
|
|
514
|
+
ifMatch: current.etag ?? undefined,
|
|
515
|
+
headers: this.davHeaders(conversationId, interactionId),
|
|
516
|
+
});
|
|
517
|
+
const fresh = await this.getObject(canonical) ?? optimistic;
|
|
518
|
+
return { object: fresh, message: `Patched ${canonical}` };
|
|
504
519
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
this._pendingMutations.set(objectId, optimistic);
|
|
510
|
-
this.emit('objectUpdated', { objectId, object: optimistic, source: 'local_user' });
|
|
520
|
+
catch (error) {
|
|
521
|
+
this.logger.error('[RoolChannel] Failed to patch object:', error);
|
|
522
|
+
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
523
|
+
throw error;
|
|
511
524
|
}
|
|
525
|
+
}
|
|
526
|
+
/** Move an object JSON file to a new machine path, optionally replacing its body. */
|
|
527
|
+
async moveObject(from, to, options) {
|
|
528
|
+
return this._moveObjectImpl(from, to, options, this._conversationId);
|
|
529
|
+
}
|
|
530
|
+
/** @internal */
|
|
531
|
+
async _moveObjectImpl(from, to, options, conversationId) {
|
|
532
|
+
const fromPath = objectPath(from);
|
|
533
|
+
const toPath = objectPath(to);
|
|
534
|
+
const optimistic = objectFromBody(toPath, options?.body ?? {});
|
|
512
535
|
try {
|
|
513
536
|
const interactionId = generateEntityId();
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
537
|
+
await this.webdav.move(fromPath, toPath, {
|
|
538
|
+
headers: this.davHeaders(conversationId, interactionId),
|
|
539
|
+
});
|
|
540
|
+
if (options?.body) {
|
|
541
|
+
await this.webdav.put(toPath, JSON.stringify(options.body), {
|
|
542
|
+
contentType: 'application/json',
|
|
543
|
+
headers: this.davHeaders(conversationId, interactionId),
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
this._objectStats.delete(fromPath);
|
|
547
|
+
const fresh = await this.getObject(toPath) ?? optimistic;
|
|
548
|
+
return { object: fresh, message: `Moved ${fromPath} to ${toPath}` };
|
|
517
549
|
}
|
|
518
550
|
catch (error) {
|
|
519
|
-
this.logger.error('[RoolChannel] Failed to
|
|
520
|
-
this._pendingMutations.delete(objectId);
|
|
521
|
-
this._cancelCollector(objectId);
|
|
551
|
+
this.logger.error('[RoolChannel] Failed to move object:', error);
|
|
522
552
|
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
523
|
-
this.emit('reset', { source: 'system' });
|
|
524
553
|
throw error;
|
|
525
554
|
}
|
|
526
555
|
}
|
|
527
|
-
/**
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
556
|
+
/** Delete object JSON files by machine path. */
|
|
557
|
+
async deleteObjects(paths) {
|
|
558
|
+
return this._deleteObjectsImpl(paths, this._conversationId);
|
|
559
|
+
}
|
|
560
|
+
/** @deprecated Use deleteObjects instead. */
|
|
561
|
+
async deletePaths(paths) {
|
|
562
|
+
return this.deleteObjects(paths);
|
|
533
563
|
}
|
|
534
564
|
/** @internal */
|
|
535
|
-
async _deleteObjectsImpl(
|
|
536
|
-
if (
|
|
565
|
+
async _deleteObjectsImpl(paths, conversationId) {
|
|
566
|
+
if (paths.length === 0)
|
|
537
567
|
return;
|
|
538
|
-
|
|
539
|
-
for (const objectId of objectIds) {
|
|
540
|
-
this._pendingMutations.set(objectId, null);
|
|
541
|
-
this.emit('objectDeleted', { objectId, source: 'local_user' });
|
|
542
|
-
}
|
|
568
|
+
const canonical = paths.map(objectPath);
|
|
543
569
|
try {
|
|
544
|
-
|
|
570
|
+
const interactionId = generateEntityId();
|
|
571
|
+
for (const path of canonical) {
|
|
572
|
+
await this.webdav.delete(path, {
|
|
573
|
+
headers: this.davHeaders(conversationId, interactionId),
|
|
574
|
+
});
|
|
575
|
+
this._objectStats.delete(path);
|
|
576
|
+
}
|
|
545
577
|
}
|
|
546
578
|
catch (error) {
|
|
547
|
-
this.logger.error('[RoolChannel] Failed to delete
|
|
548
|
-
for (const objectId of objectIds) {
|
|
549
|
-
this._pendingMutations.delete(objectId);
|
|
550
|
-
}
|
|
579
|
+
this.logger.error('[RoolChannel] Failed to delete paths:', error);
|
|
551
580
|
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
552
|
-
this.emit('reset', { source: 'system' });
|
|
553
581
|
throw error;
|
|
554
582
|
}
|
|
555
583
|
}
|
|
556
|
-
|
|
557
|
-
// Collection Schema Operations
|
|
558
|
-
// ===========================================================================
|
|
559
|
-
/**
|
|
560
|
-
* Get the current schema for this space.
|
|
561
|
-
* Returns a map of collection names to their definitions.
|
|
562
|
-
*/
|
|
584
|
+
/** Get the current schema for this space. */
|
|
563
585
|
getSchema() {
|
|
564
586
|
return this._schema;
|
|
565
587
|
}
|
|
566
|
-
/**
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
* @param fields - Field definitions for the collection
|
|
570
|
-
* @returns The created CollectionDef
|
|
571
|
-
*/
|
|
572
|
-
async createCollection(name, fields) {
|
|
573
|
-
return this._createCollectionImpl(name, fields, this._conversationId);
|
|
588
|
+
/** Create a new collection schema. */
|
|
589
|
+
async createCollection(name, fields, options) {
|
|
590
|
+
return this._createCollectionImpl(name, fields, options, this._conversationId);
|
|
574
591
|
}
|
|
575
592
|
/** @internal */
|
|
576
|
-
async _createCollectionImpl(name, fields, conversationId) {
|
|
593
|
+
async _createCollectionImpl(name, fields, options, conversationId) {
|
|
577
594
|
if (this._schema[name]) {
|
|
578
595
|
throw new Error(`Collection "${name}" already exists`);
|
|
579
596
|
}
|
|
580
597
|
// Optimistic local update
|
|
581
|
-
const optimisticDef =
|
|
598
|
+
const optimisticDef = collectionDef(fields, options);
|
|
582
599
|
this._schema[name] = optimisticDef;
|
|
583
600
|
try {
|
|
584
|
-
|
|
601
|
+
await this.webdav.mkcol(collectionPath(name), { headers: this.davHeaders(conversationId, generateEntityId()) });
|
|
602
|
+
await this.webdav.put(schemaPath(name), JSON.stringify(optimisticDef), {
|
|
603
|
+
contentType: 'application/json',
|
|
604
|
+
headers: this.davHeaders(conversationId, generateEntityId()),
|
|
605
|
+
});
|
|
606
|
+
this.emit('schemaUpdated', { schema: this._schema, source: 'local_user' });
|
|
607
|
+
return optimisticDef;
|
|
585
608
|
}
|
|
586
609
|
catch (error) {
|
|
587
610
|
this.logger.error('[RoolChannel] Failed to create collection:', error);
|
|
@@ -589,25 +612,25 @@ export class RoolChannel extends EventEmitter {
|
|
|
589
612
|
throw error;
|
|
590
613
|
}
|
|
591
614
|
}
|
|
592
|
-
/**
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
* @param fields - New field definitions (replaces all existing fields)
|
|
596
|
-
* @returns The updated CollectionDef
|
|
597
|
-
*/
|
|
598
|
-
async alterCollection(name, fields) {
|
|
599
|
-
return this._alterCollectionImpl(name, fields, this._conversationId);
|
|
615
|
+
/** Alter an existing collection schema, replacing its field definitions. */
|
|
616
|
+
async alterCollection(name, fields, options) {
|
|
617
|
+
return this._alterCollectionImpl(name, fields, options, this._conversationId);
|
|
600
618
|
}
|
|
601
619
|
/** @internal */
|
|
602
|
-
async _alterCollectionImpl(name, fields, conversationId) {
|
|
620
|
+
async _alterCollectionImpl(name, fields, options, conversationId) {
|
|
603
621
|
if (!this._schema[name]) {
|
|
604
622
|
throw new Error(`Collection "${name}" not found`);
|
|
605
623
|
}
|
|
606
624
|
const previous = this._schema[name];
|
|
607
|
-
|
|
608
|
-
this._schema[name] = { fields: fields.map(f => ({ name: f.name, type: f.type })) };
|
|
625
|
+
this._schema[name] = collectionDef(fields, options);
|
|
609
626
|
try {
|
|
610
|
-
|
|
627
|
+
const updated = this._schema[name];
|
|
628
|
+
await this.webdav.put(schemaPath(name), JSON.stringify(updated), {
|
|
629
|
+
contentType: 'application/json',
|
|
630
|
+
headers: this.davHeaders(conversationId, generateEntityId()),
|
|
631
|
+
});
|
|
632
|
+
this.emit('schemaUpdated', { schema: this._schema, source: 'local_user' });
|
|
633
|
+
return updated;
|
|
611
634
|
}
|
|
612
635
|
catch (error) {
|
|
613
636
|
this.logger.error('[RoolChannel] Failed to alter collection:', error);
|
|
@@ -615,10 +638,7 @@ export class RoolChannel extends EventEmitter {
|
|
|
615
638
|
throw error;
|
|
616
639
|
}
|
|
617
640
|
}
|
|
618
|
-
/**
|
|
619
|
-
* Drop a collection schema.
|
|
620
|
-
* @param name - Name of the collection to drop
|
|
621
|
-
*/
|
|
641
|
+
/** Drop a collection schema. */
|
|
622
642
|
async dropCollection(name) {
|
|
623
643
|
return this._dropCollectionImpl(name, this._conversationId);
|
|
624
644
|
}
|
|
@@ -628,10 +648,10 @@ export class RoolChannel extends EventEmitter {
|
|
|
628
648
|
throw new Error(`Collection "${name}" not found`);
|
|
629
649
|
}
|
|
630
650
|
const previous = this._schema[name];
|
|
631
|
-
// Optimistic local update
|
|
632
651
|
delete this._schema[name];
|
|
633
652
|
try {
|
|
634
|
-
await this.
|
|
653
|
+
await this.webdav.delete(collectionPath(name), { collection: true, headers: this.davHeaders(conversationId, generateEntityId()) });
|
|
654
|
+
this.emit('schemaUpdated', { schema: this._schema, source: 'local_user' });
|
|
635
655
|
}
|
|
636
656
|
catch (error) {
|
|
637
657
|
this.logger.error('[RoolChannel] Failed to drop collection:', error);
|
|
@@ -639,12 +659,8 @@ export class RoolChannel extends EventEmitter {
|
|
|
639
659
|
throw error;
|
|
640
660
|
}
|
|
641
661
|
}
|
|
642
|
-
// ===========================================================================
|
|
643
|
-
// System Instructions
|
|
644
|
-
// ===========================================================================
|
|
645
662
|
/**
|
|
646
663
|
* Get the system instruction for the current conversation.
|
|
647
|
-
* Returns undefined if no system instruction is set.
|
|
648
664
|
*/
|
|
649
665
|
getSystemInstruction() {
|
|
650
666
|
return this._getSystemInstructionImpl(this._conversationId);
|
|
@@ -653,16 +669,12 @@ export class RoolChannel extends EventEmitter {
|
|
|
653
669
|
_getSystemInstructionImpl(conversationId) {
|
|
654
670
|
return this._channel?.conversations[conversationId]?.systemInstruction;
|
|
655
671
|
}
|
|
656
|
-
/**
|
|
657
|
-
* Set the system instruction for the current conversation.
|
|
658
|
-
* Pass null to clear the instruction.
|
|
659
|
-
*/
|
|
672
|
+
/** Set the system instruction for the current conversation. */
|
|
660
673
|
async setSystemInstruction(instruction) {
|
|
661
674
|
return this._setSystemInstructionImpl(instruction, this._conversationId);
|
|
662
675
|
}
|
|
663
676
|
/** @internal */
|
|
664
677
|
async _setSystemInstructionImpl(instruction, conversationId) {
|
|
665
|
-
// Optimistic local update
|
|
666
678
|
this._ensureConversationImpl(conversationId);
|
|
667
679
|
const conv = this._channel.conversations[conversationId];
|
|
668
680
|
const previousInstruction = conv.systemInstruction;
|
|
@@ -672,7 +684,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
672
684
|
else {
|
|
673
685
|
conv.systemInstruction = instruction;
|
|
674
686
|
}
|
|
675
|
-
// Emit events for backward compat and new API
|
|
676
687
|
this.emit('conversationUpdated', {
|
|
677
688
|
conversationId,
|
|
678
689
|
channelId: this._channelId,
|
|
@@ -684,13 +695,11 @@ export class RoolChannel extends EventEmitter {
|
|
|
684
695
|
source: 'local_user',
|
|
685
696
|
});
|
|
686
697
|
}
|
|
687
|
-
// Call server
|
|
688
698
|
try {
|
|
689
699
|
await this.graphqlClient.updateConversation(this._id, this._channelId, conversationId, { systemInstruction: instruction });
|
|
690
700
|
}
|
|
691
701
|
catch (error) {
|
|
692
702
|
this.logger.error('[RoolChannel] Failed to set system instruction:', error);
|
|
693
|
-
// Rollback
|
|
694
703
|
if (previousInstruction === undefined) {
|
|
695
704
|
delete conv.systemInstruction;
|
|
696
705
|
}
|
|
@@ -700,15 +709,12 @@ export class RoolChannel extends EventEmitter {
|
|
|
700
709
|
throw error;
|
|
701
710
|
}
|
|
702
711
|
}
|
|
703
|
-
/**
|
|
704
|
-
* Rename the current conversation.
|
|
705
|
-
*/
|
|
712
|
+
/** Rename the current conversation. */
|
|
706
713
|
async renameConversation(name) {
|
|
707
714
|
return this._renameConversationImpl(name, this._conversationId);
|
|
708
715
|
}
|
|
709
716
|
/** @internal */
|
|
710
717
|
async _renameConversationImpl(name, conversationId) {
|
|
711
|
-
// Optimistic local update
|
|
712
718
|
this._ensureConversationImpl(conversationId);
|
|
713
719
|
const conv = this._channel.conversations[conversationId];
|
|
714
720
|
const previousName = conv.name;
|
|
@@ -724,21 +730,16 @@ export class RoolChannel extends EventEmitter {
|
|
|
724
730
|
source: 'local_user',
|
|
725
731
|
});
|
|
726
732
|
}
|
|
727
|
-
// Call server
|
|
728
733
|
try {
|
|
729
734
|
await this.graphqlClient.updateConversation(this._id, this._channelId, conversationId, { name });
|
|
730
735
|
}
|
|
731
736
|
catch (error) {
|
|
732
737
|
this.logger.error('[RoolChannel] Failed to rename conversation:', error);
|
|
733
|
-
// Rollback
|
|
734
738
|
conv.name = previousName;
|
|
735
739
|
throw error;
|
|
736
740
|
}
|
|
737
741
|
}
|
|
738
|
-
/**
|
|
739
|
-
* Ensure a conversation exists in the local channel cache.
|
|
740
|
-
* @internal
|
|
741
|
-
*/
|
|
742
|
+
/** @internal */
|
|
742
743
|
_ensureConversationImpl(conversationId) {
|
|
743
744
|
if (!this._channel) {
|
|
744
745
|
this._channel = {
|
|
@@ -755,13 +756,7 @@ export class RoolChannel extends EventEmitter {
|
|
|
755
756
|
};
|
|
756
757
|
}
|
|
757
758
|
}
|
|
758
|
-
|
|
759
|
-
// Metadata Operations
|
|
760
|
-
// ===========================================================================
|
|
761
|
-
/**
|
|
762
|
-
* Set a space-level metadata value.
|
|
763
|
-
* Metadata is stored in meta and hidden from AI operations.
|
|
764
|
-
*/
|
|
759
|
+
/** Set a space-level metadata value. */
|
|
765
760
|
setMetadata(key, value) {
|
|
766
761
|
this._setMetadataImpl(key, value, this._conversationId);
|
|
767
762
|
}
|
|
@@ -770,212 +765,156 @@ export class RoolChannel extends EventEmitter {
|
|
|
770
765
|
this._meta[key] = value;
|
|
771
766
|
this.emit('metadataUpdated', { metadata: this._meta, source: 'local_user' });
|
|
772
767
|
// Fire-and-forget server call
|
|
773
|
-
this.graphqlClient.setSpaceMeta(this.
|
|
768
|
+
this.graphqlClient.setSpaceMeta(this._id, this._meta, this._channelId, conversationId)
|
|
774
769
|
.catch((error) => {
|
|
775
770
|
this.logger.error('[RoolChannel] Failed to set meta:', error);
|
|
776
771
|
});
|
|
777
772
|
}
|
|
778
|
-
/**
|
|
779
|
-
* Get a space-level metadata value.
|
|
780
|
-
*/
|
|
773
|
+
/** Get a space-level metadata value. */
|
|
781
774
|
getMetadata(key) {
|
|
782
775
|
return this._meta[key];
|
|
783
776
|
}
|
|
784
|
-
/**
|
|
785
|
-
* Get all space-level metadata.
|
|
786
|
-
*/
|
|
777
|
+
/** Get all space-level metadata. */
|
|
787
778
|
getAllMetadata() {
|
|
788
779
|
return this._meta;
|
|
789
780
|
}
|
|
790
|
-
// ===========================================================================
|
|
791
|
-
// AI Operations
|
|
792
|
-
// ===========================================================================
|
|
793
781
|
/**
|
|
794
782
|
* Send a prompt to the AI agent for space manipulation.
|
|
795
|
-
* @returns The message from the AI and the list of objects that were created or modified
|
|
783
|
+
* @returns The message from the AI and the list of objects that were created or modified.
|
|
796
784
|
*/
|
|
797
785
|
async prompt(prompt, options) {
|
|
798
786
|
return this._promptImpl(prompt, options, this._conversationId);
|
|
799
787
|
}
|
|
800
788
|
/** @internal */
|
|
801
789
|
async _promptImpl(prompt, options, conversationId) {
|
|
802
|
-
|
|
803
|
-
const
|
|
804
|
-
let
|
|
790
|
+
const { attachments, parentInteractionId: explicitParent, signal, ...rest } = options ?? {};
|
|
791
|
+
const interactionId = generateEntityId();
|
|
792
|
+
let attachmentRefs;
|
|
805
793
|
if (attachments?.length) {
|
|
806
|
-
|
|
794
|
+
attachmentRefs = await Promise.all(attachments.map(async (attachment) => {
|
|
795
|
+
const path = typeof attachment === 'string' ? machinePath(attachment) : await this.uploadAttachment(attachment, conversationId);
|
|
796
|
+
return machineUri(path);
|
|
797
|
+
}));
|
|
807
798
|
}
|
|
808
799
|
// Auto-continue from active leaf if no explicit parent provided
|
|
809
800
|
const parentInteractionId = explicitParent !== undefined
|
|
810
801
|
? explicitParent
|
|
811
802
|
: (this._getActiveLeafImpl(conversationId) ?? null);
|
|
812
|
-
const interactionId = generateEntityId();
|
|
813
803
|
// Optimistically set active leaf before the server call.
|
|
814
804
|
this._activeLeaves.set(conversationId, interactionId);
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
const missing = [];
|
|
820
|
-
for (const id of result.modifiedObjectIds) {
|
|
821
|
-
const buffered = this._objectBuffer.get(id);
|
|
822
|
-
if (buffered) {
|
|
823
|
-
this._objectBuffer.delete(id);
|
|
824
|
-
objects.push(buffered);
|
|
805
|
+
let onAbort;
|
|
806
|
+
if (signal) {
|
|
807
|
+
if (signal.aborted) {
|
|
808
|
+
this.stopInteraction(interactionId).catch(() => { });
|
|
825
809
|
}
|
|
826
810
|
else {
|
|
827
|
-
|
|
811
|
+
onAbort = () => {
|
|
812
|
+
this.stopInteraction(interactionId).catch(() => { });
|
|
813
|
+
};
|
|
814
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
828
815
|
}
|
|
829
816
|
}
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
817
|
+
let result;
|
|
818
|
+
try {
|
|
819
|
+
result = await this.graphqlClient.prompt(this._id, prompt, this._channelId, conversationId, {
|
|
820
|
+
...rest,
|
|
821
|
+
attachmentRefs,
|
|
822
|
+
interactionId,
|
|
823
|
+
parentInteractionId,
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
finally {
|
|
827
|
+
if (onAbort)
|
|
828
|
+
signal.removeEventListener('abort', onAbort);
|
|
829
|
+
}
|
|
830
|
+
const objects = [];
|
|
831
|
+
const fetched = await Promise.all(result.modifiedObjectPaths.map((path) => this.getObject(path)));
|
|
832
|
+
for (const object of fetched) {
|
|
833
|
+
if (object)
|
|
834
|
+
objects.push(object);
|
|
837
835
|
}
|
|
838
836
|
return {
|
|
839
837
|
message: result.message,
|
|
840
838
|
objects,
|
|
841
839
|
};
|
|
842
840
|
}
|
|
843
|
-
// ===========================================================================
|
|
844
|
-
// Channel Admin
|
|
845
|
-
// ===========================================================================
|
|
846
841
|
/**
|
|
847
|
-
*
|
|
842
|
+
* Stop the in-flight interaction on the default conversation, if any.
|
|
843
|
+
*
|
|
844
|
+
* No-op returning `false` when the active leaf is already finished or the
|
|
845
|
+
* conversation has no interactions. Stopping is best-effort: the server
|
|
846
|
+
* halts the agent loop and closes the stream, but an LLM turn already in
|
|
847
|
+
* flight keeps generating server-side and is billed.
|
|
848
848
|
*/
|
|
849
|
+
async stop() {
|
|
850
|
+
return this._stopImpl(this._conversationId);
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Request that the server stop a specific in-flight interaction by ID.
|
|
854
|
+
*
|
|
855
|
+
* Returns whether the server stopped an interaction (`false` if it had
|
|
856
|
+
* already finished). Stopping is best-effort — see {@link stop}.
|
|
857
|
+
*/
|
|
858
|
+
async stopInteraction(interactionId) {
|
|
859
|
+
return this.graphqlClient.stopInteraction(this._id, interactionId);
|
|
860
|
+
}
|
|
861
|
+
/** @internal */
|
|
862
|
+
async _stopImpl(conversationId) {
|
|
863
|
+
const leafId = this._getActiveLeafImpl(conversationId);
|
|
864
|
+
if (!leafId)
|
|
865
|
+
return false;
|
|
866
|
+
const interactions = this._channel?.conversations[conversationId]?.interactions;
|
|
867
|
+
const interaction = interactions && !Array.isArray(interactions) ? interactions[leafId] : undefined;
|
|
868
|
+
// Skip the round trip when we already know the interaction has settled.
|
|
869
|
+
if (interaction && (interaction.status === 'done' || interaction.status === 'error')) {
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
return this.stopInteraction(leafId);
|
|
873
|
+
}
|
|
874
|
+
/** Rename this channel. */
|
|
849
875
|
async rename(newName) {
|
|
850
|
-
// Optimistic local update
|
|
851
876
|
const previousName = this._channel?.name;
|
|
852
877
|
if (this._channel) {
|
|
853
878
|
this._channel.name = newName;
|
|
854
879
|
}
|
|
855
880
|
this.emit('channelUpdated', { channelId: this._channelId, source: 'local_user' });
|
|
856
|
-
// Call server
|
|
857
881
|
try {
|
|
858
882
|
await this.graphqlClient.renameChannel(this._id, this._channelId, newName);
|
|
859
883
|
}
|
|
860
884
|
catch (error) {
|
|
861
885
|
this.logger.error('[RoolChannel] Failed to rename channel:', error);
|
|
862
|
-
// Rollback
|
|
863
886
|
if (this._channel) {
|
|
864
887
|
this._channel.name = previousName;
|
|
865
888
|
}
|
|
866
889
|
throw error;
|
|
867
890
|
}
|
|
868
891
|
}
|
|
869
|
-
// ===========================================================================
|
|
870
|
-
// Media Operations
|
|
871
|
-
// ===========================================================================
|
|
872
|
-
/**
|
|
873
|
-
* List all media files for this space.
|
|
874
|
-
*/
|
|
875
|
-
async listMedia() {
|
|
876
|
-
return this.mediaClient.list(this._id);
|
|
877
|
-
}
|
|
878
|
-
/**
|
|
879
|
-
* Upload a file to this space. Returns the URL.
|
|
880
|
-
*/
|
|
881
|
-
async uploadMedia(file) {
|
|
882
|
-
return this.mediaClient.upload(this._id, file);
|
|
883
|
-
}
|
|
884
|
-
/**
|
|
885
|
-
* Fetch any URL, returning headers and a blob() method (like fetch Response).
|
|
886
|
-
* Adds auth headers for backend media URLs, fetches external URLs via server proxy if CORS blocks.
|
|
887
|
-
* Pass `{ forceProxy: true }` to skip the direct fetch and go straight through the server proxy.
|
|
888
|
-
*/
|
|
889
|
-
async fetchMedia(url, options) {
|
|
890
|
-
return this.mediaClient.fetch(this._id, url, options);
|
|
891
|
-
}
|
|
892
|
-
/**
|
|
893
|
-
* Delete a media file by URL.
|
|
894
|
-
*/
|
|
895
|
-
async deleteMedia(url) {
|
|
896
|
-
return this.mediaClient.delete(this._id, url);
|
|
897
|
-
}
|
|
898
|
-
// ===========================================================================
|
|
899
|
-
// Proxied Fetch
|
|
900
|
-
// ===========================================================================
|
|
901
892
|
/**
|
|
902
893
|
* Fetch an external URL via the server proxy, bypassing CORS restrictions.
|
|
903
|
-
* Requires editor role or above. Blocked for private/internal IP ranges (SSRF protection).
|
|
904
|
-
*
|
|
905
|
-
* @param url - The URL to fetch
|
|
906
|
-
* @param init - Optional method, headers, and body
|
|
907
|
-
* @returns The proxied Response
|
|
908
894
|
*/
|
|
909
895
|
async fetch(url, init) {
|
|
910
|
-
return this.
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
resolve(buffered);
|
|
927
|
-
return;
|
|
928
|
-
}
|
|
929
|
-
const timer = setTimeout(() => {
|
|
930
|
-
this._objectResolvers.delete(objectId);
|
|
931
|
-
// Fallback: try to fetch from server
|
|
932
|
-
this.graphqlClient.getObject(this._id, objectId).then(obj => {
|
|
933
|
-
if (obj) {
|
|
934
|
-
resolve(obj);
|
|
935
|
-
}
|
|
936
|
-
else {
|
|
937
|
-
reject(new Error(`Timeout waiting for object ${objectId} from SSE`));
|
|
938
|
-
}
|
|
939
|
-
}).catch(reject);
|
|
940
|
-
}, OBJECT_COLLECT_TIMEOUT);
|
|
941
|
-
this._objectResolvers.set(objectId, (obj) => {
|
|
942
|
-
clearTimeout(timer);
|
|
943
|
-
resolve(obj);
|
|
944
|
-
});
|
|
945
|
-
});
|
|
946
|
-
}
|
|
947
|
-
/**
|
|
948
|
-
* Cancel a pending object collector (e.g., on mutation error).
|
|
949
|
-
* @internal
|
|
950
|
-
*/
|
|
951
|
-
_cancelCollector(objectId) {
|
|
952
|
-
this._objectResolvers.delete(objectId);
|
|
953
|
-
this._objectBuffer.delete(objectId);
|
|
954
|
-
}
|
|
955
|
-
/**
|
|
956
|
-
* Deliver an object to a pending collector, or buffer it for later collection.
|
|
957
|
-
* @internal
|
|
958
|
-
*/
|
|
959
|
-
_deliverObject(objectId, object) {
|
|
960
|
-
const resolver = this._objectResolvers.get(objectId);
|
|
961
|
-
if (resolver) {
|
|
962
|
-
resolver(object);
|
|
963
|
-
this._objectResolvers.delete(objectId);
|
|
964
|
-
}
|
|
965
|
-
else {
|
|
966
|
-
// Buffer for prompt() or late collectors
|
|
967
|
-
this._objectBuffer.set(objectId, object);
|
|
968
|
-
}
|
|
896
|
+
return this.restClient.proxyFetch(this._id, url, init);
|
|
897
|
+
}
|
|
898
|
+
async uploadAttachment(file, conversationId) {
|
|
899
|
+
await this.ensureCollection('/rool-drive/attachments');
|
|
900
|
+
const directory = `/rool-drive/attachments/${conversationId}`;
|
|
901
|
+
await this.ensureCollection(directory);
|
|
902
|
+
const attachment = attachmentBody(file);
|
|
903
|
+
const path = `${directory}/${attachment.filename}`;
|
|
904
|
+
await this.webdav.put(path, attachment.body, { contentType: attachment.contentType });
|
|
905
|
+
return path;
|
|
906
|
+
}
|
|
907
|
+
async ensureCollection(path) {
|
|
908
|
+
const response = await this.webdav.request('MKCOL', path, { collection: true });
|
|
909
|
+
if (response.status === 201 || response.status === 405)
|
|
910
|
+
return;
|
|
911
|
+
throw new Error(`Failed to create collection ${path}: ${response.status} ${await response.text()}`);
|
|
969
912
|
}
|
|
970
|
-
// ===========================================================================
|
|
971
|
-
// Event Handlers (internal - handles space subscription events)
|
|
972
|
-
// ===========================================================================
|
|
973
913
|
/**
|
|
974
914
|
* Handle a channel event from the subscription.
|
|
975
915
|
* @internal
|
|
976
916
|
*/
|
|
977
917
|
handleChannelEvent(event) {
|
|
978
|
-
// Ignore events after close - the channel is being torn down
|
|
979
918
|
if (this._closed)
|
|
980
919
|
return;
|
|
981
920
|
const changeSource = event.source === 'agent' ? 'remote_agent' : 'remote_user';
|
|
@@ -983,26 +922,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
983
922
|
case 'connected':
|
|
984
923
|
// Resync is handled by the client via _applyResyncData.
|
|
985
924
|
break;
|
|
986
|
-
case 'object_created':
|
|
987
|
-
if (event.objectId && event.object) {
|
|
988
|
-
if (event.objectStat)
|
|
989
|
-
this._objectStats.set(event.objectId, event.objectStat);
|
|
990
|
-
this._handleObjectCreated(event.objectId, event.object, changeSource);
|
|
991
|
-
}
|
|
992
|
-
break;
|
|
993
|
-
case 'object_updated':
|
|
994
|
-
if (event.objectId && event.object) {
|
|
995
|
-
if (event.objectStat)
|
|
996
|
-
this._objectStats.set(event.objectId, event.objectStat);
|
|
997
|
-
this._handleObjectUpdated(event.objectId, event.object, changeSource);
|
|
998
|
-
}
|
|
999
|
-
break;
|
|
1000
|
-
case 'object_deleted':
|
|
1001
|
-
if (event.objectId) {
|
|
1002
|
-
this._objectStats.delete(event.objectId);
|
|
1003
|
-
this._handleObjectDeleted(event.objectId, changeSource);
|
|
1004
|
-
}
|
|
1005
|
-
break;
|
|
1006
925
|
case 'schema_updated':
|
|
1007
926
|
if (event.schema) {
|
|
1008
927
|
this._schema = event.schema;
|
|
@@ -1016,7 +935,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
1016
935
|
}
|
|
1017
936
|
break;
|
|
1018
937
|
case 'channel_updated':
|
|
1019
|
-
// Only update if it's our channel — channel_updated is now metadata-only (name, extensionUrl)
|
|
1020
938
|
if (event.channelId === this._channelId && event.channel) {
|
|
1021
939
|
const changed = JSON.stringify(this._channel) !== JSON.stringify(event.channel);
|
|
1022
940
|
this._channel = event.channel;
|
|
@@ -1025,8 +943,14 @@ export class RoolChannel extends EventEmitter {
|
|
|
1025
943
|
}
|
|
1026
944
|
}
|
|
1027
945
|
break;
|
|
946
|
+
case 'channel_deleted':
|
|
947
|
+
if (event.channelId === this._channelId) {
|
|
948
|
+
this._channel = undefined;
|
|
949
|
+
this._activeLeaves.clear();
|
|
950
|
+
this.emit('reset', { source: changeSource });
|
|
951
|
+
}
|
|
952
|
+
break;
|
|
1028
953
|
case 'conversation_updated':
|
|
1029
|
-
// Only update if it's our channel
|
|
1030
954
|
if (event.channelId === this._channelId && event.conversationId) {
|
|
1031
955
|
if (!this._channel) {
|
|
1032
956
|
this._channel = {
|
|
@@ -1037,17 +961,13 @@ export class RoolChannel extends EventEmitter {
|
|
|
1037
961
|
}
|
|
1038
962
|
const prev = this._channel.conversations[event.conversationId];
|
|
1039
963
|
if (event.conversation) {
|
|
1040
|
-
// Update or create conversation in local cache
|
|
1041
964
|
this._channel.conversations[event.conversationId] = event.conversation;
|
|
1042
965
|
}
|
|
1043
966
|
else {
|
|
1044
|
-
// Conversation was deleted
|
|
1045
967
|
delete this._channel.conversations[event.conversationId];
|
|
1046
968
|
}
|
|
1047
|
-
// Skip emit if data is unchanged (e.g. echo of our own optimistic update)
|
|
1048
969
|
if (JSON.stringify(prev) === JSON.stringify(event.conversation))
|
|
1049
970
|
break;
|
|
1050
|
-
// Auto-advance active leaf if someone continued our current branch
|
|
1051
971
|
if (event.conversation && !Array.isArray(event.conversation.interactions)) {
|
|
1052
972
|
const currentLeaf = this._getActiveLeafImpl(event.conversationId);
|
|
1053
973
|
if (currentLeaf) {
|
|
@@ -1059,13 +979,11 @@ export class RoolChannel extends EventEmitter {
|
|
|
1059
979
|
}
|
|
1060
980
|
}
|
|
1061
981
|
}
|
|
1062
|
-
// Emit the new conversationUpdated event
|
|
1063
982
|
this.emit('conversationUpdated', {
|
|
1064
983
|
conversationId: event.conversationId,
|
|
1065
984
|
channelId: event.channelId,
|
|
1066
985
|
source: changeSource,
|
|
1067
986
|
});
|
|
1068
|
-
// Backward compat: also emit channelUpdated when the active conversation updates
|
|
1069
987
|
if (event.conversationId === this._conversationId) {
|
|
1070
988
|
this.emit('channelUpdated', { channelId: event.channelId, source: changeSource });
|
|
1071
989
|
}
|
|
@@ -1076,90 +994,9 @@ export class RoolChannel extends EventEmitter {
|
|
|
1076
994
|
break;
|
|
1077
995
|
}
|
|
1078
996
|
}
|
|
1079
|
-
/**
|
|
1080
|
-
* Handle an object_created SSE event.
|
|
1081
|
-
* Deduplicates against optimistic local creates.
|
|
1082
|
-
* @internal
|
|
1083
|
-
*/
|
|
1084
|
-
_handleObjectCreated(objectId, object, source) {
|
|
1085
|
-
// Deliver to any pending collector (for mutation return values)
|
|
1086
|
-
this._deliverObject(objectId, object);
|
|
1087
|
-
// Maintain local ID list — prepend (most recently modified first)
|
|
1088
|
-
this._objectIds = [objectId, ...this._objectIds.filter(id => id !== objectId)];
|
|
1089
|
-
const pending = this._pendingMutations.get(objectId);
|
|
1090
|
-
if (pending !== undefined) {
|
|
1091
|
-
// This is our own mutation echoed back
|
|
1092
|
-
this._pendingMutations.delete(objectId);
|
|
1093
|
-
if (pending !== null) {
|
|
1094
|
-
// It was a create — already emitted objectCreated optimistically.
|
|
1095
|
-
// Emit objectUpdated only if AI resolved placeholders (data changed).
|
|
1096
|
-
if (JSON.stringify(pending) !== JSON.stringify(object)) {
|
|
1097
|
-
this.emit('objectUpdated', { objectId, object, source });
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
else {
|
|
1102
|
-
// Remote event — emit normally
|
|
1103
|
-
this.emit('objectCreated', { objectId, object, source });
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
/**
|
|
1107
|
-
* Handle an object_updated SSE event.
|
|
1108
|
-
* Deduplicates against optimistic local updates.
|
|
1109
|
-
* @internal
|
|
1110
|
-
*/
|
|
1111
|
-
_handleObjectUpdated(objectId, object, source) {
|
|
1112
|
-
// Deliver to any pending collector
|
|
1113
|
-
this._deliverObject(objectId, object);
|
|
1114
|
-
// Maintain local ID list — move to front (most recently modified)
|
|
1115
|
-
this._objectIds = [objectId, ...this._objectIds.filter(id => id !== objectId)];
|
|
1116
|
-
const pending = this._pendingMutations.get(objectId);
|
|
1117
|
-
if (pending !== undefined) {
|
|
1118
|
-
// This is our own mutation echoed back
|
|
1119
|
-
this._pendingMutations.delete(objectId);
|
|
1120
|
-
if (pending !== null) {
|
|
1121
|
-
// Already emitted objectUpdated optimistically.
|
|
1122
|
-
// Emit again only if data changed (AI resolved placeholders).
|
|
1123
|
-
if (JSON.stringify(pending) !== JSON.stringify(object)) {
|
|
1124
|
-
this.emit('objectUpdated', { objectId, object, source });
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
else {
|
|
1129
|
-
// Remote event
|
|
1130
|
-
this.emit('objectUpdated', { objectId, object, source });
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
/**
|
|
1134
|
-
* Handle an object_deleted SSE event.
|
|
1135
|
-
* Deduplicates against optimistic local deletes.
|
|
1136
|
-
* @internal
|
|
1137
|
-
*/
|
|
1138
|
-
_handleObjectDeleted(objectId, source) {
|
|
1139
|
-
// Remove from local ID list
|
|
1140
|
-
this._objectIds = this._objectIds.filter(id => id !== objectId);
|
|
1141
|
-
const pending = this._pendingMutations.get(objectId);
|
|
1142
|
-
if (pending !== undefined) {
|
|
1143
|
-
// This is our own delete echoed back — already emitted
|
|
1144
|
-
this._pendingMutations.delete(objectId);
|
|
1145
|
-
}
|
|
1146
|
-
else {
|
|
1147
|
-
// Remote event
|
|
1148
|
-
this.emit('objectDeleted', { objectId, source });
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
997
|
}
|
|
1152
|
-
// =============================================================================
|
|
1153
|
-
// ConversationHandle — Scoped proxy for a specific conversation
|
|
1154
|
-
// =============================================================================
|
|
1155
998
|
/**
|
|
1156
999
|
* A lightweight handle for a specific conversation within a channel.
|
|
1157
|
-
*
|
|
1158
|
-
* Scopes AI and mutation operations to a particular conversation's interaction
|
|
1159
|
-
* history, while sharing the channel's single SSE connection and object state.
|
|
1160
|
-
*
|
|
1161
|
-
* Obtain via `channel.conversation('thread-id')`.
|
|
1162
|
-
* Conversations are auto-created on first interaction.
|
|
1163
1000
|
*/
|
|
1164
1001
|
export class ConversationHandle {
|
|
1165
1002
|
/** @internal */
|
|
@@ -1172,9 +1009,6 @@ export class ConversationHandle {
|
|
|
1172
1009
|
}
|
|
1173
1010
|
/** The conversation ID this handle is scoped to. */
|
|
1174
1011
|
get conversationId() { return this._conversationId; }
|
|
1175
|
-
// ---------------------------------------------------------------------------
|
|
1176
|
-
// Conversation History
|
|
1177
|
-
// ---------------------------------------------------------------------------
|
|
1178
1012
|
/** Get the active branch of this conversation as a flat array (root → leaf). */
|
|
1179
1013
|
getInteractions() {
|
|
1180
1014
|
return this._channel._getInteractionsImpl(this._conversationId);
|
|
@@ -1203,51 +1037,50 @@ export class ConversationHandle {
|
|
|
1203
1037
|
async rename(name) {
|
|
1204
1038
|
return this._channel._renameConversationImpl(name, this._conversationId);
|
|
1205
1039
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1040
|
+
/** Create or replace an object JSON file. */
|
|
1041
|
+
async putObject(path, body) {
|
|
1042
|
+
return this._channel._putObjectImpl(path, body, this._conversationId);
|
|
1043
|
+
}
|
|
1044
|
+
/** Patch an existing object JSON file. */
|
|
1045
|
+
async patchObject(path, options) {
|
|
1046
|
+
return this._channel._patchObjectImpl(path, options, this._conversationId);
|
|
1212
1047
|
}
|
|
1213
|
-
/**
|
|
1214
|
-
async
|
|
1215
|
-
return this._channel.
|
|
1048
|
+
/** Move (rename/relocate) an object. */
|
|
1049
|
+
async moveObject(from, to, options) {
|
|
1050
|
+
return this._channel._moveObjectImpl(from, to, options, this._conversationId);
|
|
1216
1051
|
}
|
|
1217
|
-
/**
|
|
1218
|
-
async
|
|
1219
|
-
return this._channel.
|
|
1052
|
+
/** Delete object JSON files by path. */
|
|
1053
|
+
async deleteObjects(paths) {
|
|
1054
|
+
return this._channel._deleteObjectsImpl(paths, this._conversationId);
|
|
1220
1055
|
}
|
|
1221
|
-
/**
|
|
1222
|
-
async
|
|
1223
|
-
return this.
|
|
1056
|
+
/** @deprecated Use deleteObjects instead. */
|
|
1057
|
+
async deletePaths(paths) {
|
|
1058
|
+
return this.deleteObjects(paths);
|
|
1224
1059
|
}
|
|
1225
|
-
// ---------------------------------------------------------------------------
|
|
1226
|
-
// AI
|
|
1227
|
-
// ---------------------------------------------------------------------------
|
|
1228
1060
|
/** Send a prompt to the AI agent, scoped to this conversation's history. */
|
|
1229
1061
|
async prompt(text, options) {
|
|
1230
1062
|
return this._channel._promptImpl(text, options, this._conversationId);
|
|
1231
1063
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1064
|
+
/**
|
|
1065
|
+
* Stop this conversation's in-flight interaction, if any. No-op returning
|
|
1066
|
+
* `false` when nothing is running. Stopping is best-effort — see
|
|
1067
|
+
* {@link RoolChannel.stop}.
|
|
1068
|
+
*/
|
|
1069
|
+
async stop() {
|
|
1070
|
+
return this._channel._stopImpl(this._conversationId);
|
|
1071
|
+
}
|
|
1235
1072
|
/** Create a new collection schema. */
|
|
1236
|
-
async createCollection(name, fields) {
|
|
1237
|
-
return this._channel._createCollectionImpl(name, fields, this._conversationId);
|
|
1073
|
+
async createCollection(name, fields, options) {
|
|
1074
|
+
return this._channel._createCollectionImpl(name, fields, options, this._conversationId);
|
|
1238
1075
|
}
|
|
1239
1076
|
/** Alter an existing collection schema. */
|
|
1240
|
-
async alterCollection(name, fields) {
|
|
1241
|
-
return this._channel._alterCollectionImpl(name, fields, this._conversationId);
|
|
1077
|
+
async alterCollection(name, fields, options) {
|
|
1078
|
+
return this._channel._alterCollectionImpl(name, fields, options, this._conversationId);
|
|
1242
1079
|
}
|
|
1243
1080
|
/** Drop a collection schema. */
|
|
1244
1081
|
async dropCollection(name) {
|
|
1245
1082
|
return this._channel._dropCollectionImpl(name, this._conversationId);
|
|
1246
1083
|
}
|
|
1247
|
-
// ---------------------------------------------------------------------------
|
|
1248
|
-
// Metadata (scoped to this conversation's interaction history)
|
|
1249
|
-
// ---------------------------------------------------------------------------
|
|
1250
|
-
/** Set a space-level metadata value. */
|
|
1251
1084
|
setMetadata(key, value) {
|
|
1252
1085
|
return this._channel._setMetadataImpl(key, value, this._conversationId);
|
|
1253
1086
|
}
|