dsh-quick-toc 0.2.2 → 0.3.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/README.en.md CHANGED
@@ -2,18 +2,28 @@
2
2
 
3
3
  > **English** | [中文](README.md)
4
4
 
5
- A quick conversation TOC plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH): extracts Markdown headings (H1–H6) from AI replies into a navigable outline panel, grouped by conversation turn, with auto-follow highlighting.
5
+ A quick conversation TOC plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH): extracts Markdown headings (H1–H6) from AI replies into a navigable outline panel, grouped by conversation turn, with auto-follow highlighting, keyword search and in-chat match highlighting.
6
6
 
7
7
  ## Features
8
8
 
9
- - **Turn-grouped outline** — each user message + its AI replies form one group, with the group's end time as the header
9
+ - **Turn-grouped outline** — each user message + its AI replies form one group, with the group's end time as the header (plus the turn's first-line preview; click it to jump to the turn start)
10
+ - **Keyword search** — a magnifier in the header opens the search box; Enter cycles matches (n/N counter), Esc closes; toggle between **title / full-text** scope
11
+ - **In-chat highlighting** — matched keywords are highlighted in the conversation; the current match gets a distinct highlight and is scrolled to the upper-middle of the viewport; every occurrence counts toward n/N
10
12
  - **Auto-follow highlight** — as you scroll the conversation, the turns visible in the viewport light up in the outline (multiple at once); the outline auto-loads and scrolls to keep them visible
11
13
  - **Smooth jump** — click a heading to glide to the exact heading position in the conversation
12
14
  - **Dockable & resizable** — dock left or right (drag the top bar), resize from the edges/corner, collapse to a draggable edge handle; panel size/position remembered
13
- - **Scrollbar follows the dock** — scrollbar sits on the left when docked left, right when docked right
14
15
  - **Paged rendering** — shows the latest groups first; scrolling to the top of the outline loads older ones
15
16
  - **Markdown-aware titles** — inline `**bold**`, *italic*, `` `code` ``, `[links](url)`, `~~strike~~` are stripped from heading text
16
- - Auto-hides when the conversation has no headings; works in light/dark themes
17
+ - Auto-hides when the conversation has no headings; works in light/dark themes; the inner-shadow card look adapts to the theme (white bevel in dark)
18
+
19
+ ## Compatibility
20
+
21
+ | Plugin | DSH |
22
+ | --- | --- |
23
+ | **0.3.0** | **≥ 0.1.2-rc.1** (new slot architecture) |
24
+ | 0.2.2 | DSH versions before 0.1.2-rc.1 |
25
+
26
+ 0.3.0 re-targeted the host integration (new slot architecture + `useChat` session data) and **only supports DSH 0.1.2-rc.1 and above**; for older DSH builds install 0.2.2. On install/update the DSH market pre-flights host compatibility from `dsh.compatibility.dshReleases` and `peerDependencies` in package.json and warns on a mismatch.
17
27
 
18
28
  ## Install
19
29
 
@@ -47,9 +57,10 @@ Restart DSH (double-click `restart-dsh.bat` on Windows) and open the Web UI. The
47
57
  ## Development
48
58
 
49
59
  - `lib/client.js` — all UI logic (browser side)
50
- - `lib/index.js` — host-side no-op entry
60
+ - `lib/index.js` — host-side no-op entry (extend with a version gate if needed)
51
61
  - `cordis.patch.yml` — loader patch (official DSH bundle format)
52
- - Client changes hot-load after a browser hard refresh; loader/patch changes need a DSH restart
62
+ - **Since 0.3.0 the plugin targets the new plugin model**: the panel registers into the session-scoped `conversation.input.overlay` slot (session hooks are contributed via `ctx.uiSession.provide`, `useChat` by ui-chat), and the panel body `createPortal`s to `document.body` as a fixed floating dock; data comes from `props.useChat` (`ChatSnapshot.order` + `nodes`; node shape: `kind: user/assistant-step`, `location.turn`, `data.blocks`)
63
+ - Client changes need a DSH restart (boot rev is content-based)
53
64
 
54
65
  ## License
55
66
 
package/README.md CHANGED
@@ -2,18 +2,28 @@
2
2
 
