dsh-form-fill-agent 0.2.28 → 0.2.30

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.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # AI填表智能体
2
2
 
3
- > 0.2.28:会话隔离的只读历史、维度搜索与真实查询进度;侧栏仍为可选依赖。
3
+ > 0.2.30:统一简洁首页、稳定菜单与可恢复品牌标题,明确任务状态;侧栏仍为可选依赖。
4
4
 
5
5
  > 统一侧栏适配需要 Better Sidebar `>=0.17.1 <0.19.0`。五个流程入口打开同一 Session Tab;侧栏展开、收起和关闭由宿主管理。缺少依赖时会显示安装升级提示。
6
6
 
@@ -11,7 +11,7 @@ AI填表智能体:支持自动填表、表格填充、表格补全、Excel填
11
11
  ### 基础安装(不需要侧栏)
12
12
 
13
13
  ```sh
14
- dsh plugin --profile web add dsh-form-fill-agent@0.2.28
14
+ dsh plugin --profile web add dsh-form-fill-agent@0.2.30
15
15
  ```
16
16
 
17
17
  只需 Node.js >=22、完整 DSH 与主包自动安装的内核/Provider。Better Sidebar 和 dsh-context 都是可选项,不会由基础安装强装。
@@ -102,7 +102,7 @@ dsh plugin --profile web add dsh-form-fill-agent@latest
102
102
 
103
103
  ## 使用边界与数据保存
104
104
 
105
- 补充要求为模型可读文本;字段或工作表范围请通过向导/工作台修改,不能仅改文字后假定执行范围已同步。任务 schema 5 可读取 schema 1–5;回退旧版本请使用升级前目录副本或独立任务目录。主包为 0.2.28,共享内核为 0.2.10,Provider 为 0.2.2,共享字段契约为 0.1.0。
105
+ 补充要求为模型可读文本;字段或工作表范围请通过向导/工作台修改,不能仅改文字后假定执行范围已同步。任务 schema 5 可读取 schema 1–5;回退旧版本请使用升级前目录副本或独立任务目录。主包为 0.2.30,共享内核为 0.2.10,Provider 为 0.2.2,共享字段契约为 0.1.0。
106
106
 
107
107
  支持企业完整登记名称或 18 位信用代码;简称、主体不一致或多候选不会自动猜选。仅有信用代码的表格也可作为查询锚点。连接候选检索工具时可检索简称,仍需人工确认;缺少工具时降级人工输入。字段映射采用确定性规则及人工确认,不让模型编造事实。支持普通内部公式、固定区域下拉、基础条件格式和普通表格对象;复杂 Excel 结构和 Word 暂不支持,保真边界见下方安全说明。
108
108
 
