@batalabs/virlow-mcp-core 3.19.0 → 3.68.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/LICENSE +8 -18
- package/README.md +1 -1
- package/dist/api.d.ts +14 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +6 -0
- package/dist/api.js.map +1 -1
- package/dist/attrs-extract.d.ts +48 -0
- package/dist/attrs-extract.d.ts.map +1 -0
- package/dist/attrs-extract.js +153 -0
- package/dist/attrs-extract.js.map +1 -0
- package/dist/attrs.d.ts +445 -0
- package/dist/attrs.d.ts.map +1 -0
- package/dist/attrs.js +604 -0
- package/dist/attrs.js.map +1 -0
- package/dist/cjs/api.d.ts +14 -0
- package/dist/cjs/api.js +6 -0
- package/dist/cjs/attrs-extract.d.ts +47 -0
- package/dist/cjs/attrs-extract.js +159 -0
- package/dist/cjs/attrs.d.ts +444 -0
- package/dist/cjs/attrs.js +635 -0
- package/dist/cjs/index.d.ts +4 -0
- package/dist/cjs/index.js +22 -1
- package/dist/cjs/memories.d.ts +79 -1
- package/dist/cjs/memories.js +249 -15
- package/dist/cjs/memory-graph.d.ts +105 -0
- package/dist/cjs/memory-graph.js +270 -0
- package/dist/cjs/memory-note.d.ts +57 -0
- package/dist/cjs/memory-note.js +174 -10
- package/dist/cjs/memory-secrets.d.ts +19 -0
- package/dist/cjs/memory-secrets.js +108 -0
- package/dist/cjs/notes.d.ts +208 -2
- package/dist/cjs/notes.js +721 -12
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/memories.d.ts +79 -1
- package/dist/memories.d.ts.map +1 -1
- package/dist/memories.js +249 -15
- package/dist/memories.js.map +1 -1
- package/dist/memory-graph.d.ts +106 -0
- package/dist/memory-graph.d.ts.map +1 -0
- package/dist/memory-graph.js +258 -0
- package/dist/memory-graph.js.map +1 -0
- package/dist/memory-note.d.ts +57 -0
- package/dist/memory-note.d.ts.map +1 -1
- package/dist/memory-note.js +170 -10
- package/dist/memory-note.js.map +1 -1
- package/dist/memory-secrets.d.ts +20 -0
- package/dist/memory-secrets.d.ts.map +1 -0
- package/dist/memory-secrets.js +103 -0
- package/dist/memory-secrets.js.map +1 -0
- package/dist/notes.d.ts +208 -2
- package/dist/notes.d.ts.map +1 -1
- package/dist/notes.js +722 -13
- package/dist/notes.js.map +1 -1
- package/dist/tools.d.ts +2 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +464 -9
- package/dist/tools.js.map +1 -1
- package/package.json +15 -5
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SCHEMA_MAX_BYTES = exports.ATTRS_MAX_BYTES = exports.TAGS_PROP = exports.CREATED = exports.LAST_CHANGED = exports.HOME_PRESET_LABELS = exports.HOME_PRESETS = exports.DEFAULT_HOME_WIDGETS = exports.HOME_SUMMARY_MAX = exports.HOME_TEXT_MAX = exports.HOME_TITLE_MAX = exports.PINNED_MAX = void 0;
|
|
4
|
+
exports.isPropertyFilter = isPropertyFilter;
|
|
5
|
+
exports.matchesFilter = matchesFilter;
|
|
6
|
+
exports.matchesAll = matchesAll;
|
|
7
|
+
exports.isHomeSort = isHomeSort;
|
|
8
|
+
exports.spanOf = spanOf;
|
|
9
|
+
exports.isHomeWidget = isHomeWidget;
|
|
10
|
+
exports.withHomeWidgets = withHomeWidgets;
|
|
11
|
+
exports.newWidgetId = newWidgetId;
|
|
12
|
+
exports.homeWidgets = homeWidgets;
|
|
13
|
+
exports.isHomePreset = isHomePreset;
|
|
14
|
+
exports.presetWidgets = presetWidgets;
|
|
15
|
+
exports.isPinned = isPinned;
|
|
16
|
+
exports.withPinToggled = withPinToggled;
|
|
17
|
+
exports.isNoteDate = isNoteDate;
|
|
18
|
+
exports.formatProp = formatProp;
|
|
19
|
+
exports.emptyAttrs = emptyAttrs;
|
|
20
|
+
exports.emptySchema = emptySchema;
|
|
21
|
+
exports.newPropId = newPropId;
|
|
22
|
+
exports.sortedProps = sortedProps;
|
|
23
|
+
exports.defineProp = defineProp;
|
|
24
|
+
exports.updateProp = updateProp;
|
|
25
|
+
exports.removeProp = removeProp;
|
|
26
|
+
exports.reorderProps = reorderProps;
|
|
27
|
+
exports.encodeAttrs = encodeAttrs;
|
|
28
|
+
exports.decodeAttrs = decodeAttrs;
|
|
29
|
+
exports.encodeSchema = encodeSchema;
|
|
30
|
+
exports.decodeSchema = decodeSchema;
|
|
31
|
+
exports.tagsFromAttrs = tagsFromAttrs;
|
|
32
|
+
exports.withTags = withTags;
|
|
33
|
+
const virlow_crypto_1 = require("@batalabs/virlow-crypto");
|
|
34
|
+
const FILTER_OPS = ['is', 'has', 'checked', 'unchecked', 'empty', 'notEmpty'];
|
|
35
|
+
function isPropertyFilter(value) {
|
|
36
|
+
if (!isRecord(value))
|
|
37
|
+
return false;
|
|
38
|
+
if (typeof value.propId !== 'string' || value.propId === '')
|
|
39
|
+
return false;
|
|
40
|
+
if (!FILTER_OPS.includes(value.op))
|
|
41
|
+
return false;
|
|
42
|
+
if (value.op === 'is' || value.op === 'has')
|
|
43
|
+
return typeof value.value === 'string';
|
|
44
|
+
return value.value === undefined;
|
|
45
|
+
}
|
|
46
|
+
/** Whether a value counts as absent. An empty string and an empty list read as
|
|
47
|
+
* nothing to the person looking at the row, so they filter as nothing too. */
|
|
48
|
+
function isEmptyValue(value) {
|
|
49
|
+
if (value === undefined)
|
|
50
|
+
return true;
|
|
51
|
+
switch (value.t) {
|
|
52
|
+
case 'text':
|
|
53
|
+
case 'url':
|
|
54
|
+
case 'select':
|
|
55
|
+
case 'date':
|
|
56
|
+
return value.v === '';
|
|
57
|
+
case 'multiSelect':
|
|
58
|
+
case 'relation':
|
|
59
|
+
return value.v.length === 0;
|
|
60
|
+
case 'number':
|
|
61
|
+
return Number.isNaN(value.v);
|
|
62
|
+
case 'checkbox':
|
|
63
|
+
// A checkbox is never absent: false is an answer, not a blank.
|
|
64
|
+
return false;
|
|
65
|
+
default:
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function matchesFilter(attrs, filter) {
|
|
70
|
+
const value = attrs?.props[filter.propId];
|
|
71
|
+
switch (filter.op) {
|
|
72
|
+
case 'is':
|
|
73
|
+
return value?.t === 'select' && value.v === filter.value;
|
|
74
|
+
case 'has':
|
|
75
|
+
return value?.t === 'multiSelect' && value.v.includes(filter.value);
|
|
76
|
+
case 'checked':
|
|
77
|
+
return value?.t === 'checkbox' && value.v === true;
|
|
78
|
+
case 'unchecked':
|
|
79
|
+
// Unticked and never touched are the same thing to the reader, so a note
|
|
80
|
+
// with no value for the box counts as unchecked rather than dropping out
|
|
81
|
+
// of both halves of the filter.
|
|
82
|
+
return value === undefined || (value.t === 'checkbox' && value.v === false);
|
|
83
|
+
case 'empty':
|
|
84
|
+
return isEmptyValue(value);
|
|
85
|
+
case 'notEmpty':
|
|
86
|
+
return !isEmptyValue(value);
|
|
87
|
+
default:
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Every filter passes. Filters across different properties are an AND: each
|
|
92
|
+
* one narrows what the last one left, which is what picking two of them from
|
|
93
|
+
* a menu looks like it should do. */
|
|
94
|
+
function matchesAll(attrs, filters) {
|
|
95
|
+
return (filters ?? []).every((filter) => matchesFilter(attrs, filter));
|
|
96
|
+
}
|
|
97
|
+
function isHomeSort(value) {
|
|
98
|
+
return (isRecord(value) &&
|
|
99
|
+
typeof value.by === 'string' &&
|
|
100
|
+
value.by !== '' &&
|
|
101
|
+
(value.dir === 'asc' || value.dir === 'desc'));
|
|
102
|
+
}
|
|
103
|
+
const PINNED_LOOKS = ['list', 'notes'];
|
|
104
|
+
/** The most notes one pinned block holds. Past this it is a listing, and a
|
|
105
|
+
* table with a filter is the block for a listing. */
|
|
106
|
+
exports.PINNED_MAX = 50;
|
|
107
|
+
/** The longest a table block's title may be: a heading, not a description. */
|
|
108
|
+
exports.HOME_TITLE_MAX = 60;
|
|
109
|
+
/** The longest a `text` widget may be. A home is a surface, not a document;
|
|
110
|
+
* anything longer belongs in a note. */
|
|
111
|
+
exports.HOME_TEXT_MAX = 280;
|
|
112
|
+
/** The longest a `summary` may be. Room for a paragraph or two of context,
|
|
113
|
+
* and short of the point where the dashboard becomes a document. */
|
|
114
|
+
exports.HOME_SUMMARY_MAX = 2000;
|
|
115
|
+
const HOME_KINDS = [
|
|
116
|
+
'counts',
|
|
117
|
+
'recent',
|
|
118
|
+
'starred',
|
|
119
|
+
'breakdown',
|
|
120
|
+
'upcoming',
|
|
121
|
+
'text',
|
|
122
|
+
'summary',
|
|
123
|
+
'table',
|
|
124
|
+
'calendar',
|
|
125
|
+
'board',
|
|
126
|
+
'activity',
|
|
127
|
+
'stale',
|
|
128
|
+
'tasks',
|
|
129
|
+
'pinned',
|
|
130
|
+
'quick',
|
|
131
|
+
];
|
|
132
|
+
const SPANS = ['half', 'full'];
|
|
133
|
+
/**
|
|
134
|
+
* A block's width when it has not been given one.
|
|
135
|
+
*
|
|
136
|
+
* A table's columns and a month grid need the room; a stat row and a short
|
|
137
|
+
* list do not, and pairing them is what makes the page read as a dashboard
|
|
138
|
+
* rather than a column of cards.
|
|
139
|
+
*/
|
|
140
|
+
const DEFAULT_SPAN = {
|
|
141
|
+
counts: 'half',
|
|
142
|
+
recent: 'half',
|
|
143
|
+
starred: 'half',
|
|
144
|
+
breakdown: 'half',
|
|
145
|
+
upcoming: 'half',
|
|
146
|
+
text: 'full',
|
|
147
|
+
summary: 'full',
|
|
148
|
+
table: 'full',
|
|
149
|
+
calendar: 'full',
|
|
150
|
+
board: 'full',
|
|
151
|
+
activity: 'half',
|
|
152
|
+
stale: 'half',
|
|
153
|
+
tasks: 'half',
|
|
154
|
+
pinned: 'half',
|
|
155
|
+
quick: 'full',
|
|
156
|
+
};
|
|
157
|
+
function spanOf(widget) {
|
|
158
|
+
return widget.span ?? DEFAULT_SPAN[widget.kind] ?? 'half';
|
|
159
|
+
}
|
|
160
|
+
/** True for a block this build can draw. Exported so the MCP layer can refuse
|
|
161
|
+
* one at the tool boundary rather than writing it and skipping it on read. */
|
|
162
|
+
function isHomeWidget(value) {
|
|
163
|
+
if (!isRecord(value))
|
|
164
|
+
return false;
|
|
165
|
+
if (typeof value.id !== 'string')
|
|
166
|
+
return false;
|
|
167
|
+
if (!HOME_KINDS.includes(value.kind))
|
|
168
|
+
return false;
|
|
169
|
+
// An unknown span is dropped rather than rendered at some guessed width.
|
|
170
|
+
if (value.span !== undefined && !SPANS.includes(value.span))
|
|
171
|
+
return false;
|
|
172
|
+
if (value.kind === 'breakdown' ||
|
|
173
|
+
value.kind === 'upcoming' ||
|
|
174
|
+
value.kind === 'calendar' ||
|
|
175
|
+
value.kind === 'board') {
|
|
176
|
+
return typeof value.propId === 'string';
|
|
177
|
+
}
|
|
178
|
+
if (value.kind === 'table' || value.kind === 'tasks') {
|
|
179
|
+
// A malformed filter is refused rather than ignored: a table that silently
|
|
180
|
+
// shows everything is wrong in the way that is hardest to notice.
|
|
181
|
+
if (value.filters !== undefined) {
|
|
182
|
+
if (!Array.isArray(value.filters) || !value.filters.every(isPropertyFilter))
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
if (value.kind === 'table') {
|
|
186
|
+
if (value.sort !== undefined && !isHomeSort(value.sort))
|
|
187
|
+
return false;
|
|
188
|
+
if (value.title !== undefined) {
|
|
189
|
+
if (typeof value.title !== 'string' || value.title.length > exports.HOME_TITLE_MAX)
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
if (value.kind === 'pinned') {
|
|
196
|
+
if (!Array.isArray(value.noteIds) ||
|
|
197
|
+
value.noteIds.length > exports.PINNED_MAX ||
|
|
198
|
+
!value.noteIds.every((id) => typeof id === 'string' && id !== ''))
|
|
199
|
+
return false;
|
|
200
|
+
if (value.look !== undefined && !PINNED_LOOKS.includes(value.look))
|
|
201
|
+
return false;
|
|
202
|
+
if (value.colors !== undefined) {
|
|
203
|
+
if (!isRecord(value.colors))
|
|
204
|
+
return false;
|
|
205
|
+
if (!Object.values(value.colors).every((c) => typeof c === 'string' && c.length <= 24))
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
if (value.kind === 'text')
|
|
211
|
+
return typeof value.body === 'string';
|
|
212
|
+
if (value.kind === 'summary') {
|
|
213
|
+
if (typeof value.body !== 'string')
|
|
214
|
+
return false;
|
|
215
|
+
return value.at === undefined || typeof value.at === 'string';
|
|
216
|
+
}
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* What a workspace shows when nobody has arranged its home.
|
|
221
|
+
*
|
|
222
|
+
* A default rather than an empty screen: a workspace should be worth opening
|
|
223
|
+
* the first time, and these three need no configuration to mean something.
|
|
224
|
+
*/
|
|
225
|
+
exports.DEFAULT_HOME_WIDGETS = [
|
|
226
|
+
{ id: 'default-counts', kind: 'counts' },
|
|
227
|
+
{ id: 'default-recent', kind: 'recent', limit: 5 },
|
|
228
|
+
{ id: 'default-starred', kind: 'starred', limit: 5 },
|
|
229
|
+
];
|
|
230
|
+
/**
|
|
231
|
+
* The widgets to draw, dropping any this build cannot read.
|
|
232
|
+
*
|
|
233
|
+
* Mirrors sortedProps: a newer client may write a kind this one does not
|
|
234
|
+
* know, and the right answer is to skip it rather than to throw away the
|
|
235
|
+
* whole home or crash rendering it.
|
|
236
|
+
*/
|
|
237
|
+
/**
|
|
238
|
+
* A home with `widgets` written out in full.
|
|
239
|
+
*
|
|
240
|
+
* The first edit to a workspace that has never been arranged has to write the
|
|
241
|
+
* defaults down alongside the change. Otherwise removing a default block does
|
|
242
|
+
* nothing: homeWidgets would see an empty list, fall back, and put it
|
|
243
|
+
* straight back. Materialising turns "no opinion" into "this arrangement",
|
|
244
|
+
* which is what an edit means.
|
|
245
|
+
*/
|
|
246
|
+
function withHomeWidgets(schema, widgets) {
|
|
247
|
+
return { ...schema, home: { ...schema.home, widgets } };
|
|
248
|
+
}
|
|
249
|
+
/** A widget id that will not collide with another in the same home. */
|
|
250
|
+
function newWidgetId() {
|
|
251
|
+
const bytes = crypto.getRandomValues(new Uint8Array(6));
|
|
252
|
+
return [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
253
|
+
}
|
|
254
|
+
function homeWidgets(schema,
|
|
255
|
+
/** The workspace's properties. Given them, an unarranged home leads with a
|
|
256
|
+
* calendar when there is a date property to place notes on. Omitted, the
|
|
257
|
+
* default is the plain three, which is what a caller with no schema to
|
|
258
|
+
* hand should get. */
|
|
259
|
+
defs) {
|
|
260
|
+
const configured = schema?.home?.widgets;
|
|
261
|
+
if (!Array.isArray(configured))
|
|
262
|
+
return defaultWidgets(defs);
|
|
263
|
+
const known = configured.filter(isHomeWidget);
|
|
264
|
+
return known.length > 0 ? known : defaultWidgets(defs);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* What an unarranged workspace shows.
|
|
268
|
+
*
|
|
269
|
+
* A calendar first: dates are what people open a workspace to see, and a
|
|
270
|
+
* month grid answers "what is happening here" in a way three counts cannot.
|
|
271
|
+
*/
|
|
272
|
+
function defaultWidgets(defs) {
|
|
273
|
+
// A workspace's own date property when it has one, and otherwise the notes'
|
|
274
|
+
// edit times, so every workspace opens on a calendar rather than only the
|
|
275
|
+
// ones that have been configured.
|
|
276
|
+
const date = defs?.find((def) => def.type === 'date');
|
|
277
|
+
return [
|
|
278
|
+
{
|
|
279
|
+
id: 'default-calendar',
|
|
280
|
+
kind: 'calendar',
|
|
281
|
+
propId: date ? date.id : exports.LAST_CHANGED,
|
|
282
|
+
},
|
|
283
|
+
...exports.DEFAULT_HOME_WIDGETS,
|
|
284
|
+
];
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Starting arrangements.
|
|
288
|
+
*
|
|
289
|
+
* An empty page is where most people stall: every block exists, and none of
|
|
290
|
+
* them says which six to put together for a project. A preset is that
|
|
291
|
+
* answer written down, built from the workspace's own properties where it
|
|
292
|
+
* has them (a board wants a select, a calendar wants a date) and from the
|
|
293
|
+
* notes' own dates where it does not, so every preset draws something on
|
|
294
|
+
* the workspace it is applied to. Applying one replaces the arrangement,
|
|
295
|
+
* and the one it replaced is kept for undo like any other change.
|
|
296
|
+
*/
|
|
297
|
+
exports.HOME_PRESETS = ['project', 'research', 'journal'];
|
|
298
|
+
exports.HOME_PRESET_LABELS = {
|
|
299
|
+
project: {
|
|
300
|
+
name: 'Project',
|
|
301
|
+
blurb: 'Open tasks, a board by stage, a calendar, and what has gone quiet.',
|
|
302
|
+
},
|
|
303
|
+
research: {
|
|
304
|
+
name: 'Research',
|
|
305
|
+
blurb: 'Pinned sources, the latest additions, and a calendar of when things arrived.',
|
|
306
|
+
},
|
|
307
|
+
journal: {
|
|
308
|
+
name: 'Journal',
|
|
309
|
+
blurb: 'This week, recent entries, open tasks, and how often you write.',
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
function isHomePreset(value) {
|
|
313
|
+
return exports.HOME_PRESETS.includes(value);
|
|
314
|
+
}
|
|
315
|
+
function presetWidgets(preset, defs = [], id = newWidgetId) {
|
|
316
|
+
const select = defs.find((d) => d.type === 'select');
|
|
317
|
+
const date = defs.find((d) => d.type === 'date');
|
|
318
|
+
switch (preset) {
|
|
319
|
+
case 'project':
|
|
320
|
+
return [
|
|
321
|
+
{ id: id(), kind: 'tasks', limit: 10, span: 'half' },
|
|
322
|
+
{ id: id(), kind: 'counts', span: 'half' },
|
|
323
|
+
select
|
|
324
|
+
? { id: id(), kind: 'board', propId: select.id, span: 'full' }
|
|
325
|
+
: { id: id(), kind: 'table', limit: 10, span: 'full' },
|
|
326
|
+
{ id: id(), kind: 'calendar', propId: date ? date.id : exports.LAST_CHANGED, span: 'full' },
|
|
327
|
+
{ id: id(), kind: 'pinned', noteIds: [], look: 'notes', span: 'half' },
|
|
328
|
+
{ id: id(), kind: 'stale', days: 30, limit: 5, span: 'half' },
|
|
329
|
+
];
|
|
330
|
+
case 'research':
|
|
331
|
+
return [
|
|
332
|
+
{ id: id(), kind: 'pinned', noteIds: [], look: 'notes', span: 'half' },
|
|
333
|
+
{ id: id(), kind: 'recent', limit: 10, span: 'half' },
|
|
334
|
+
{
|
|
335
|
+
id: id(),
|
|
336
|
+
kind: 'table',
|
|
337
|
+
title: 'Latest additions',
|
|
338
|
+
limit: 10,
|
|
339
|
+
sort: { by: exports.CREATED, dir: 'desc' },
|
|
340
|
+
span: 'full',
|
|
341
|
+
},
|
|
342
|
+
select
|
|
343
|
+
? { id: id(), kind: 'breakdown', propId: select.id, span: 'half' }
|
|
344
|
+
: { id: id(), kind: 'activity', weeks: 12, span: 'half' },
|
|
345
|
+
{ id: id(), kind: 'tasks', limit: 10, span: 'half' },
|
|
346
|
+
{ id: id(), kind: 'calendar', propId: exports.CREATED, span: 'full' },
|
|
347
|
+
];
|
|
348
|
+
case 'journal':
|
|
349
|
+
return [
|
|
350
|
+
{ id: id(), kind: 'quick', span: 'full' },
|
|
351
|
+
{ id: id(), kind: 'calendar', propId: exports.LAST_CHANGED, range: 'week', span: 'full' },
|
|
352
|
+
{ id: id(), kind: 'recent', limit: 7, span: 'half' },
|
|
353
|
+
{ id: id(), kind: 'tasks', limit: 10, span: 'half' },
|
|
354
|
+
{ id: id(), kind: 'activity', weeks: 26, span: 'half' },
|
|
355
|
+
{ id: id(), kind: 'stale', days: 14, limit: 5, span: 'half' },
|
|
356
|
+
];
|
|
357
|
+
default:
|
|
358
|
+
return defaultWidgets(defs);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/** True when a note is on one of this home's Pinned blocks. */
|
|
362
|
+
function isPinned(schema, noteId) {
|
|
363
|
+
return homeWidgets(schema).some((w) => w.kind === 'pinned' && w.noteIds.includes(noteId));
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Pins a note to a workspace's home, or unpins it: what "Pin to dashboard" in
|
|
367
|
+
* a note's menu writes.
|
|
368
|
+
*
|
|
369
|
+
* Unpinning takes the note off every Pinned block it is on. Pinning puts it
|
|
370
|
+
* on the first block with room, and when the home has no Pinned block yet it
|
|
371
|
+
* adds one, as sticky notes, at the top: pinning from a note is a request to
|
|
372
|
+
* see it on the dashboard, and a pin into a block nobody can see is not that.
|
|
373
|
+
* An unarranged home is written out as its defaults plus the new block, so
|
|
374
|
+
* nothing already on it disappears.
|
|
375
|
+
*
|
|
376
|
+
* Null when every Pinned block is full, which the caller has to say.
|
|
377
|
+
*/
|
|
378
|
+
function withPinToggled(schema, defs, noteId) {
|
|
379
|
+
const widgets = homeWidgets(schema, defs);
|
|
380
|
+
if (isPinned(schema, noteId)) {
|
|
381
|
+
return withHomeWidgets(schema, widgets.map((w) => w.kind === 'pinned' && w.noteIds.includes(noteId)
|
|
382
|
+
? { ...w, noteIds: w.noteIds.filter((id) => id !== noteId) }
|
|
383
|
+
: w));
|
|
384
|
+
}
|
|
385
|
+
const target = widgets.find((w) => w.kind === 'pinned' && w.noteIds.length < exports.PINNED_MAX);
|
|
386
|
+
if (target) {
|
|
387
|
+
return withHomeWidgets(schema, widgets.map((w) => w === target && w.kind === 'pinned'
|
|
388
|
+
? { ...w, noteIds: [...w.noteIds, noteId] }
|
|
389
|
+
: w));
|
|
390
|
+
}
|
|
391
|
+
if (widgets.some((w) => w.kind === 'pinned'))
|
|
392
|
+
return null;
|
|
393
|
+
return withHomeWidgets(schema, [
|
|
394
|
+
{ id: newWidgetId(), kind: 'pinned', noteIds: [noteId], look: 'notes' },
|
|
395
|
+
...widgets,
|
|
396
|
+
]);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* The reserved property id meaning "place notes by when they last changed".
|
|
400
|
+
*
|
|
401
|
+
* Not a property: it is a fact every note already carries. A calendar could
|
|
402
|
+
* previously only be drawn by a workspace that had defined a date property,
|
|
403
|
+
* which made "calendar by default" false for exactly the workspaces that had
|
|
404
|
+
* never set anything up, which is most of them at the point somebody first
|
|
405
|
+
* opens a dashboard. A calendar of when you last touched things needs no
|
|
406
|
+
* setup and answers a real question.
|
|
407
|
+
*
|
|
408
|
+
* A calendar in this mode is read only: you cannot decide when a note was
|
|
409
|
+
* last edited.
|
|
410
|
+
*/
|
|
411
|
+
exports.LAST_CHANGED = '@lastChanged';
|
|
412
|
+
/**
|
|
413
|
+
* The reserved property id meaning "place notes by when they were created".
|
|
414
|
+
*
|
|
415
|
+
* The other half of the same idea as LAST_CHANGED. They answer different
|
|
416
|
+
* questions and people want both: what have I touched lately, and what did I
|
|
417
|
+
* start when. Neither needs a workspace to have defined anything, and neither
|
|
418
|
+
* can be dragged, because you cannot decide when a note came into existence.
|
|
419
|
+
*/
|
|
420
|
+
exports.CREATED = '@created';
|
|
421
|
+
/** True for the built-in date sources, which come from the note itself rather
|
|
422
|
+
* than from a property somebody defined. */
|
|
423
|
+
function isNoteDate(propId) {
|
|
424
|
+
return propId === exports.LAST_CHANGED || propId === exports.CREATED;
|
|
425
|
+
}
|
|
426
|
+
/** The reserved property id that carries what used to be the plaintext tags column. */
|
|
427
|
+
exports.TAGS_PROP = 'tags';
|
|
428
|
+
/** Matches the server's cap on the encrypted attrs blob (HTTP 413
|
|
429
|
+
* `ATTRS_TOO_LARGE`), measured as UTF-8 bytes of the ciphertext string. The
|
|
430
|
+
* single source of truth for both mcp-core (notes.ts) and the web app
|
|
431
|
+
* (src/lib/attrs/next.ts), so the two never drift apart. */
|
|
432
|
+
exports.ATTRS_MAX_BYTES = 16384;
|
|
433
|
+
/** Matches the server's cap on the encrypted folder schema blob (HTTP 413
|
|
434
|
+
* `SCHEMA_TOO_LARGE`), measured as UTF-8 bytes of the ciphertext string. Lives
|
|
435
|
+
* beside ATTRS_MAX_BYTES for the same reason: mcp-core (notes.ts) and the web
|
|
436
|
+
* app (src/hooks/useFolderSchema.ts) both refuse an oversize blob before
|
|
437
|
+
* sending it, and two copies of the number would eventually disagree. */
|
|
438
|
+
exports.SCHEMA_MAX_BYTES = 32768;
|
|
439
|
+
const DATE = new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
|
440
|
+
function day(value) {
|
|
441
|
+
// Date-only strings are parsed as UTC; formatting them in the local zone
|
|
442
|
+
// would show the day before for anyone west of Greenwich.
|
|
443
|
+
const iso = /^\d{4}-\d{2}-\d{2}$/.test(value) ? `${value}T12:00:00Z` : value;
|
|
444
|
+
const parsed = new Date(iso);
|
|
445
|
+
return Number.isNaN(parsed.getTime()) ? '' : DATE.format(parsed);
|
|
446
|
+
}
|
|
447
|
+
function formatProp(def, value) {
|
|
448
|
+
if (!value)
|
|
449
|
+
return '';
|
|
450
|
+
switch (value.t) {
|
|
451
|
+
case 'text':
|
|
452
|
+
case 'url':
|
|
453
|
+
return value.v;
|
|
454
|
+
case 'number':
|
|
455
|
+
return String(value.v);
|
|
456
|
+
case 'checkbox':
|
|
457
|
+
return value.v ? 'Yes' : 'No';
|
|
458
|
+
case 'date':
|
|
459
|
+
return value.end ? `${day(value.v)} to ${day(value.end)}` : day(value.v);
|
|
460
|
+
case 'select':
|
|
461
|
+
return def.options?.find((o) => o.id === value.v)?.name ?? '';
|
|
462
|
+
case 'multiSelect':
|
|
463
|
+
return value.v.map((id) => def.options?.find((o) => o.id === id)?.name).filter(Boolean).join(', ');
|
|
464
|
+
case 'relation':
|
|
465
|
+
return value.v.length === 1 ? '1 note' : `${value.v.length} notes`;
|
|
466
|
+
default:
|
|
467
|
+
return '';
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function emptyAttrs(now = new Date().toISOString()) {
|
|
471
|
+
return { v: 1, props: {}, links: [], mentions: [], updated: now };
|
|
472
|
+
}
|
|
473
|
+
function emptySchema() {
|
|
474
|
+
return { v: 1, properties: [], views: { default: 'list' } };
|
|
475
|
+
}
|
|
476
|
+
const PROP_TYPES = ['text', 'number', 'select', 'multiSelect', 'date', 'checkbox', 'url', 'relation'];
|
|
477
|
+
/** 12 hex characters, enough entropy that two properties never collide. */
|
|
478
|
+
function newPropId() {
|
|
479
|
+
const bytes = crypto.getRandomValues(new Uint8Array(6));
|
|
480
|
+
return [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
481
|
+
}
|
|
482
|
+
function isPropDefinition(value) {
|
|
483
|
+
if (!isRecord(value))
|
|
484
|
+
return false;
|
|
485
|
+
return (typeof value.id === 'string' &&
|
|
486
|
+
typeof value.name === 'string' &&
|
|
487
|
+
PROP_TYPES.includes(value.type) &&
|
|
488
|
+
typeof value.order === 'number');
|
|
489
|
+
}
|
|
490
|
+
/** Definitions in display order. Unknown shapes are dropped rather than
|
|
491
|
+
* thrown on: a newer client may write a type this one does not know. */
|
|
492
|
+
function sortedProps(schema) {
|
|
493
|
+
const list = Array.isArray(schema?.properties) ? schema.properties : [];
|
|
494
|
+
return list
|
|
495
|
+
.filter(isPropDefinition)
|
|
496
|
+
.slice()
|
|
497
|
+
.sort((a, b) => a.order - b.order);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Everything in `properties` that `sortedProps` drops: an entry whose type
|
|
501
|
+
* this build does not know, or whose shape it cannot read. A newer client
|
|
502
|
+
* writes those, and this one must hand them back untouched.
|
|
503
|
+
*
|
|
504
|
+
* Every mutator below rebuilds `properties`, and rebuilding it from
|
|
505
|
+
* `sortedProps` alone deleted these: renaming one property in the web app
|
|
506
|
+
* silently destroyed a property the desktop build had defined. The same vault
|
|
507
|
+
* is edited by three clients, and `setProperties` already keeps unknown attrs
|
|
508
|
+
* fields, so dropping them here made the codec disagree with itself.
|
|
509
|
+
*
|
|
510
|
+
* They are re-appended at the end and never renumbered. Their `order` belongs
|
|
511
|
+
* to a definition this build cannot read, and rewriting it would be guessing.
|
|
512
|
+
*/
|
|
513
|
+
function unreadableProps(schema) {
|
|
514
|
+
const list = Array.isArray(schema?.properties) ? schema.properties : [];
|
|
515
|
+
return list.filter((p) => !isPropDefinition(p));
|
|
516
|
+
}
|
|
517
|
+
/** The id an entry carries, whether or not this build can read the rest of
|
|
518
|
+
* it, so an explicit remove can still find it. */
|
|
519
|
+
function idOf(prop) {
|
|
520
|
+
const id = prop.id;
|
|
521
|
+
return typeof id === 'string' ? id : undefined;
|
|
522
|
+
}
|
|
523
|
+
/** Appends a new property definition, assigning it a fresh id (unless one is
|
|
524
|
+
* given) and an order past the current last property. Never mutates schema. */
|
|
525
|
+
function defineProp(schema, def) {
|
|
526
|
+
const props = sortedProps(schema);
|
|
527
|
+
const last = props[props.length - 1];
|
|
528
|
+
const order = last === undefined ? 0 : last.order + 1;
|
|
529
|
+
const { id, ...rest } = def;
|
|
530
|
+
return { ...schema, properties: [...props, { ...rest, id: id ?? newPropId(), order }, ...unreadableProps(schema)] };
|
|
531
|
+
}
|
|
532
|
+
/** Patches a property definition by id, keeping its id fixed regardless of
|
|
533
|
+
* what the patch contains. Never mutates schema. */
|
|
534
|
+
function updateProp(schema, id, patch) {
|
|
535
|
+
return {
|
|
536
|
+
...schema,
|
|
537
|
+
properties: [
|
|
538
|
+
...sortedProps(schema).map((p) => (p.id === id ? { ...p, ...patch, id: p.id } : p)),
|
|
539
|
+
// Never patched: a patch is written against a type this build knows,
|
|
540
|
+
// and applying it to a definition it cannot read would corrupt it.
|
|
541
|
+
...unreadableProps(schema),
|
|
542
|
+
],
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
/** Removes a property definition by id. Never mutates schema. */
|
|
546
|
+
function removeProp(schema, id) {
|
|
547
|
+
return {
|
|
548
|
+
...schema,
|
|
549
|
+
properties: [
|
|
550
|
+
...sortedProps(schema).filter((p) => p.id !== id),
|
|
551
|
+
// Keeping the unreadable ones is about not deleting them by accident.
|
|
552
|
+
// A remove naming that id is not an accident, so it takes effect.
|
|
553
|
+
...unreadableProps(schema).filter((p) => idOf(p) !== id),
|
|
554
|
+
],
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
/** Reassigns order so the definitions match orderedIds; any definition whose
|
|
558
|
+
* id is not in the list keeps its relative position after the known ones.
|
|
559
|
+
* Never mutates schema. */
|
|
560
|
+
function reorderProps(schema, orderedIds) {
|
|
561
|
+
const props = sortedProps(schema);
|
|
562
|
+
const rank = new Map(orderedIds.map((id, i) => [id, i]));
|
|
563
|
+
const next = props
|
|
564
|
+
.slice()
|
|
565
|
+
.sort((a, b) => (rank.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (rank.get(b.id) ?? Number.MAX_SAFE_INTEGER))
|
|
566
|
+
.map((p, i) => ({ ...p, order: i }));
|
|
567
|
+
return { ...schema, properties: [...next, ...unreadableProps(schema)] };
|
|
568
|
+
}
|
|
569
|
+
function isRecord(value) {
|
|
570
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
571
|
+
}
|
|
572
|
+
function stringArray(value) {
|
|
573
|
+
return Array.isArray(value) ? value.filter((x) => typeof x === 'string') : [];
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* The type says v: 1, but a blob written by a future client may carry a
|
|
577
|
+
* higher version number. That numeric value is preserved on purpose, not
|
|
578
|
+
* downgraded, so a round trip through this reader does not lose it; only a
|
|
579
|
+
* missing or non-numeric v falls back to 1. The cast is needed because the
|
|
580
|
+
* field's declared type is the literal 1, not number.
|
|
581
|
+
*/
|
|
582
|
+
function decodedVersion(r) {
|
|
583
|
+
return (typeof r.v === 'number' && Number.isFinite(r.v) ? r.v : 1);
|
|
584
|
+
}
|
|
585
|
+
/** Normalize a decoded object into a NoteAttrsV1, keeping unknown fields. */
|
|
586
|
+
function asAttrs(raw) {
|
|
587
|
+
const r = isRecord(raw) ? raw : {};
|
|
588
|
+
return {
|
|
589
|
+
...r,
|
|
590
|
+
v: decodedVersion(r),
|
|
591
|
+
props: isRecord(r.props) ? r.props : {},
|
|
592
|
+
links: stringArray(r.links),
|
|
593
|
+
mentions: stringArray(r.mentions),
|
|
594
|
+
...(r.aliases !== undefined ? { aliases: stringArray(r.aliases) } : {}),
|
|
595
|
+
updated: typeof r.updated === 'string' ? r.updated : new Date(0).toISOString(),
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
function asSchema(raw) {
|
|
599
|
+
const r = isRecord(raw) ? raw : {};
|
|
600
|
+
const views = isRecord(r.views) ? r.views : {};
|
|
601
|
+
const def = views.default;
|
|
602
|
+
// 'home' was briefly one of these. It is not a way of listing notes, so it
|
|
603
|
+
// moved to the editor pane and out of this union; a workspace saved as
|
|
604
|
+
// 'home' by that build clamps to 'list' here rather than rendering nothing.
|
|
605
|
+
const allowed = ['list', 'table', 'board', 'calendar'];
|
|
606
|
+
return {
|
|
607
|
+
...r,
|
|
608
|
+
v: decodedVersion(r),
|
|
609
|
+
properties: Array.isArray(r.properties) ? r.properties : [],
|
|
610
|
+
views: { ...views, default: allowed.includes(def) ? def : 'list' },
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
async function encodeAttrs(key, attrs) {
|
|
614
|
+
return (0, virlow_crypto_1.encryptStringBlob)(key, JSON.stringify(attrs));
|
|
615
|
+
}
|
|
616
|
+
async function decodeAttrs(key, blob) {
|
|
617
|
+
if (!blob)
|
|
618
|
+
return null;
|
|
619
|
+
return asAttrs(JSON.parse(await (0, virlow_crypto_1.decryptStringBlob)(key, blob)));
|
|
620
|
+
}
|
|
621
|
+
async function encodeSchema(key, schema) {
|
|
622
|
+
return (0, virlow_crypto_1.encryptStringBlob)(key, JSON.stringify(schema));
|
|
623
|
+
}
|
|
624
|
+
async function decodeSchema(key, blob) {
|
|
625
|
+
if (!blob)
|
|
626
|
+
return null;
|
|
627
|
+
return asSchema(JSON.parse(await (0, virlow_crypto_1.decryptStringBlob)(key, blob)));
|
|
628
|
+
}
|
|
629
|
+
function tagsFromAttrs(attrs) {
|
|
630
|
+
const value = attrs?.props[exports.TAGS_PROP];
|
|
631
|
+
return value && value.t === 'multiSelect' ? [...value.v] : [];
|
|
632
|
+
}
|
|
633
|
+
function withTags(attrs, tags, now = new Date().toISOString()) {
|
|
634
|
+
return { ...attrs, props: { ...attrs.props, [exports.TAGS_PROP]: { t: 'multiSelect', v: [...tags] } }, updated: now };
|
|
635
|
+
}
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -14,6 +14,10 @@ export type { Exposure } from './exposure.js';
|
|
|
14
14
|
export { EmbeddingCache } from './embedding-cache.js';
|
|
15
15
|
export type { EmbeddingCacheLike } from './embedding-cache.js';
|
|
16
16
|
export { MEMORIES_DISABLED_MESSAGE, memoriesAllowed } from './memories-enabled.js';
|
|
17
|
+
export { ATTRS_MAX_BYTES, TAGS_PROP, decodeAttrs, decodeSchema, defineProp, emptyAttrs, emptySchema, encodeAttrs, encodeSchema, newPropId, removeProp, reorderProps, sortedProps, tagsFromAttrs, updateProp, withTags, } from './attrs.js';
|
|
18
|
+
export type { FolderSchemaV1, NoteAttrsV1, PropDefinition, PropOption, PropType, PropValue, TagsValue } from './attrs.js';
|
|
19
|
+
export { buildTitleMatcher, extractLinkIds, htmlToText } from './attrs-extract.js';
|
|
20
|
+
export type { TitleEntry, TitleMatcher } from './attrs-extract.js';
|
|
17
21
|
/**
|
|
18
22
|
* The MCP tool surface is NOT exported here. It lives at the `/tools` subpath,
|
|
19
23
|
* which is ESM-only, because it depends on @modelcontextprotocol/sdk — an
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.memoriesAllowed = exports.MEMORIES_DISABLED_MESSAGE = exports.EmbeddingCache = exports.filteredNotice = exports.computeExposure = exports.resolveMemoryTree = exports.ensureNamespace = exports.MEMORIES_ROOT_NAME = exports.toCodeEnvelope = exports.serializeMemoryBody = exports.parseMemoryNote = exports.mergeMeta = exports.MEMORY_NOTE_TYPE = exports.SYNC_PAGE_LIMIT = exports.MemoryStore = exports.SEARCH_SCAN_CAP = exports.NotesService = exports.Vault = exports.LockedError = exports.VirlowApi = exports.ApiError = void 0;
|
|
3
|
+
exports.htmlToText = exports.extractLinkIds = exports.buildTitleMatcher = exports.withTags = exports.updateProp = exports.tagsFromAttrs = exports.sortedProps = exports.reorderProps = exports.removeProp = exports.newPropId = exports.encodeSchema = exports.encodeAttrs = exports.emptySchema = exports.emptyAttrs = exports.defineProp = exports.decodeSchema = exports.decodeAttrs = exports.TAGS_PROP = exports.ATTRS_MAX_BYTES = exports.memoriesAllowed = exports.MEMORIES_DISABLED_MESSAGE = exports.EmbeddingCache = exports.filteredNotice = exports.computeExposure = exports.resolveMemoryTree = exports.ensureNamespace = exports.MEMORIES_ROOT_NAME = exports.toCodeEnvelope = exports.serializeMemoryBody = exports.parseMemoryNote = exports.mergeMeta = exports.MEMORY_NOTE_TYPE = exports.SYNC_PAGE_LIMIT = exports.MemoryStore = exports.SEARCH_SCAN_CAP = exports.NotesService = exports.Vault = exports.LockedError = exports.VirlowApi = exports.ApiError = void 0;
|
|
4
4
|
var api_js_1 = require("./api.js");
|
|
5
5
|
Object.defineProperty(exports, "ApiError", { enumerable: true, get: function () { return api_js_1.ApiError; } });
|
|
6
6
|
Object.defineProperty(exports, "VirlowApi", { enumerable: true, get: function () { return api_js_1.VirlowApi; } });
|
|
@@ -31,6 +31,27 @@ Object.defineProperty(exports, "EmbeddingCache", { enumerable: true, get: functi
|
|
|
31
31
|
var memories_enabled_js_1 = require("./memories-enabled.js");
|
|
32
32
|
Object.defineProperty(exports, "MEMORIES_DISABLED_MESSAGE", { enumerable: true, get: function () { return memories_enabled_js_1.MEMORIES_DISABLED_MESSAGE; } });
|
|
33
33
|
Object.defineProperty(exports, "memoriesAllowed", { enumerable: true, get: function () { return memories_enabled_js_1.memoriesAllowed; } });
|
|
34
|
+
var attrs_js_1 = require("./attrs.js");
|
|
35
|
+
Object.defineProperty(exports, "ATTRS_MAX_BYTES", { enumerable: true, get: function () { return attrs_js_1.ATTRS_MAX_BYTES; } });
|
|
36
|
+
Object.defineProperty(exports, "TAGS_PROP", { enumerable: true, get: function () { return attrs_js_1.TAGS_PROP; } });
|
|
37
|
+
Object.defineProperty(exports, "decodeAttrs", { enumerable: true, get: function () { return attrs_js_1.decodeAttrs; } });
|
|
38
|
+
Object.defineProperty(exports, "decodeSchema", { enumerable: true, get: function () { return attrs_js_1.decodeSchema; } });
|
|
39
|
+
Object.defineProperty(exports, "defineProp", { enumerable: true, get: function () { return attrs_js_1.defineProp; } });
|
|
40
|
+
Object.defineProperty(exports, "emptyAttrs", { enumerable: true, get: function () { return attrs_js_1.emptyAttrs; } });
|
|
41
|
+
Object.defineProperty(exports, "emptySchema", { enumerable: true, get: function () { return attrs_js_1.emptySchema; } });
|
|
42
|
+
Object.defineProperty(exports, "encodeAttrs", { enumerable: true, get: function () { return attrs_js_1.encodeAttrs; } });
|
|
43
|
+
Object.defineProperty(exports, "encodeSchema", { enumerable: true, get: function () { return attrs_js_1.encodeSchema; } });
|
|
44
|
+
Object.defineProperty(exports, "newPropId", { enumerable: true, get: function () { return attrs_js_1.newPropId; } });
|
|
45
|
+
Object.defineProperty(exports, "removeProp", { enumerable: true, get: function () { return attrs_js_1.removeProp; } });
|
|
46
|
+
Object.defineProperty(exports, "reorderProps", { enumerable: true, get: function () { return attrs_js_1.reorderProps; } });
|
|
47
|
+
Object.defineProperty(exports, "sortedProps", { enumerable: true, get: function () { return attrs_js_1.sortedProps; } });
|
|
48
|
+
Object.defineProperty(exports, "tagsFromAttrs", { enumerable: true, get: function () { return attrs_js_1.tagsFromAttrs; } });
|
|
49
|
+
Object.defineProperty(exports, "updateProp", { enumerable: true, get: function () { return attrs_js_1.updateProp; } });
|
|
50
|
+
Object.defineProperty(exports, "withTags", { enumerable: true, get: function () { return attrs_js_1.withTags; } });
|
|
51
|
+
var attrs_extract_js_1 = require("./attrs-extract.js");
|
|
52
|
+
Object.defineProperty(exports, "buildTitleMatcher", { enumerable: true, get: function () { return attrs_extract_js_1.buildTitleMatcher; } });
|
|
53
|
+
Object.defineProperty(exports, "extractLinkIds", { enumerable: true, get: function () { return attrs_extract_js_1.extractLinkIds; } });
|
|
54
|
+
Object.defineProperty(exports, "htmlToText", { enumerable: true, get: function () { return attrs_extract_js_1.htmlToText; } });
|
|
34
55
|
/**
|
|
35
56
|
* The MCP tool surface is NOT exported here. It lives at the `/tools` subpath,
|
|
36
57
|
* which is ESM-only, because it depends on @modelcontextprotocol/sdk — an
|