@derive-to/mcp 0.5.0 → 0.6.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 +1 -1
- package/README.md +79 -0
- package/SKILL.md +133 -126
- package/package.json +16 -10
- package/references/compatibility.md +23 -0
- package/references/connect.md +66 -0
- package/src/client.ts +312 -46
- package/src/filename.ts +30 -0
- package/src/index.ts +360 -53
- package/src/template-resources.ts +201 -0
package/src/client.ts
CHANGED
|
@@ -17,6 +17,9 @@ export interface PublishArgs {
|
|
|
17
17
|
slug?: string
|
|
18
18
|
spa?: boolean
|
|
19
19
|
message?: string
|
|
20
|
+
/** Browse tags to set on the artifact at publish time. Given ⇒ replaces the set;
|
|
21
|
+
* omitted ⇒ leaves existing tags untouched on a republish. */
|
|
22
|
+
tags?: string[]
|
|
20
23
|
/** The v2 access triple for a NEW artifact (see access-model.md); ignored on a
|
|
21
24
|
* republish. */
|
|
22
25
|
workspaceAccess?: WorkspaceAccess
|
|
@@ -61,27 +64,36 @@ export interface ArtifactSummaryJson {
|
|
|
61
64
|
workspace_access?: string
|
|
62
65
|
link_role?: string
|
|
63
66
|
listed?: string
|
|
67
|
+
/** Browse tags on the artifact — present when the server returns them (newer servers). */
|
|
68
|
+
tags?: string[]
|
|
69
|
+
current_content_type?: string | null
|
|
70
|
+
is_linked_bundle?: boolean
|
|
64
71
|
}
|
|
65
72
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
/** Exact-match search/replace against the current stored source, INSTEAD of
|
|
75
|
-
* `content`. */
|
|
76
|
-
edits?: DocEdit[]
|
|
77
|
-
baseVersion?: number
|
|
73
|
+
export interface TagCountJson {
|
|
74
|
+
tag: string
|
|
75
|
+
count: number
|
|
76
|
+
}
|
|
77
|
+
export interface TagSuggestionsJson {
|
|
78
|
+
current: string[]
|
|
79
|
+
suggested: TagCountJson[]
|
|
80
|
+
vocabulary: TagCountJson[]
|
|
78
81
|
}
|
|
79
|
-
|
|
82
|
+
/** The result of adding/removing/replacing tags across one or many artifacts. */
|
|
83
|
+
export interface TagResultJson {
|
|
84
|
+
updated: number
|
|
85
|
+
skipped: number
|
|
86
|
+
failed: number
|
|
87
|
+
results: { short_id: string; tags: string[] }[]
|
|
88
|
+
}
|
|
89
|
+
export interface CollectionSummaryJson {
|
|
80
90
|
id: string
|
|
81
|
-
|
|
82
|
-
|
|
91
|
+
title: string
|
|
92
|
+
count: number
|
|
83
93
|
}
|
|
84
94
|
|
|
95
|
+
/** A revision submitted for human review instead of published live. */
|
|
96
|
+
|
|
85
97
|
export interface NewCommentArgs {
|
|
86
98
|
body_md: string
|
|
87
99
|
thread_id?: string
|
|
@@ -89,6 +101,7 @@ export interface NewCommentArgs {
|
|
|
89
101
|
base_version?: number
|
|
90
102
|
path?: string
|
|
91
103
|
anchor?: unknown
|
|
104
|
+
visual_target?: string
|
|
92
105
|
}
|
|
93
106
|
|
|
94
107
|
export interface VersionJson {
|
|
@@ -118,17 +131,34 @@ export interface ArtifactJson {
|
|
|
118
131
|
listed?: string
|
|
119
132
|
current_version: number
|
|
120
133
|
versions: VersionJson[]
|
|
134
|
+
/** Browse tags — present on the detail endpoint (newer servers). */
|
|
135
|
+
tags?: string[]
|
|
136
|
+
/** Collection ids this artifact belongs to — present on the detail endpoint. */
|
|
137
|
+
collections?: string[]
|
|
121
138
|
/** Time-grouped version view (newest-first); present on the detail endpoint. */
|
|
122
139
|
sessions?: SessionJson[]
|
|
123
140
|
/** Publish-response extras (agent-credentialed publishes only). */
|
|
124
141
|
review_requested?: boolean
|
|
125
142
|
opened_in_tab?: boolean
|
|
143
|
+
linked_bundle?: {
|
|
144
|
+
schema: "derive.linked-bundle/v1"
|
|
145
|
+
purpose: string
|
|
146
|
+
members: {
|
|
147
|
+
id: string
|
|
148
|
+
ref: string
|
|
149
|
+
label: string
|
|
150
|
+
role?: string
|
|
151
|
+
note?: string
|
|
152
|
+
available: boolean
|
|
153
|
+
}[]
|
|
154
|
+
diagrams?: { id: string; title: string; type: "loop" | "graph" }[]
|
|
155
|
+
}
|
|
126
156
|
}
|
|
127
157
|
|
|
128
158
|
/** One review round: the human-ack primitive of the /derive loop. */
|
|
129
159
|
export interface ReviewRoundJson {
|
|
130
160
|
id: string
|
|
131
|
-
state: "pending" | "sent_back"
|
|
161
|
+
state: "pending" | "sent_back"
|
|
132
162
|
version: number
|
|
133
163
|
note: string | null
|
|
134
164
|
created_at: string
|
|
@@ -147,17 +177,55 @@ export interface DiffJson {
|
|
|
147
177
|
|
|
148
178
|
export interface ViewStatsJson {
|
|
149
179
|
total: number
|
|
180
|
+
last24h: number
|
|
150
181
|
unique: number
|
|
151
182
|
perVersion: { version: number; count: number }[]
|
|
152
183
|
daily: { day: string; count: number }[]
|
|
153
184
|
recent: { viewer: string; kind: "user" | "anon"; at: string }[]
|
|
154
185
|
}
|
|
155
186
|
|
|
187
|
+
/** A library is an access-scoped catalog of immutable template starters. */
|
|
188
|
+
export interface TemplateLibraryEntryJson {
|
|
189
|
+
id: string
|
|
190
|
+
library_id: string
|
|
191
|
+
source_version: number
|
|
192
|
+
kind: "artifact" | "context"
|
|
193
|
+
category: string
|
|
194
|
+
format: "md" | "html"
|
|
195
|
+
title: string
|
|
196
|
+
description: string
|
|
197
|
+
outcome: string
|
|
198
|
+
sections: string[]
|
|
199
|
+
inputs: { name: string; description: string; required?: boolean }[]
|
|
200
|
+
tags: string[]
|
|
201
|
+
created_at: string
|
|
202
|
+
}
|
|
203
|
+
export interface TemplateLibraryJson {
|
|
204
|
+
id: string
|
|
205
|
+
title: string
|
|
206
|
+
description: string
|
|
207
|
+
scope: "private" | "workspace" | "public"
|
|
208
|
+
created_at: string
|
|
209
|
+
updated_at: string | null
|
|
210
|
+
entry_count: number
|
|
211
|
+
entries?: TemplateLibraryEntryJson[]
|
|
212
|
+
}
|
|
213
|
+
export interface TemplateLibraryPageJson {
|
|
214
|
+
libraries: TemplateLibraryJson[]
|
|
215
|
+
truncated: boolean
|
|
216
|
+
next_cursor: string | null
|
|
217
|
+
}
|
|
218
|
+
export interface TemplateStarterJson {
|
|
219
|
+
entry: TemplateLibraryEntryJson
|
|
220
|
+
source: string
|
|
221
|
+
mime_type: string
|
|
222
|
+
}
|
|
223
|
+
|
|
156
224
|
export interface ContentOpts {
|
|
157
225
|
version?: number
|
|
158
226
|
/** A heading slug (single-file) or page path (bundle, optionally page#slug). */
|
|
159
227
|
section?: string
|
|
160
|
-
format?: "markdown" | "text"
|
|
228
|
+
format?: "markdown" | "text" | "html"
|
|
161
229
|
}
|
|
162
230
|
|
|
163
231
|
/** A content read: the body plus the server's X-Derive-* capability headers, so a
|
|
@@ -179,14 +247,48 @@ export interface OutlineSectionJson {
|
|
|
179
247
|
chars: number
|
|
180
248
|
}
|
|
181
249
|
|
|
250
|
+
export interface SearchOpts {
|
|
251
|
+
caseSensitive?: boolean
|
|
252
|
+
/** source (default): the exact stored bytes. text: the visible text (tags stripped). */
|
|
253
|
+
in?: "source" | "text"
|
|
254
|
+
/** Lines of surrounding context per match (default 0, max 5, server-clamped). */
|
|
255
|
+
context?: number
|
|
256
|
+
/** Cap on matches returned per artifact (default 40, max 200, server-clamped). */
|
|
257
|
+
maxMatches?: number
|
|
258
|
+
/** Single-artifact search only; ignored in workspace-wide mode. */
|
|
259
|
+
version?: number
|
|
260
|
+
}
|
|
261
|
+
|
|
182
262
|
export interface DeriveClient {
|
|
183
|
-
/** List the workspace's artifacts (optionally filtered by a title query). */
|
|
184
|
-
list(query?: string): Promise<ArtifactSummaryJson[]>
|
|
263
|
+
/** List the workspace's artifacts (optionally filtered by a title query and/or a browse tag). */
|
|
264
|
+
list(query?: string, tag?: string, archived?: boolean): Promise<ArtifactSummaryJson[]>
|
|
265
|
+
/** Move an artifact onto or off the reversible archive shelf. */
|
|
266
|
+
archive(shortId: string, archived: boolean): Promise<{ archived: boolean }>
|
|
267
|
+
/** The workspace tag vocabulary (tag → count, most-used first). */
|
|
268
|
+
listTags(): Promise<TagCountJson[]>
|
|
269
|
+
/** Suggest tags for an artifact: its current tags, neighbors' tags, and the vocabulary. */
|
|
270
|
+
suggestTags(shortId: string): Promise<TagSuggestionsJson>
|
|
271
|
+
/** Add/remove/replace browse tags across one or more artifacts. `set` replaces the
|
|
272
|
+
* whole set (overriding add/remove); otherwise add then remove. */
|
|
273
|
+
tag(
|
|
274
|
+
shortIds: string[],
|
|
275
|
+
ops: { add?: string[]; remove?: string[]; set?: string[] },
|
|
276
|
+
): Promise<TagResultJson>
|
|
277
|
+
/** The workspace's team-visible collections with item counts. */
|
|
278
|
+
listCollections(): Promise<CollectionSummaryJson[]>
|
|
279
|
+
/** Add artifacts to a collection identified by id OR name (created if a new name). */
|
|
280
|
+
collect(
|
|
281
|
+
shortIds: string[],
|
|
282
|
+
ref: string,
|
|
283
|
+
): Promise<{ collection: { id: string; title: string }; added: number; skipped: number }>
|
|
185
284
|
publish(args: PublishArgs): Promise<ArtifactJson>
|
|
186
285
|
/** Submit a single-file revision for human review (does not go live). */
|
|
187
|
-
propose(shortId: string, args: ProposeArgs): Promise<ProposalJson>
|
|
188
286
|
get(shortId: string): Promise<ArtifactJson>
|
|
189
287
|
getContent(shortId: string, opts?: ContentOpts): Promise<ContentResult>
|
|
288
|
+
/** Grep WITHIN one artifact (shortId set) or ACROSS the workspace (shortId
|
|
289
|
+
* omitted) — the same engine and ripgrep-style text report the remote MCP
|
|
290
|
+
* `search` tool returns, relayed here verbatim. */
|
|
291
|
+
search(shortId: string | undefined, query: string, opts?: SearchOpts): Promise<string>
|
|
190
292
|
/** The heading (single-file) or page (bundle) outline. Empty `sections` on an
|
|
191
293
|
* older server that doesn't understand `?outline=1` (it 400s or ignores it). */
|
|
192
294
|
getOutline(
|
|
@@ -210,6 +312,10 @@ export interface DeriveClient {
|
|
|
210
312
|
restore(shortId: string, version: number): Promise<ArtifactJson>
|
|
211
313
|
/** Aggregated view analytics. */
|
|
212
314
|
viewStats(shortId: string): Promise<ViewStatsJson>
|
|
315
|
+
/** Accessible public + workspace + personal libraries, for MCP resources only. */
|
|
316
|
+
listTemplateLibraries(cursor?: string): Promise<TemplateLibraryPageJson>
|
|
317
|
+
getTemplateLibrary(id: string): Promise<TemplateLibraryJson>
|
|
318
|
+
getTemplateStarter(libraryId: string, entryId: string): Promise<TemplateStarterJson>
|
|
213
319
|
}
|
|
214
320
|
|
|
215
321
|
export interface ClientOptions {
|
|
@@ -239,12 +345,151 @@ export function createClient(opts: ClientOptions): DeriveClient {
|
|
|
239
345
|
}
|
|
240
346
|
|
|
241
347
|
return {
|
|
242
|
-
async list(query) {
|
|
243
|
-
const
|
|
348
|
+
async list(query, tag, archived) {
|
|
349
|
+
const qs = new URLSearchParams()
|
|
350
|
+
if (query) qs.set("query", query)
|
|
351
|
+
if (tag) qs.set("tag", tag.trim().toLowerCase())
|
|
352
|
+
if (archived) qs.set("scope", "archived")
|
|
353
|
+
const q = qs.toString() ? `?${qs}` : ""
|
|
244
354
|
const r = (await ok(await f(`${base}/v1/artifacts${q}`, { headers: authHeaders }))) as {
|
|
245
355
|
artifacts: ArtifactSummaryJson[]
|
|
246
356
|
}
|
|
247
|
-
return r.artifacts
|
|
357
|
+
return r.artifacts.map((artifact) => ({
|
|
358
|
+
...artifact,
|
|
359
|
+
is_linked_bundle: artifact.current_content_type === "text/x-derive-linked-bundle",
|
|
360
|
+
}))
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
async archive(shortId, archived) {
|
|
364
|
+
return ok(
|
|
365
|
+
await f(`${base}/v1/artifacts/${shortId}/archive`, {
|
|
366
|
+
method: archived ? "PUT" : "DELETE",
|
|
367
|
+
headers: authHeaders,
|
|
368
|
+
}),
|
|
369
|
+
) as Promise<{ archived: boolean }>
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
async listTags() {
|
|
373
|
+
const r = (await ok(await f(`${base}/v1/tags`, { headers: authHeaders }))) as {
|
|
374
|
+
tags: TagCountJson[]
|
|
375
|
+
}
|
|
376
|
+
return (r.tags ?? []).slice().sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag))
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
async suggestTags(shortId) {
|
|
380
|
+
return ok(
|
|
381
|
+
await f(`${base}/v1/artifacts/${shortId}/tag-suggestions`, { headers: authHeaders }),
|
|
382
|
+
) as Promise<TagSuggestionsJson>
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
async tag(shortIds, ops) {
|
|
386
|
+
// `set` replaces the whole set in one bulk call. Otherwise apply the additive `add`
|
|
387
|
+
// (the bulk-add route merges per artifact) and/or `remove` — each a bulk call over
|
|
388
|
+
// the same id set, so the server does the per-artifact authorization + skip counting.
|
|
389
|
+
const post = (path: string, body: unknown) =>
|
|
390
|
+
f(`${base}${path}`, {
|
|
391
|
+
method: "POST",
|
|
392
|
+
headers: { ...authHeaders, "content-type": "application/json" },
|
|
393
|
+
body: JSON.stringify(body),
|
|
394
|
+
})
|
|
395
|
+
if (ops.set) {
|
|
396
|
+
// No bulk "set" route; set each artifact's tags directly (the union with nothing).
|
|
397
|
+
const results: { short_id: string; tags: string[] }[] = []
|
|
398
|
+
let updated = 0
|
|
399
|
+
let failed = 0
|
|
400
|
+
for (const id of shortIds) {
|
|
401
|
+
try {
|
|
402
|
+
const r = (await ok(
|
|
403
|
+
await f(`${base}/v1/artifacts/${id}/tags`, {
|
|
404
|
+
method: "PUT",
|
|
405
|
+
headers: { ...authHeaders, "content-type": "application/json" },
|
|
406
|
+
body: JSON.stringify({ tags: ops.set }),
|
|
407
|
+
}),
|
|
408
|
+
)) as { tags: string[] }
|
|
409
|
+
results.push({ short_id: id, tags: r.tags })
|
|
410
|
+
updated++
|
|
411
|
+
} catch {
|
|
412
|
+
failed++
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return { updated, skipped: 0, failed, results }
|
|
416
|
+
}
|
|
417
|
+
let updated = 0
|
|
418
|
+
let skipped = 0
|
|
419
|
+
let failed = 0
|
|
420
|
+
if (ops.add?.length) {
|
|
421
|
+
const r = (await ok(await post("/v1/bulk/tags", { shortIds, add: ops.add }))) as {
|
|
422
|
+
ok: number
|
|
423
|
+
skipped: number
|
|
424
|
+
failed: number
|
|
425
|
+
}
|
|
426
|
+
updated = r.ok
|
|
427
|
+
skipped = r.skipped
|
|
428
|
+
failed = r.failed
|
|
429
|
+
}
|
|
430
|
+
if (ops.remove?.length) {
|
|
431
|
+
// No bulk-remove route: read-modify-write each artifact via the single tag route.
|
|
432
|
+
const removeSet = new Set(ops.remove.map((t) => t.trim().toLowerCase()))
|
|
433
|
+
for (const id of shortIds) {
|
|
434
|
+
try {
|
|
435
|
+
const a = await (ok(
|
|
436
|
+
await f(`${base}/v1/artifacts/${id}`, { headers: authHeaders }),
|
|
437
|
+
) as Promise<{
|
|
438
|
+
tags?: string[]
|
|
439
|
+
}>)
|
|
440
|
+
const next = (a.tags ?? []).filter((t) => !removeSet.has(t))
|
|
441
|
+
await ok(
|
|
442
|
+
await f(`${base}/v1/artifacts/${id}/tags`, {
|
|
443
|
+
method: "PUT",
|
|
444
|
+
headers: { ...authHeaders, "content-type": "application/json" },
|
|
445
|
+
body: JSON.stringify({ tags: next }),
|
|
446
|
+
}),
|
|
447
|
+
)
|
|
448
|
+
} catch {
|
|
449
|
+
/* a remove miss (not editable) is reflected by the add pass's skipped, or ignored */
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return { updated, skipped, failed, results: [] }
|
|
454
|
+
},
|
|
455
|
+
|
|
456
|
+
async listCollections() {
|
|
457
|
+
const r = (await ok(await f(`${base}/v1/collections`, { headers: authHeaders }))) as {
|
|
458
|
+
collections: { id: string; title: string; count: number }[]
|
|
459
|
+
}
|
|
460
|
+
return r.collections.map((c) => ({ id: c.id, title: c.title, count: c.count }))
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
async collect(shortIds, ref) {
|
|
464
|
+
// Resolve `ref` against the workspace's collections by id OR title, else create it.
|
|
465
|
+
const name = ref.trim()
|
|
466
|
+
const cols = (
|
|
467
|
+
(await ok(await f(`${base}/v1/collections`, { headers: authHeaders }))) as {
|
|
468
|
+
collections: { id: string; title: string }[]
|
|
469
|
+
}
|
|
470
|
+
).collections
|
|
471
|
+
const match = cols.find((c) => c.id === name || c.title.toLowerCase() === name.toLowerCase())
|
|
472
|
+
let collectionId = match?.id
|
|
473
|
+
let title = match?.title ?? ""
|
|
474
|
+
if (!collectionId) {
|
|
475
|
+
const created = (await ok(
|
|
476
|
+
await f(`${base}/v1/collections`, {
|
|
477
|
+
method: "POST",
|
|
478
|
+
headers: { ...authHeaders, "content-type": "application/json" },
|
|
479
|
+
body: JSON.stringify({ title: name }),
|
|
480
|
+
}),
|
|
481
|
+
)) as { id: string; title: string }
|
|
482
|
+
collectionId = created.id
|
|
483
|
+
title = created.title
|
|
484
|
+
}
|
|
485
|
+
const r = (await ok(
|
|
486
|
+
await f(`${base}/v1/bulk/collections`, {
|
|
487
|
+
method: "POST",
|
|
488
|
+
headers: { ...authHeaders, "content-type": "application/json" },
|
|
489
|
+
body: JSON.stringify({ shortIds, collectionIds: [collectionId] }),
|
|
490
|
+
}),
|
|
491
|
+
)) as { ok: number; skipped: number }
|
|
492
|
+
return { collection: { id: collectionId, title }, added: r.ok, skipped: r.skipped }
|
|
248
493
|
},
|
|
249
494
|
|
|
250
495
|
async publish(args) {
|
|
@@ -264,6 +509,7 @@ export function createClient(opts: ClientOptions): DeriveClient {
|
|
|
264
509
|
slug: args.slug,
|
|
265
510
|
spa: args.spa,
|
|
266
511
|
message: args.message,
|
|
512
|
+
tags: args.tags,
|
|
267
513
|
workspaceAccess: args.workspaceAccess,
|
|
268
514
|
linkRole: args.linkRole,
|
|
269
515
|
listed: args.listed,
|
|
@@ -277,29 +523,6 @@ export function createClient(opts: ClientOptions): DeriveClient {
|
|
|
277
523
|
) as Promise<ArtifactJson>
|
|
278
524
|
},
|
|
279
525
|
|
|
280
|
-
async propose(shortId, args) {
|
|
281
|
-
const form = new FormData()
|
|
282
|
-
if (args.edits) {
|
|
283
|
-
form.append("edits", JSON.stringify(args.edits))
|
|
284
|
-
if (args.baseVersion != null) form.append("base_version", String(args.baseVersion))
|
|
285
|
-
} else {
|
|
286
|
-
const bytes =
|
|
287
|
-
typeof args.content === "string" || args.content === undefined
|
|
288
|
-
? new TextEncoder().encode(args.content ?? "")
|
|
289
|
-
: args.content
|
|
290
|
-
form.append("file", new Blob([bytes as BlobPart]), args.filename ?? "index.html")
|
|
291
|
-
}
|
|
292
|
-
form.append("message", args.message)
|
|
293
|
-
if (args.addresses?.length) form.append("addresses", args.addresses.join(","))
|
|
294
|
-
return ok(
|
|
295
|
-
await f(`${base}/v1/artifacts/${shortId}/proposals`, {
|
|
296
|
-
method: "POST",
|
|
297
|
-
body: form,
|
|
298
|
-
headers: authHeaders,
|
|
299
|
-
}),
|
|
300
|
-
) as Promise<ProposalJson>
|
|
301
|
-
},
|
|
302
|
-
|
|
303
526
|
async get(shortId) {
|
|
304
527
|
return ok(
|
|
305
528
|
await f(`${base}/v1/artifacts/${shortId}`, { headers: authHeaders }),
|
|
@@ -334,6 +557,24 @@ export function createClient(opts: ClientOptions): DeriveClient {
|
|
|
334
557
|
}
|
|
335
558
|
},
|
|
336
559
|
|
|
560
|
+
async search(shortId, query, opts) {
|
|
561
|
+
const q = new URLSearchParams({ query })
|
|
562
|
+
if (opts?.caseSensitive) q.set("case_sensitive", "true")
|
|
563
|
+
if (opts?.in) q.set("in", opts.in)
|
|
564
|
+
if (opts?.context != null) q.set("context", String(opts.context))
|
|
565
|
+
if (opts?.maxMatches != null) q.set("max_matches", String(opts.maxMatches))
|
|
566
|
+
if (opts?.version != null) q.set("v", String(opts.version))
|
|
567
|
+
const path = shortId
|
|
568
|
+
? `${base}/v1/artifacts/${shortId}/search?${q}`
|
|
569
|
+
: `${base}/v1/artifacts/search?${q}`
|
|
570
|
+
const res = await f(path, { headers: authHeaders })
|
|
571
|
+
if (!res.ok) {
|
|
572
|
+
const body = (await res.json().catch(() => ({}))) as { error?: string }
|
|
573
|
+
throw new Error(`derive ${res.status}: ${body.error ?? res.statusText}`)
|
|
574
|
+
}
|
|
575
|
+
return res.text()
|
|
576
|
+
},
|
|
577
|
+
|
|
337
578
|
async getOutline(shortId, version) {
|
|
338
579
|
const q = new URLSearchParams({ outline: "1" })
|
|
339
580
|
if (version) q.set("v", String(version))
|
|
@@ -415,5 +656,30 @@ export function createClient(opts: ClientOptions): DeriveClient {
|
|
|
415
656
|
await f(`${base}/v1/artifacts/${shortId}/analytics`, { headers: authHeaders }),
|
|
416
657
|
) as Promise<ViewStatsJson>
|
|
417
658
|
},
|
|
659
|
+
|
|
660
|
+
async listTemplateLibraries(cursor) {
|
|
661
|
+
const query = new URLSearchParams({ limit: "100" })
|
|
662
|
+
if (cursor) query.set("cursor", cursor)
|
|
663
|
+
return ok(
|
|
664
|
+
await f(`${base}/v1/template-libraries?${query}`, { headers: authHeaders }),
|
|
665
|
+
) as Promise<TemplateLibraryPageJson>
|
|
666
|
+
},
|
|
667
|
+
|
|
668
|
+
async getTemplateLibrary(id) {
|
|
669
|
+
return ok(
|
|
670
|
+
await f(`${base}/v1/template-libraries/${encodeURIComponent(id)}`, {
|
|
671
|
+
headers: authHeaders,
|
|
672
|
+
}),
|
|
673
|
+
) as Promise<TemplateLibraryJson>
|
|
674
|
+
},
|
|
675
|
+
|
|
676
|
+
async getTemplateStarter(libraryId, entryId) {
|
|
677
|
+
return ok(
|
|
678
|
+
await f(
|
|
679
|
+
`${base}/v1/template-libraries/${encodeURIComponent(libraryId)}/entries/${encodeURIComponent(entryId)}/starter`,
|
|
680
|
+
{ headers: authHeaders },
|
|
681
|
+
),
|
|
682
|
+
) as Promise<TemplateStarterJson>
|
|
683
|
+
},
|
|
418
684
|
}
|
|
419
685
|
}
|
package/src/filename.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Choosing the upload filename for an inline `content` publish when the caller gave
|
|
2
|
+
// none. The Derive server types an artifact by its filename FIRST, so a blind
|
|
3
|
+
// `index.html` default silently stores Markdown as text/html — the page then renders
|
|
4
|
+
// the raw Markdown as markup and swallows tag-like text (the 2026-07 retype incident).
|
|
5
|
+
//
|
|
6
|
+
// These mirror the server's own sniff, inlined here because the stdio shim keeps NO
|
|
7
|
+
// @derive/core dependency at runtime (it is a thin HTTP client).
|
|
8
|
+
|
|
9
|
+
/** An HTML page opener: the doctype/`<html>` markers, plus the head/meta/style/
|
|
10
|
+
* title/body openers a headless designed page starts with — matched after skipping
|
|
11
|
+
* leading HTML comments (a comment alone never decides). A leading `<div>`/`<p>`/
|
|
12
|
+
* custom tag is NOT one: that is how HTML-flavored Markdown (a centered README)
|
|
13
|
+
* legitimately opens. Mirrors @derive/core's looksLikeHtmlDocument exactly. */
|
|
14
|
+
export const looksLikeHtmlDocument = (s: string): boolean => {
|
|
15
|
+
let head = s.replace(/^/, "").trimStart()
|
|
16
|
+
for (let i = 0; i < 8 && head.startsWith("<!--"); i++) {
|
|
17
|
+
const end = head.indexOf("-->")
|
|
18
|
+
if (end === -1) return false
|
|
19
|
+
head = head.slice(end + 3).trimStart()
|
|
20
|
+
}
|
|
21
|
+
return /^<(!doctype\s+html|html|head|body|meta|style|title)[\s/>]/i.test(head.slice(0, 64))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The fallback filename for inline content with none given: an HTML page →
|
|
25
|
+
* `index.html`, anything else → `index.md`. So Markdown is never stored as HTML.
|
|
26
|
+
* Caveat: fragment HTML that opens with a `<div>` (indistinguishable from
|
|
27
|
+
* HTML-flavored Markdown) still lands as `.md` — pass an explicit
|
|
28
|
+
* `filename` (or use `edits`) to keep it HTML. */
|
|
29
|
+
export const fallbackFilename = (content: string | undefined): string =>
|
|
30
|
+
looksLikeHtmlDocument(content ?? "") ? "index.html" : "index.md"
|