@my-life-buddies/cli 0.3.0 → 0.5.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/dist/web/app.css CHANGED
@@ -513,3 +513,11 @@
513
513
  *, *::before, *::after { animation-duration: 120ms !important; animation-iteration-count: 1 !important; transition-duration: 120ms !important; }
514
514
  }
515
515
  }
516
+
517
+ @layer debugger {
518
+ .widget-delivery { margin: var(--space-lg) var(--page-gutter) 0; padding: var(--space-lg); border: var(--rule-hair) solid var(--color-rule); border-radius: var(--radius-lg); background: var(--color-paper-raised); }
519
+ .widget-delivery summary { cursor: pointer; font-weight: 600; }
520
+ .widget-delivery p, .widget-delivery li { font-size: var(--text-sm); line-height: 1.65; color: var(--color-ink-soft); }
521
+ .widget-delivery li + li { margin-top: var(--space-sm); }
522
+ .widget-delivery button { padding: var(--space-sm) var(--space-md); border: var(--rule-hair) solid var(--color-rule); border-radius: var(--radius-md); background: var(--color-paper); }
523
+ }
package/dist/web/app.js CHANGED
@@ -433,6 +433,9 @@
433
433
  }
434
434
  }
435
435
 
436
+ const widgets = window.createSimulatorWidgets?.({ api, avatar: () => messageAvatar("assistant") });
437
+ function renderWidgets() { widgets?.update({ active: state.dev.phase === "ready" && !state.resetting, conversationRevision: state.simulator.conversationRevision, devRevision: state.dev.revision }); }
438
+
436
439
  let messageSignature = "";
437
440
  let followMessages = true;
438
441
 
@@ -443,6 +446,7 @@
443
446
  }
444
447
 
445
448
  function renderMessages() {
449
+ renderWidgets();
446
450
  const signature = JSON.stringify([state.simulator.messages, Boolean(state.simulator.activeRun)]);
447
451
  if (signature === messageSignature) return;
448
452
  messageSignature = signature;
@@ -461,7 +465,7 @@
461
465
  }
462
466
  const row = node("div", { class: "message-row", "data-role": message.role });
463
467
  const wrap = node("div", { class: "message-content" });
464
- wrap.append(node("div", { class: "message-bubble" }, message.text));
468
+ wrap.append(message.widget && widgets ? widgets.card(message.widget) : node("div", { class: "message-bubble" }, message.text));
465
469
  if (message.role === "user") row.append(wrap);
466
470
  else row.append(messageAvatar("assistant"), wrap);
467
471
  elements.chatMessages.append(row);
@@ -669,6 +673,7 @@
669
673
  if (elements.resetConversation.disabled) return;
670
674
  if (!window.confirm("清空当前对话并重新开始?\n这会结束当前开发版会话,重置聊天上下文、记忆和该会话的主动服务。旧记录仅在服务端留档;搭子资料和工程文件会保留。")) return;
671
675
  state.resetting = true;
676
+ widgets?.reset();
672
677
  state.simulatorRequest++;
673
678
  elements.resetConversation.textContent = "正在清空…";
674
679
  renderDev();
@@ -700,7 +705,7 @@
700
705
  const events = new EventSource("/api/events");
