@meshagent/meshagent-agents 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/LICENSE +1 -0
- package/README.md +206 -0
- package/dist/browser/agent-messages.d.ts +343 -0
- package/dist/browser/agent-messages.js +1015 -0
- package/dist/browser/change-emitter.d.ts +8 -0
- package/dist/browser/change-emitter.js +20 -0
- package/dist/browser/chat-client.d.ts +225 -0
- package/dist/browser/chat-client.js +642 -0
- package/dist/browser/images-dataset.d.ts +83 -0
- package/dist/browser/images-dataset.js +282 -0
- package/dist/browser/index.d.ts +6 -0
- package/dist/browser/index.js +22 -0
- package/dist/browser/package.json +1 -0
- package/dist/browser/room-shell-toolkit.d.ts +14 -0
- package/dist/browser/room-shell-toolkit.js +274 -0
- package/dist/browser/room-storage-toolkit.d.ts +11 -0
- package/dist/browser/room-storage-toolkit.js +513 -0
- package/dist/browser/thread-storage.d.ts +102 -0
- package/dist/browser/thread-storage.js +476 -0
- package/dist/esm/agent-messages.d.ts +343 -0
- package/dist/esm/agent-messages.js +989 -0
- package/dist/esm/change-emitter.d.ts +8 -0
- package/dist/esm/change-emitter.js +16 -0
- package/dist/esm/chat-client.d.ts +225 -0
- package/dist/esm/chat-client.js +632 -0
- package/dist/esm/images-dataset.d.ts +83 -0
- package/dist/esm/images-dataset.js +274 -0
- package/dist/esm/index.d.ts +6 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/room-shell-toolkit.d.ts +14 -0
- package/dist/esm/room-shell-toolkit.js +270 -0
- package/dist/esm/room-storage-toolkit.d.ts +11 -0
- package/dist/esm/room-storage-toolkit.js +509 -0
- package/dist/esm/thread-storage.d.ts +102 -0
- package/dist/esm/thread-storage.js +465 -0
- package/dist/node/agent-messages.d.ts +343 -0
- package/dist/node/agent-messages.js +1015 -0
- package/dist/node/change-emitter.d.ts +8 -0
- package/dist/node/change-emitter.js +20 -0
- package/dist/node/chat-client.d.ts +225 -0
- package/dist/node/chat-client.js +642 -0
- package/dist/node/images-dataset.d.ts +83 -0
- package/dist/node/images-dataset.js +282 -0
- package/dist/node/index.d.ts +6 -0
- package/dist/node/index.js +22 -0
- package/dist/node/package.json +1 -0
- package/dist/node/room-shell-toolkit.d.ts +14 -0
- package/dist/node/room-shell-toolkit.js +274 -0
- package/dist/node/room-storage-toolkit.d.ts +11 -0
- package/dist/node/room-storage-toolkit.js +513 -0
- package/dist/node/thread-storage.d.ts +102 -0
- package/dist/node/thread-storage.js +476 -0
- package/package.json +57 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { Field, Schema, Utf8, tableFromArrays, } from "apache-arrow";
|
|
2
|
+
import { ChangeEmitter } from "./change-emitter.js";
|
|
3
|
+
import { DeleteThread, ListThreads, RenameThread, } from "./agent-messages.js";
|
|
4
|
+
export const defaultUntitledThreadName = "New Chat";
|
|
5
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
6
|
+
export class ThreadListEntry {
|
|
7
|
+
constructor({ path, name, createdAt, modifiedAt }) {
|
|
8
|
+
this.path = path;
|
|
9
|
+
this.name = name;
|
|
10
|
+
this.createdAt = createdAt;
|
|
11
|
+
this.modifiedAt = modifiedAt;
|
|
12
|
+
}
|
|
13
|
+
renamed(name, modifiedAt) {
|
|
14
|
+
return new ThreadListEntry({
|
|
15
|
+
path: this.path,
|
|
16
|
+
name,
|
|
17
|
+
createdAt: this.createdAt,
|
|
18
|
+
modifiedAt,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
static fromDatasetRow(row) {
|
|
22
|
+
const rawPath = row.path;
|
|
23
|
+
if (typeof rawPath !== "string" || rawPath.trim() === "") {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const threadPath = rawPath.trim();
|
|
27
|
+
const rawName = row.name;
|
|
28
|
+
const rawCreatedAt = row.created_at;
|
|
29
|
+
const rawModifiedAt = row.modified_at;
|
|
30
|
+
return new ThreadListEntry({
|
|
31
|
+
path: threadPath,
|
|
32
|
+
name: typeof rawName === "string" && rawName.trim() !== "" ? rawName.trim() : defaultThreadDisplayNameFromPath(threadPath),
|
|
33
|
+
createdAt: typeof rawCreatedAt === "string" ? rawCreatedAt : "",
|
|
34
|
+
modifiedAt: typeof rawModifiedAt === "string" ? rawModifiedAt : "",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export class ThreadStorage {
|
|
39
|
+
}
|
|
40
|
+
export class ThreadStorageRepository extends ChangeEmitter {
|
|
41
|
+
}
|
|
42
|
+
export class DatasetThreadListRef {
|
|
43
|
+
constructor({ namespace, table }) {
|
|
44
|
+
this.namespace = namespace;
|
|
45
|
+
this.table = table;
|
|
46
|
+
}
|
|
47
|
+
static parse(url) {
|
|
48
|
+
let path = url.trim();
|
|
49
|
+
if (!path.startsWith("dataset://")) {
|
|
50
|
+
throw new TypeError("dataset thread list URL must start with dataset://");
|
|
51
|
+
}
|
|
52
|
+
path = path.slice("dataset://".length);
|
|
53
|
+
if (path.startsWith("/")) {
|
|
54
|
+
throw new TypeError("dataset thread list URL must use dataset://path");
|
|
55
|
+
}
|
|
56
|
+
const parts = path.split("/").map((part) => part.trim()).filter((part) => part !== "");
|
|
57
|
+
if (parts.length === 0) {
|
|
58
|
+
throw new TypeError("dataset thread list URL must include a table name");
|
|
59
|
+
}
|
|
60
|
+
return new DatasetThreadListRef({
|
|
61
|
+
namespace: parts.length === 1 ? undefined : parts.slice(0, -1),
|
|
62
|
+
table: parts[parts.length - 1],
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export class DatasetThreadStorageRepository extends ThreadStorageRepository {
|
|
67
|
+
constructor({ room, path }) {
|
|
68
|
+
super();
|
|
69
|
+
this.entriesByPath = new Map();
|
|
70
|
+
this.initialSnapshotReady = false;
|
|
71
|
+
this.closed = true;
|
|
72
|
+
this.room = room;
|
|
73
|
+
this.path = path.trim();
|
|
74
|
+
this.ref = DatasetThreadListRef.parse(this.path);
|
|
75
|
+
}
|
|
76
|
+
async open() {
|
|
77
|
+
this.closed = false;
|
|
78
|
+
await this.ensureTable();
|
|
79
|
+
this.abortController = new AbortController();
|
|
80
|
+
const ready = new Promise((resolve, reject) => {
|
|
81
|
+
this.watchTask = this.watchLoop(resolve, reject);
|
|
82
|
+
});
|
|
83
|
+
await ready;
|
|
84
|
+
}
|
|
85
|
+
async watchLoop(resolveReady, rejectReady) {
|
|
86
|
+
let readyResolved = false;
|
|
87
|
+
try {
|
|
88
|
+
for await (const event of this.room.datasets.watchTable({ table: this.ref.table, namespace: this.ref.namespace })) {
|
|
89
|
+
if (this.closed) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const initialSnapshotCompleted = this.handleWatchEvent(event);
|
|
93
|
+
if (initialSnapshotCompleted && !readyResolved) {
|
|
94
|
+
readyResolved = true;
|
|
95
|
+
resolveReady();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!readyResolved && !this.closed) {
|
|
99
|
+
rejectReady(new Error("Dataset thread list watch closed before initial snapshot."));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (this.closed) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (!readyResolved) {
|
|
107
|
+
rejectReady(error);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async close() {
|
|
115
|
+
this.abortController?.abort();
|
|
116
|
+
this.abortController = undefined;
|
|
117
|
+
this.initialSnapshotReady = false;
|
|
118
|
+
this.closed = true;
|
|
119
|
+
this.watchTask = undefined;
|
|
120
|
+
const refreshEntriesFuture = this.refreshEntriesFuture;
|
|
121
|
+
this.refreshEntriesFuture = undefined;
|
|
122
|
+
if (refreshEntriesFuture != null) {
|
|
123
|
+
try {
|
|
124
|
+
await refreshEntriesFuture;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
entries() {
|
|
131
|
+
return [...this.entriesByPath.values()];
|
|
132
|
+
}
|
|
133
|
+
async addOrUpdateThread(entry) {
|
|
134
|
+
await this.ensureTable();
|
|
135
|
+
await this.room.datasets.merge({
|
|
136
|
+
table: this.ref.table,
|
|
137
|
+
on: "path",
|
|
138
|
+
records: this.batchFor([entry]),
|
|
139
|
+
namespace: this.ref.namespace,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async deleteThread(threadPath) {
|
|
143
|
+
const normalized = normalizeThreadPath(threadPath);
|
|
144
|
+
await this.ensureTable();
|
|
145
|
+
await this.room.datasets.delete({
|
|
146
|
+
table: this.ref.table,
|
|
147
|
+
where: pathWhere(normalized),
|
|
148
|
+
namespace: this.ref.namespace,
|
|
149
|
+
});
|
|
150
|
+
if (this.entriesByPath.delete(normalized)) {
|
|
151
|
+
this.notifyListeners();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async renameThread(threadPath, name) {
|
|
155
|
+
const normalized = normalizeThreadPath(threadPath);
|
|
156
|
+
const trimmedName = name.trim();
|
|
157
|
+
if (trimmedName === "") {
|
|
158
|
+
throw new TypeError("thread name cannot be empty");
|
|
159
|
+
}
|
|
160
|
+
const now = new Date().toISOString();
|
|
161
|
+
const existing = this.entriesByPath.get(normalized);
|
|
162
|
+
await this.addOrUpdateThread(new ThreadListEntry({
|
|
163
|
+
path: normalized,
|
|
164
|
+
name: trimmedName,
|
|
165
|
+
createdAt: existing?.createdAt ?? now,
|
|
166
|
+
modifiedAt: now,
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
async ensureTable() {
|
|
170
|
+
await this.room.datasets.createTableWithSchema({
|
|
171
|
+
name: this.ref.table,
|
|
172
|
+
schema: DatasetThreadStorageRepository.schema,
|
|
173
|
+
mode: "create_if_not_exists",
|
|
174
|
+
namespace: this.ref.namespace,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
handleWatchEvent(event) {
|
|
178
|
+
const rows = event.table == null ? [] : rowsFromTable(event.table);
|
|
179
|
+
if (event.phase === "initial") {
|
|
180
|
+
for (const row of rows) {
|
|
181
|
+
const entry = ThreadListEntry.fromDatasetRow(row);
|
|
182
|
+
if (entry != null) {
|
|
183
|
+
this.entriesByPath.set(entry.path, entry);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (event.kind === "ready") {
|
|
187
|
+
this.initialSnapshotReady = true;
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
if (this.initialSnapshotReady && rows.length > 0) {
|
|
191
|
+
this.notifyListeners();
|
|
192
|
+
}
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
if (event.kind === "ready") {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
const changeType = event.changeType?.trim().toLowerCase() ?? "";
|
|
199
|
+
if (changeType === "deleted" ||
|
|
200
|
+
changeType === "delete" ||
|
|
201
|
+
changeType === "removed" ||
|
|
202
|
+
changeType === "remove" ||
|
|
203
|
+
event.deletePredicate != null ||
|
|
204
|
+
event.kind.trim().toLowerCase() === "delete") {
|
|
205
|
+
let removed = false;
|
|
206
|
+
for (const row of rows) {
|
|
207
|
+
const entry = ThreadListEntry.fromDatasetRow(row);
|
|
208
|
+
if (entry != null) {
|
|
209
|
+
removed = this.entriesByPath.delete(entry.path) || removed;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const deletedPath = threadPathFromDeletePredicate(event.deletePredicate);
|
|
213
|
+
if (deletedPath != null) {
|
|
214
|
+
removed = this.entriesByPath.delete(deletedPath) || removed;
|
|
215
|
+
}
|
|
216
|
+
if (rows.length === 0 && deletedPath == null) {
|
|
217
|
+
const refreshEntriesFuture = this.refreshEntries();
|
|
218
|
+
this.refreshEntriesFuture = refreshEntriesFuture;
|
|
219
|
+
void refreshEntriesFuture.catch(() => undefined);
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
if (removed || rows.length === 0) {
|
|
223
|
+
this.notifyListeners();
|
|
224
|
+
}
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
let changed = false;
|
|
228
|
+
for (const row of rows) {
|
|
229
|
+
const entry = ThreadListEntry.fromDatasetRow(row);
|
|
230
|
+
if (entry != null) {
|
|
231
|
+
this.entriesByPath.set(entry.path, entry);
|
|
232
|
+
changed = true;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (changed) {
|
|
236
|
+
this.notifyListeners();
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
async refreshEntries() {
|
|
241
|
+
if (this.closed) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const table = await this.room.datasets.search({
|
|
245
|
+
table: this.ref.table,
|
|
246
|
+
namespace: this.ref.namespace,
|
|
247
|
+
select: ["path", "name", "created_at", "modified_at"],
|
|
248
|
+
});
|
|
249
|
+
if (this.closed) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const nextEntries = new Map();
|
|
253
|
+
for (const row of rowsFromTable(table)) {
|
|
254
|
+
const entry = ThreadListEntry.fromDatasetRow(row);
|
|
255
|
+
if (entry != null) {
|
|
256
|
+
nextEntries.set(entry.path, entry);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
this.entriesByPath.clear();
|
|
260
|
+
for (const [path, entry] of nextEntries) {
|
|
261
|
+
this.entriesByPath.set(path, entry);
|
|
262
|
+
}
|
|
263
|
+
if (!this.closed) {
|
|
264
|
+
this.notifyListeners();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
batchFor(entries) {
|
|
268
|
+
return tableFromArrays({
|
|
269
|
+
path: entries.map((entry) => entry.path),
|
|
270
|
+
name: entries.map((entry) => entry.name),
|
|
271
|
+
created_at: entries.map((entry) => entry.createdAt),
|
|
272
|
+
modified_at: entries.map((entry) => entry.modifiedAt),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
DatasetThreadStorageRepository.schema = new Schema([
|
|
277
|
+
new Field("path", new Utf8(), false),
|
|
278
|
+
new Field("name", new Utf8(), true),
|
|
279
|
+
new Field("created_at", new Utf8(), true),
|
|
280
|
+
new Field("modified_at", new Utf8(), true),
|
|
281
|
+
]);
|
|
282
|
+
export class AgentThreadStorageRepository extends ThreadStorageRepository {
|
|
283
|
+
constructor({ chatClient, limit = 200 }) {
|
|
284
|
+
super();
|
|
285
|
+
this.entriesByPath = new Map();
|
|
286
|
+
this.closed = true;
|
|
287
|
+
this.chatClient = chatClient;
|
|
288
|
+
this.limit = limit;
|
|
289
|
+
}
|
|
290
|
+
async open() {
|
|
291
|
+
if (!this.closed) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
this.closed = false;
|
|
295
|
+
const iterator = this.chatClient.events[Symbol.asyncIterator]();
|
|
296
|
+
let active = true;
|
|
297
|
+
this.subscription = () => { active = false; };
|
|
298
|
+
void (async () => {
|
|
299
|
+
while (active && !this.closed) {
|
|
300
|
+
const next = await iterator.next();
|
|
301
|
+
if (next.done) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
this.handleEvent(next.value);
|
|
305
|
+
}
|
|
306
|
+
})();
|
|
307
|
+
await new Promise((resolve) => {
|
|
308
|
+
this.pendingOpen = { resolve };
|
|
309
|
+
void this.requestList();
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
async close() {
|
|
313
|
+
this.closed = true;
|
|
314
|
+
this.subscription?.();
|
|
315
|
+
this.subscription = undefined;
|
|
316
|
+
this.pendingListMessageId = undefined;
|
|
317
|
+
this.pendingOpen?.resolve();
|
|
318
|
+
this.pendingOpen = undefined;
|
|
319
|
+
}
|
|
320
|
+
entries() {
|
|
321
|
+
return [...this.entriesByPath.values()];
|
|
322
|
+
}
|
|
323
|
+
async addOrUpdateThread(_entry) {
|
|
324
|
+
throw new Error("Agent thread storage cannot directly upsert thread entries.");
|
|
325
|
+
}
|
|
326
|
+
async deleteThread(threadPath) {
|
|
327
|
+
const normalized = normalizeThreadPath(threadPath);
|
|
328
|
+
await this.chatClient.sendAgentMessage(new DeleteThread({ threadId: normalized }));
|
|
329
|
+
if (this.entriesByPath.delete(normalized)) {
|
|
330
|
+
this.notifyListeners();
|
|
331
|
+
}
|
|
332
|
+
void this.requestList();
|
|
333
|
+
}
|
|
334
|
+
async renameThread(threadPath, name) {
|
|
335
|
+
const normalized = normalizeThreadPath(threadPath);
|
|
336
|
+
const trimmedName = name.trim();
|
|
337
|
+
if (trimmedName === "") {
|
|
338
|
+
throw new TypeError("thread name cannot be empty");
|
|
339
|
+
}
|
|
340
|
+
await this.chatClient.sendAgentMessage(new RenameThread({ threadId: normalized, name: trimmedName }));
|
|
341
|
+
const existing = this.entriesByPath.get(normalized);
|
|
342
|
+
if (existing != null) {
|
|
343
|
+
this.entriesByPath.set(normalized, existing.renamed(trimmedName, new Date().toISOString()));
|
|
344
|
+
this.notifyListeners();
|
|
345
|
+
}
|
|
346
|
+
void this.requestList();
|
|
347
|
+
}
|
|
348
|
+
async requestList() {
|
|
349
|
+
if (this.closed) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const request = new ListThreads({ limit: this.limit, offset: 0 });
|
|
353
|
+
this.pendingListMessageId = request.messageId;
|
|
354
|
+
await this.chatClient.sendAgentMessage(request, { ignoreOffline: true });
|
|
355
|
+
}
|
|
356
|
+
handleEvent(event) {
|
|
357
|
+
const message = event.message;
|
|
358
|
+
if (message.type === "meshagent.agent.thread.listed") {
|
|
359
|
+
this.handleThreadsListed(message);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (message.type === "meshagent.agent.thread.created") {
|
|
363
|
+
this.upsertThread(message.thread);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (message.type === "meshagent.agent.thread.updated") {
|
|
367
|
+
this.upsertThread(message.thread);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (message.type === "meshagent.agent.thread.deleted") {
|
|
371
|
+
const normalized = normalizeThreadPath(String(message.path));
|
|
372
|
+
if (this.entriesByPath.delete(normalized) && !this.closed) {
|
|
373
|
+
this.notifyListeners();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
handleThreadsListed(message) {
|
|
378
|
+
const nextEntries = new Map();
|
|
379
|
+
for (const thread of message.threads) {
|
|
380
|
+
const entry = this.entryFromAgentThread(thread);
|
|
381
|
+
if (entry != null) {
|
|
382
|
+
nextEntries.set(entry.path, entry);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
this.entriesByPath.clear();
|
|
386
|
+
for (const [path, entry] of nextEntries) {
|
|
387
|
+
this.entriesByPath.set(path, entry);
|
|
388
|
+
}
|
|
389
|
+
if (this.pendingOpen != null && (this.pendingListMessageId == null || message.sourceMessageId === this.pendingListMessageId)) {
|
|
390
|
+
this.pendingOpen.resolve();
|
|
391
|
+
this.pendingOpen = undefined;
|
|
392
|
+
}
|
|
393
|
+
if (!this.closed) {
|
|
394
|
+
this.notifyListeners();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
upsertThread(thread) {
|
|
398
|
+
const entry = this.entryFromAgentThread(thread);
|
|
399
|
+
if (entry == null) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
this.entriesByPath.set(entry.path, entry);
|
|
403
|
+
if (!this.closed) {
|
|
404
|
+
this.notifyListeners();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
entryFromAgentThread(thread) {
|
|
408
|
+
const path = thread.path.trim();
|
|
409
|
+
if (path === "") {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
const name = thread.name.trim();
|
|
413
|
+
return new ThreadListEntry({
|
|
414
|
+
path,
|
|
415
|
+
name: name !== "" ? name : defaultThreadDisplayNameFromPath(path),
|
|
416
|
+
createdAt: thread.createdAt,
|
|
417
|
+
modifiedAt: thread.modifiedAt,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
export class DatasetThreadStorage extends DatasetThreadStorageRepository {
|
|
422
|
+
}
|
|
423
|
+
export function defaultThreadDisplayNameFromPath(path) {
|
|
424
|
+
const segments = path.split("/").filter((segment) => segment !== "");
|
|
425
|
+
const basename = segments.length === 0 ? path : segments[segments.length - 1];
|
|
426
|
+
const rawName = basename.endsWith(".thread") ? basename.slice(0, -".thread".length) : basename;
|
|
427
|
+
const trimmed = rawName.trim();
|
|
428
|
+
if (trimmed === "" || uuidPattern.test(trimmed)) {
|
|
429
|
+
return defaultUntitledThreadName;
|
|
430
|
+
}
|
|
431
|
+
const normalized = trimmed.replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
|
|
432
|
+
if (normalized === "") {
|
|
433
|
+
return defaultUntitledThreadName;
|
|
434
|
+
}
|
|
435
|
+
return normalized
|
|
436
|
+
.split(" ")
|
|
437
|
+
.filter((segment) => segment !== "")
|
|
438
|
+
.map((segment) => segment.length === 1 ? segment.toUpperCase() : `${segment[0].toUpperCase()}${segment.slice(1)}`)
|
|
439
|
+
.join(" ");
|
|
440
|
+
}
|
|
441
|
+
function normalizeThreadPath(threadPath) {
|
|
442
|
+
const normalized = threadPath.trim();
|
|
443
|
+
if (normalized === "") {
|
|
444
|
+
throw new TypeError("thread path cannot be empty");
|
|
445
|
+
}
|
|
446
|
+
return normalized;
|
|
447
|
+
}
|
|
448
|
+
function pathWhere(threadPath) {
|
|
449
|
+
return `path = ${datasetStringLiteral(threadPath)}`;
|
|
450
|
+
}
|
|
451
|
+
function datasetStringLiteral(value) {
|
|
452
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
453
|
+
}
|
|
454
|
+
function threadPathFromDeletePredicate(predicate) {
|
|
455
|
+
const trimmed = predicate?.trim();
|
|
456
|
+
if (trimmed == null || trimmed === "") {
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
const match = /^path\s*=\s*(['"])(.*)\1$/i.exec(trimmed);
|
|
460
|
+
return match == null ? undefined : match[2].replace(/''/g, "'");
|
|
461
|
+
}
|
|
462
|
+
function rowsFromTable(table) {
|
|
463
|
+
const tables = Array.isArray(table) ? table : [table];
|
|
464
|
+
return tables.flatMap((entry) => entry.toArray().map((row) => ({ ...row })));
|
|
465
|
+
}
|