3
3
  > [English](README.en.md) | **中文**
4
4
 
5
- **DeepSeek Harness(DSH)对话大纲插件**:把 AI 回复中的 Markdown 标题(H1–H6)提取成可导航的大纲面板,按对话回合分组,自动跟随阅读位置。
5
+ **DeepSeek Harness(DSH)对话大纲插件**:把 AI 回复中的 Markdown 标题(H1–H6)提取成可导航的大纲面板,按对话回合分组,自动跟随阅读位置,支持关键字搜索与对话内高亮定位。
6
6
 
7
7
  ## 功能
8
8
 
9
- - **按回合分组** —— 每条用户消息 + 其后续 AI 回复为一组,组头显示该组最后一条消息的结束时间
9
+ - **按回合分组** —— 每条用户消息 + 其后续 AI 回复为一组,组头显示该组最后一条消息的结束时间(附带该回合首行预览,点击可跳到回合开头)
10
+ - **关键字搜索** —— 标题栏放大镜打开搜索框,回车逐个定位(n/N 计数),Esc 关闭;支持 **标题 / 全文** 范围切换
11
+ - **对话内高亮** —— 命中关键字在对话中高亮,当前命中单独标亮并滚动到视口上方约 1/3 处;同一条消息内的多次命中都计入 n/N
10
12
  - **自动跟随高亮** —— 滚动对话框时,视口内可见的回合在大纲中自动点亮(多组可同时点亮);大纲自动加载并滚动,保证正在读的组始终可见
11
13
  - **平滑跳转** —— 点击标题平滑滚动到对话中该标题的准确位置
12
14
  - **可停靠、可缩放** —— 左右停靠(拖顶部横条移动)、边缘/角部拖拽调整大小、收起成可拖动的边缘把手;大小与位置自动记忆
13
- - **滚动条跟随停靠方向** —— 停靠左侧时滚动条在左,停靠右侧时在右
14
15
  - **分页渲染** —— 默认显示最近几组,大纲滚动到顶部自动加载更早的组
15
16
  - **Markdown 清理** —— 标题中的 `**加粗**`、*斜体*、`` `代码` ``、`[链接](url)`、`~~删除线~~` 等行内标记自动剥离
16
- - 对话无标题时自动隐藏;适配深色 / 浅色主题
17
+ - 对话无标题时自动隐藏;适配深色 / 浅色主题;内阴影卡片质感随主题切换(深色为白色高光)
18
+
19
+ ## 兼容性
20
+
21
+ | 插件版本 | DSH 版本 |
22
+ | --- | --- |
23
+ | **0.3.0** | **≥ 0.1.2-rc.1**(新 slot 架构) |
24
+ | 0.2.2 | 0.1.2-rc.1 之前的旧版 DSH |
25
+
26
+ 0.3.0 重写了与宿主的对接(新版 slot 架构 + `useChat` 会话数据),**只支持 DSH 0.1.2-rc.1 及以上**;旧版 DSH 请安装 0.2.2。安装/更新时 DSH 市场会根据 package.json 的 `dsh.compatibility.dshReleases` 与 `peerDependencies` 做 host 兼容预检,版本不符会给出提示。
17
27
 
18
28
  ## 安装
19
29
 
@@ -47,9 +57,10 @@ dsh plugin --profile web add <本目录路径>
47
57
  ## 开发
48
58
 
49
59
  - `lib/client.js` —— 全部 UI 逻辑(浏览器端)
50
- - `lib/index.js` —— 宿主端空入口
60
+ - `lib/index.js` —— 宿主端空入口(版本守卫可在此扩展)
51
61
  - `cordis.patch.yml` —— loader patch(符合官方 bundle 规范)
52
- - client.js 后浏览器硬刷新即可生效;改 loader / patch 需重启 DSH
62
+ - **0.3.0 起对接新版插件模型**:面板注册进会话级 `conversation.input.overlay` 槽(会话级 hook `ctx.uiSession.provide` 贡献,`useChat` 由 ui-chat 贡献),面板本体 `createPortal` 到 `document.body` 渲染成固定浮层;数据入口为 `props.useChat`(`ChatSnapshot.order` + `nodes`,节点形状:`kind: user/assistant-step`、`location.turn`、`data.blocks`)
63
+ - 改 client.js 后需重启 DSH(boot rev 按内容算)
53
64
 