701
706
  events.addEventListener("dev.state", (event) => {
702
707
  try {
703
- if (applyDevState(JSON.parse(event.data))) renderDev();
708
+ if (applyDevState(JSON.parse(event.data))) { renderDev(); renderWidgets(); }
704
709
  } catch {
705
710
  // A malformed local event is ignored; the next API refresh repairs state.
706
711
  }
@@ -12,7 +12,10 @@
12
12
  <link rel="stylesheet" href="/assets/tokens.css">
13
13
  <link rel="stylesheet" href="/assets/app.css">
14
14
  <script src="/assets/iphone-16-max.js" defer></script>
15
+ <script src="/assets/widgets.js" defer></script>
16
+ <script src="/assets/widget-delivery.js" defer></script>
15
17
  <script src="/assets/app.js" defer></script>
18
+ <link rel="stylesheet" href="/assets/widgets.css">
16
19
  </head>
17
20
  <body>
18
21
  <a class="skip-link" href="#debug-content">跳到调试内容</a>
@@ -119,6 +122,12 @@
119
122
  </div>
120
123
 
121
124
  <div class="debug-panels">
125
+ <section id="widget-delivery" class="widget-delivery" aria-label="小挂件设计落地检查" hidden>
126
+ <details><summary>小挂件设计落地:<span id="widget-delivery-status"></span></summary>
127
+ <p id="widget-delivery-notice"></p><ul id="widget-delivery-items"></ul>
128
+ <button id="widget-delivery-refresh" type="button">重新检查</button>
129
+ </details>
130
+ </section>
122
131
  <section id="panel-overview" class="debug-panel" role="tabpanel" aria-labelledby="tab-overview">
123
132
  <div class="overview-grid">
124
133
  <article class="run-summary">
@@ -197,15 +206,14 @@
197
206
  </button>
198
207
  <div class="buddy-identity"><h3 id="buddy-name">我的搭子</h3><p id="buddy-status" class="sr-only">DEV 未连接</p></div>
199
208
  <div class="chat-navigation-actions" aria-hidden="true">
200
- <button class="chat-widgets" type="button" aria-label="查看 Widget(预览中不可用)" disabled tabindex="-1">
201
- <svg viewBox="0 0 24 24"><rect x="4" y="4" width="6" height="6" rx="1.5"/><rect x="14" y="4" width="6" height="6" rx="1.5"/><rect x="4" y="14" width="6" height="6" rx="1.5"/><rect x="14" y="14" width="6" height="6" rx="1.5"/></svg>
202
- </button>
203
209
  <button class="chat-more" type="button" aria-label="更多(预览中不可用)" disabled tabindex="-1">
204
210
  <svg viewBox="0 0 24 24"><circle cx="5" cy="12" r="1.3"/><circle cx="12" cy="12" r="1.3"/><circle cx="19" cy="12" r="1.3"/></svg>
205
211
  </button>
206
212
  </div>
207
213
  </header>
208
214
 
215
+ <div id="widget-shortcut" class="widget-shortcut" hidden></div>
216
+ <div id="widget-overlay" class="widget-overlay" hidden></div>
209
217
  <div class="chat-messages" id="chat-messages" aria-live="polite" aria-label="模拟器聊天记录">
210
218
  <div class="chat-empty" id="chat-empty" aria-label="暂无消息"></div>
211
219
  </div>
@@ -0,0 +1,28 @@
1
+ // This report is local-only and separate from runtime connectivity.
2
+ (() => {
3
+ const panel = document.getElementById("widget-delivery");
4
+ if (!panel) return;
5
+ const refresh = async () => {
6
+ const button = document.getElementById("widget-delivery-refresh");
7
+ button.disabled = true;
8
+ try {
9
+ const response = await fetch("/api/widgets/delivery", { credentials: "same-origin" });
10
+ if (!response.ok) throw new Error("检查暂时无法完成,请重新检查");
11
+ const report = await response.json();
12
+ panel.hidden = report.status === "not-configured";
13
+ document.getElementById("widget-delivery-status").textContent = report.status === "ready" ? "结构与核对记录齐全" : "尚未完成";
14
+ document.getElementById("widget-delivery-notice").textContent = report.notice;
15
+ const list = document.getElementById("widget-delivery-items");
16
+ list.replaceChildren();
17
+ for (const message of [...report.issues, ...report.items.map(item => `${item.name}:${item.status === "verified" ? "核对已记录" : item.status === "implemented" ? "待预览核对" : "待补齐实现"}${item.issues.length ? ";" + item.issues.join(";") : ""}`)]) {
18
+ const li = document.createElement("li"); li.textContent = message; list.append(li);
19
+ }
20
+ } catch (error) {
21
+ panel.hidden = false;
22
+ document.getElementById("widget-delivery-status").textContent = "检查失败";
23
+ document.getElementById("widget-delivery-notice").textContent = error.message;
24
+ } finally { button.disabled = false; }
25
+ };
26
+ document.getElementById("widget-delivery-refresh").addEventListener("click", refresh);
27
+ void refresh();
28
+ })();
@@ -0,0 +1,31 @@
1
+ /* Native points expressed against the simulator's 415 pt canvas. */
2
+ .widget-shortcut { position:absolute; z-index:3; top:24.1cqw; left:2.89cqw; max-width:calc(100% - 5.78cqw); }
3
+ .widget-shortcut[hidden], .widget-overlay[hidden] { display:none; }
4
+ .widget-pill { display:flex; align-items:center; gap:1.93cqw; min-height:12.53cqw; max-width:62.65cqw; border-radius:99px; padding:1.93cqw 4.82cqw 1.93cqw 3.855cqw; color:#20252b; background:linear-gradient(135deg,#fff,#bbf1a7); border:.5px solid #20252b1f; font-size:3.855cqw; font-weight:400; box-shadow:none; }
5
+ .widget-pill .widget-pill-avatar { width:6.747cqw; height:6.747cqw; flex:none; border-radius:50%; }
6
+ .widget-pill-title { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
7
+ .widget-chevron { margin-left:1.9cqw; font-size:2.65cqw; }
8
+ .widget-choices,.widget-options { position:absolute; z-index:5; top:calc(100% + 2cqw); left:0; width:55.9cqw; padding:1cqw 0; border-radius:3cqw; background:#f7f7f9; box-shadow:0 3cqw 9cqw #0003; overflow:hidden; }
9
+ .widget-choices button,.widget-options button { display:block; width:100%; min-height:10.6cqw; padding:2.9cqw 3.855cqw; text-align:left; background:transparent; color:#20252b; font-size:3.37cqw; font-weight:400; border-bottom:.3px solid #0002; }
10
+ .widget-overlay { position:absolute; z-index:10; inset:0; display:flex; align-items:center; justify-content:center; background:#0003; animation:widget-fade .2s ease-in-out; }
11
+ .widget-modal { position:relative; width:calc(100% - 7.71cqw); height:min(134.94cqw,calc(100% - 11.57cqw)); max-width:560px; border-radius:5.783cqw; background:#fff; color:#20252b; overflow:hidden; display:flex; flex-direction:column; padding-bottom:2.89cqw; }
12
+ .widget-modal-header { display:flex; gap:1.93cqw; align-items:center; padding:1.93cqw 1.93cqw 0; flex:none; }
13
+ .widget-modal-header button { width:10.6cqw; height:10.6cqw; flex:none; padding:0; background:transparent; color:inherit; font-size:5.3cqw; font-weight:400; }
14
+ .widget-modal-header h3 { flex:1; margin:0; text-align:center; font-size:3.37cqw; font-weight:500; line-height:1.4; }
15
+ .widget-modal-body { flex:1; min-height:0; overflow:auto; text-align:center; font-size:3.37cqw; }
16
+ .widget-modal-body>p,.widget-modal-body>h4 { padding:0 5.78cqw; }
17
+ .widget-modal-body>h4,.widget-loading { margin-top:25%; }
18
+ .widget-modal-body>button { background:#f3f3f5; color:inherit; padding:2.9cqw; border-radius:2cqw; }
19
+ .widget-document { width:100%; height:100%; border:0; display:block; background:white; }
20
+ .widget-options { top:12.5cqw; right:2cqw; left:auto; }
21
+ .widget-options p { padding:3cqw; font-size:3.37cqw; }
22
+ .widget-pin-note { position:absolute; top:14.45cqw; left:50%; transform:translateX(-50%); white-space:nowrap; border-radius:99px; background:#f5f5f5ed; padding:2.9cqw; font-size:2.89cqw; }
23
+ .widget-entry { width:71.57cqw; max-width:100%; padding:2.89cqw; border:.3px solid #dadbdd; border-radius:3.855cqw; background:#fff; color:#20252b; font-size:3.3735cqw; }
24
+ .widget-entry h4 { margin:0 0 2.89cqw; font:500 3.3735cqw/1.5 sans-serif; }
25
+ .widget-entry p { margin:0; color:#717782; line-height:1.5; overflow-wrap:anywhere; }
26
+ .widget-entry-details { display:flex; align-items:center; gap:2.89cqw; }
27
+ .widget-entry-photo { width:15.42cqw; height:15.42cqw; border-radius:50%; object-fit:cover; flex:none; }
28
+ .widget-entry-open { width:100%; min-height:10.6cqw; margin-top:2.89cqw; background:#f3f3f5; color:inherit; border-radius:1.93cqw; font-size:3.3735cqw; font-weight:500; }
29
+ .widget-entry button:focus-visible,.widget-shortcut button:focus-visible,.widget-overlay button:focus-visible { outline:2px solid #007a9b; outline-offset:2px; }
30
+ @keyframes widget-fade { from { opacity:0; } to { opacity:1; } }
31
+ @media(prefers-reduced-motion:reduce) { .widget-overlay { animation-duration:.1s; } }
@@ -0,0 +1,127 @@
1
+ /* Client reference: my-life-buddies-ios dev 547818d, DadaConversationWidgets / DadaHostedWidgetPage. */
2
+ (() => {
3
+ const el = (tag, className, text) => { const n = document.createElement(tag); n.className = className; if (text !== undefined) n.textContent = text; return n; };
4
+ window.createSimulatorWidgets = ({ api, avatar }) => {
5
+ const shortcut = document.querySelector('#widget-shortcut');
6
+ const overlay = document.querySelector('#widget-overlay');
7
+ const phone = document.querySelector('.phone-screen');
8
+ let items = [], scope = '', generation = 0, ready = false, loading = false, lastRefresh = 0, selected, focusBefore, openRevision = 0;
9
+ let failed = false, renderSignature = '', frameObserver;
10
+ function close() {
11
+ frameObserver?.disconnect(); frameObserver = undefined;
12
+ const wasOpen = !overlay.hidden;
13
+ openRevision++; selected = undefined; overlay.hidden = true; overlay.replaceChildren();
14
+ for (const child of phone.children) if (child !== overlay) child.inert = false;
15
+ if (wasOpen) focusBefore?.focus?.();
16
+ }
17
+ function button(label, action, className = '') { const n = el('button', className, label); n.type = 'button'; n.addEventListener('click', action); return n; }
18
+ function render() {
19
+ const signature = JSON.stringify([ready, items, failed]);
20
+ if (signature === renderSignature) return;
21
+ renderSignature = signature;
22
+ shortcut.replaceChildren(); shortcut.hidden = !ready || (!items.length && !failed);
23
+ if (shortcut.hidden) return;
24
+ if (!items.length) { shortcut.append(button('重新加载小挂件', () => refresh(true), 'widget-pill')); return; }
25
+ const pill = button('', () => {
26
+ if (items.length === 1) { void open(items[0]); return; }
27
+ const old = shortcut.querySelector('.widget-choices');
28
+ if (old) { old.remove(); pill.setAttribute('aria-expanded', 'false'); return; }
29
+ const choices = el('div', 'widget-choices'); choices.setAttribute('role', 'menu');
30
+ for (const item of items) { const b = button(item.title + ' ›', () => { choices.remove(); pill.setAttribute('aria-expanded', 'false'); void open(item); }); b.setAttribute('role', 'menuitem'); choices.append(b); }
31
+ shortcut.append(choices); pill.setAttribute('aria-expanded', 'true'); choices.firstChild?.focus();
32
+ }, 'widget-pill');
33
+ const image = avatar(); image.textContent = ''; image.classList.add('widget-pill-avatar');
34
+ pill.append(image, el('span', 'widget-pill-title', items.length === 1 ? items[0].title : '全部小挂件'));
35
+ if (items.length > 1) { pill.append(el('span', 'widget-chevron', '⌄')); pill.setAttribute('aria-haspopup', 'menu'); pill.setAttribute('aria-expanded', 'false'); }
36
+ shortcut.append(pill);
37
+ }
38
+ async function refresh(force = false) {
39
+ if (!ready || loading || (!force && Date.now() - lastRefresh < 1500)) return;
40
+ loading = true; const mine = generation;
41
+ try {
42
+ const value = await api('/api/simulator/widgets');
43
+ if (mine !== generation || !ready) return;
44
+ items = value.widgets || []; failed = false;
45
+ if (selected && !items.some(item => item.id === selected.id)) close();
46
+ } catch { if (mine === generation) failed = true; }
47
+ finally { if (mine === generation) { loading = false; lastRefresh = Date.now(); render(); } }
48
+ }
49
+ async function open(item) {
50
+ if (!item.type || !ready) return;
51
+ const mine = ++openRevision, current = generation;
52
+ if (overlay.hidden) focusBefore = document.activeElement;
53
+ frameObserver?.disconnect();
54
+ selected = item; document.querySelector('#message-input').blur();
55
+ overlay.replaceChildren(); overlay.hidden = false;
56
+ const card = el('section', 'widget-modal'); card.setAttribute('role', 'dialog'); card.setAttribute('aria-modal', 'true'); card.setAttribute('aria-label', item.title);
57
+ const header = el('header', 'widget-modal-header');
58
+ const dismiss = button('×', close); dismiss.setAttribute('aria-label', '关闭小挂件');
59
+ const more = button('···', () => {
60
+ const old = card.querySelector('.widget-options'); if (old) { old.remove(); return; }
61
+ const menu = el('div', 'widget-options'); menu.setAttribute('role', 'menu');
62
+ const currentItem = items.find(value => value.id === item.id);
63
+ if (currentItem) menu.append(button(currentItem.isPinned ? '从首页取消固定' : '固定到首页', async () => {
64
+ menu.querySelectorAll('button').forEach(b => { b.disabled = true; });
65
+ try {
66
+ await api(`/api/simulator/widgets/${encodeURIComponent(item.id)}/pin`, { method: currentItem.isPinned ? 'DELETE' : 'PUT' });
67
+ if (mine !== openRevision || current !== generation) return;
68
+ currentItem.isPinned = !currentItem.isPinned; menu.remove();
69
+ const note = el('div', 'widget-pin-note', currentItem.isPinned ? '已固定到首页' : '已从首页取消固定'); note.setAttribute('role', 'status'); card.append(note); setTimeout(() => note.remove(), 4000);
70
+ } catch (error) { if (mine === openRevision) { menu.replaceChildren(el('p', '', error.message), button('知道了', () => menu.remove())); } }
71
+ }));
72
+ menu.append(button('刷新', () => open(item))); card.append(menu);
73
+ }); more.setAttribute('aria-label', '小挂件更多');
74
+ header.append(dismiss, el('h3', '', item.title), more);
75
+ const body = el('div', 'widget-modal-body'); body.append(el('p', 'widget-loading', '正在打开小挂件…'));
76
+ card.append(header, body); overlay.append(card);
77
+ for (const child of phone.children) if (child !== overlay) child.inert = true;
78
+ dismiss.focus();
79
+ try {
80
+ const url = `/api/simulator/widgets/${encodeURIComponent(item.id)}`;
81
+ // Probe errors in the parent; actual HTML executes only inside the opaque sandbox document.
82
+ const response = await fetch(url, { credentials: 'same-origin' });
83
+ if (!response.ok) { const error = await response.json(); throw new Error(error.error?.message || error.error || '小挂件暂时无法打开'); }
84
+ await response.arrayBuffer();
85
+ if (mine !== openRevision || current !== generation) return;
86
+ const frame = el('iframe', 'widget-document'); frame.title = item.title; frame.setAttribute('sandbox', 'allow-scripts'); frame.referrerPolicy = 'no-referrer'; frame.src = url;
87
+ body.replaceChildren(frame);
88
+ frameObserver = new ResizeObserver(() => {
89
+ const scale = body.clientWidth / 383;
90
+ if (!scale) return;
91
+ frame.style.width = '383px'; frame.style.height = `${body.clientHeight / scale}px`;
92
+ frame.style.transform = `scale(${scale})`; frame.style.transformOrigin = 'top left';
93
+ });
94
+ frameObserver.observe(body);
95
+ } catch (error) {
96
+ if (mine !== openRevision || current !== generation) return;
97
+ body.replaceChildren(el('h4', '', '小挂件暂时无法打开'), el('p', '', error.message), button('重新加载', () => open(item)));
98
+ }
99
+ }
100
+ overlay.addEventListener('click', event => { if (event.target === overlay) close(); });
101
+ document.addEventListener('keydown', event => {
102
+ if (event.key === 'Escape') { if (!overlay.hidden) close(); else { shortcut.querySelector('.widget-choices')?.remove(); shortcut.querySelector('button')?.setAttribute('aria-expanded', 'false'); } }
103
+ if (event.key === 'Tab' && !overlay.hidden) {
104
+ const controls = [...overlay.querySelectorAll('button:not(:disabled), iframe')]; const first = controls[0], last = controls.at(-1);
105
+ if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); }
106
+ else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); }
107
+ }
108
+ });
109
+ document.addEventListener('visibilitychange', () => { if (!document.hidden) void refresh(true); });
110
+ return {
111
+ update({ active, conversationRevision, devRevision }) {
112
+ const key = `${conversationRevision || 0}:${devRevision || 0}`;
113
+ if (key !== scope || !active) { scope = key; generation++; items = []; failed = false; loading = false; lastRefresh = 0; close(); }
114
+ ready = active; render(); void refresh();
115
+ },
116
+ reset() { generation++; ready = false; items = []; loading = false; failed = false; close(); render(); },
117
+ card(item) {
118
+ const card = el('div', 'widget-entry'); card.append(el('h4', '', item.title));
119
+ const details = el('div', 'widget-entry-details');
120
+ if (item.type && item.imageMediaId) { const image = el('img', 'widget-entry-photo'); image.alt = `${item.title}的照片`; image.src = `/api/simulator/widgets/${encodeURIComponent(item.id)}/image?mediaId=${encodeURIComponent(item.imageMediaId)}`; image.addEventListener('error', () => { image.hidden = true; }); details.append(image); }
121
+ details.append(el('p', '', item.type ? item.summary?.trim() || '打开查看最新内容' : '此小挂件已删除或不可用')); card.append(details);
122
+ if (item.type) card.append(button('去查看', () => open(item), 'widget-entry-open'));
123
+ return card;
124
+ },
125
+ };
126
+ };
127
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@my-life-buddies/cli",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "搭搭 Developer Platform 命令行工具",
6
6
  "publishConfig": {
@@ -39,7 +39,7 @@
39
39
  "ws": "8.21.3"
40
40
  },
41
41
  "devDependencies": {
42
- "@buddy/cli-core": "0.3.0",
43
- "@buddy/preview": "0.3.0"
42
+ "@buddy/cli-core": "0.5.0",
43
+ "@buddy/preview": "0.5.0"
44
44
  }
45
45
  }
@@ -8,7 +8,7 @@
8
8
  "name": "buddy-agent-project",
9
9
  "version": "0.0.0",
10
10
  "dependencies": {
11
- "@my-life-buddies/buddy-runtime": "0.7.0",
11
+ "@my-life-buddies/buddy-runtime": "0.11.0",
12
12
  "typebox": "1.3.7"
13
13
  },
14
14
  "engines": {
@@ -981,9 +981,9 @@
981
981
  }
982
982
  },
983
983
  "node_modules/@my-life-buddies/buddy-runtime": {
984
- "version": "0.7.0",
985
- "resolved": "https://registry.npmjs.org/@my-life-buddies/buddy-runtime/-/buddy-runtime-0.7.0.tgz",
986
- "integrity": "sha512-0pbgWb6CHPqDeL1HMSQm3Kpl+xpHGlMz4+cmL6hKWPYNlhSogAqri8NjbFLmoq+v8lyrElAS0M8DvDW4yu990A==",
984
+ "version": "0.11.0",
985
+ "resolved": "https://registry.npmjs.org/@my-life-buddies/buddy-runtime/-/buddy-runtime-0.11.0.tgz",
986
+ "integrity": "sha512-6mqO05OK3GRtJwbQXlyKFxk63iq5MUctnihA0d75iBQlbSoxbOSs06FI4+dvIrZ8JBU0brioag4+8jD9Fy482g==",
987
987
  "dependencies": {
988
988
  "@earendil-works/pi-agent-core": "0.85.1",
989
989
  "@earendil-works/pi-ai": "0.85.1",
@@ -10,7 +10,7 @@
10
10
  "node": ">=24"
11
11
  },
12
12
  "dependencies": {
13
- "@my-life-buddies/buddy-runtime": "0.7.0",
13
+ "@my-life-buddies/buddy-runtime": "0.11.0",
14
14
  "typebox": "1.3.7"
15
15
  }
16
16
  }