relic-mcp 0.14.2 → 0.14.4

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.
@@ -4,23 +4,23 @@
4
4
  "description": "Relic: zero-knowledge publishing for agent output. Turns a local file into a shareable link without handing the file to anybody.",
5
5
  "owner": {
6
6
  "name": "The Bushido Collective",
7
- "url": "https://thebushido.co"
7
+ "url": "https://withbushido.ai"
8
8
  },
9
9
  "plugins": [
10
10
  {
11
11
  "name": "relic",
12
12
  "description": "Publish a file from your machine as an encrypted, shareable link. The agent encrypts locally, uploads only ciphertext, and hands back a URL whose fragment holds the key, so the service stores something it cannot read.",
13
- "version": "0.14.2",
13
+ "version": "0.14.4",
14
14
  "source": "./",
15
15
  "author": {
16
16
  "name": "The Bushido Collective",
17
- "url": "https://thebushido.co"
17
+ "url": "https://withbushido.ai"
18
18
  },
19
19
  "homepage": "https://github.com/TheBushidoCollective/relic",
20
20
  "category": "productivity"
21
21
  }
22
22
  ],
23
23
  "metadata": {
24
- "version": "0.14.2"
24
+ "version": "0.14.4"
25
25
  }
26
26
  }
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "relic",
3
- "version": "0.14.2",
3
+ "version": "0.14.4",
4
4
  "description": "Publish a file from your machine as an encrypted, shareable link. The agent encrypts locally, uploads only ciphertext, and hands back a URL whose fragment holds the key, so the service stores something it cannot read.",
5
5
  "mcpServers": "./mcp-servers.json",
6
6
  "author": {
7
7
  "name": "The Bushido Collective",
8
- "url": "https://thebushido.co"
8
+ "url": "https://withbushido.ai"
9
9
  },
10
10
  "homepage": "https://github.com/TheBushidoCollective/relic",
11
11
  "repository": "https://github.com/TheBushidoCollective/relic",
package/README.md CHANGED
@@ -32,9 +32,9 @@ For any client that takes a JSON config:
32
32
  3. Encrypts locally with AES-128-GCM under RFC 8188 `aes128gcm` framing.
33
33
  4. Uploads **only ciphertext**, straight to object storage under a signed URL.
34
34
  It does not pass through the Relic service.
35
- 5. Tells the service four things: a coarse renderer class from a ten-value
36
- list (`markdown`, `code`, `html`, `jsx`, `image`, `pdf`, `media`, `archive`,
37
- `binary`, `otio`), this client's name, the ciphertext's byte length, and the
35
+ 5. Tells the service four things: a coarse renderer class from an eleven-value
36
+ list (`markdown`, `code`, `html`, `jsx`, `image`, `pdf`, `model`, `media`,
37
+ `archive`, `binary`, `otio`), this client's name, the ciphertext's byte length, and the
38
38
  title, which defaults to the filename and is stored in the clear. By default,
39
39
  it also renders a 1200x630 cover image of the file's first screen in local
40
40
  headless Chrome and uploads it unencrypted via `POST /api/relics/{id}/cover`
@@ -42,6 +42,7 @@ For any client that takes a JSON config:
42
42
  timeline relics (`otio`) skip unencrypted first-screen media rendering by
43
43
  default, using a generic card carrying the title and format label to protect
44
44
  private timeline cuts and media from leaking into unencrypted link previews.
45
+ STL and OBJ models also use a generic card instead of a geometry preview.
45
46
  Not the mimetype, not the contents.
46
47
  6. Records the relic's id, key, and publish token locally, in a 0600 file
47
48
  under your user config directory, so the relic can be republished from