54
65
  ## License
55
66
 
package/lib/client.js CHANGED
@@ -6,11 +6,16 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
8
  let react_jsx_runtime = require("react/jsx-runtime");
9
- require("@deepseek-ai/dsh-client-runtime/client");
9
+ let react_dom = require("react-dom"); // createPortal -> panel stays a top-level overlay (highest pointer priority)
10
+ // DSH 0.1.2-rc.1: the old @deepseek-ai/dsh-client-runtime package is gone.
11
+ // Runtime hooks (useChat etc.) now arrive as session-scope slot props —
12
+ // no runtime package to require here anymore.
10
13
 
11
14
  // ---------- constants ----------
12
15
  var PANEL_WIDTH = 288;
13
- var Z_BASE = 950;
16
+ // Very high base so the portal-rendered panel (top-level stacking context)
17
+ // always sits above DSH content and its transcript width handles.
18
+ var Z_BASE = 2200;
14
19
  var EASE = "cubic-bezier(0.22, 0.9, 0.3, 1)"; // smooth non-linear slide
15
20
 
16
21
  // ---------- markdown helpers ----------
@@ -223,6 +228,42 @@ window.__ModuleLoader__.load({
223
228
  chip: "rgba(79, 140, 255, 0.18)"
224
229
  };
225
230
 
231
+ // ---- theme scheme detection (for the panel's 3D inner shadow) ----
232
+ // Prefer a positive dark signal from the OS preference, then DSH's theme
233
+ // attribute/class, then the --dsw-alias-bg-base luminance as a last resort.
234
+ // Failed-open to light so a light card never gets a dark inner shadow by
235
+ // mistake; the caller flips to the white bevel only on a confident dark read.
236
+ function isDarkScheme() {
237
+ try {
238
+ // DSH's authoritative dark marker: <body data-ds-dark-theme> is set when
239
+ // the dark scheme is active (system OR manual). Check it first.
240
+ var body = document.body;
241
+ if (body) {
242
+ if (body.hasAttribute && body.hasAttribute("data-ds-dark-theme")) return true;
243
+ var cls = body.className || "";
244
+ if (/dark|theme-dark/i.test(cls)) return true;
245
+ var attr = (body.getAttribute("data-theme") || body.getAttribute("data-color-scheme") || "").toLowerCase();
246
+ if (attr === "dark" || /dark/.test(attr)) return true;
247
+ }
248
+ if (typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches) return true;
249
+ var root = body || document.documentElement;
250
+ var cs = getComputedStyle(root).colorScheme;
251
+ if (typeof cs === "string" && /dark/i.test(cs)) return true;
252
+ var v = getComputedStyle(root).getPropertyValue("--dsw-alias-bg-base");
253
+ var m = /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/.exec(v);
254
+ if (m) return (Number(m[1]) + Number(m[2]) + Number(m[3])) < 384;
255
+ } catch (e) {}
256
+ return false;
257
+ }
258
+ // Inner shadow: computed per-render (not once) so it follows the live theme.
259
+ // Dark scheme -> soft WHITE bevel (recessed card highlight); light -> soft
260
+ // dark shade. Outer shadow stays off (clip-path would clip it).
261
+ function innerShadow() {
262
+ return isDarkScheme()
263
+ ? "inset 0 0 0 1px rgba(255,255,255,0.16), inset 0 1px 0 rgba(255,255,255,0.12), inset 0 -10px 16px rgba(255,255,255,0.08)"
264
+ : "inset 0 0 0 1px rgba(0,0,0,0.08), inset 0 2px 10px rgba(0,0,0,0.18)";
265
+ }
266
+
226
267
  // ---------- error boundary ----------
227
268
  var ErrorBoundary = (function () {
228
269
  if (!react.Component) return function (p) { return p.children; };
@@ -262,13 +303,17 @@ window.__ModuleLoader__.load({
262
303
 
263
304
  // ---------- component ----------
264
305
  function OutlinePanel(props) {
265
- var useSession = props.useSession;
266
- if (!useSession) {
267
- console.warn("[dsh-quick-toc] useSession prop missing");
306
+ // DSH 0.1.2-rc.1: the conversation moved out of the session snapshot —
307
+ // it is now the session-scope `chat` hook (ChatSnapshot: order + nodes
308
+ // map + legacy projection), contributed by dsh-client-ui-chat. Same node
309
+ // shape as before (kind user/assistant-step, location.turn, data.blocks).
310
+ var useChat = props.useChat;
311
+ if (!useChat) {
312
+ console.warn("[dsh-quick-toc] useChat prop missing (requires DSH >= 0.1.2-rc.1)");
268
313
  return null;
269
314
  }
270
- var order = useSession(function (s) { return s.chat.order; });
271
- var nodes = useSession(function (s) { return s.chat.nodes; });
315
+ var order = useChat(function (s) { return s.order; });
316
+ var nodes = useChat(function (s) { return s.nodes; });
272
317
 
273
318
  // ---- hooks (ALL before any conditional return) ----
274
319
  // collapsed by default; user expands via the edge handle (default dock: left)
@@ -793,24 +838,20 @@ window.__ModuleLoader__.load({
793
838
  // ---- geometry: always position by `left` (px) so dock-switch & slide share one animated transition
794
839
  var dockRight = dock === "right";
795
840
  var panelLeft;
796
- if (open) {
797
- panelLeft = dockRight
798
- ? window.innerWidth - (viewport ? viewport.right + 48 : 60) - panelW
799
- : (viewport ? viewport.left + 8 : 8);
841
+ if (dockRight) {
842
+ // RIGHT dock: keep the panel's VISIBLE right edge pinned at the SAME
843
+ // line when collapsed as when expanded (the expand edge is correct).
844
+ // open: left = openRight, visible right edge = openRight+panelW
845
+ // closed: left = openRight + panelW, clip-right panelW hides it fully,
846
+ // so the visible right edge stays at openRight+panelW -> the
847
+ // collapse shrinks toward that line and vanishes exactly there.
848
+ var openRight = window.innerWidth - (viewport ? viewport.right + 48 : 60) - panelW;
849
+ panelLeft = open ? openRight : (openRight + panelW);
850
+ } else if (open) {
851
+ // LEFT dock open (left dock is the confirmed-correct reference — untouched)
852
+ panelLeft = (viewport ? viewport.left + 8 : 8);
800
853
  } else {
801
- // collapsed position:
802
- // left dock -> slide just past the left sidebar edge (stop there)
803
- // right dock -> boundary is the right sidebar when it is open,
804
- // otherwise the screen's right edge
805
- if (dockRight) {
806
- // with the right sidebar open, slide INTO the sidebar area (covered
807
- // by the cover wall); otherwise fly off the screen's right edge
808
- panelLeft = (viewport && viewport.right > 80)
809
- ? (window.innerWidth - viewport.right)
810
- : window.innerWidth + 24;
811
- } else {
812
- panelLeft = (viewport ? viewport.left - panelW - 8 : -(panelW + 48));
813
- }
854
+ panelLeft = (viewport ? viewport.left - panelW - 8 : -(panelW + 48));
814
855
  }
815
856
  var vMaxH = viewport ? Math.max(200, viewport.height - 28) : "72vh";
816
857
  var baseTopPx = (viewport ? viewport.top + 14 : window.innerHeight * 0.12) + panelY;
@@ -962,9 +1003,12 @@ window.__ModuleLoader__.load({
962
1003
  // like being covered by the sidebar (the sidebar stays untouched):
963
1004
  // left dock -> clipped from the left up to the sidebar line
964
1005
  // right dock -> clipped from the right at the screen/sidebar line
1006
+ // LEFT dock keeps its original clip (collapse left). RIGHT dock clips the
1007
+ // LEFT side so the panel closes toward its right (dock) edge in place.
965
1008
  var panelClip = "inset(0 0 0 0px)";
966
1009
  if (!open) {
967
1010
  if (dockRight) {
1011
+ // mirror of LEFT: clip the RIGHT side (panel flies right, shrinking from its right)
968
1012
  panelClip = "inset(0 " + panelW + "px 0 0)";
969
1013
  } else {
970
1014
  panelClip = "inset(0 0 0 " + (panelW + 8) + "px)";
@@ -985,13 +1029,15 @@ window.__ModuleLoader__.load({
985
1029
  background: C.panelBg,
986
1030
  border: "none",
987
1031
  borderRadius: 12,
988
- boxShadow: "0 2px 10px rgba(0,0,0,0.18)",
989
1032
  zIndex: Z_BASE,
990
1033
  overflow: "hidden",
991
1034
  color: C.text,
992
1035
  opacity: panelOpacity,
993
1036
  clipPath: panelClip,
994
- boxShadow: "none",
1037
+ // inner shadow only (no outer): theme-aware (white bevel in dark mode,
1038
+ // soft shade in light) — outer shadow would be clipped by the collapse
1039
+ // clip-path and the panel edges.
1040
+ boxShadow: innerShadow(),
995
1041
  transition: panelTransition,
996
1042
  pointerEvents: "auto"
997
1043
  },
@@ -1306,6 +1352,7 @@ window.__ModuleLoader__.load({
1306
1352
  // the scrollbar follows the dock side (rtl flips it to the left)
1307
1353
  react_jsx_runtime.jsx("div", {
1308
1354
  ref: listRef,
1355
+ className: "dqt-list",
1309
1356
  style: {
1310
1357
  overflowY: "auto",
1311
1358
  padding: "6px 8px",
@@ -1419,9 +1466,10 @@ window.__ModuleLoader__.load({
1419
1466
  })
1420
1467
  }) : null;
1421
1468
 
1422
- return react_jsx_runtime.jsx(ErrorBoundary, {
1423
- children: [panelEl, edgeEl]
1424
- });
1469
+ return react_dom.createPortal(
1470
+ react_jsx_runtime.jsx(ErrorBoundary, { children: [panelEl, edgeEl] }),
1471
+ document.body
1472
+ );
1425
1473
  }
1426
1474
 
1427
1475
  function renderItem(n, depth, jump, C, uid) {
@@ -1540,15 +1588,6 @@ window.__ModuleLoader__.load({
1540
1588
  return out;
1541
1589
  }
1542
1590
 
1543
- function OutlineOverlay(props) {
1544
- var SessionProvider = props.SessionProvider;
1545
- var renderSlot = props.renderSlot;
1546
- return react_jsx_runtime.jsx(SessionProvider, {
1547
- empty: function () { return null; },
1548
- children: function () { return renderSlot("quick-toc.panel", {}); }
1549
- });
1550
- }
1551
-
1552
1591
  var zh = {
1553
1592
  "panel.title": "对话大纲"
1554
1593
  };
@@ -1556,38 +1595,48 @@ window.__ModuleLoader__.load({
1556
1595
  "panel.title": "Conversation Outline"
1557
1596
  };
1558
1597
 
1559
- var inject = ["slots", "sessions", "locale"];
1598
+ var inject = ["slots", "locale"];
1560
1599
 
1561
1600
  function apply(ctx) {
1562
1601
  ctx.effect(function () {
1563
1602
  return ctx.locale.register("dsh-quick-toc", { zh: zh, en: en });
1564
1603
  }, "dsh-quick-toc: dictionaries");
1565
- ctx.slots.inject("shell.overlay", function () {
1604
+ // DSH 0.1.2-rc.1: session-scoped hooks (useChat/useSession/sessionId) only arrive
1605
+ // inside a declared session slot. Register the panel into the session-scoped
1606
+ // conversation.input.overlay (list/additive) so it receives useChat; the panel
1607
+ // itself renders a fixed, frame-floating dock, so the overlay seat is just the
1608
+ // hook source (it is NOT the panel's visual container).
1609
+ ctx.slots.inject("conversation.input.overlay", function () {
1566
1610
  return ctx.slots.register({
1567
- name: "shell.overlay",
1611
+ name: "conversation.input.overlay",
1568
1612
  id: "quick-toc",
1569
1613
  order: 90,
1570
- children: { "quick-toc.panel": { kind: "single", scope: "session" } }
1571
- }, OutlineOverlay);
1572
- });
1573
- ctx.slots.inject("quick-toc.panel", function () {
1574
- return ctx.slots.register({
1575
- name: "quick-toc.panel",
1576
1614
  locale: "dsh-quick-toc"
1577
1615
  }, OutlinePanel);
1578
1616
  });
1579
1617
  }
1580
1618
 
1619
+ exports.name = "dsh-quick-toc"; // runner attributes slot entries to this registrant
1581
1620
  exports.apply = apply;
1582
1621
  exports.inject = inject;
1583
1622
 
1584
- // inject the small keyframe stylesheet once (spinner rotation)
1623
+ // inject the small keyframe + panel-scrollbar stylesheet once
1585
1624
  (function () {
1586
1625
  if (typeof document === "undefined") return;
1587
1626
  if (document.getElementById("dsh-quick-toc-css")) return;
1588
1627
  var s = document.createElement("style");
1589
1628
  s.id = "dsh-quick-toc-css";
1590
- s.textContent = "@keyframes dqt-spin{to{transform:rotate(360deg)}}@keyframes dqt-fade-in{from{opacity:0}to{opacity:1}}@keyframes dqt-fade-out{from{opacity:1}to{opacity:0}}";
1629
+ s.textContent = "@keyframes dqt-spin{to{transform:rotate(360deg)}}@keyframes dqt-fade-in{from{opacity:0}to{opacity:1}}@keyframes dqt-fade-out{from{opacity:1}to{opacity:0}}" +
1630
+ // Hide the native scrollbar entirely (no arrow buttons, no grey bar) —
1631
+ // custom webkit scrollbar styling was not reliably suppressing the
1632
+ // default arrows, so a hidden native bar + the panel's inner shadow gives
1633
+ // the cleanest look. Scrolling still works (auto-follow + wheel-load-older).
1634
+ ".dqt-list{scrollbar-width:none;-ms-overflow-style:none}" +
1635
+ ".dqt-list::-webkit-scrollbar{display:none;width:0;height:0}" +
1636
+ ".dqt-list::-webkit-scrollbar-button{display:none;height:0;width:0}" +
1637
+ ".dqt-list::-webkit-scrollbar-track{background:transparent}" +
1638
+ ".dqt-list::-webkit-scrollbar-thumb{background:transparent}" +
1639
+ ".dqt-list::-webkit-scrollbar-corner{background:transparent}";
1591
1640
  document.head.appendChild(s);
1592
1641
  })();
1593
1642
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-quick-toc",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Quick conversation TOC for DeepSeek Harness: markdown heading outline grouped by turn, with auto-follow highlight and smooth jump navigation",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -8,8 +8,12 @@
8
8
  "lib",
9
9
  "cordis.patch.yml",
10
10
  "README.md",
11
+ "README.en.md",
11
12
  "LICENSE"
12
13
  ],
14
+ "engines": {
15
+ "dsh": ">=0.1.2-rc.1"
16
+ },
13
17
  "exports": {
14
18
  ".": "./lib/index.js",
15
19
  "./client": "./lib/client.js",
@@ -21,13 +25,23 @@
21
25
  },
22
26
  "client": {
23
27
  "inject": [
24
- "@deepseek-ai/dsh-client-runtime",
28
+ "@deepseek-ai/dsh-client-ui-chat",
25
29
  "@deepseek-ai/dsh-client-ui-conversation",
26
30
  "@deepseek-ai/dsh-client-ui-layout"
27
31
  ],
28
32
  "platform": "web"
33
+ },
34
+ "compatibility": {
35
+ "dshReleases": {
36
+ "0.1.2-rc.1": "compatible"
37
+ }
29
38
  }
30
39
  },
40
+ "peerDependencies": {
41
+ "react": "^18.2.0",
42
+ "@deepseek-ai/dsh-client-ui-chat": ">=0.1.2-rc.1",
43
+ "@deepseek-ai/dsh-client-ui-conversation": ">=0.1.2-rc.1"
44
+ },
31
45
  "license": "MIT",
32
46
  "keywords": [
33
47
  "dsh",