package/lib/brand.css CHANGED
@@ -3,7 +3,7 @@
3
3
  .ff-ui[data-ff-theme=light]{--brand:#128bed;--action:#0875d1;--hover:#0666b7;--soft:#e6f4ff;--head:#f2f9fc;--page:#f6f8fa;--surface:#fff;--text:#202c3b;--muted:#626f80;--line:#dce4ec;--success:#12805c;--success-bg:#edf8f2;--review:#946000;--review-bg:#fff7e5;--error:#b42318;color-scheme:light}
4
4
  .ff-ui[data-ff-theme=dark]{--brand:#55adff;--action:#82c3ff;--hover:#acd7ff;--soft:#173449;--head:#172c3b;--page:#101820;--surface:#18232e;--text:#e7eef6;--muted:#a2b1c2;--line:#344657;--success:#78d8b3;--success-bg:#193a30;--review:#f3c66c;--review-bg:#3d321d;--error:#ffaaa5;color-scheme:dark}
5
5
  .ff-ui *{box-sizing:border-box}.ff-ui button,.ff-ui input,.ff-ui select,.ff-ui textarea{font:inherit}.ff-ui button,.ff-ui a.download{border:1px solid var(--line);border-radius:6px;padding:8px 12px;background:var(--surface);color:var(--text);min-height:36px;cursor:pointer;text-decoration:none}.ff-ui button:hover:not(:disabled),.ff-ui a.download:hover{border-color:var(--action);color:var(--action)}.ff-ui button:disabled{opacity:.5;cursor:not-allowed}.ff-ui :is(button,a,input,textarea,select):focus-visible{outline:2px solid var(--action);outline-offset:3px}.ff-ui .primary,.ff-ui a.download{background:var(--action);border-color:var(--action);color:var(--surface)}.ff-ui .primary:hover:not(:disabled),.ff-ui a.download:hover{background:var(--hover);color:var(--surface)}.ff-ui input:not([type=checkbox]):not([type=file]),.ff-ui textarea,.ff-ui select{border:1px solid var(--line);border-radius:6px;padding:8px;color:var(--text);background:var(--surface);max-width:100%;min-width:0}.ff-ui input[type=checkbox]{accent-color:var(--action);width:16px;height:16px}.ff-ui [hidden]{display:none!important}
6
- .ff-ui .ff-icon{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round;flex-shrink:0}.ff-ui .ff-mark{width:40px;height:40px;display:inline-grid;place-items:center;background:var(--soft);color:var(--action);border-radius:8px;flex-shrink:0}.ff-ui .ff-mark .ff-icon{width:26px;height:26px}.ff-ui .ff-brand{display:flex;align-items:center;gap:12px;min-width:0}.ff-ui .ff-brand h1,.ff-ui .ff-brand h2{margin:0;line-height:1.4}.ff-ui .ff-hero-brand{justify-content:center}.ff-ui.ff-hero{text-align:center;margin-bottom:12px}.ff-ui.ff-hero h1{font-size:28px;font-weight:600}.ff-ui.ff-hero p{color:var(--muted);margin:12px 0 0}.ff-ui.ff-hero .ff-mark{width:48px;height:48px}
6
+ .ff-ui .ff-icon{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round;flex-shrink:0}.ff-ui .ff-mark{width:40px;height:40px;display:inline-grid;place-items:center;background:var(--soft);color:var(--action);border-radius:8px;flex-shrink:0}.ff-ui .ff-mark .ff-icon{width:26px;height:26px}.ff-ui .ff-brand{display:flex;align-items:center;gap:12px;min-width:0}.ff-ui .ff-brand h1,.ff-ui .ff-brand h2{margin:0;line-height:1.4}.ff-ui .ff-hero-brand{justify-content:center}.ff-ui.ff-hero{text-align:center;margin-bottom:12px}.ff-ui.ff-hero h1{font-size:28px;font-weight:600}.ff-ui.ff-hero .ff-mark{width:48px;height:48px}
7
7
  .ff-ui.ff-modal-host{background:transparent;padding:0;overflow:hidden}.ff-ui.ff-modal-host> :not(dialog):not(script):not(link){visibility:hidden}html:has(>body.ff-ui.ff-modal-host){background:transparent;color-scheme:normal}.ff-ui #wizard-message{color:var(--error);padding:0 20px;margin:4px 0;flex-shrink:0}
8
8
  [data-composer-card]:has(.ff-ui .ff-prompt){padding-top:48px}.ff-ui .ff-prompt{margin:6px 10px}.ff-ui dialog>.tabs{padding:8px 20px;flex-shrink:0}
9
9
  .ff-ui .ff-launcher{display:flex;align-items:center;gap:8px;color:var(--text);padding:10px 8px;text-decoration:none;border-radius:6px;width:100%}.ff-ui .ff-launcher:hover,.ff-ui .ff-launcher[aria-current=page]{background:var(--soft);color:var(--action)}.ff-ui .ff-launcher .ff-icon{color:var(--brand)}.ff-ui.ff-top{width:100%;min-width:0;padding:4px var(--dsh-sidebar-inline-padding,12px) 4px 0}.ff-ui .ff-shortcuts{display:flex;justify-content:center;gap:6px;flex-wrap:wrap;padding:12px 0}.ff-ui .ff-shortcuts button{display:flex;flex-direction:column;align-items:center;gap:6px;border-color:transparent;background:transparent;color:var(--muted);font-size:12px;min-width:72px}.ff-ui .ff-shortcuts button[aria-pressed=true]{background:var(--soft);color:var(--action)}.ff-ui .ff-prompt{color:var(--action);background:var(--soft);border-color:transparent;display:inline-flex;align-items:center;gap:6px;font-size:13px}
package/lib/client.js CHANGED
@@ -20,7 +20,7 @@ window.__ModuleLoader__.load({
20
20
  return state;
21
21
  });
22
22
  };
23
- const unregister=service.registerTab({id:SIDEBAR_TAB,title:'AI填表',icon,order:20,single:true,hidden:true,component});
23
+ const unregister=service.registerTab({id:SIDEBAR_TAB,title:'AI 填表',icon,order:20,single:true,hidden:true,component});
24
24
  const unsubscribe=service.subscribeState(flush);
25
25
  return {
26
26
  open(scope){if(disposed)throw Error(SIDEBAR_HELP);if(!service.isTabEnabled(SIDEBAR_TAB))throw Error('AI填表 Tab 已禁用,请在 Better Sidebar 设置中启用。');opened.set(scope.sessionId,scope);pending.add(scope.sessionId);service.openTab({type:SIDEBAR_TAB},scope);flush();},
@@ -28,6 +28,35 @@ window.__ModuleLoader__.load({
28
28
  dispose(){if(disposed)return;disposed=true;unsubscribe();pending.clear();targets.clear();for(const scope of opened.values())service.closeTab(SIDEBAR_TAB,scope);opened.clear();unregister();}
29
29
  };
30
30
  }
31
+ // Admission, not draft changes or runtime polling, owns automatic reveal.
32
+ function installSubmittedWorkbenchBridge(sessions,reveal){
33
+ let active,activeFace,stop=()=>{},disposed=false;const seen=new Set();
34
+ const owned=id=>typeof id==='string'&&id.startsWith('session-dsh-form-fill-agent-');
35
+ const admitted=(id,text,identity)=>{
36
+ if(disposed||!owned(id))return;
37
+ const task=String(text||'').match(/taskId=([a-f0-9-]{36})/i)?.[1];
38
+ const revision=String(text||'').match(/expectedRevision=(\d+)/)?.[1];
39
+ const key=task&&revision?`${id}:${task}:${revision}`:identity;
40
+ if(seen.has(key))return;seen.add(key);
41
+ if(sessions.list.getSnapshot().current!==id)return;
42
+ // openPanel reports companion errors through the visible manual fallback.
43
+ try{reveal(id,'preview')}catch{}
44
+ };
45
+ const connect=()=>{
46
+ const id=sessions?.list?.getSnapshot?.()?.current,face=owned(id)?sessions.binding?.(id)?.session:undefined;
47
+ if(id===active&&face===activeFace)return;
48
+ stop();stop=()=>{};active=id;activeFace=face;if(!owned(id))return;
49
+ if(typeof face?.beginSubmission==='function'){
50
+ const original=face.beginSubmission;
51
+ const wrapped=function(input){const identity={};return original.call(this,{...input,onRetire:settlement=>{
52
+ try{input.onRetire?.(settlement)}finally{if(settlement.reason==='observed')admitted(id,input.text,identity)}
53
+ }})};
54
+ face.beginSubmission=wrapped;stop=()=>{if(face.beginSubmission===wrapped)face.beginSubmission=original};
55
+ }
56
+ };
57
+ connect();const unsubscribe=sessions?.list?.subscribe?.(connect);
58
+ return()=>{disposed=true;stop();unsubscribe?.();seen.clear()};
59
+ }
31
60
  function apply(ctx) {
32
61
  let React, portal;
33
62
  try { React=require('react');portal=require('react-dom').createPortal; } catch {}
@@ -64,9 +93,9 @@ window.__ModuleLoader__.load({
64
93
  if(result!==id)throw Error('宿主不支持独立业务会话');
65
94
  await ctx.sessions.open(id);
66
95
  }
67
- const launcher=()=>h('a',{className:'ff-launcher',href:'/form-fill/',target:'_blank',rel:'noopener noreferrer','aria-label':'AI填表',onClick:async e=>{
96
+ const launcher=()=>h('a',{className:'ff-launcher',href:'/form-fill/',target:'_blank',rel:'noopener noreferrer','aria-label':'AI 填表',onClick:async e=>{
68
97
  if(!ctx.sessions?.create)return;e.preventDefault();try{await start()}catch{window.open('/form-fill/','_blank','noopener')}
69
- }},icon(),h('span',null,'AI填表'));
98
+ }},icon(),h('span',null,'AI 填表'));
70
99
  function Sidebar(){
71
100
  const [mount,setMount]=React.useState(null);
72
101
  React.useEffect(()=>{
@@ -100,19 +129,36 @@ window.__ModuleLoader__.load({
100
129
  const [mount,setMount]=React.useState(null);
101
130
  React.useEffect(()=>{
102
131
  if(!owned(sessionId))return;
103
- let mount,original,previous;
132
+ let mount,original,previous,disposed=false;
133
+ const release=()=>{
134
+ mount?.remove();mount=undefined;
135
+ if(original?.style.display==='none')original.style.display=previous;
136
+ original=undefined;setMount(null);
137
+ };
104
138
  const sync=()=>{
105
- if(mount?.isConnected)return;
139
+ if(disposed)return;
140
+ if(!activePlugin||current()!==sessionId){if(mount||original)release();return;}
106
141
  const marker=[...document.querySelectorAll('[data-form-fill-session]')].find(n=>n.dataset.formFillSession===sessionId);
107
142
  const hero=marker?.closest('[data-phase="hero"]'),title=hero?.querySelector('[class*="headlineText"]');
108
- if(!title||!['探索未至之境','Into the Unknown'].includes(title.textContent.trim()))return;
143
+ // This only replaces the host welcome brand, never a user-owned Session title.
144
+ if(!title||!['探索未至之境','Into the Unknown'].includes(title.textContent.trim())){if(mount||original)release();return;}
145
+ if(mount?.isConnected&&original===title.parentElement)return;
146
+ if(mount||original)release();
109
147
  original=title.parentElement;previous=original.style.display;original.style.display='none';
110
148
  mount=document.createElement('div');mount.className='ff-ui ff-hero';mount.dataset.ffTheme=theme();original.before(mount);setMount(mount);
111
149
  };
112
- sync();const timer=setInterval(sync,200);return()=>{clearInterval(timer);mount?.remove();if(original?.style.display==='none')original.style.display=previous;setMount(null)};
150
+ const marker=[...document.querySelectorAll('[data-form-fill-session]')].find(n=>n.dataset.formFillSession===sessionId);
151
+ const scope=marker?.closest('[data-slot="conversation"]')||marker?.closest('[data-phase="hero"]');
152
+ const observer=new MutationObserver(sync);
153
+ if(scope)observer.observe(scope,{childList:true,subtree:true,characterData:true});
154
+ const unsubscribe=ctx.sessions.list.subscribe?.(sync);
155
+ const dispose=()=>{if(disposed)return;disposed=true;observer.disconnect();if(typeof unsubscribe==='function')unsubscribe();release();};
156
+ disposers.push(dispose);sync();
157
+ return()=>{dispose();const index=disposers.indexOf(dispose);if(index>=0)disposers.splice(index,1);};
113
158
  },[sessionId]);
114
- return mount?portal(h(React.Fragment,null,h('div',{className:'ff-brand ff-hero-brand'},mark(),h('h1',null,'AI填表智能体')),h('p',null,'上传已有表格,核对字段映射,确认后生成新副本。')),mount):null;
159
+ return mount?portal(h('div',{className:'ff-brand ff-hero-brand'},mark(),h('h1',null,'AI 填表智能体')),mount):null;
115
160
  }
161
+
116
162
  function Menu({sessionId}){
117
163
  const ref=React.useRef(null),[mount,setMount]=React.useState(null),[error,setError]=React.useState('');
118
164
  React.useEffect(()=>{const report=message=>setError(message||'');viewListeners.add(report);return()=>viewListeners.delete(report)},[]);
@@ -188,10 +234,11 @@ window.__ModuleLoader__.load({
188
234
  try{const adapter=createSidebarAdapter(scope.betterSidebar,props=>h(WorkbenchTab,{...props,key:props.scope.sessionId}),icon());sidebar=adapter;sidebarError='';const disconnect=()=>{if(sidebar===adapter)sidebar=undefined;adapter.dispose()};scope.effect?.(()=>disconnect);disposers.push(disconnect)}catch(error){sidebarError=error.message}
189
235
  };
190
236
  if(ctx.inject)ctx.inject(['betterSidebar'],connect);else if(ctx.betterSidebar)connect(ctx);
237
+ disposers.push(installSubmittedWorkbenchBridge(ctx.sessions,openPanel));
191
238
  register('conversation.input.dock',Menu);
192
239
  register('conversation.input.overlay',props=>owned(props.sessionId)?h('div',{className:'ff-ui','data-ff-theme':theme()},h('button',{className:'ff-prompt',type:'button',onClick:()=>openPanel(props.sessionId,'wizard')},icon('spark'),'提示词生成')):null);
193
240
  ctx.effect?.(()=>()=>{activePlugin=false;for(const dispose of disposers)dispose();viewListeners.clear();views.clear();sessionTasks.clear();lastDrafts.clear()});
194
241
  }
195
- return {createSidebarAdapter,name:'form-fill-agent',inject:['slots','sessions','workspaces','conversation'],apply};
242
+ return {createSidebarAdapter,installSubmittedWorkbenchBridge,name:'form-fill-agent',inject:['slots','sessions','workspaces','conversation'],apply};
196
243
  }
197
244
  });
package/lib/ui.html CHANGED
@@ -70,7 +70,7 @@ function controls(value){busy=value;document.querySelectorAll('button').forEach(
70
70
  function row(target,values,labels){const tr=document.createElement('tr');values.forEach((v,i)=>{const td=document.createElement('td');td.textContent=v;td.title=String(v??'');td.dataset.label=labels[i];tr.append(td)});$(target).append(tr);return tr}
71
71
  function syncActions(){
72
72
  const running=current?.state==='enriching';
73
- const p=current?.presentation;$('progress-panel').hidden=!p||(!p.running&&!p.ended);if(p){$('task-progress').value=p.percent;$('task-progress').dataset.ended=String(p.ended);$('progress-label').textContent=(p.running?'处理中':p.ended?'处理结束':'待处理')+' · '+p.percent+'% · '+p.completed+' / '+p.total+' 查询计划项';$('progress-label').dataset.ended=String(p.ended);$('review-count').textContent='待核验 '+p.review+' 项';$('failure-count').textContent='失败 '+p.failures+' 项';$('progress-detail').textContent=p.currentAction+' · 已处理 '+p.completed+' 项 · 可填 '+p.filled+' 格 · 无数据 '+p.noData+' 格 · 用时 '+Math.floor(p.elapsedMs/1000)+' 秒'+(p.recentItem?' · 最近:'+p.recentItem:'')+';查询结果:'+Object.entries(p.outcomes||{}).map(([k,v])=>({'data':'有数据','zero-records':'零记录','not-required':'无需执行','no-permission':'无权限',failed:'失败',unknown:'未知',review:'需核验'}[k]||'未知')+' '+v).join(' / ');}
73
+ const p=current?.presentation;$('progress-panel').hidden=!p||(!p.running&&!p.ended);if(p){$('task-progress').value=p.percent;$('task-progress').dataset.ended=String(p.ended);$('progress-label').textContent=(p.running?'处理中':current.confirmed?'已完成':current.state==='cancelled'||current.state==='interrupted'?'已停止但未完成':p.ended?'等待核验与确认':'待处理')+' · '+p.percent+'% · '+p.completed+' / '+p.total+' 查询计划项';$('progress-label').dataset.ended=String(p.ended);$('review-count').textContent='待核验 '+p.review+' 项';$('failure-count').textContent='失败 '+p.failures+' 项';$('progress-detail').textContent=p.currentAction+' · 已处理 '+p.completed+' 项 · 可填 '+p.filled+' 格 · 无数据 '+p.noData+' 格 · 用时 '+Math.floor(p.elapsedMs/1000)+' 秒'+(p.recentItem?' · 最近:'+p.recentItem:'')+';查询结果:'+Object.entries(p.outcomes||{}).map(([k,v])=>({'data':'有数据','zero-records':'零记录','not-required':'无需执行','no-permission':'无权限',failed:'失败',unknown:'未知',review:'需核验'}[k]||'未知')+' '+v).join(' / ');}
74
74
  $('cancel-run').hidden=!running;$('cancel-run').disabled=busy||!running;
75
75
  $('retry-run').hidden=!current||current.confirmed||!current.changeSet.incomplete.some(i=>['provider-error','cancelled','not-started'].includes(i.reason));
76
76
  $('retry-run').disabled=busy||!current||current.confirmed||running||!current.changeSet.incomplete.some(i=>['provider-error','cancelled','not-started'].includes(i.reason));
@@ -297,7 +297,7 @@ initWizard();filterResults();syncActions();if($('wizard').open)renderWizard();
297
297
  const missing=data.analysis.incomplete.filter(i=>i.reason==='missing-anchor');
298
298
  $('anchor-alert').textContent=missing.length?'有 '+missing.length+' 行缺少可用主体标识。请把实际包含名称的列映射为“企业名称”,或提供非空信用代码;当前不会查询这些行。':'';
299
299
  }
300
- async function restore(silent=false){if(busy||!current&& !location.hash)return;const id=new URLSearchParams(location.hash.slice(1)).get('task');if(!id)return;try{const data=await api('/task/'+encodeURIComponent(id));if(silent&&data.id===current?.id&&data.revision===current?.revision){current.presentation=data.presentation;syncActions()}if(!silent||data.id!==current?.id||data.revision!==current?.revision){const wasRunning=current?.state==='enriching';render(data);if(silent&&data.state==='enriching'){navigate('identity');$('status').textContent=$('execution-progress').textContent}else if(silent&&wasRunning){navigate('preview');$('status').textContent='查询已结束,请检查填写预览。'}if(!silent){const target=pendingNavigation||(data.confirmed?'download':'preview');pendingNavigation=undefined;if(target==='history')$('history-tab').click();else navigate(target)}}}catch(e){if(!silent)$('status').textContent=e.message}}
300
+ async function restore(silent=false){if(busy||!current&& !location.hash)return;const id=new URLSearchParams(location.hash.slice(1)).get('task');if(!id)return;try{const data=await api('/task/'+encodeURIComponent(id));if(silent&&data.id===current?.id&&data.revision===current?.revision){current.presentation=data.presentation;syncActions()}if(!silent||data.id!==current?.id||data.revision!==current?.revision){const wasRunning=current?.state==='enriching';render(data);if(silent&&data.state==='enriching'){navigate('identity');$('status').textContent=$('execution-progress').textContent}else if(silent&&wasRunning){navigate('preview');$('status').textContent='查询已结束,请检查填写预览。'}if(!silent){const target=pendingNavigation||(data.confirmed?'download':'preview');pendingNavigation=undefined;if(target==='history')$('history-tab').click();else navigate(target)}}}catch(e){if(!silent)$('status').textContent=e.message;else $('execution-progress').textContent='状态同步异常,保留已显示进度,正在等待下次同步。'}}
301
301
  async function preview(buffer,name,demo=false){controls(true);try{const bytes=new Uint8Array(buffer);if(bytes.length>8388608)throw Error('文件超过 8 MiB');let binary='';for(let i=0;i<bytes.length;i+=32768)binary+=String.fromCharCode(...bytes.subarray(i,i+32768));$('grid').open=true;render(await api('/preview',{base64:btoa(binary),filename:name,sessionId,originWorkspaceId,analyzeOnly:!demo}));navigate(demo?'preview':'rules');$('status').textContent='预览已准备好,请检查后确认。'}catch(e){$('status').textContent=e.message;$('wizard-message').textContent=e.message}finally{controls(false)}}
302
302
  document.querySelectorAll('[data-step]').forEach(b=>b.onclick=()=>navigate(b.dataset.step));
303
303
  $('file').onchange=async()=>{const input=$('file'),f=input.files[0];if(!f||busy)return;input.disabled=true;controls(true);$('status').textContent='正在自动解析表格…';try{await preview(await f.arrayBuffer(),f.name)}catch(e){$('status').textContent=e.message}finally{input.disabled=false;input.value='';controls(false)}};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-form-fill-agent",
3
- "version": "0.2.28",
3
+ "version": "0.2.30",
4
4
  "private": false,
5
5
  "description": "AI form filling and spreadsheet autofill for XLSX in DeepSeek Harness, using authorized Qichacha MCP facts with cell previews, user confirmation and export to a new copy.",
6
6
  "repository": {