@@ -1300,8 +1300,15 @@ function renderMarkdown(source) {
1300
1300
  flushParagraph();
1301
1301
  const ordered = /^\s*\d+\./.test(line);
1302
1302
  const items = [];
1303
- while (index < lines.length && /^\s*([-*+]|\d+\.)\s+/.test(lines[index])) {
1304
- items.push(lines[index].replace(/^\s*([-*+]|\d+\.)\s+/, ""));
1303
+ while (index < lines.length) {
1304
+ const current = lines[index];
1305
+ if (/^\s*([-*+]|\d+\.)\s+/.test(current)) {
1306
+ items.push(current.replace(/^\s*([-*+]|\d+\.)\s+/, ""));
1307
+ } else if (/^\s+\S/.test(current) && !/^\s*(>|```)/.test(current)) {
1308
+ items[items.length - 1] += ` ${current.trim()}`;
1309
+ } else {
1310
+ break;
1311
+ }
1305
1312
  index++;
1306
1313
  }
1307
1314
  const tag = ordered ? "ol" : "ul";
@@ -4786,7 +4786,16 @@ canvas.relic-page {
4786
4786
  .stage-otio > .notice { flex: none; }
4787
4787
  .otio-view { --otio-label-width: 174px; display: flex; flex-direction: column; flex: 1 1 0; min-height: 0; min-width: 0; width: 100%; }
4788
4788
  .otio-loading { margin: 1rem; font: .8rem var(--mono); color: var(--ink-soft); }
4789
- .otio-monitor-row { display: grid; grid-template-columns: minmax(0, 1fr) 244px; grid-template-rows: minmax(0, 1fr); flex: 1 1 0; min-height: 20%; border-bottom: 1px solid var(--rule-strong); }
4789
+ .otio-monitor-row { display: grid; grid-template-columns: minmax(0, 1fr) 244px; grid-template-rows: minmax(0, 1fr); flex: 1 1 0; min-height: 0; border-bottom: 1px solid var(--rule-strong); }
4790
+ .otio-resize-handle { position: relative; flex: 0 0 16px; min-width: 0; cursor: row-resize; touch-action: none; user-select: none; background: var(--ground); border-bottom: 1px solid var(--rule-strong); }
4791
+ .otio-resize-handle::before { content: ""; position: absolute; top: 50%; left: 50%; width: 36px; height: 4px; transform: translate(-50%, -50%); border-block: 1px solid var(--ink-soft); }
4792
+ .otio-resize-handle:hover::before, .otio-resize-handle:focus-visible::before, .otio-view.is-resizing .otio-resize-handle::before { border-color: var(--patina); }
4793
+ .otio-resize-handle:focus-visible { outline: 2px solid var(--patina); outline-offset: -2px; }
4794
+ .otio-resize-handle[aria-disabled="true"] { cursor: default; }
4795
+ .otio-view.is-resizing, .otio-view.is-resizing * { cursor: row-resize !important; user-select: none !important; }
4796
+ @media (pointer: coarse) {
4797
+ .otio-resize-handle { flex-basis: 24px; }
4798
+ }
4790
4799
  .otio-preview { position: relative; background: #080a0b; min-width: 0; min-height: 0; display: grid; place-items: center; overflow: hidden; }
4791
4800
  .otio-canvas { display: block; width: 100%; height: 100%; min-width: 0; min-height: 0; object-fit: contain; }
4792
4801
  .otio-preview-scope { position: absolute; top: .7rem; left: .7rem; max-width: calc(100% - 1.4rem); padding: .2rem .35rem; color: #e6e9e7; background: #080a0bd9; font: .6rem/1.5 var(--mono); }
@@ -4856,6 +4865,10 @@ canvas.relic-page {
4856
4865
  .otio-transport { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; min-height: 46px; padding: .55rem .75rem; border-bottom: 1px solid var(--rule-strong); background: var(--surface); }
4857
4866
  .otio-transport button { padding: .3rem .45rem; border: 1px solid var(--rule-strong); background: transparent; color: var(--ink); font: .65rem var(--sans); cursor: pointer; }
4858
4867
  .otio-transport .otio-play { background: var(--patina); border-color: var(--patina); color: var(--surface); min-width: 44px; }
4868
+ .otio-focus-context { display: flex; align-items: center; gap: .35rem .65rem; flex: 1 0 100%; min-width: 0; padding-bottom: .35rem; border-bottom: 1px solid var(--rule); }
4869
+ .otio-focus-title { flex: 1 1 0; min-width: 0; overflow-wrap: anywhere; color: var(--patina-ink); font: 600 .68rem/1.5 var(--sans); }
4870
+ .otio-transport .otio-back-composition { flex: none; color: var(--patina-ink); }
4871
+ .otio-frame-icon { display: none; width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.5; }
4859
4872
  .otio-time { font: .7rem var(--mono); font-variant-numeric: tabular-nums; }
4860
4873
  .otio-seek { min-width: 65px; width: 100px; flex: 1; accent-color: var(--patina); cursor: pointer; }
4861
4874
  .otio-transport .otio-inspector-toggle { display: none; }
@@ -4867,6 +4880,7 @@ canvas.relic-page {
4867
4880
  .otio-mode[aria-pressed="true"] { border-color: var(--patina); background: var(--diff-added); color: var(--patina-ink); }
4868
4881
  .otio-track-select, .otio-timeline-select { max-width: 190px; min-width: 0; border: 1px solid var(--rule-strong); background: var(--surface); color: var(--ink); padding: .28rem; padding-inline-end: 1.5rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: .62rem var(--sans); }
4869
4882
  .otio-timeline-scroll { flex: 0 1 auto; min-height: 0; max-height: min(240px, 30%); max-width: 100%; overflow: auto; overscroll-behavior: contain; scroll-padding-inline-start: var(--otio-label-width); }
4883
+ .otio-view.is-resized .otio-timeline-scroll { flex: 0 0 auto; max-height: none; }
4870
4884
  .otio-track-grid { position: relative; min-width: 760px; }
4871
4885
  .otio-ruler, .otio-track-row { display: grid; grid-template-columns: var(--otio-label-width) minmax(0, 1fr); border-bottom: 1px solid var(--rule); }
4872
4886
  .otio-ruler-label { position: sticky; z-index: 7; left: 0; padding: .35rem .6rem; border-right: 1px solid var(--rule-strong); background: var(--surface); color: var(--ink-soft); font: .6rem var(--mono); }
@@ -4880,7 +4894,8 @@ canvas.relic-page {
4880
4894
  .otio-track-label { position: sticky; left: 0; z-index: 6; display: flex; align-items: center; gap: .35rem; min-width: 0; padding: .4rem .45rem; border-right: 1px solid var(--rule-strong); background: var(--surface); }
4881
4895
  .otio-track-code { flex: 0 0 auto; color: var(--ink-soft); font: .6rem var(--mono); }
4882
4896
  .otio-track-name { flex: 1; min-width: 0; line-height: 1.35; }
4883
- .otio-track-name strong { display: block; font: 500 .65rem var(--sans); overflow-wrap: anywhere; }
4897
+ .otio-track-activate { display: block; width: 100%; padding: 0; border: 0; background: transparent; color: var(--ink); text-align: left; font: 500 .65rem/1.35 var(--sans); overflow-wrap: anywhere; cursor: pointer; }
4898
+ .otio-track-activate[aria-pressed="true"] { color: var(--patina-ink); text-decoration: underline; text-underline-offset: .2em; }
4884
4899
  .otio-track-name small { display: block; color: var(--ink-soft); font: .48rem var(--mono); overflow-wrap: anywhere; }
4885
4900
  .otio-track-row:not([data-depth="0"]) .otio-track-label { padding-left: calc(.45rem + min(var(--otio-depth, 0), 4) * .35rem); background: var(--ground); }
4886
4901
  .otio-track-toggle, .otio-disclosure { flex: 0 0 auto; border: 0; background: transparent; color: var(--ink-soft); padding: .2rem; font: .53rem var(--sans); cursor: pointer; }
@@ -4903,6 +4918,9 @@ canvas.relic-page {
4903
4918
  .otio-item.has-diagnostic { border-color: var(--oxide); color: var(--oxide); }
4904
4919
  .otio-item.is-unplaced, .otio-item.is-trimmed-out { position: relative; display: block; left: auto; top: auto; bottom: auto; width: auto; margin: .2rem; border-style: dashed; color: var(--oxide); white-space: normal; }
4905
4920
  .otio-item.is-trimmed-out { color: var(--ink-soft); opacity: .6; }
4921
+ .otio-item[aria-pressed="true"] { border-color: var(--patina); box-shadow: inset 0 0 0 1px var(--patina); background-color: var(--diff-added); color: var(--patina-ink); opacity: 1; }
4922
+ .otio-item[aria-pressed="true"][data-enabled="false"] { border-style: dashed; }
4923
+ .otio-focus-status { font: .75rem/1.6 var(--mono); color: #e6e9e7; }
4906
4924
  .otio-timeline-marker { position: absolute; top: 0; height: 5px; min-width: 7px; z-index: 4; padding: 0; border: 1px solid var(--ink); border-radius: 0; background: var(--otio-marker-color, var(--patina)); cursor: pointer; }
4907
4925
  .otio-timeline-marker.is-point { top: 1px; width: 7px; height: 7px; z-index: 5; transform: translateX(-50%) rotate(45deg); }
4908
4926
  .otio-selection { position: absolute; top: 1px; bottom: 1px; z-index: 3; min-width: 4px; border: 2px dashed var(--patina); background: color-mix(in srgb, var(--patina) 22%, transparent); pointer-events: none; }
@@ -4965,6 +4983,10 @@ canvas.relic-page {
4965
4983
  padding-top: 0.15rem;
4966
4984
  background: var(--surface);
4967
4985
  }
4986
+ .popover-card.is-otio-composer.is-focused-otio-composer { padding: .2rem .4rem; }
4987
+ .popover-card.is-otio-composer.is-focused-otio-composer .popover-bounded-body { min-height: 0; }
4988
+ .popover-card.is-otio-composer.is-focused-otio-composer .popover-field { flex: 0 1 3rem; height: 3rem; min-height: calc(1.3em + .4rem + 2px); padding: .2rem .3rem; font-size: .85rem; line-height: 1.3; }
4989
+ .popover-card.is-otio-composer.is-focused-otio-composer .popover-tools { margin-top: .15rem; padding-top: .15rem; }
4968
4990
  @media (max-width: 64rem) {
4969
4991
  .otio-monitor-row { grid-template-columns: minmax(0, 1fr) 210px; }
4970
4992
  .otio-selection-tools { width: 100%; }
@@ -4978,8 +5000,13 @@ canvas.relic-page {
4978
5000
  .otio-transport .otio-inspector-toggle { display: block; }
4979
5001
  .otio-transport { gap: .4rem; padding: .5rem; }
4980
5002
  .otio-seek { flex-basis: 100%; order: 2; }
5003
+ .otio-transport .otio-previous-frame, .otio-transport .otio-next-frame { display: inline-grid; place-items: center; width: 32px; min-height: 28px; padding: .25rem; font-size: 0; }
5004
+ .otio-frame-icon { display: block; }
5005
+ .otio-focus-context { gap: .25rem .5rem; }
5006
+ .otio-focus-title { flex-basis: 8rem; font-size: .63rem; }
4981
5007
  .otio-timeline-heading { padding: .65rem .5rem; }
4982
- .otio-track-select { max-width: 160px; }
5008
+ .otio-track-select { flex: 1 1 7rem; width: 7rem; max-width: 160px; }
5009
+ .otio-view.is-element-focused .otio-selection-label { display: none; }
4983
5010
  .otio-preview-scope { font-size: .5rem; }
4984
5011
  .otio-preview-status { padding: 0 .5rem; overscroll-behavior: contain; }
4985
5012
  .otio-preview:has(> .otio-preview-status:not([hidden])) > .otio-preview-scope { margin-top: .35rem; }
package/dist/relic-mcp.js CHANGED
@@ -26495,6 +26495,69 @@ function generateRelicId() {
26495
26495
  // ../relic-format/src/index.ts
26496
26496
  init_mnemonic();
26497
26497
 
26498
+ // ../relic-format/src/model.ts
26499
+ function modelFormatFromFilename(filename) {
26500
+ const dot = filename.lastIndexOf(".");
26501
+ if (dot <= filename.lastIndexOf("/") || dot < 0)
26502
+ return;
26503
+ const ext = filename.slice(dot + 1).toLowerCase();
26504
+ return ext === "stl" || ext === "obj" ? ext : undefined;
26505
+ }
26506
+ function modelFormatFromMime(mimetype) {
26507
+ const mime = mimetype.toLowerCase().split(";")[0]?.trim();
26508
+ if (mime === "model/stl")
26509
+ return "stl";
26510
+ if (mime === "model/obj")
26511
+ return "obj";
26512
+ return;
26513
+ }
26514
+ function sniffModelFormat(content) {
26515
+ if (content.length >= 84) {
26516
+ const count = new DataView(content.buffer, content.byteOffset, content.byteLength).getUint32(80, true);
26517
+ if (count > 0 && 84 + count * 50 === content.length)
26518
+ return "stl";
26519
+ }
26520
+ const decoder = new TextDecoder("utf-8", { fatal: true });
26521
+ let pending = "";
26522
+ let hasSolid = false;
26523
+ let hasFacet = false;
26524
+ let hasNormal = false;
26525
+ let hasVertex = false;
26526
+ let hasFace = false;
26527
+ try {
26528
+ for (let offset = 0;offset < content.length; offset += 65536) {
26529
+ const end = Math.min(content.length, offset + 65536);
26530
+ pending += decoder.decode(content.subarray(offset, end), {
26531
+ stream: end < content.length
26532
+ });
26533
+ const lines = pending.split(/[\r\n]/);
26534
+ pending = lines.pop() ?? "";
26535
+ if (pending.length > 65536)
26536
+ pending = "";
26537
+ if (end === content.length)
26538
+ lines.push(pending);
26539
+ for (const line of lines) {
26540
+ if (/^\s*solid(?:\s|$)/i.test(line))
26541
+ hasSolid = true;
26542
+ if (/\bfacet\b/i.test(line))
26543
+ hasFacet = true;
26544
+ if (/\bnormal\b/i.test(line))
26545
+ hasNormal = true;
26546
+ if (/^\s*v\s+[-+\d.]/.test(line))
26547
+ hasVertex = true;
26548
+ if (/^\s*f\s+\S+\s+\S+\s+\S+/.test(line))
26549
+ hasFace = true;
26550
+ if (hasSolid && hasFacet && hasNormal)
26551
+ return "stl";
26552
+ if (hasVertex && hasFace)
26553
+ return "obj";
26554
+ }
26555
+ }
26556
+ } catch {
26557
+ return;
26558
+ }
26559
+ return;
26560
+ }
26498
26561
  // ../relic-format/src/otio-json.ts
26499
26562
  class OtioRawNumber {
26500
26563
  raw;
@@ -29562,6 +29625,7 @@ function privilegeTier(cls) {
29562
29625
  return 3;
29563
29626
  case "image":
29564
29627
  case "pdf":
29628
+ case "model":
29565
29629
  return 2;
29566
29630
  case "markdown":
29567
29631
  case "code":
@@ -29583,6 +29647,11 @@ function deriveRendererClass(content, filename) {
29583
29647
  }
29584
29648
  }
29585
29649
  const extension = extensionOf(filename);
29650
+ const candidate = sniffModelFormat(content);
29651
+ const declaredModel = modelFormatFromFilename(filename);
29652
+ if (candidate && (declaredModel === candidate || !declaredModel && (extension === undefined || EXTENSIONS[extension] === undefined)) && sniffContentClass(content) !== "html") {
29653
+ return "model";
29654
+ }
29586
29655
  if (extension !== undefined) {
29587
29656
  const fromExtension = EXTENSIONS[extension];
29588
29657
  if (fromExtension !== undefined) {
@@ -29610,6 +29679,7 @@ function leastPrivileged(first, second) {
29610
29679
  "jsx",
29611
29680
  "image",
29612
29681
  "pdf",
29682
+ "model",
29613
29683
  "markdown",
29614
29684
  "code",
29615
29685
  "media",
@@ -30294,6 +30364,23 @@ var MAX_RENDER_BYTES = 100 * 1024 * 1024;
30294
30364
  function routeFor(filename, declaredMimetype, content) {
30295
30365
  const declared = classFromMimetype(declaredMimetype, filename);
30296
30366
  let sniffed = sniffContentClass(content);
30367
+ const filenameModel = modelFormatFromFilename(filename);
30368
+ const mimeModel = modelFormatFromMime(declaredMimetype);
30369
+ const mime = declaredMimetype.toLowerCase().split(";")[0]?.trim() ?? "";
30370
+ const genericMime = mime === "" || mime === "application/octet-stream" || mime === "text/plain";
30371
+ const candidate = sniffed === "code" || sniffed === "binary" ? sniffModelFormat(content) : undefined;
30372
+ const declaredModel = filenameModel ?? mimeModel;
30373
+ if (declaredModel || candidate) {
30374
+ const namedClass = deriveRendererClass(new Uint8Array([0]), filename);
30375
+ const conflicting = filenameModel && mimeModel && filenameModel !== mimeModel || declaredModel && candidate && declaredModel !== candidate || filenameModel && !genericMime && !mimeModel || candidate && !filenameModel && namedClass !== "binary" || candidate && !declaredModel && !genericMime;
30376
+ if (candidate && !conflicting && (!declaredModel || candidate === declaredModel)) {
30377
+ return { route: "model", notice: undefined };
30378
+ }
30379
+ return {
30380
+ route: "download",
30381
+ notice: "The file name or type does not match its contents. Download it to inspect it."
30382
+ };
30383
+ }
30297
30384
  if (declared === "jsx" && sniffed === "code") {
30298
30385
  const source = new TextDecoder("utf-8", { fatal: false }).decode(content);
30299
30386
  if (isComponentSource(source, filename))
@@ -30320,6 +30407,8 @@ function routeForClass(cls) {
30320
30407
  return "media";
30321
30408
  case "otio":
30322
30409
  return "otio";
30410
+ case "model":
30411
+ return "model";
30323
30412
  case "html":
30324
30413
  return "sandboxed-html";
30325
30414
  case "jsx":
@@ -30340,6 +30429,8 @@ function classFromMimetype(mimetype, filename) {
30340
30429
  return "html";
30341
30430
  if (type === "text/jsx" || type === "text/tsx")
30342
30431
  return "jsx";
30432
+ if (type === "model/stl" || type === "model/obj")
30433
+ return "model";
30343
30434
  if (type.startsWith("image/"))
30344
30435
  return "image";
30345
30436
  if (type.startsWith("video/") || type.startsWith("audio/"))
@@ -30376,6 +30467,8 @@ function describe(cls) {
30376
30467
  return "audio or video";
30377
30468
  case "otio":
30378
30469
  return "an OpenTimelineIO timeline";
30470
+ case "model":
30471
+ return "a 3D model";
30379
30472
  case "archive":
30380
30473
  return "an archive";
30381
30474
  default:
@@ -31274,6 +31367,7 @@ async function publish(input, deps) {
31274
31367
  });
31275
31368
  }
31276
31369
  const filename = input.filename ?? source.basename;
31370
+ const coverFilename = modelFormatFromFilename(source.basename) === undefined ? filename : source.basename;
31277
31371
  const normalizedTitle = normalizeTitle(input.title ?? filename);
31278
31372
  const rendererClass = deriveRendererClass(source.bytes, filename);
31279
31373
  const digest = await contentDigest({
@@ -31293,7 +31387,7 @@ async function publish(input, deps) {
31293
31387
  try {
31294
31388
  coverOutcome = await deps.renderCover({
31295
31389
  bytes: source.bytes,
31296
- filename,
31390
+ filename: coverFilename,
31297
31391
  mimetype: guessMimetype(filename, rendererClass),
31298
31392
  rendererClass
31299
31393
  });
@@ -31595,12 +31689,16 @@ var CLASS_FALLBACK = {
31595
31689
  pdf: "application/pdf",
31596
31690
  image: "application/octet-stream",
31597
31691
  media: "application/octet-stream",
31692
+ model: "application/octet-stream",
31598
31693
  otio: "application/x-otioz",
31599
31694
  archive: "application/octet-stream",
31600
31695
  binary: "application/octet-stream"
31601
31696
  };
31602
31697
  function guessMimetype(filename, rendererClass) {
31603
31698
  const base = filename.slice(filename.lastIndexOf("/") + 1).toLowerCase();
31699
+ const modelFormat = modelFormatFromFilename(base);
31700
+ if (modelFormat !== undefined)
31701
+ return `model/${modelFormat}`;
31604
31702
  const dot = base.lastIndexOf(".");
31605
31703
  if (dot > 0 && dot < base.length - 1) {
31606
31704
  const found = MIMETYPES[base.slice(dot + 1)];
@@ -34523,6 +34621,16 @@ async function extractMediaCover(bytes, filename, mimetype, ffmpegPath, totalTim
34523
34621
  // src/cover/inputs.ts
34524
34622
  var TEXT_PREFIX_BYTES = 1048576;
34525
34623
  async function planCover(source, options) {
34624
+ if (source.rendererClass === "model" || modelFormatFromFilename(source.filename) !== undefined || modelFormatFromMime(source.mimetype) !== undefined || sniffModelFormat(source.bytes) !== undefined) {
34625
+ return {
34626
+ kind: "skip",
34627
+ outcome: {
34628
+ status: "skipped",
34629
+ reason: "no_cover_for_class",
34630
+ detail: "Generic cover used for 3D model (public title and format label only)"
34631
+ }
34632
+ };
34633
+ }
34526
34634
  if (source.bytes.length === 0 && (source.rendererClass === "markdown" || source.rendererClass === "code")) {
34527
34635
  return {
34528
34636
  kind: "skip",
@@ -35479,6 +35587,7 @@ async function republish(input, deps) {
35479
35587
  }
35480
35588
  const source = await readSource(input.path, deps.files);
35481
35589
  const filename = input.filename ?? source.basename;
35590
+ const coverFilename = modelFormatFromFilename(source.basename) === undefined ? filename : source.basename;
35482
35591
  const normalizedTitle = normalizeTitle(input.title ?? filename);
35483
35592
  const rendererClass = deriveRendererClass(source.bytes, filename);
35484
35593
  const digest = await contentDigest({
@@ -35509,7 +35618,7 @@ async function republish(input, deps) {
35509
35618
  }
35510
35619
  const outcome = await deps.renderCover({
35511
35620
  bytes: source.bytes,
35512
- filename,
35621
+ filename: coverFilename,
35513
35622
  mimetype: guessMimetype(filename, rendererClass),
35514
35623
  rendererClass
35515
35624
  });
@@ -35539,7 +35648,7 @@ async function republish(input, deps) {
35539
35648
  try {
35540
35649
  coverOutcome = await deps.renderCover({
35541
35650
  bytes: source.bytes,
35542
- filename,
35651
+ filename: coverFilename,
35543
35652
  mimetype: guessMimetype(filename, rendererClass),
35544
35653
  rendererClass
35545
35654
  });
@@ -36993,7 +37102,7 @@ var DESCRIBE_TOOL_DEFINITION = {
36993
37102
  additionalProperties: false
36994
37103
  }
36995
37104
  };
36996
- var SERVER_VERSION = "0.14.2";
37105
+ var SERVER_VERSION = "0.14.4";
36997
37106
  var SERVER_INFO = {
36998
37107
  name: "relic",
36999
37108
  title: "Relic",
package/mcp-servers.json CHANGED
@@ -4,10 +4,10 @@
4
4
  "command": "npx",
5
5
  "args": [
6
6
  "-y",
7
- "relic-mcp@0.14.2"
7
+ "relic-mcp@0.14.4"
8
8
  ],
9
9
  "env": {
10
- "RELIC_CLIENT_NAME": "relic-plugin/0.14.2"
10
+ "RELIC_CLIENT_NAME": "relic-plugin/0.14.4"
11
11
  }
12
12
  }
13
13
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relic-mcp",
3
- "version": "0.14.2",
3
+ "version": "0.14.4",
4
4
  "description": "Publish a file as an encrypted relic. The key is generated on your machine and never sent to the service.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "nodeApiVersion": 8,
21
21
  "schemaVersion": 3,
22
- "sourceSha256": "4848f49a1af80a1da9be79d16a5555efe709e76e91a34146015874b34e752921",
22
+ "sourceSha256": "d9c7ba4de9fdca6b2652ff25d58aa1aa2afe38312af26376561ba1543ff5fad0",
23
23
  "target": "darwin-arm64",
24
24
  "toolchain": {
25
25
  "binaryDeploymentTarget": "15.0",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "nodeApiVersion": 8,
21
21
  "schemaVersion": 3,
22
- "sourceSha256": "4848f49a1af80a1da9be79d16a5555efe709e76e91a34146015874b34e752921",
22
+ "sourceSha256": "d9c7ba4de9fdca6b2652ff25d58aa1aa2afe38312af26376561ba1543ff5fad0",
23
23
  "target": "darwin-x64",
24
24
  "toolchain": {
25
25
  "binaryDeploymentTarget": "15.0",
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "nodeApiVersion": 8,
22
22
  "schemaVersion": 3,
23
- "sourceSha256": "4848f49a1af80a1da9be79d16a5555efe709e76e91a34146015874b34e752921",
23
+ "sourceSha256": "d9c7ba4de9fdca6b2652ff25d58aa1aa2afe38312af26376561ba1543ff5fad0",
24
24
  "target": "linux-arm64-gnu",
25
25
  "toolchain": {
26
26
  "compilerPath": "/opt/rh/gcc-toolset-14/root/usr/bin/gcc",
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "nodeApiVersion": 8,
22
22
  "schemaVersion": 3,
23
- "sourceSha256": "4848f49a1af80a1da9be79d16a5555efe709e76e91a34146015874b34e752921",
23
+ "sourceSha256": "d9c7ba4de9fdca6b2652ff25d58aa1aa2afe38312af26376561ba1543ff5fad0",
24
24
  "target": "linux-x64-gnu",
25
25
  "toolchain": {
26
26
  "compilerPath": "/opt/rh/gcc-toolset-14/root/usr/bin/gcc",
@@ -1,4 +1,9 @@
1
- import { sniffContentClass } from '@relic/format';
1
+ import {
2
+ modelFormatFromFilename,
3
+ modelFormatFromMime,
4
+ sniffContentClass,
5
+ sniffModelFormat,
6
+ } from '@relic/format';
2
7
  import {
3
8
  COVER_INPUT_PART_BYTES,
4
9
  type CoverOutcome,
@@ -31,6 +36,25 @@ export async function planCover(
31
36
  source: CoverSource,
32
37
  options?: PlanCoverOptions
33
38
  ): Promise<CoverPlan> {
39
+ // Covers are public. Never pass declared or byte-recognized model content
40
+ // to a text, image, or media renderer, even when declarations disagree.
41
+ if (
42
+ source.rendererClass === 'model' ||
43
+ modelFormatFromFilename(source.filename) !== undefined ||
44
+ modelFormatFromMime(source.mimetype) !== undefined ||
45
+ sniffModelFormat(source.bytes) !== undefined
46
+ ) {
47
+ return {
48
+ kind: 'skip',
49
+ outcome: {
50
+ status: 'skipped',
51
+ reason: 'no_cover_for_class',
52
+ detail:
53
+ 'Generic cover used for 3D model (public title and format label only)',
54
+ },
55
+ };
56
+ }
57
+
34
58
  if (
35
59
  source.bytes.length === 0 &&
36
60
  (source.rendererClass === 'markdown' || source.rendererClass === 'code')
package/src/publish.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  generateKey,
22
22
  generateRelicId,
23
23
  indexOtioZip,
24
+ modelFormatFromFilename,
24
25
  normalizeTitle,
25
26
  PLAINTEXT_CAP_BYTES,
26
27
  parseOtioJson,
@@ -378,6 +379,12 @@ export async function publish(
378
379
  }
379
380
 
380
381
  const filename = input.filename ?? source.basename;
382
+ // A display-name override must not hide the source's model extension from
383
+ // the public-cover guard.
384
+ const coverFilename =
385
+ modelFormatFromFilename(source.basename) === undefined
386
+ ? filename
387
+ : source.basename;
381
388
  const normalizedTitle = normalizeTitle(input.title ?? filename);
382
389
  // The class is derived from bytes this process already holds, never taken as
383
390
  // a tool input. Exposing it as a parameter would make the taxonomy
@@ -404,7 +411,7 @@ export async function publish(
404
411
  try {
405
412
  coverOutcome = await deps.renderCover({
406
413
  bytes: source.bytes,
407
- filename,
414
+ filename: coverFilename,
408
415
  mimetype: guessMimetype(filename, rendererClass),
409
416
  rendererClass,
410
417
  });
@@ -977,6 +984,7 @@ const CLASS_FALLBACK: Readonly<Record<RendererClass, string>> = {
977
984
  pdf: 'application/pdf',
978
985
  image: 'application/octet-stream',
979
986
  media: 'application/octet-stream',
987
+ model: 'application/octet-stream',
980
988
  otio: 'application/x-otioz',
981
989
  archive: 'application/octet-stream',
982
990
  binary: 'application/octet-stream',
@@ -994,6 +1002,8 @@ export function guessMimetype(
994
1002
  rendererClass: RendererClass
995
1003
  ): string {
996
1004
  const base = filename.slice(filename.lastIndexOf('/') + 1).toLowerCase();
1005
+ const modelFormat = modelFormatFromFilename(base);
1006
+ if (modelFormat !== undefined) return `model/${modelFormat}`;
997
1007
  const dot = base.lastIndexOf('.');
998
1008
  if (dot > 0 && dot < base.length - 1) {
999
1009
  const found = MIMETYPES[base.slice(dot + 1)];
package/src/republish.ts CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  deriveShareProof,
17
17
  encryptRelic,
18
18
  isValidRelicId,
19
+ modelFormatFromFilename,
19
20
  normalizeTitle,
20
21
  type RendererClass,
21
22
  shareVerifierFor,
@@ -214,6 +215,10 @@ export async function republish(
214
215
  // still open, so naming the duplicate first names the cause.
215
216
  const source = await readSource(input.path, deps.files);
216
217
  const filename = input.filename ?? source.basename;
218
+ const coverFilename =
219
+ modelFormatFromFilename(source.basename) === undefined
220
+ ? filename
221
+ : source.basename;
217
222
  const normalizedTitle = normalizeTitle(input.title ?? filename);
218
223
  // Same rule as a first publish: the class comes from bytes in hand, never
219
224
  // from a tool input, so the taxonomy stays machine-attested.
@@ -265,7 +270,7 @@ export async function republish(
265
270
  // same && requested && !previous -> render first
266
271
  const outcome = await deps.renderCover({
267
272
  bytes: source.bytes,
268
- filename,
273
+ filename: coverFilename,
269
274
  mimetype: guessMimetype(filename, rendererClass),
270
275
  rendererClass,
271
276
  });
@@ -305,7 +310,7 @@ export async function republish(
305
310
  try {
306
311
  coverOutcome = await deps.renderCover({
307
312
  bytes: source.bytes,
308
- filename,
313
+ filename: coverFilename,
309
314
  mimetype: guessMimetype(filename, rendererClass),
310
315
  rendererClass,
311
316
  });