@zibby/skills 2.0.18 → 2.0.19

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/index.js CHANGED
@@ -425,7 +425,7 @@ Docs access is PER-USER: each teammate connects their OWN Google account (Integr
425
425
  These tools return { ok:false, error } on failure \u2014 treat an unavailable Google connection as "cannot deliver to Docs" and report it rather than blocking the task.`,resolve(){let r=Jp();if(!r)return null;let e={};for(let t of["PROJECT_API_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV","ZIBBY_INJECTED_GOOGLE_TOKEN","ZIBBY_INJECTED_GOOGLE_EMAIL","ZIBBY_SENDER_IS_NON_OWNER","ZIBBY_CHAT_STRICT_PERSONAL"])process.env[t]&&(e[t]=process.env[t]);return{type:"stdio",command:"node",args:[r,"../dist/googleDocs.js","googleDocsSkill"],env:e,description:this.description}},async handleToolCall(r,e){try{switch(r){case"gdocs_create_doc":{let t=typeof e?.title=="string"&&e.title.trim()?e.title.trim():null;if(!t)return JSON.stringify({ok:!1,error:"title is required"});let s=(await pe(`${De}/documents`,{method:"POST",body:{title:t}}))?.documentId;if(!s)return JSON.stringify({ok:!1,error:"Google Docs create returned no documentId"});let i=bi(e);if(i&&i.trim()){let{requests:o}=gi(i,1);o.length&&await pe(`${De}/documents/${s}:batchUpdate`,{method:"POST",body:{requests:o}})}return JSON.stringify({ok:!0,documentId:s,title:t,url:It(s)})}case"gdocs_append":{let t=cn(e?.documentId||e?.url||e?.id);if(!t)return JSON.stringify({ok:!1,error:"A valid Google Docs documentId or URL is required"});let n=bi(e);if(!n||!n.trim())return JSON.stringify({ok:!1,error:"markdown or text content is required"});let i=(await pe(`${De}/documents/${t}`))?.body,o=Array.isArray(i?.content)?i.content:[],a=o.length&&o[o.length-1].endIndex||2,c=Math.max(1,a-1),l=[],d=c;c>1&&(l.push({insertText:{location:{index:c},text:`
426
426
  `}}),d=c+1);let p=gi(n,d);return l.push(...p.requests),await pe(`${De}/documents/${t}:batchUpdate`,{method:"POST",body:{requests:l}}),JSON.stringify({ok:!0,documentId:t,url:It(t)})}case"gdocs_insert_image":{let t=cn(e?.documentId||e?.url||e?.id);if(!t)return JSON.stringify({ok:!1,error:"A valid Google Docs documentId or URL is required"});if(!e?.imagePath||typeof e.imagePath!="string"||!e.imagePath.trim())return JSON.stringify({ok:!1,error:"imagePath is required"});let{bytes:n,fileName:s,mimeType:i}=Qp(e.imagePath),{rawBody:o,contentType:a}=eu({name:s,mimeType:i},n,i),l=(await pe(`${Gp}/files?uploadType=multipart&fields=id`,{method:"POST",rawBody:o,contentType:a}))?.id;if(!l)return JSON.stringify({ok:!1,error:"Drive upload returned no file id"});await pe(`${yi}/files/${l}/permissions`,{method:"POST",body:{role:"reader",type:"anyone"}});let d=await pe(`${De}/documents/${t}`),p=Array.isArray(d?.body?.content)?d.body.content:[],u=p.length&&p[p.length-1].endIndex||2,h={location:{index:Math.max(1,u-1)},uri:`https://drive.google.com/uc?export=download&id=${l}`},f=Number(e?.width),_=Number(e?.height);return(Number.isFinite(f)&&f>0||Number.isFinite(_)&&_>0)&&(h.objectSize={...Number.isFinite(f)&&f>0?{width:{magnitude:f,unit:"PT"}}:{},...Number.isFinite(_)&&_>0?{height:{magnitude:_,unit:"PT"}}:{}}),await pe(`${De}/documents/${t}:batchUpdate`,{method:"POST",body:{requests:[{insertInlineImage:h}]}}),JSON.stringify({ok:!0,documentId:t,fileId:l,url:It(t)})}case"gdocs_get":{let t=cn(e?.documentId||e?.url||e?.id);if(!t)return JSON.stringify({ok:!1,error:"A valid Google Docs documentId or URL is required"});let n=await pe(`${De}/documents/${t}`),s=Xp(n?.body);return JSON.stringify({ok:!0,documentId:t,title:n?.title||"",url:It(t),text:s,...s.length>=ln?{truncated:!0}:{}})}case"gdocs_list_created":{let t=new URLSearchParams({q:"'me' in owners and mimeType='application/vnd.google-apps.document' and trashed=false",fields:"files(id,name,modifiedTime,webViewLink)",pageSize:String(Yp),orderBy:"modifiedTime desc"}),n=await pe(`${yi}/files?${t.toString()}`),s=(Array.isArray(n?.files)?n.files:[]).map(i=>({documentId:i.id,title:i.name,modifiedTime:i.modifiedTime,url:i.webViewLink||It(i.id)}));return JSON.stringify({ok:!0,count:s.length,files:s})}default:return JSON.stringify({ok:!1,error:`Unknown tool: ${r}`})}}catch(t){return JSON.stringify({ok:!1,error:t.message})}},tools:[{name:"gdocs_create_doc",description:"Create a new Google Doc with a title and optional content (markdown: #/##/### headings, - bullets, 1. numbered lists, **bold**, [links](url), `code`; or plain text). Returns { ok, documentId, url } \u2014 share the url with the user.",input_schema:{type:"object",properties:{title:{type:"string",description:"Document title."},markdown:{type:"string",description:"Document body as markdown (preferred)."},text:{type:"string",description:"Document body as plain text (used when markdown is absent)."}},required:["title"]}},{name:"gdocs_append",description:"Append markdown/text content to the END of an existing Google Doc. Only works on docs this app created or the user explicitly picked (drive.file scope). Accepts a documentId or a full docs.google.com URL. Returns { ok, documentId, url }.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Google Docs documentId OR a full https://docs.google.com/document/d/... URL."},markdown:{type:"string",description:"Content to append, as markdown (preferred)."},text:{type:"string",description:"Content to append, as plain text (used when markdown is absent)."}},required:["documentId"]}},{name:"gdocs_insert_image",description:"Append a LOCAL image file (png/jpg, max 5MB) to the END of an existing Google Doc. The image is uploaded to the user's Drive, made link-readable (role reader / type anyone \u2014 required: the Docs API only renders publicly fetchable image URIs, <2KB URI length, image <50MB and <25 megapixels), then inserted inline. Optional width/height in points (PT). Returns { ok, documentId, fileId, url }.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Google Docs documentId OR a full https://docs.google.com/document/d/... URL."},imagePath:{type:"string",description:"Local filesystem path to a .png or .jpg/.jpeg image (max 5MB)."},width:{type:"number",description:"Optional display width in points (PT)."},height:{type:"number",description:"Optional display height in points (PT)."}},required:["documentId","imagePath"]}},{name:"gdocs_get",description:"Read a Google Doc back as plain text (truncated to ~20k chars). The drive.file scope grants access ONLY to docs this app created or the user explicitly picked; to read an arbitrary pre-existing doc the user must PICK it once first via the Google Picker (drive.file cannot see un-picked files). Returns { ok, documentId, title, url, text }.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Google Docs documentId OR a full https://docs.google.com/document/d/... URL."}},required:["documentId"]}},{name:"gdocs_list_created",description:"List the Google Docs visible to this integration, newest first (max 25). NOTE: under the drive.file scope this lists ONLY docs the app created or the user explicitly picked \u2014 it is NOT a full Drive search. Returns { ok, count, files:[{ documentId, title, modifiedTime, url }] }.",input_schema:{type:"object",properties:{}}}]};import{existsSync as Ei,statSync as nu,readFileSync as su}from"fs";import{fileURLToPath as iu}from"url";import{basename as ou,dirname as au,resolve as cu}from"path";import{resolveIntegrationToken as wi}from"@zibby/core/backend-client.js";var tu="https://open.larksuite.com",Si=Object.freeze([Object.freeze({appId:"LARK_DOCS_APP_ID",appSecret:"LARK_DOCS_APP_SECRET",host:"LARK_DOCS_HOST"}),Object.freeze({appId:"LARK_APP_ID",appSecret:"LARK_APP_SECRET",host:"LARK_HOST"})]),mr=Object.freeze(Si.flatMap(r=>[r.appId,r.appSecret,r.host]));function dn(r){let e=String(r||"").trim().replace(/\/+$/,"");return e?/^https?:\/\//i.test(e)?e:`https://${e}`:tu}function ru(r){let e=String(process.env[r.appId]||"").trim(),t=String(process.env[r.appSecret]||"").trim();return!e||!t?null:{appId:e,appSecret:t,host:dn(process.env[r.host])}}async function hr(){for(let r of Si){let e=ru(r);if(e)return e}try{let r=await wi(I.LARK_DOCS);return{appId:r?.appId,appSecret:r?.appSecret,host:dn(r?.host)}}catch(r){if(!/unknown provider/i.test(String(r?.message||"")))throw r;let e=await wi(I.LARK);return{appId:e?.appId,appSecret:e?.appSecret,host:dn(e?.host)}}}function lu(){if(process.env.MCP_SKILL_PATH)return process.env.MCP_SKILL_PATH;let r=au(iu(import.meta.url)),e=cu(r,"..","bin","mcp-skill.mjs");return Ei(e)?e:null}var vi=2e4,Ii=50,du=50,pu=20,uu=500,mu=200,hu=1e3,xi=100,fn=20,fu=3e4,yu=new Set([5,1001,1002]),gu=10*1024*1024,bu=6e3*1e3,Tt=null,_u=mr;async function yn(){let{appId:r,appSecret:e,host:t}=await hr();if(Tt&&Tt.appId===r&&Tt.expiresAt>Date.now())return{token:Tt.token,host:t};let s=await(await w(`${t}/open-apis/auth/v3/tenant_access_token/internal`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({app_id:r,app_secret:e})},{kind:"api",what:"Lark tenant_access_token"})).json();if(s.code!==0)throw new Error(`Lark tenant_access_token failed: ${s.msg||s.code}`);return Tt={token:s.tenant_access_token,expiresAt:Date.now()+bu,appId:r},{token:s.tenant_access_token,host:t}}async function D(r,e,t){let{token:n,host:s}=await yn(),i={method:r,headers:{Authorization:`Bearer ${n}`,"Content-Type":"application/json; charset=utf-8"}};r!=="GET"&&t!==void 0&&(i.body=JSON.stringify(t));let a=await(await w(`${s}${e}`,i,{kind:"api",what:`Lark Docx ${r} ${e}`})).json();if(a.code!==0)throw new Error(`Lark Docx API ${e} error: ${a.msg||a.code}`);return{data:a.data||{},host:s}}function Nt(r,e){return String(r||"").includes("feishu")?`https://feishu.cn/docx/${e}`:`https://www.larksuite.com/docx/${e}`}function ku(r,e){return String(r||"").includes("feishu")?`https://feishu.cn/wiki/${e}`:`https://www.larksuite.com/wiki/${e}`}function wu(r){let e=r.indexOf("?");if(e<0)return{};let t=r.slice(e+1).split("#")[0],n=new URLSearchParams(t),s={},i=n.get("table"),o=n.get("view");return i&&/^tbl[A-Za-z0-9]+$/.test(i)&&(s.tableId=i),o&&/^vew[A-Za-z0-9]+$/.test(o)&&(s.viewId=o),s}function ue(r){if(!r||typeof r!="string")return null;let e=r.trim();if(!e)return null;let t=e.match(/\/(docx|wiki|base)\/([A-Za-z0-9]+)/);return t?{type:t[1],token:t[2],...wu(e)}:/^[A-Za-z0-9]{10,}$/.test(e)?{type:"docx",token:e}:null}async function st(r){let e=typeof r=="string"?ue(r):r;if(!e)throw new Error("A valid Lark doc id or URL is required");if(e.type==="docx")return e.token;let{data:t}=await D("GET",`/open-apis/wiki/v2/spaces/get_node?token=${encodeURIComponent(e.token)}`),n=t?.node||{};if(n.obj_type!=="docx"||!n.obj_token){let s=n.obj_type||"unknown";throw s==="bitable"?new Error("This Lark wiki link is a Base / \u591A\u7EF4\u8868\u683C (obj_type=bitable), not a document \u2014 read it with larkbitable_read_records (larkbitable_list_tables lists its tables)"):new Error(`Wiki node is not a docx document (obj_type=${s})`)}return n.obj_token}function pn(r){let e=String(r??"").replace(/\r\n/g,`
427
427
  `),t=[];for(let n of e.split(`
428
- `)){let s=n.replace(/\s+$/,"");if(!s.trim())continue;let i=/^(#{1,3})\s+(.*)$/.exec(s),o=/^\s*[-*]\s+(.*)$/.exec(s),a=/^\s*\d+[.)]\s+(.*)$/.exec(s),c,l,d;if(i){let p=i[1].length;c=`heading${p}`,l=2+p,d=i[2]}else o?(c="bullet",l=12,d=o[1]):a?(c="ordered",l=13,d=a[1]):(c="text",l=2,d=s);t.push({block_type:l,[c]:{elements:[{text_run:{content:d}}],style:{}}})}return t}function Ot(r){let e=typeof r?.markdown=="string"?r.markdown:null,t=typeof r?.text=="string"?r.text:null;return e??t}async function un(r,e){let t;for(let n=0;n<e.length;n+=Ii){let s=e.slice(n,n+Ii);t=(await D("POST",`/open-apis/docx/v1/documents/${r}/blocks/${r}/children?document_revision_id=-1`,{children:s})).host}return t}function Su(r){let e=typeof r=="string"?r.trim():"";if(!e)throw new Error("imagePath is required");if(!Ei(e)||!nu(e).isFile())throw new Error(`imagePath not found (or not a file): ${e}`);if(!/\.(png|jpe?g)$/i.test(e))throw new Error("imagePath must be a .png or .jpg/.jpeg file");let t=su(e);if(t.length>gu)throw new Error(`image is ${(t.length/(1024*1024)).toFixed(1)}MB \u2014 max 10MB`);return t}async function vu({fileName:r,parentNode:e,bytes:t}){let{token:n,host:s}=await yn(),i=new FormData;i.set("file_name",r),i.set("parent_type","docx_image"),i.set("parent_node",e),i.set("size",String(t.length)),i.set("file",new Blob([t]),r);let a=await(await w(`${s}/open-apis/drive/v1/medias/upload_all`,{method:"POST",headers:{Authorization:`Bearer ${n}`},body:i},{kind:"transfer",what:"Lark Docx image upload"})).json();if(a.code!==0)throw new Error(`Lark media upload_all error: ${a.msg||a.code}`);let c=a.data?.file_token;if(!c)throw new Error("Lark media upload_all returned no file_token");return c}var Iu="docx",Tu=50;function Ti(r){return[{type:"text_run",text_run:{text:String(r??"")}}]}function Nu(r){return Array.isArray(r)?r.map(e=>e?.text_run?.text??e?.docs_link?.url??(e?.person?`@${e.person.user_id||""}`:"")).join(""):""}function Ou(r){let e=Array.isArray(r?.reply_list?.replies)?r.reply_list.replies:[];return{commentId:r?.comment_id||"",resolved:!!r?.is_solved,replies:e.map(t=>({replyId:t?.reply_id||"",author:t?.user_id||"",text:Nu(t?.content?.elements),createTime:t?.create_time||""}))}}function mn(r){return typeof r?.fileType=="string"&&r.fileType.trim()?r.fileType.trim():Iu}function Ni(r,e,t,n){let i=String(r||"").includes("feishu")?"https://feishu.cn":"https://www.larksuite.com",o=new URLSearchParams;t&&o.set("table",t),n&&o.set("view",n);let a=o.toString();return`${i}/base/${e}${a?`?${a}`:""}`}async function Oi(r){let e=typeof r=="string"?ue(r):r;if(!e)throw new Error("A valid Lark Base id or URL is required");let t={tableId:e.tableId||null,viewId:e.viewId||null};if(e.type!=="wiki")return{appToken:e.token,...t};let{data:n}=await D("GET",`/open-apis/wiki/v2/spaces/get_node?token=${encodeURIComponent(e.token)}`),s=n?.node||{};if(s.obj_type!=="bitable"||!s.obj_token)throw s.obj_type==="docx"?new Error("This Lark wiki link is a document (obj_type=docx), not a Base / \u591A\u7EF4\u8868\u683C \u2014 read it with larkdoc_get"):new Error(`Wiki node is not a Base / \u591A\u7EF4\u8868\u683C (obj_type=${s.obj_type||"unknown"})`);return{appToken:s.obj_token,...t}}function At(r){if(r==null)return"";if(typeof r=="string")return r;if(typeof r=="number"||typeof r=="boolean")return String(r);if(Array.isArray(r))return r.map(At).filter(e=>e!=="").join(", ");if(typeof r=="object"){let e=r;return typeof e.text=="string"&&e.text?e.text:typeof e.name=="string"&&e.name?e.name:typeof e.en_name=="string"&&e.en_name?e.en_name:typeof e.full_address=="string"&&e.full_address?e.full_address:typeof e.link=="string"&&e.link?e.link:e.value!==void 0?At(e.value):JSON.stringify(e)}return String(r)}function Au(r,e){if(yu.has(Number(e))){let t=Number(Array.isArray(r)?r[0]:r);if(Number.isFinite(t)&&t>0)return new Date(t).toISOString()}return At(r)}async function Li(r){let e=[],t=null;for(let n=0;n<fn;n++){let s=new URLSearchParams({page_size:String(xi)});t&&s.set("page_token",t);let{data:i}=await D("GET",`/open-apis/bitable/v1/apps/${encodeURIComponent(r)}/tables?${s.toString()}`);for(let o of Array.isArray(i?.items)?i.items:[])e.push({tableId:String(o?.table_id||""),name:String(o?.name||"")});if(!i?.has_more||!i?.page_token)break;t=i.page_token}return e}async function Ru(r,e){let t=[],n=null;for(let s=0;s<fn;s++){let i=new URLSearchParams({page_size:String(xi)});n&&i.set("page_token",n);let{data:o}=await D("GET",`/open-apis/bitable/v1/apps/${encodeURIComponent(r)}/tables/${encodeURIComponent(e)}/fields?${i.toString()}`);for(let a of Array.isArray(o?.items)?o.items:[])t.push({name:String(a?.field_name||""),type:Number(a?.type)||0});if(!o?.has_more||!o?.page_token)break;n=o.page_token}return t}async function Eu({appToken:r,tableId:e}){if(e)return e;let t=await Li(r);if(t.length===1)return t[0].tableId;if(!t.length)throw new Error("This Lark Base has no tables");let n=t.map(s=>`${s.name} (${s.tableId})`).join("; ");throw new Error(`tableId is required \u2014 this Base has ${t.length} tables: ${n}`)}var Ai=new Set(["is","isNot","contains","doesNotContain","isEmpty","isNotEmpty","isGreater","isGreaterEqual","isLess","isLessEqual"]),Ri=new Set(["isEmpty","isNotEmpty"]);function hn(r,e,t){let n=new Set(r.map(o=>o.name)),s=e.filter(o=>!n.has(o));if(!s.length)return;let i=s.flatMap(o=>r.filter(a=>a.name.trim()===String(o).trim()).map(a=>JSON.stringify(a.name)));throw new Error(`unknown column(s) in ${t}: ${s.map(o=>JSON.stringify(o)).join(", ")}`+(i.length?` \u2014 did you mean ${i.join(", ")}? (names can carry spaces)`:"")+`. This table's columns: ${r.map(o=>JSON.stringify(o.name)).join(", ")}`)}function xu({columns:r,viewId:e,fieldNames:t,filter:n,sort:s,includeRowMeta:i}){let o={};if(e&&(o.view_id=e),t?.length&&(hn(r,t,"fieldNames"),o.field_names=t),n){let a=Array.isArray(n.conditions)?n.conditions:[];if(!a.length)throw new Error("filter.conditions must be a non-empty array");hn(r,a.map(c=>c?.fieldName??c?.field_name),"filter"),o.filter={conjunction:n.conjunction==="or"?"or":"and",conditions:a.map(c=>{let l=String(c?.operator||"is");if(!Ai.has(l))throw new Error(`unknown filter operator ${JSON.stringify(l)} \u2014 Lark accepts: ${[...Ai].join(", ")}`);let d=c?.value,p=d==null?[]:(Array.isArray(d)?d:[d]).map(u=>String(u));if(!Ri.has(l)&&!p.length)throw new Error(`filter operator ${JSON.stringify(l)} requires a value`);return{field_name:String(c?.fieldName??c?.field_name),operator:l,...Ri.has(l)?{}:{value:p}}})}}return s?.length&&(hn(r,s.map(a=>a?.fieldName??a?.field_name),"sort"),o.sort=s.map(a=>({field_name:String(a?.fieldName??a?.field_name),desc:!!a?.desc}))),i&&(o.automatic_fields=!0),o}function Lu(r){let e={},t=a=>{let c=Number(a);return!Number.isFinite(c)||c<=0?"":new Date(c<1e11?c*1e3:c).toISOString()},n=t(r?.created_time),s=t(r?.last_modified_time),i=At(r?.created_by),o=At(r?.last_modified_by);return n&&(e.createdTime=n),i&&(e.createdBy=i),s&&(e.lastModifiedTime=s),o&&(e.lastModifiedBy=o),e}async function $u({appToken:r,tableId:e,viewId:t,fieldNames:n,filter:s,sort:i,includeRowMeta:o,maxRecords:a,pageToken:c}){let l=await Ru(r,e),d=new Map(l.map(b=>[b.name,b.type])),p=xu({columns:l,viewId:t,fieldNames:n,filter:s,sort:i,includeRowMeta:o}),u=[],m=0,h=c||null,f=!1,_=0,y=!1;for(let b=0;b<fn;b++){let g=new URLSearchParams({page_size:String(Math.min(uu,a))});h&&g.set("page_token",h);let{data:k}=await D("POST",`/open-apis/bitable/v1/apps/${encodeURIComponent(r)}/tables/${encodeURIComponent(e)}/records/search?${g.toString()}`,p);_=Number(k?.total)||_,f=!!k?.has_more,h=k?.page_token||null;for(let S of Array.isArray(k?.items)?k.items:[]){let L={};for(let[A,Z]of Object.entries(S?.fields||{}))L[A]=Au(Z,d.get(A));let U={recordId:String(S?.record_id||""),...o?Lu(S):{},fields:L},R=JSON.stringify(U).length;if(u.length&&m+R>fu){y=!0;break}if(m+=R,u.push(U),u.length>=a)break}if(y||u.length>=a||!f||!h)break}return{columns:l,records:u,total:_,hasMore:f||y,nextPageToken:h||"",truncated:y,...y&&!h?{note:"stopped at the character budget, and these rows have no resume token \u2014 re-read with fieldNames set to the few columns you need (see columns[])."}:{}}}var $i={id:"lark-docs",callsBackend:!0,serverName:"larkdocs",allowedTools:["mcp__larkdocs__*"],requiresIntegration:[I.LARK_DOCS,I.LARK],description:"Lark / Feishu Docs \u2014 read, create, append, and insert images into Lark documents (docx), and read Lark Bases (\u591A\u7EF4\u8868\u683C / bitable).",envKeys:["PROJECT_API_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV",..._u],promptFragment:'## Lark Docs\nYou can read, create, and append Lark/Feishu documents (docx), read/post/reply to their comments, and READ Lark Bases (\u591A\u7EF4\u8868\u683C / bitable). This runs on the connected Lark Docs app (the chat app is used when no separate docs app is connected). Each `larkdoc_*` / `larkwiki_*` / `larkbitable_*` tool documents its own params and return shape in its tool description \u2014 they are not restated here.\nA Lark link is not always a document: a `/base/\u2026` link, or a `/wiki/\u2026` link carrying `?table=tbl\u2026`, is a Base \u2014 read it with `larkbitable_read_records`, NOT `larkdoc_get` (which refuses it, since a Base has no document body). Base reads need the `bitable:app:readonly` scope on the connected app.\nA Base question about CHANGE \u2014 what moved since yesterday, who edited a row, which rows are stale \u2014 is answered by `larkbitable_read_records` with `includeRowMeta:true` (per-row created/last-modified time and person) and a `filter`/`sort` on that timestamp, NOT by reading the whole table twice and diffing. Lark exposes no per-cell edit history to any API, so a field\'s PREVIOUS value cannot be fetched \u2014 say so rather than inferring it from snapshots.\n`createdTime` and `lastModifiedTime` are DIFFERENT QUESTIONS and must never stand in for one another: a row edited yesterday was not added yesterday. Rows APPEARING in a read you did not see before is not evidence they were created \u2014 a shorter earlier read (see `truncated`) explains it just as well. Answer "what was added" from `createdTime` ONLY, and if you did not ask for row metadata, say the read cannot tell rather than reasoning from row counts.\nThese tools return { ok:false, error } on failure \u2014 treat an unavailable Lark connection as "cannot read/deliver to Lark Docs" and continue rather than blocking the task.',resolve(){let r=lu();if(!r)return null;let e={};for(let t of this.envKeys)process.env[t]&&(e[t]=process.env[t]);return{type:"stdio",command:"node",args:[r,"../dist/larkDocs.js","larkDocsSkill"],env:e,description:this.description}},async handleToolCall(r,e){try{switch(r){case"larkdoc_get":{let t=e?.documentId||e?.url||e?.id,n=ue(t);if(!n)return JSON.stringify({ok:!1,error:"A valid Lark doc id or URL is required"});let s=await st(n),i="";try{i=(await D("GET",`/open-apis/docx/v1/documents/${s}`)).data?.document?.title||""}catch{}let{data:o,host:a}=await D("GET",`/open-apis/docx/v1/documents/${s}/raw_content?lang=0`),c=String(o?.content||""),l=!1;return c.length>vi&&(c=c.slice(0,vi),l=!0),JSON.stringify({ok:!0,documentId:s,title:i,url:Nt(a,s),text:c,...l?{truncated:!0}:{}})}case"larkdoc_create":{let t=typeof e?.title=="string"&&e.title.trim()?e.title.trim():null;if(!t)return JSON.stringify({ok:!1,error:"title is required"});let n=typeof e?.wikiSpaceId=="string"&&e.wikiSpaceId.trim()?e.wikiSpaceId.trim():null;if(n){let c=typeof e?.parentNodeToken=="string"&&e.parentNodeToken.trim()?e.parentNodeToken.trim():null,{data:l,host:d}=await D("POST",`/open-apis/wiki/v2/spaces/${encodeURIComponent(n)}/nodes`,{obj_type:"docx",node_type:"origin",title:t,...c?{parent_node_token:c}:{}}),p=l?.node||{},u=p.obj_token;if(!u)return JSON.stringify({ok:!1,error:"Lark wiki node create returned no obj_token"});let m=Ot(e);if(m&&m.trim()){let h=pn(m);h.length&&await un(u,h)}return JSON.stringify({ok:!0,documentId:u,title:t,wikiSpaceId:n,wikiNodeToken:p.node_token||"",url:p.node_token?ku(d,p.node_token):Nt(d,u)})}let{data:s,host:i}=await D("POST","/open-apis/docx/v1/documents",{title:t,...e?.folderToken?{folder_token:String(e.folderToken)}:{}}),o=s?.document?.document_id;if(!o)return JSON.stringify({ok:!1,error:"Lark Docs create returned no document_id"});let a=Ot(e);if(a&&a.trim()){let c=pn(a);c.length&&await un(o,c)}return JSON.stringify({ok:!0,documentId:o,title:t,url:Nt(i,o)})}case"larkdoc_append":{let t=e?.documentId||e?.url||e?.id,n=ue(t);if(!n)return JSON.stringify({ok:!1,error:"A valid Lark doc id or URL is required"});let s=Ot(e);if(!s||!s.trim())return JSON.stringify({ok:!1,error:"markdown or text content is required"});let i=await st(n),o=pn(s);if(!o.length)return JSON.stringify({ok:!1,error:"no non-empty content to append"});let a=await un(i,o);return JSON.stringify({ok:!0,documentId:i,url:Nt(a,i)})}case"larkdoc_insert_image":{let t=e?.documentId||e?.url||e?.id,n=ue(t);if(!n)return JSON.stringify({ok:!1,error:"A valid Lark doc id or URL is required"});if(!e?.imagePath||typeof e.imagePath!="string"||!e.imagePath.trim())return JSON.stringify({ok:!1,error:"imagePath is required"});let s=await st(n),i=e.imagePath.trim(),o=Su(i),a=ou(i),l=(await D("POST",`/open-apis/docx/v1/documents/${s}/blocks/${s}/children?document_revision_id=-1`,{children:[{block_type:27,image:{}}]})).data?.children?.[0]?.block_id;if(!l)return JSON.stringify({ok:!1,error:"Lark image block create returned no block_id"});let d=await vu({fileName:a,parentNode:l,bytes:o}),p={token:d},u=Number(e?.width),m=Number(e?.height);Number.isFinite(u)&&u>0&&(p.width=Math.round(u)),Number.isFinite(m)&&m>0&&(p.height=Math.round(m));let{host:h}=await D("PATCH",`/open-apis/docx/v1/documents/${s}/blocks/${l}?document_revision_id=-1`,{replace_image:p});return JSON.stringify({ok:!0,documentId:s,blockId:l,fileToken:d,url:Nt(h,s)})}case"larkwiki_list_spaces":{let t=[],n=null;for(let s=0;s<pu;s++){let i=new URLSearchParams({page_size:String(du)});n&&i.set("page_token",n);let{data:o}=await D("GET",`/open-apis/wiki/v2/spaces?${i.toString()}`),a=Array.isArray(o?.items)?o.items:[];for(let c of a)t.push({spaceId:String(c?.space_id||""),name:String(c?.name||"")});if(!o?.has_more||!o?.page_token)break;n=o.page_token}return JSON.stringify({ok:!0,count:t.length,spaces:t})}case"larkbitable_list_tables":{let t=e?.baseId||e?.url||e?.documentId||e?.id,n=ue(t);if(!n)return JSON.stringify({ok:!1,error:"A valid Lark Base id or URL is required"});let{appToken:s,tableId:i,viewId:o}=await Oi(n),a=await Li(s),c="",l="";try{let d=await D("GET",`/open-apis/bitable/v1/apps/${encodeURIComponent(s)}`);c=d.data?.app?.name||"",l=d.host}catch{}return JSON.stringify({ok:!0,appToken:s,name:c,...l?{url:Ni(l,s,i,o)}:{},...i?{linkedTableId:i}:{},...o?{linkedViewId:o}:{},count:a.length,tables:a})}case"larkbitable_read_records":{let t=e?.baseId||e?.url||e?.documentId||e?.id,n=ue(t);if(!n)return JSON.stringify({ok:!1,error:"A valid Lark Base id or URL is required"});let s=await Oi(n),i=s.appToken,o=await Eu({appToken:i,tableId:typeof e?.tableId=="string"&&e.tableId.trim()?e.tableId.trim():s.tableId}),a=typeof e?.viewId=="string"&&e.viewId.trim()?e.viewId.trim():s.viewId,c=Array.isArray(e?.fieldNames)?e.fieldNames.map(b=>String(b)).filter(Boolean):null,l=Number(e?.maxRecords),d=Number.isFinite(l)&&l>0?Math.min(Math.round(l),hu):mu,p=typeof e?.pageToken=="string"&&e.pageToken.trim()?e.pageToken.trim():null,u=e?.filter&&typeof e.filter=="object"?e.filter:null,m=Array.isArray(e?.sort)&&e.sort.length?e.sort:null,h=!!e?.includeRowMeta,f=await $u({appToken:i,tableId:o,viewId:a,fieldNames:c,filter:u,sort:m,includeRowMeta:h,maxRecords:d,pageToken:p}),{host:_}=await yn(),y="";try{y=(await D("GET",`/open-apis/bitable/v1/apps/${encodeURIComponent(i)}`)).data?.app?.name||""}catch{}return JSON.stringify({ok:!0,appToken:i,name:y,tableId:o,...a?{viewId:a}:{},url:Ni(_,i,o,a),total:f.total,...u?{filtered:!0}:{},count:f.records.length,columns:f.columns,records:f.records,hasMore:f.hasMore,...f.nextPageToken?{nextPageToken:f.nextPageToken}:{},...f.truncated?{truncated:!0}:{},...f.note?{note:f.note}:{}})}case"larkdoc_list_comments":{let t=e?.documentId||e?.url||e?.id||e?.fileToken,n=ue(t);if(!n)return JSON.stringify({ok:!1,error:"A valid Lark doc id or URL is required"});let s=await st(n),i=mn(e),o=new URLSearchParams({file_type:i,page_size:String(Tu)}),{data:a}=await D("GET",`/open-apis/drive/v1/files/${s}/comments?${o.toString()}`),l=(Array.isArray(a?.items)?a.items:[]).map(Ou);return JSON.stringify({ok:!0,documentId:s,count:l.length,comments:l})}case"larkdoc_add_comment":{let t=e?.documentId||e?.url||e?.id||e?.fileToken,n=ue(t);if(!n)return JSON.stringify({ok:!1,error:"A valid Lark doc id or URL is required"});let s=Ot({text:e?.text??e?.body});if(!s||!s.trim())return JSON.stringify({ok:!1,error:"text is required"});let i=await st(n),o=mn(e),{data:a}=await D("POST",`/open-apis/drive/v1/files/${i}/comments?file_type=${encodeURIComponent(o)}`,{reply_list:{replies:[{content:{elements:Ti(s)}}]}});return JSON.stringify({ok:!0,documentId:i,commentId:a?.comment_id||""})}case"larkdoc_reply_comment":{let t=e?.documentId||e?.url||e?.id||e?.fileToken,n=ue(t);if(!n)return JSON.stringify({ok:!1,error:"A valid Lark doc id or URL is required"});let s=e?.commentId||e?.comment_id;if(!s)return JSON.stringify({ok:!1,error:"commentId is required"});let i=Ot({text:e?.text??e?.body});if(!i||!i.trim())return JSON.stringify({ok:!1,error:"text is required"});let o=await st(n),a=mn(e),{data:c}=await D("POST",`/open-apis/drive/v1/files/${o}/comments/${encodeURIComponent(s)}/replies?file_type=${encodeURIComponent(a)}`,{content:{elements:Ti(i)}});return JSON.stringify({ok:!0,documentId:o,commentId:String(s),replyId:c?.reply_id||""})}default:return JSON.stringify({ok:!1,error:`Unknown tool: ${r}`})}}catch(t){return JSON.stringify({ok:!1,error:t.message})}},tools:[{name:"larkdoc_get",description:"Read a Lark/Feishu document (docx) as plain text, for use as reference context. Accepts a raw doc id OR a full Lark doc URL (a /docx/ or /wiki/ link \u2014 wiki links are resolved to their backing docx). Returns { ok, documentId, title, url, text }. Text is truncated to ~20k chars.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Lark doc id (docx token) OR a full Lark/Feishu doc URL (/docx/\u2026 or /wiki/\u2026)."}},required:["documentId"]}},{name:"larkdoc_create",description:"Create a new Lark/Feishu document (docx) with a title and optional content (markdown: #/##/### headings, - bullets, 1. ordered; or plain text). Pass wikiSpaceId (from larkwiki_list_spaces) to create the doc INSIDE a wiki space instead of as a standalone doc \u2014 optionally nested under parentNodeToken. Returns { ok, documentId, url } (plus wikiNodeToken when created in a wiki) \u2014 share the url with the user.",input_schema:{type:"object",properties:{title:{type:"string",description:"Document title."},markdown:{type:"string",description:"Document body as markdown (preferred)."},text:{type:"string",description:"Document body as plain text (used when markdown is absent)."},folderToken:{type:"string",description:"Optional Lark drive folder token to create the doc in. Absent = the app root. Ignored when wikiSpaceId is set."},wikiSpaceId:{type:"string",description:"Optional wiki space id (from larkwiki_list_spaces) \u2014 create the doc as a node inside this wiki space."},parentNodeToken:{type:"string",description:"Optional wiki node token to nest the new doc under (only with wikiSpaceId). Absent = the space root."}},required:["title"]}},{name:"larkwiki_list_spaces",description:"List the Lark/Feishu wiki spaces visible to the connected app. Returns { ok, count, spaces:[{ spaceId, name }] }. Use to discover the wikiSpaceId for larkdoc_create.",input_schema:{type:"object",properties:{}}},{name:"larkdoc_append",description:"Append markdown/text content to the END of an existing Lark/Feishu document (docx). Accepts a documentId or a full Lark doc URL. Returns { ok, documentId, url }.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Lark doc id (docx token) OR a full Lark/Feishu doc URL."},markdown:{type:"string",description:"Content to append, as markdown (preferred)."},text:{type:"string",description:"Content to append, as plain text (used when markdown is absent)."}},required:["documentId"]}},{name:"larkdoc_insert_image",description:"Append a LOCAL image file (png/jpg, max 10MB) to the END of an existing Lark/Feishu document (docx). Accepts a documentId or full doc URL plus a local file path. Optional width/height in pixels. Returns { ok, documentId, blockId, fileToken, url }.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Lark doc id (docx token) OR a full Lark/Feishu doc URL."},imagePath:{type:"string",description:"Local filesystem path to a .png or .jpg/.jpeg image (max 10MB)."},width:{type:"number",description:"Optional display width in pixels."},height:{type:"number",description:"Optional display height in pixels."}},required:["documentId","imagePath"]}},{name:"larkbitable_list_tables",description:"List the tables in a Lark/Feishu Base (\u591A\u7EF4\u8868\u683C / bitable). Accepts a Base URL (a /base/\u2026 link, or a /wiki/\u2026 link whose node IS a Base) or a raw app token. Returns { ok, appToken, name, tables:[{ tableId, name }] } plus linkedTableId/linkedViewId when the URL pinned them. A Base is NOT a document \u2014 larkdoc_get cannot read one; use this and larkbitable_read_records instead.",input_schema:{type:"object",properties:{baseId:{type:"string",description:"Lark Base app token OR a full Base/wiki URL (/base/\u2026 or /wiki/\u2026)."}},required:["baseId"]}},{name:"larkbitable_read_records",description:"Read the rows of one table in a Lark/Feishu Base (\u591A\u7EF4\u8868\u683C / bitable) as flat text. Accepts a Base URL (/base/\u2026 or a /wiki/\u2026 link fronting a Base) or app token; the table and view are taken from the URL's table=/view= when present, and tableId may be omitted when the Base has exactly one table. Optionally restrict to viewId or fieldNames. Returns { ok, appToken, name, tableId, total, count, columns:[{name,type}], records:[{ recordId, fields:{ <column>: <text> } }], hasMore, nextPageToken }. Every cell is a STRING (dates as ISO). Defaults to 200 records; pass maxRecords (max 1000) and pageToken to page through a bigger table. A WIDE table (dozens of columns) hits a character budget before the row cap: the reply then carries truncated:true, hasMore:true and a note, and the fix is fieldNames, not paging. Column names are matched EXACTLY and real ones can carry a leading space or a ' (1)' suffix \u2014 an unknown name is refused with this table's actual column list, so read columns[] (or one small call) before naming any. TO ANSWER 'what changed, when, by whom': pass includeRowMeta:true and each record also carries createdTime/createdBy/lastModifiedTime/lastModifiedBy (ISO + display name) \u2014 row METADATA that exists even when the table declares no such column. This is the ONLY way to see a row's edit time; Lark exposes NO per-cell edit history to any API, so a field's previous VALUE is unavailable (only the Base's own web UI record history has it). Filter and sort SERVER-SIDE rather than pulling the table and counting: filter = { conjunction:'and'|'or', conditions:[{ fieldName, operator, value }] } with operator one of is/isNot/contains/doesNotContain/isEmpty/isNotEmpty/isGreater/isGreaterEqual/isLess/isLessEqual, and sort = [{ fieldName, desc }]. With a filter the reply carries filtered:true and `total` is the MATCH count, so one call with maxRecords:1 answers 'how many rows are X'. Date/datetime columns compare against value ['ExactDate','<epoch ms>'] (Lark also accepts 'Today', 'Yesterday', 'CurrentWeek'); everything else takes plain strings.",input_schema:{type:"object",properties:{baseId:{type:"string",description:"Lark Base app token OR a full Base/wiki URL (/base/\u2026 or /wiki/\u2026)."},tableId:{type:"string",description:"Table id (tbl\u2026). Optional when the URL carries table= or the Base has exactly one table; larkbitable_list_tables lists them."},viewId:{type:"string",description:"Optional view id (vew\u2026) \u2014 restricts the read to that view's rows and order."},fieldNames:{type:"array",items:{type:"string"},description:"Optional column names to return. Absent = every column."},includeRowMeta:{type:"boolean",description:"Also return each row's createdTime/createdBy/lastModifiedTime/lastModifiedBy. Use for any 'what changed / who edited / when' question \u2014 without it those are absent, and a row count cannot substitute for them. createdTime answers WHAT WAS ADDED; lastModifiedTime answers WHAT WAS EDITED \u2014 a row edited yesterday was not added yesterday."},filter:{type:"object",description:"Server-side filter, evaluated by Lark. { conjunction:'and'|'or', conditions:[{ fieldName, operator, value }] }.",properties:{conjunction:{type:"string",description:"'and' (default) or 'or'."},conditions:{type:"array",items:{type:"object",properties:{fieldName:{type:"string",description:"Exact column name (see columns[])."},operator:{type:"string",description:"is | isNot | contains | doesNotContain | isEmpty | isNotEmpty | isGreater | isGreaterEqual | isLess | isLessEqual."},value:{description:"String, number, or array of them. Omit for isEmpty/isNotEmpty. Date columns take ['ExactDate','<epoch ms>'] or 'Today'/'Yesterday'/'CurrentWeek'."}},required:["fieldName","operator"]}}},required:["conditions"]},sort:{type:"array",description:'Server-side sort, e.g. [{ fieldName: "\u6700\u540E\u66F4\u65B0\u65F6\u95F4", desc: true }].',items:{type:"object",properties:{fieldName:{type:"string",description:"Exact column name."},desc:{type:"boolean",description:"Descending when true."}},required:["fieldName"]}},maxRecords:{type:"number",description:"Max records to return (default 200, hard cap 1000)."},pageToken:{type:"string",description:"Resume token from a previous call's nextPageToken."}},required:["baseId"]}},{name:"larkdoc_list_comments",description:"List the comment threads on a Lark/Feishu document. Accepts a documentId or full doc URL. Returns { ok, comments:[{ commentId, replies:[{ replyId, author, text }] }] }. Use to read the thread you are replying to.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Lark doc id (docx token) OR a full Lark/Feishu doc URL."},fileType:{type:"string",description:"Drive file type \u2014 defaults to 'docx'. Only change for non-docx files (doc/sheet/bitable/file/slides)."}},required:["documentId"]}},{name:"larkdoc_add_comment",description:"Post a NEW top-level comment on a Lark/Feishu document. Accepts a documentId or full doc URL plus text. Returns { ok, documentId, commentId }.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Lark doc id (docx token) OR a full Lark/Feishu doc URL."},text:{type:"string",description:"The comment body (plain text)."},fileType:{type:"string",description:"Drive file type \u2014 defaults to 'docx'."}},required:["documentId","text"]}},{name:"larkdoc_reply_comment",description:"Reply INSIDE an existing comment thread on a Lark/Feishu document. Pass { documentId, commentId, text }. Use to answer a user who @mentioned Zibby in a doc comment (reply in the same commentId). Returns { ok, documentId, commentId, replyId }.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Lark doc id (docx token) OR a full Lark/Feishu doc URL."},commentId:{type:"string",description:"The comment_id of the thread to reply into (from larkdoc_list_comments or the webhook event)."},text:{type:"string",description:"The reply body (plain text)."},fileType:{type:"string",description:"Drive file type \u2014 defaults to 'docx'."}},required:["documentId","commentId","text"]}}]};import{existsSync as ju}from"fs";import{fileURLToPath as Pu}from"url";import{dirname as Cu,resolve as Uu}from"path";function Bu(){if(process.env.MCP_SKILL_PATH)return process.env.MCP_SKILL_PATH;let r=Cu(Pu(import.meta.url)),e=Uu(r,"..","bin","mcp-skill.mjs");return ju(e)?e:null}var Du=6e3*1e3,Rt=null,Mu=50,qu=50,Ju=40,Gu=31,Fu=200,Ku=31,Hu=50,ji=50,Pi=Object.freeze(["daily","month"]),Ci=Object.freeze(["zh","en","ja"]),zu=Object.freeze(["employee_id","employee_no"]),Wu=mr;async function Yu(){let{appId:r,appSecret:e,host:t}=await hr();if(Rt&&Rt.appId===r&&Rt.expiresAt>Date.now())return{token:Rt.token,host:t};let s=await(await w(`${t}/open-apis/auth/v3/tenant_access_token/internal`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({app_id:r,app_secret:e})},{kind:"api",what:"Lark tenant_access_token"})).json();if(s.code!==0)throw new Error(`Lark tenant_access_token failed: ${s.msg||s.code}`);return Rt={token:s.tenant_access_token,expiresAt:Date.now()+Du,appId:r},{token:s.tenant_access_token,host:t}}function Zu(r,e,t){let n=`Lark attendance API ${r} error ${e}: ${t||"(no message)"}`,s=r.includes("/attendance/v1/groups")?"attendance:rule:readonly (\u6253\u5361\u89C4\u5219/\u8003\u52E4\u7EC4 \u8BFB\u53D6)":r.includes("/contact/v3/users")?"contact:user.id:readonly (\u90AE\u7BB1 \u2192 \u7528\u6237 ID)":"attendance:task:readonly (\u6253\u5361\u7ED3\u679C/\u7EDF\u8BA1\u6570\u636E \u8BFB\u53D6)";return String(e)==="99991672"||/permission|scope|权限/i.test(String(t||""))?`${n}. The connected Lark app is missing the ${s} permission \u2014 add it in the Lark/Feishu developer console (\u5F00\u53D1\u8005\u540E\u53F0 > \u6743\u9650\u7BA1\u7406), then RE-PUBLISH a new app version so the scope takes effect.`:String(e)==="1220002"?`${n}. The tenant token was rejected \u2014 the Lark integration's app credentials look wrong; reconnect Lark in Integrations.`:String(e)==="1220001"?`${n}. Lark rejected the parameters \u2014 check the date range (yyyyMMdd, and within the documented span), stats_type, and that every user id matches the employeeType you passed.`:n}async function it(r,e,t){let{token:n,host:s}=await Yu(),i={method:r,headers:{Authorization:`Bearer ${n}`,"Content-Type":"application/json; charset=utf-8"}};r!=="GET"&&t!==void 0&&(i.body=JSON.stringify(t));let a=await(await w(`${s}${e}`,i,{kind:"api",what:`Lark Attendance ${r} ${e}`})).json();if(a.code!==0)throw new Error(Zu(e,a.code,a.msg));return a.data||{}}function Ui(r){if(r==null||r==="")return null;let e=String(r).trim().replace(/[-/.]/g,"");if(!/^\d{8}$/.test(e))return null;let t=Number(e.slice(0,4)),n=Number(e.slice(4,6)),s=Number(e.slice(6,8));if(t<1970||t>9999||n<1||n>12||s<1||s>31)return null;let i=new Date(Date.UTC(t,n-1,s));return i.getUTCFullYear()!==t||i.getUTCMonth()!==n-1||i.getUTCDate()!==s?null:Number(e)}function Vu(r,e){let t=n=>Date.UTC(Math.floor(n/1e4),Math.floor(n%1e4/100)-1,n%100);return Math.floor((t(e)-t(r))/864e5)+1}function gn(r,e){let t=Ui(r?.startDate??r?.start_date),n=Ui(r?.endDate??r?.end_date);if(!t||!n)return{error:"startDate and endDate are required, as yyyyMMdd (e.g. 20260401) or YYYY-MM-DD"};if(n<t)return{error:"endDate must not be earlier than startDate"};let s=Vu(t,n);return s>e?{error:`Lark caps this query at ${e} days; the requested range is ${s} days. Split it into consecutive windows and merge the results.`}:{startDate:t,endDate:n}}function Bi(r,e){let t=r?.userIds??r?.user_ids,n=(Array.isArray(t)?t:typeof t=="string"?[t]:[]).map(s=>String(s||"").trim()).filter(Boolean);return n.length?n.length>e?{error:`Lark accepts at most ${e} user ids per call; ${n.length} were passed. Split into batches.`}:{ids:n}:{error:"userIds is required \u2014 attendance data is personal, so this tool never returns a whole tenant. Get the ids from larkattendance_get_group (bind_user_ids) or larkattendance_resolve_users."}}function Et(r,e,t){let n=String(r||"").trim().toLowerCase();return e.includes(n)?n:t}function fr(r){return Et(r?.employeeType??r?.employee_type,zu,"employee_id")}function Xu(r){return{code:String(r?.code||""),title:String(r?.title||""),fields:(Array.isArray(r?.child_fields)?r.child_fields:[]).map(e=>({code:String(e?.code||""),title:String(e?.title||""),...e?.time_unit?{timeUnit:String(e.time_unit)}:{}}))}}function Qu(r,e){let n=(Array.isArray(r?.datas)?r.datas:[]).filter(s=>!e||e.has(String(s?.code||""))||e.has(String(s?.title||""))).map(s=>({code:String(s?.code||""),title:String(s?.title||""),value:s?.value===void 0||s?.value===null?"":String(s.value),...s?.duration_num?{durationNum:s.duration_num}:{},...Array.isArray(s?.features)&&s.features.length?{features:s.features}:{}}));return{userId:String(r?.user_id||""),name:String(r?.name||""),fields:n}}function em(r){return{userId:String(r?.user_id||""),name:String(r?.employee_name||""),day:r?.day??null,groupId:String(r?.group_id||""),shiftId:String(r?.shift_id||""),records:(Array.isArray(r?.records)?r.records:[]).map(e=>({checkInTime:e?.check_in_record?.check_time??null,checkInResult:String(e?.check_in_result||""),checkInResultSupplement:String(e?.check_in_result_supplement||""),checkInShiftTime:e?.check_in_shift_time??null,checkOutTime:e?.check_out_record?.check_time??null,checkOutResult:String(e?.check_out_result||""),checkOutResultSupplement:String(e?.check_out_result_supplement||""),checkOutShiftTime:e?.check_out_shift_time??null,...e?.task_shift_type===void 0?{}:{taskShiftType:e.task_shift_type}}))}}var Di={id:"lark-attendance",callsBackend:!0,serverName:"larkattendance",allowedTools:["mcp__larkattendance__*"],requiresIntegration:[I.LARK_DOCS,I.LARK],description:"Lark / Feishu attendance (\u8003\u52E4) \u2014 discover attendance groups and statistic fields, then read per-user work-hour / attendance statistics and clock-in records. Read-only.",envKeys:["PROJECT_API_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV",...Wu],promptFragment:"## Lark Attendance (\u8003\u52E4)\nRead-only access to this tenant's Lark/Feishu attendance data. NOTHING about the tenant's setup is known in advance \u2014 group names, what counts as \"\u5DE5\u65F6\", and the per-tenant \u81EA\u5B9A\u4E49\u5B57\u6BB5 all differ \u2014 so DISCOVER before you fetch:\n1. `larkattendance_list_groups` \u2192 the \u8003\u52E4\u7EC4 that exist.\n2. `larkattendance_get_group` \u2192 that group's member user ids (the scope for every data call).\n3. `larkattendance_list_stats_fields` \u2192 every statistic column available for the period, INCLUDING this tenant's \u81EA\u5B9A\u4E49\u5B57\u6BB5. Match the user's words (\"\u5DE5\u65F6\", \"\u51FA\u52E4\u5929\u6570\", \u2026) against the returned `title`s and pass the matching `code`s onward.\n4. `larkattendance_query_stats` (aggregated numbers) and/or `larkattendance_query_records` (raw clock-in/out).\n`larkattendance_query_stats` also needs an `operatorUserId` \u2014 the attendance admin the query runs AS. Lark rejects the call without one, and it selects the saved statistics view that decides which columns come back, so this skill refuses to invent it: if the prompt did not supply one, ASK for it rather than substituting a member id.\nIdentity mapping: attendance ids are the tenant `user_id` (employee_id), NOT the `open_id` the `lark` skill's `lark_lookup_user_by_email` returns \u2014 use `larkattendance_resolve_users` to turn emails into attendance ids. For chat/DM routing keep using the `lark` skill's tools.\nAttendance is personal HR data: every data tool requires an explicit `userIds` list, and there is no whole-tenant dump. Report the numbers as returned; do not invent a column the tenant does not have.\nThese tools return { ok:false, error } on failure \u2014 read the error, it names the missing permission or the range limit.",resolve(){let r=Bu();if(!r)return null;let e={};for(let t of this.envKeys)process.env[t]&&(e[t]=process.env[t]);return{type:"stdio",command:"node",args:[r,"../dist/larkAttendance.js","larkAttendanceSkill"],env:e,description:this.description}},async handleToolCall(r,e){try{switch(r){case"larkattendance_list_groups":{let t=Number(e?.pageSize??e?.page_size),n=Number.isFinite(t)&&t>0?Math.min(Math.floor(t),Mu):qu,s=new URLSearchParams({page_size:String(n)}),i=String(e?.pageToken??e?.page_token??"").trim();i&&s.set("page_token",i);let o=await it("GET",`/open-apis/attendance/v1/groups?${s.toString()}`),a=(Array.isArray(o?.group_list)?o.group_list:[]).map(c=>({groupId:String(c?.group_id||""),groupName:String(c?.group_name||"")}));return JSON.stringify({ok:!0,count:a.length,groups:a,hasMore:!!o?.has_more,...o?.has_more&&o?.page_token?{pageToken:String(o.page_token)}:{}})}case"larkattendance_get_group":{let t=String(e?.groupId??e?.group_id??"").trim();if(!t)return JSON.stringify({ok:!1,error:"groupId is required (from larkattendance_list_groups)"});let n=fr(e),s=new URLSearchParams({employee_type:n,dept_type:"open_id"}),i=await it("GET",`/open-apis/attendance/v1/groups/${encodeURIComponent(t)}?${s.toString()}`),o=a=>Array.isArray(a)?a.map(c=>String(c)):[];return JSON.stringify({ok:!0,groupId:String(i?.group_id||t),groupName:String(i?.group_name||""),employeeType:n,memberUserIds:o(i?.bind_user_ids),memberDeptIds:o(i?.bind_dept_ids),excludedUserIds:o(i?.except_user_ids),excludedDeptIds:o(i?.except_dept_ids),noPunchUserIds:o(i?.bind_default_user_ids),noPunchDeptIds:o(i?.bind_default_dept_ids),...Array.isArray(i?.group_leader_ids)?{leaderUserIds:o(i.group_leader_ids)}:{}})}case"larkattendance_list_stats_fields":{let t=gn(e,Ju);if(t.error)return JSON.stringify({ok:!1,error:t.error});let n=Et(e?.statsType??e?.stats_type,Pi,"month"),s=Et(e?.locale,Ci,"zh"),i=fr(e),a=(await it("POST",`/open-apis/attendance/v1/user_stats_fields/query?employee_type=${encodeURIComponent(i)}`,{locale:s,stats_type:n,start_date:t.startDate,end_date:t.endDate}))?.user_stats_field||{},c=(Array.isArray(a?.fields)?a.fields:[]).map(Xu);return JSON.stringify({ok:!0,statsType:n,locale:s,startDate:t.startDate,endDate:t.endDate,fieldGroupCount:c.length,fieldGroups:c})}case"larkattendance_query_stats":{let t=gn(e,Gu);if(t.error)return JSON.stringify({ok:!1,error:t.error});let n=Bi(e,Fu);if(n.error)return JSON.stringify({ok:!1,error:n.error});let s=Et(e?.statsType??e?.stats_type,Pi,"month"),i=Et(e?.locale,Ci,"zh"),o=fr(e),a=String(e?.operatorUserId??e?.user_id??"").trim();if(!a)return JSON.stringify({ok:!1,error:'operatorUserId is required by Lark (the API rejects this call with 1220001 "Need user_id"). It is the OPERATOR performing the query \u2014 a third id, not one of `userIds` and not `employeeType`. It must be a user whose saved attendance statistics view contains the columns you want (normally an attendance admin), because that view decides which columns Lark returns. This skill will NOT pick one for you: substituting an arbitrary member would silently return the wrong column set instead of failing. Ask the caller/prompt for the attendance admin\'s user id, or resolve their email with larkattendance_resolve_users.'});let c={locale:i,stats_type:s,start_date:t.startDate,end_date:t.endDate,user_ids:n.ids,user_id:a,need_history:e?.needHistory===!0,current_group_only:e?.currentGroupOnly===!0},l=Array.isArray(e?.fieldCodes)?e.fieldCodes:null,d=l?l.map(h=>String(h||"").trim()).filter(Boolean):[],p=d.length?new Set(d):null,u=await it("POST",`/open-apis/attendance/v1/user_stats_datas/query?employee_type=${encodeURIComponent(o)}`,c),m=(Array.isArray(u?.user_datas)?u.user_datas:[]).map(h=>Qu(h,p));return JSON.stringify({ok:!0,statsType:s,startDate:t.startDate,endDate:t.endDate,employeeType:o,userCount:m.length,users:m,invalidUserIds:Array.isArray(u?.invalid_user_list)?u.invalid_user_list.map(h=>String(h)):[]})}case"larkattendance_query_records":{let t=gn({startDate:e?.startDate??e?.checkDateFrom??e?.check_date_from,endDate:e?.endDate??e?.checkDateTo??e?.check_date_to},Ku);if(t.error)return JSON.stringify({ok:!1,error:t.error});let n=Bi(e,Hu);if(n.error)return JSON.stringify({ok:!1,error:n.error});let s=fr(e),i=new URLSearchParams({employee_type:s,ignore_invalid_users:"true"});e?.includeTerminatedUser===!0&&i.set("include_terminated_user","true");let o=await it("POST",`/open-apis/attendance/v1/user_tasks/query?${i.toString()}`,{user_ids:n.ids,check_date_from:t.startDate,check_date_to:t.endDate,need_overtime_result:e?.needOvertimeResult===!0}),a=(Array.isArray(o?.user_task_results)?o.user_task_results:[]).map(em);return JSON.stringify({ok:!0,startDate:t.startDate,endDate:t.endDate,employeeType:s,count:a.length,results:a,invalidUserIds:Array.isArray(o?.invalid_user_ids)?o.invalid_user_ids.map(c=>String(c)):[],unauthorizedUserIds:Array.isArray(o?.unauthorized_user_ids)?o.unauthorized_user_ids.map(c=>String(c)):[]})}case"larkattendance_resolve_users":{let t=e?.emails,n=(Array.isArray(t)?t:typeof t=="string"?[t]:[]).map(c=>String(c||"").trim()).filter(Boolean);if(!n.length)return JSON.stringify({ok:!1,error:"emails is required (a non-empty array of work email addresses)"});if(n.length>ji)return JSON.stringify({ok:!1,error:`At most ${ji} emails per call; ${n.length} were passed. Split into batches.`});let s=await it("POST","/open-apis/contact/v3/users/batch_get_id?user_id_type=user_id",{emails:n}),o=(Array.isArray(s?.user_list)?s.user_list:[]).filter(c=>c?.user_id).map(c=>({email:String(c?.email||""),userId:String(c.user_id)})),a=new Set(o.map(c=>c.email));return JSON.stringify({ok:!0,employeeType:"employee_id",users:o,notFound:n.filter(c=>!a.has(c))})}default:return JSON.stringify({ok:!1,error:`Unknown tool: ${r}`})}}catch(t){return JSON.stringify({ok:!1,error:t?.message||String(t)})}},tools:[{name:"larkattendance_list_groups",description:"DISCOVERY (start here). List this tenant's Lark/Feishu attendance groups (\u8003\u52E4\u7EC4) \u2014 their names are tenant-specific, so never assume one. Returns { ok, count, groups:[{ groupId, groupName }], hasMore, pageToken }. When hasMore is true, call again with pageToken to continue. Needs the attendance:rule:readonly permission on the connected Lark app.",input_schema:{type:"object",properties:{pageSize:{type:"number",description:"Groups per page, 1-50 (default 50)."},pageToken:{type:"string",description:"Cursor from a previous call's pageToken \u2014 omit for the first page."}}}},{name:"larkattendance_get_group",description:"DISCOVERY. Read one attendance group and, crucially, ITS MEMBERS \u2014 memberUserIds is the scope you pass as userIds to larkattendance_query_stats / larkattendance_query_records (a real group can hold hundreds, so expect to batch). Returns { ok, groupId, groupName, employeeType, memberUserIds, memberDeptIds, excludedUserIds, excludedDeptIds, noPunchUserIds, noPunchDeptIds }. THREE different exclusion lists, do not conflate them: excludedUserIds are removed from the group; noPunchUserIds are in the group but not required to clock in (they still have statistics). Members bound by DEPARTMENT appear in memberDeptIds, not memberUserIds \u2014 resolve those people another way (e.g. larkattendance_resolve_users from emails you already have). Lark does not reliably return the group leaders here, so leaderUserIds may be absent \u2014 never source the query operator id from this tool.",input_schema:{type:"object",properties:{groupId:{type:"string",description:"Attendance group id from larkattendance_list_groups."},employeeType:{type:"string",description:"Which id form to return members in: 'employee_id' (tenant user_id, the default and what every other tool here expects) or 'employee_no' (\u5DE5\u53F7)."}},required:["groupId"]}},{name:"larkattendance_list_stats_fields",description:'DISCOVERY \u2014 call this BEFORE larkattendance_query_stats. Lists every statistic column this tenant has for the period, grouped (e.g. \u57FA\u672C\u4FE1\u606F / \u51FA\u52E4\u7EDF\u8BA1 / \u5F02\u5E38\u7EDF\u8BA1 / \u6BCF\u65E5\u7EDF\u8BA1 / \u81EA\u5B9A\u4E49\u5B57\u6BB5). The \u81EA\u5B9A\u4E49\u5B57\u6BB5 group is tenant-defined and exists nowhere else, so this is the only way to learn its codes. Match the user\'s wording ("\u5DE5\u65F6", "\u51FA\u52E4\u5929\u6570", "\u8FDF\u5230\u6B21\u6570") against the returned `title`s and pass the matching `code`s as fieldCodes to larkattendance_query_stats. Returns { ok, statsType, locale, startDate, endDate, fieldGroups:[{ code, title, fields:[{ code, title, timeUnit }] }] }. Range is capped at 40 days.',input_schema:{type:"object",properties:{startDate:{type:"string",description:"Period start as yyyyMMdd (20260401) or YYYY-MM-DD."},endDate:{type:"string",description:"Period end, same format. At most 40 days after startDate."},statsType:{type:"string",description:"'month' (default) for period totals, or 'daily' for per-day columns."},locale:{type:"string",description:"Field-title language: 'zh' (default), 'en', or 'ja'."},employeeType:{type:"string",description:"'employee_id' (default) or 'employee_no'."}},required:["startDate","endDate"]}},{name:"larkattendance_query_stats",description:'The numbers: per-user attendance statistics (work hours, attendance days, exceptions, custom fields) for a period. Needs THREE separate things: userIds (whose data you want, max 200), operatorUserId (WHO IS ASKING \u2014 required by Lark, see below), and the date range (max 31 days). Attendance is personal data and there is no whole-tenant read; get ids from larkattendance_get_group or larkattendance_resolve_users. Returns { ok, statsType, startDate, endDate, users:[{ userId, name, fields:[{ code, title, value, durationNum? }] }], invalidUserIds }. Values are STRINGS exactly as Lark returns them \u2014 report them as-is, do not re-derive. EXPECT MANY COLUMNS: a one-month query returns roughly 40+ cells per person, because Lark mixes period totals (e.g. an "actual attendance hours" column, late/early counts) with identity columns (department, employee number) AND one column PER DAY of the range \u2014 so match on `title` rather than assuming a short list, and pass fieldCodes to trim. The exact set of columns follows the SAVED STATISTICS VIEW of the operatorUserId (\u8003\u52E4 > \u7EDF\u8BA1\u8BBE\u7F6E), so if a field that larkattendance_list_stats_fields advertises is missing from the result, it is switched off in that operator\'s view and an admin must enable it \u2014 or you are querying as the wrong operator.',input_schema:{type:"object",properties:{userIds:{type:"array",items:{type:"string"},description:"The people to report on, in the employeeType id form. Max 200. Required."},startDate:{type:"string",description:"Period start as yyyyMMdd (20260401) or YYYY-MM-DD."},endDate:{type:"string",description:"Period end, same format. At most 31 days after startDate."},statsType:{type:"string",description:"'month' (default) for period totals, or 'daily' for per-day rows."},fieldCodes:{type:"array",items:{type:"string"},description:"Optional filter \u2014 only these field codes/titles are returned. Omit to get every column."},locale:{type:"string",description:"Field-title language: 'zh' (default), 'en', or 'ja'."},employeeType:{type:"string",description:"'employee_id' (default) or 'employee_no' \u2014 must match the form of userIds."},operatorUserId:{type:"string",description:'REQUIRED. The user id of the OPERATOR performing the query \u2014 a third id, distinct from `userIds` (whose data you want) and from `employeeType` (the id form). Lark rejects the call without it (1220001 "Need user_id"). It should be an attendance admin whose saved statistics view contains the columns you need, because that view decides which columns come back. This skill will not pick one for you \u2014 get it from the caller/prompt, or from an email via larkattendance_resolve_users.'},needHistory:{type:"boolean",description:"Include transferred/departed staff (default false)."},currentGroupOnly:{type:"boolean",description:"Restrict to the user's current attendance group (default false)."}},required:["userIds","operatorUserId","startDate","endDate"]}},{name:"larkattendance_query_records",description:"The raw clock-in/out results (\u6253\u5361\u7ED3\u679C) per person per day \u2014 use when the aggregated statistics are not enough (e.g. to show which days were late, or the actual punch times). REQUIRES an explicit userIds list (max 50); range capped at 31 days. Returns { ok, results:[{ userId, name, day, groupId, shiftId, records:[{ checkInTime, checkInResult, checkOutTime, checkOutResult, checkInShiftTime, checkOutShiftTime }] }], invalidUserIds, unauthorizedUserIds }. Unknown ids are reported in invalidUserIds rather than failing the batch. Result values are Lark's own enums (Normal / Early / Late / Lack / \u2026).",input_schema:{type:"object",properties:{userIds:{type:"array",items:{type:"string"},description:"The people to read, in the employeeType id form. Max 50. Required."},startDate:{type:"string",description:"First day as yyyyMMdd (20260401) or YYYY-MM-DD."},endDate:{type:"string",description:"Last day, same format. At most 31 days after startDate."},employeeType:{type:"string",description:"'employee_id' (default) or 'employee_no' \u2014 must match the form of userIds."},needOvertimeResult:{type:"boolean",description:"Also return overtime shift records (default false)."},includeTerminatedUser:{type:"boolean",description:"Include departed employees (default false)."}},required:["userIds","startDate","endDate"]}},{name:"larkattendance_resolve_users",description:"Map work EMAIL addresses to the attendance user ids (tenant user_id = the attendance API's employee_id). Use this to line a report's people up with attendance rows. NOTE this is deliberately different from the `lark` skill's lark_lookup_user_by_email, which returns an open_id for chat/DM \u2014 the attendance endpoints reject open_ids. Max 50 emails per call. Returns { ok, employeeType:\"employee_id\", users:[{ email, userId }], notFound:[email] }. Needs the contact:user.id:readonly permission.",input_schema:{type:"object",properties:{emails:{type:"array",items:{type:"string"},description:"Work email addresses to resolve. Max 50."}},required:["emails"]}}]};var Mi={id:"chat_notify",description:"Chat notification meta-skill \u2014 routes to whichever messaging integration (Slack OR Lark) the user has configured for this project.",envKeys:[...B.envKeys||[],...H.envKeys||[]],get serverName(){if(process.env.SLACK_CHANNEL)return B.serverName;if(process.env.LARK_RECEIVE_ID)return H.serverName;if(process.env.SLACK_BOT_TOKEN)return B.serverName},get allowedTools(){return process.env.SLACK_CHANNEL?B.allowedTools||[]:process.env.LARK_RECEIVE_ID?H.allowedTools||[]:process.env.SLACK_BOT_TOKEN?B.allowedTools||[]:[]},promptFragment:`## Chat notifications (Slack OR Lark \u2014 at least one connected)
428
+ `)){let s=n.replace(/\s+$/,"");if(!s.trim())continue;let i=/^(#{1,3})\s+(.*)$/.exec(s),o=/^\s*[-*]\s+(.*)$/.exec(s),a=/^\s*\d+[.)]\s+(.*)$/.exec(s),c,l,d;if(i){let p=i[1].length;c=`heading${p}`,l=2+p,d=i[2]}else o?(c="bullet",l=12,d=o[1]):a?(c="ordered",l=13,d=a[1]):(c="text",l=2,d=s);t.push({block_type:l,[c]:{elements:[{text_run:{content:d}}],style:{}}})}return t}function Ot(r){let e=typeof r?.markdown=="string"?r.markdown:null,t=typeof r?.text=="string"?r.text:null;return e??t}async function un(r,e){let t;for(let n=0;n<e.length;n+=Ii){let s=e.slice(n,n+Ii);t=(await D("POST",`/open-apis/docx/v1/documents/${r}/blocks/${r}/children?document_revision_id=-1`,{children:s})).host}return t}function Su(r){let e=typeof r=="string"?r.trim():"";if(!e)throw new Error("imagePath is required");if(!Ei(e)||!nu(e).isFile())throw new Error(`imagePath not found (or not a file): ${e}`);if(!/\.(png|jpe?g)$/i.test(e))throw new Error("imagePath must be a .png or .jpg/.jpeg file");let t=su(e);if(t.length>gu)throw new Error(`image is ${(t.length/(1024*1024)).toFixed(1)}MB \u2014 max 10MB`);return t}async function vu({fileName:r,parentNode:e,bytes:t}){let{token:n,host:s}=await yn(),i=new FormData;i.set("file_name",r),i.set("parent_type","docx_image"),i.set("parent_node",e),i.set("size",String(t.length)),i.set("file",new Blob([t]),r);let a=await(await w(`${s}/open-apis/drive/v1/medias/upload_all`,{method:"POST",headers:{Authorization:`Bearer ${n}`},body:i},{kind:"transfer",what:"Lark Docx image upload"})).json();if(a.code!==0)throw new Error(`Lark media upload_all error: ${a.msg||a.code}`);let c=a.data?.file_token;if(!c)throw new Error("Lark media upload_all returned no file_token");return c}var Iu="docx",Tu=50;function Ti(r){return[{type:"text_run",text_run:{text:String(r??"")}}]}function Nu(r){return Array.isArray(r)?r.map(e=>e?.text_run?.text??e?.docs_link?.url??(e?.person?`@${e.person.user_id||""}`:"")).join(""):""}function Ou(r){let e=Array.isArray(r?.reply_list?.replies)?r.reply_list.replies:[];return{commentId:r?.comment_id||"",resolved:!!r?.is_solved,replies:e.map(t=>({replyId:t?.reply_id||"",author:t?.user_id||"",text:Nu(t?.content?.elements),createTime:t?.create_time||""}))}}function mn(r){return typeof r?.fileType=="string"&&r.fileType.trim()?r.fileType.trim():Iu}function Ni(r,e,t,n){let i=String(r||"").includes("feishu")?"https://feishu.cn":"https://www.larksuite.com",o=new URLSearchParams;t&&o.set("table",t),n&&o.set("view",n);let a=o.toString();return`${i}/base/${e}${a?`?${a}`:""}`}async function Oi(r){let e=typeof r=="string"?ue(r):r;if(!e)throw new Error("A valid Lark Base id or URL is required");let t={tableId:e.tableId||null,viewId:e.viewId||null};if(e.type!=="wiki")return{appToken:e.token,...t};let{data:n}=await D("GET",`/open-apis/wiki/v2/spaces/get_node?token=${encodeURIComponent(e.token)}`),s=n?.node||{};if(s.obj_type!=="bitable"||!s.obj_token)throw s.obj_type==="docx"?new Error("This Lark wiki link is a document (obj_type=docx), not a Base / \u591A\u7EF4\u8868\u683C \u2014 read it with larkdoc_get"):new Error(`Wiki node is not a Base / \u591A\u7EF4\u8868\u683C (obj_type=${s.obj_type||"unknown"})`);return{appToken:s.obj_token,...t}}function At(r){if(r==null)return"";if(typeof r=="string")return r;if(typeof r=="number"||typeof r=="boolean")return String(r);if(Array.isArray(r))return r.map(At).filter(e=>e!=="").join(", ");if(typeof r=="object"){let e=r;return typeof e.text=="string"&&e.text?e.text:typeof e.name=="string"&&e.name?e.name:typeof e.en_name=="string"&&e.en_name?e.en_name:typeof e.full_address=="string"&&e.full_address?e.full_address:typeof e.link=="string"&&e.link?e.link:e.value!==void 0?At(e.value):JSON.stringify(e)}return String(r)}function Au(r,e){if(yu.has(Number(e))){let t=Number(Array.isArray(r)?r[0]:r);if(Number.isFinite(t)&&t>0)return new Date(t).toISOString()}return At(r)}async function Li(r){let e=[],t=null;for(let n=0;n<fn;n++){let s=new URLSearchParams({page_size:String(xi)});t&&s.set("page_token",t);let{data:i}=await D("GET",`/open-apis/bitable/v1/apps/${encodeURIComponent(r)}/tables?${s.toString()}`);for(let o of Array.isArray(i?.items)?i.items:[])e.push({tableId:String(o?.table_id||""),name:String(o?.name||"")});if(!i?.has_more||!i?.page_token)break;t=i.page_token}return e}async function Ru(r,e){let t=[],n=null;for(let s=0;s<fn;s++){let i=new URLSearchParams({page_size:String(xi)});n&&i.set("page_token",n);let{data:o}=await D("GET",`/open-apis/bitable/v1/apps/${encodeURIComponent(r)}/tables/${encodeURIComponent(e)}/fields?${i.toString()}`);for(let a of Array.isArray(o?.items)?o.items:[])t.push({name:String(a?.field_name||""),type:Number(a?.type)||0});if(!o?.has_more||!o?.page_token)break;n=o.page_token}return t}async function Eu({appToken:r,tableId:e}){if(e)return e;let t=await Li(r);if(t.length===1)return t[0].tableId;if(!t.length)throw new Error("This Lark Base has no tables");let n=t.map(s=>`${s.name} (${s.tableId})`).join("; ");throw new Error(`tableId is required \u2014 this Base has ${t.length} tables: ${n}`)}var Ai=new Set(["is","isNot","contains","doesNotContain","isEmpty","isNotEmpty","isGreater","isGreaterEqual","isLess","isLessEqual"]),Ri=new Set(["isEmpty","isNotEmpty"]);function hn(r,e,t){let n=new Set(r.map(o=>o.name)),s=e.filter(o=>!n.has(o));if(!s.length)return;let i=s.flatMap(o=>r.filter(a=>a.name.trim()===String(o).trim()).map(a=>JSON.stringify(a.name)));throw new Error(`unknown column(s) in ${t}: ${s.map(o=>JSON.stringify(o)).join(", ")}`+(i.length?` \u2014 did you mean ${i.join(", ")}? (names can carry spaces)`:"")+`. This table's columns: ${r.map(o=>JSON.stringify(o.name)).join(", ")}`)}function xu({columns:r,viewId:e,fieldNames:t,filter:n,sort:s,includeRowMeta:i}){let o={};if(e&&(o.view_id=e),t?.length&&(hn(r,t,"fieldNames"),o.field_names=t),n){let a=Array.isArray(n.conditions)?n.conditions:[];if(!a.length)throw new Error("filter.conditions must be a non-empty array");hn(r,a.map(c=>c?.fieldName??c?.field_name),"filter"),o.filter={conjunction:n.conjunction==="or"?"or":"and",conditions:a.map(c=>{let l=String(c?.operator||"is");if(!Ai.has(l))throw new Error(`unknown filter operator ${JSON.stringify(l)} \u2014 Lark accepts: ${[...Ai].join(", ")}`);let d=c?.value,p=d==null?[]:(Array.isArray(d)?d:[d]).map(u=>String(u));if(!Ri.has(l)&&!p.length)throw new Error(`filter operator ${JSON.stringify(l)} requires a value`);return{field_name:String(c?.fieldName??c?.field_name),operator:l,...Ri.has(l)?{}:{value:p}}})}}return s?.length&&(hn(r,s.map(a=>a?.fieldName??a?.field_name),"sort"),o.sort=s.map(a=>({field_name:String(a?.fieldName??a?.field_name),desc:!!a?.desc}))),i&&(o.automatic_fields=!0),o}function Lu(r){let e={},t=a=>{let c=Number(a);return!Number.isFinite(c)||c<=0?"":new Date(c<1e11?c*1e3:c).toISOString()},n=t(r?.created_time),s=t(r?.last_modified_time),i=At(r?.created_by),o=At(r?.last_modified_by);return n&&(e.createdTime=n),i&&(e.createdBy=i),s&&(e.lastModifiedTime=s),o&&(e.lastModifiedBy=o),e}async function $u({appToken:r,tableId:e,viewId:t,fieldNames:n,filter:s,sort:i,includeRowMeta:o,maxRecords:a,pageToken:c}){let l=await Ru(r,e),d=new Map(l.map(b=>[b.name,b.type])),p=xu({columns:l,viewId:t,fieldNames:n,filter:s,sort:i,includeRowMeta:o}),u=[],m=0,h=c||null,f=!1,_=0,y=!1;for(let b=0;b<fn;b++){let g=new URLSearchParams({page_size:String(Math.min(uu,a))});h&&g.set("page_token",h);let{data:k}=await D("POST",`/open-apis/bitable/v1/apps/${encodeURIComponent(r)}/tables/${encodeURIComponent(e)}/records/search?${g.toString()}`,p);_=Number(k?.total)||_,f=!!k?.has_more,h=k?.page_token||null;for(let S of Array.isArray(k?.items)?k.items:[]){let L={};for(let[A,Z]of Object.entries(S?.fields||{}))L[A]=Au(Z,d.get(A));let U={recordId:String(S?.record_id||""),...o?Lu(S):{},fields:L},R=JSON.stringify(U).length;if(u.length&&m+R>fu){y=!0;break}if(m+=R,u.push(U),u.length>=a)break}if(y||u.length>=a||!f||!h)break}return{columns:l,records:u,total:_,hasMore:f||y,nextPageToken:h||"",truncated:y,...y&&!h?{note:"stopped at the character budget, and these rows have no resume token \u2014 re-read with fieldNames set to the few columns you need (see columns[])."}:{}}}var $i={id:"lark-docs",callsBackend:!0,serverName:"larkdocs",allowedTools:["mcp__larkdocs__*"],requiresIntegration:[I.LARK_DOCS,I.LARK],description:"Lark / Feishu Docs \u2014 read, create, append, and insert images into Lark documents (docx), and read Lark Bases (\u591A\u7EF4\u8868\u683C / bitable).",envKeys:["PROJECT_API_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV",..._u],promptFragment:'## Lark Docs\nYou can read, create, and append Lark/Feishu documents (docx), read/post/reply to their comments, and READ Lark Bases (\u591A\u7EF4\u8868\u683C / bitable). This runs on the connected Lark Docs app (the chat app is used when no separate docs app is connected). Each `larkdoc_*` / `larkwiki_*` / `larkbitable_*` tool documents its own params and return shape in its tool description \u2014 they are not restated here.\nA Lark link is not always a document: a `/base/\u2026` link, or a `/wiki/\u2026` link carrying `?table=tbl\u2026`, is a Base \u2014 read it with `larkbitable_read_records`, NOT `larkdoc_get` (which refuses it, since a Base has no document body). Base reads need the `bitable:app:readonly` scope on the connected app.\nA Base question about CHANGE \u2014 what moved since yesterday, who edited a row, which rows are stale \u2014 is answered by `larkbitable_read_records` with `includeRowMeta:true` (per-row created/last-modified time and person) and a `filter`/`sort` on that timestamp, NOT by reading the whole table twice and diffing. Lark exposes no per-cell edit history to any API, so a field\'s PREVIOUS value cannot be fetched \u2014 say so rather than inferring it from snapshots.\n`createdTime` and `lastModifiedTime` are DIFFERENT QUESTIONS and must never stand in for one another: a row edited yesterday was not added yesterday. Rows APPEARING in a read you did not see before is not evidence they were created \u2014 a shorter earlier read (see `truncated`) explains it just as well. Answer "what was added" from `createdTime` ONLY, and if you did not ask for row metadata, say the read cannot tell rather than reasoning from row counts. A Base read is a SNAPSHOT OF NOW: every field holds only its CURRENT value, and `createdTime` says when the ROW appeared, never when it acquired that value. So a question about a PAST STATE of a mutable field has no answerable form here \u2014 only a PROXY (rows whose value is X today and whose `createdTime` precedes the date) that is wrong in BOTH directions: rows given the value after that date inflate it, and rows that held it then but were since changed away or deleted are absent from the read entirely and deflate it. Offer the proxy labelled as a proxy with both directions stated \u2014 never as the answer.\nThese tools return { ok:false, error } on failure \u2014 treat an unavailable Lark connection as "cannot read/deliver to Lark Docs" and continue rather than blocking the task.',resolve(){let r=lu();if(!r)return null;let e={};for(let t of this.envKeys)process.env[t]&&(e[t]=process.env[t]);return{type:"stdio",command:"node",args:[r,"../dist/larkDocs.js","larkDocsSkill"],env:e,description:this.description}},async handleToolCall(r,e){try{switch(r){case"larkdoc_get":{let t=e?.documentId||e?.url||e?.id,n=ue(t);if(!n)return JSON.stringify({ok:!1,error:"A valid Lark doc id or URL is required"});let s=await st(n),i="";try{i=(await D("GET",`/open-apis/docx/v1/documents/${s}`)).data?.document?.title||""}catch{}let{data:o,host:a}=await D("GET",`/open-apis/docx/v1/documents/${s}/raw_content?lang=0`),c=String(o?.content||""),l=!1;return c.length>vi&&(c=c.slice(0,vi),l=!0),JSON.stringify({ok:!0,documentId:s,title:i,url:Nt(a,s),text:c,...l?{truncated:!0}:{}})}case"larkdoc_create":{let t=typeof e?.title=="string"&&e.title.trim()?e.title.trim():null;if(!t)return JSON.stringify({ok:!1,error:"title is required"});let n=typeof e?.wikiSpaceId=="string"&&e.wikiSpaceId.trim()?e.wikiSpaceId.trim():null;if(n){let c=typeof e?.parentNodeToken=="string"&&e.parentNodeToken.trim()?e.parentNodeToken.trim():null,{data:l,host:d}=await D("POST",`/open-apis/wiki/v2/spaces/${encodeURIComponent(n)}/nodes`,{obj_type:"docx",node_type:"origin",title:t,...c?{parent_node_token:c}:{}}),p=l?.node||{},u=p.obj_token;if(!u)return JSON.stringify({ok:!1,error:"Lark wiki node create returned no obj_token"});let m=Ot(e);if(m&&m.trim()){let h=pn(m);h.length&&await un(u,h)}return JSON.stringify({ok:!0,documentId:u,title:t,wikiSpaceId:n,wikiNodeToken:p.node_token||"",url:p.node_token?ku(d,p.node_token):Nt(d,u)})}let{data:s,host:i}=await D("POST","/open-apis/docx/v1/documents",{title:t,...e?.folderToken?{folder_token:String(e.folderToken)}:{}}),o=s?.document?.document_id;if(!o)return JSON.stringify({ok:!1,error:"Lark Docs create returned no document_id"});let a=Ot(e);if(a&&a.trim()){let c=pn(a);c.length&&await un(o,c)}return JSON.stringify({ok:!0,documentId:o,title:t,url:Nt(i,o)})}case"larkdoc_append":{let t=e?.documentId||e?.url||e?.id,n=ue(t);if(!n)return JSON.stringify({ok:!1,error:"A valid Lark doc id or URL is required"});let s=Ot(e);if(!s||!s.trim())return JSON.stringify({ok:!1,error:"markdown or text content is required"});let i=await st(n),o=pn(s);if(!o.length)return JSON.stringify({ok:!1,error:"no non-empty content to append"});let a=await un(i,o);return JSON.stringify({ok:!0,documentId:i,url:Nt(a,i)})}case"larkdoc_insert_image":{let t=e?.documentId||e?.url||e?.id,n=ue(t);if(!n)return JSON.stringify({ok:!1,error:"A valid Lark doc id or URL is required"});if(!e?.imagePath||typeof e.imagePath!="string"||!e.imagePath.trim())return JSON.stringify({ok:!1,error:"imagePath is required"});let s=await st(n),i=e.imagePath.trim(),o=Su(i),a=ou(i),l=(await D("POST",`/open-apis/docx/v1/documents/${s}/blocks/${s}/children?document_revision_id=-1`,{children:[{block_type:27,image:{}}]})).data?.children?.[0]?.block_id;if(!l)return JSON.stringify({ok:!1,error:"Lark image block create returned no block_id"});let d=await vu({fileName:a,parentNode:l,bytes:o}),p={token:d},u=Number(e?.width),m=Number(e?.height);Number.isFinite(u)&&u>0&&(p.width=Math.round(u)),Number.isFinite(m)&&m>0&&(p.height=Math.round(m));let{host:h}=await D("PATCH",`/open-apis/docx/v1/documents/${s}/blocks/${l}?document_revision_id=-1`,{replace_image:p});return JSON.stringify({ok:!0,documentId:s,blockId:l,fileToken:d,url:Nt(h,s)})}case"larkwiki_list_spaces":{let t=[],n=null;for(let s=0;s<pu;s++){let i=new URLSearchParams({page_size:String(du)});n&&i.set("page_token",n);let{data:o}=await D("GET",`/open-apis/wiki/v2/spaces?${i.toString()}`),a=Array.isArray(o?.items)?o.items:[];for(let c of a)t.push({spaceId:String(c?.space_id||""),name:String(c?.name||"")});if(!o?.has_more||!o?.page_token)break;n=o.page_token}return JSON.stringify({ok:!0,count:t.length,spaces:t})}case"larkbitable_list_tables":{let t=e?.baseId||e?.url||e?.documentId||e?.id,n=ue(t);if(!n)return JSON.stringify({ok:!1,error:"A valid Lark Base id or URL is required"});let{appToken:s,tableId:i,viewId:o}=await Oi(n),a=await Li(s),c="",l="";try{let d=await D("GET",`/open-apis/bitable/v1/apps/${encodeURIComponent(s)}`);c=d.data?.app?.name||"",l=d.host}catch{}return JSON.stringify({ok:!0,appToken:s,name:c,...l?{url:Ni(l,s,i,o)}:{},...i?{linkedTableId:i}:{},...o?{linkedViewId:o}:{},count:a.length,tables:a})}case"larkbitable_read_records":{let t=e?.baseId||e?.url||e?.documentId||e?.id,n=ue(t);if(!n)return JSON.stringify({ok:!1,error:"A valid Lark Base id or URL is required"});let s=await Oi(n),i=s.appToken,o=await Eu({appToken:i,tableId:typeof e?.tableId=="string"&&e.tableId.trim()?e.tableId.trim():s.tableId}),a=typeof e?.viewId=="string"&&e.viewId.trim()?e.viewId.trim():s.viewId,c=Array.isArray(e?.fieldNames)?e.fieldNames.map(b=>String(b)).filter(Boolean):null,l=Number(e?.maxRecords),d=Number.isFinite(l)&&l>0?Math.min(Math.round(l),hu):mu,p=typeof e?.pageToken=="string"&&e.pageToken.trim()?e.pageToken.trim():null,u=e?.filter&&typeof e.filter=="object"?e.filter:null,m=Array.isArray(e?.sort)&&e.sort.length?e.sort:null,h=!!e?.includeRowMeta,f=await $u({appToken:i,tableId:o,viewId:a,fieldNames:c,filter:u,sort:m,includeRowMeta:h,maxRecords:d,pageToken:p}),{host:_}=await yn(),y="";try{y=(await D("GET",`/open-apis/bitable/v1/apps/${encodeURIComponent(i)}`)).data?.app?.name||""}catch{}return JSON.stringify({ok:!0,appToken:i,name:y,tableId:o,...a?{viewId:a}:{},url:Ni(_,i,o,a),total:f.total,...u?{filtered:!0}:{},count:f.records.length,columns:f.columns,records:f.records,hasMore:f.hasMore,...f.nextPageToken?{nextPageToken:f.nextPageToken}:{},...f.truncated?{truncated:!0}:{},...f.note?{note:f.note}:{}})}case"larkdoc_list_comments":{let t=e?.documentId||e?.url||e?.id||e?.fileToken,n=ue(t);if(!n)return JSON.stringify({ok:!1,error:"A valid Lark doc id or URL is required"});let s=await st(n),i=mn(e),o=new URLSearchParams({file_type:i,page_size:String(Tu)}),{data:a}=await D("GET",`/open-apis/drive/v1/files/${s}/comments?${o.toString()}`),l=(Array.isArray(a?.items)?a.items:[]).map(Ou);return JSON.stringify({ok:!0,documentId:s,count:l.length,comments:l})}case"larkdoc_add_comment":{let t=e?.documentId||e?.url||e?.id||e?.fileToken,n=ue(t);if(!n)return JSON.stringify({ok:!1,error:"A valid Lark doc id or URL is required"});let s=Ot({text:e?.text??e?.body});if(!s||!s.trim())return JSON.stringify({ok:!1,error:"text is required"});let i=await st(n),o=mn(e),{data:a}=await D("POST",`/open-apis/drive/v1/files/${i}/comments?file_type=${encodeURIComponent(o)}`,{reply_list:{replies:[{content:{elements:Ti(s)}}]}});return JSON.stringify({ok:!0,documentId:i,commentId:a?.comment_id||""})}case"larkdoc_reply_comment":{let t=e?.documentId||e?.url||e?.id||e?.fileToken,n=ue(t);if(!n)return JSON.stringify({ok:!1,error:"A valid Lark doc id or URL is required"});let s=e?.commentId||e?.comment_id;if(!s)return JSON.stringify({ok:!1,error:"commentId is required"});let i=Ot({text:e?.text??e?.body});if(!i||!i.trim())return JSON.stringify({ok:!1,error:"text is required"});let o=await st(n),a=mn(e),{data:c}=await D("POST",`/open-apis/drive/v1/files/${o}/comments/${encodeURIComponent(s)}/replies?file_type=${encodeURIComponent(a)}`,{content:{elements:Ti(i)}});return JSON.stringify({ok:!0,documentId:o,commentId:String(s),replyId:c?.reply_id||""})}default:return JSON.stringify({ok:!1,error:`Unknown tool: ${r}`})}}catch(t){return JSON.stringify({ok:!1,error:t.message})}},tools:[{name:"larkdoc_get",description:"Read a Lark/Feishu document (docx) as plain text, for use as reference context. Accepts a raw doc id OR a full Lark doc URL (a /docx/ or /wiki/ link \u2014 wiki links are resolved to their backing docx). Returns { ok, documentId, title, url, text }. Text is truncated to ~20k chars.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Lark doc id (docx token) OR a full Lark/Feishu doc URL (/docx/\u2026 or /wiki/\u2026)."}},required:["documentId"]}},{name:"larkdoc_create",description:"Create a new Lark/Feishu document (docx) with a title and optional content (markdown: #/##/### headings, - bullets, 1. ordered; or plain text). Pass wikiSpaceId (from larkwiki_list_spaces) to create the doc INSIDE a wiki space instead of as a standalone doc \u2014 optionally nested under parentNodeToken. Returns { ok, documentId, url } (plus wikiNodeToken when created in a wiki) \u2014 share the url with the user.",input_schema:{type:"object",properties:{title:{type:"string",description:"Document title."},markdown:{type:"string",description:"Document body as markdown (preferred)."},text:{type:"string",description:"Document body as plain text (used when markdown is absent)."},folderToken:{type:"string",description:"Optional Lark drive folder token to create the doc in. Absent = the app root. Ignored when wikiSpaceId is set."},wikiSpaceId:{type:"string",description:"Optional wiki space id (from larkwiki_list_spaces) \u2014 create the doc as a node inside this wiki space."},parentNodeToken:{type:"string",description:"Optional wiki node token to nest the new doc under (only with wikiSpaceId). Absent = the space root."}},required:["title"]}},{name:"larkwiki_list_spaces",description:"List the Lark/Feishu wiki spaces visible to the connected app. Returns { ok, count, spaces:[{ spaceId, name }] }. Use to discover the wikiSpaceId for larkdoc_create.",input_schema:{type:"object",properties:{}}},{name:"larkdoc_append",description:"Append markdown/text content to the END of an existing Lark/Feishu document (docx). Accepts a documentId or a full Lark doc URL. Returns { ok, documentId, url }.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Lark doc id (docx token) OR a full Lark/Feishu doc URL."},markdown:{type:"string",description:"Content to append, as markdown (preferred)."},text:{type:"string",description:"Content to append, as plain text (used when markdown is absent)."}},required:["documentId"]}},{name:"larkdoc_insert_image",description:"Append a LOCAL image file (png/jpg, max 10MB) to the END of an existing Lark/Feishu document (docx). Accepts a documentId or full doc URL plus a local file path. Optional width/height in pixels. Returns { ok, documentId, blockId, fileToken, url }.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Lark doc id (docx token) OR a full Lark/Feishu doc URL."},imagePath:{type:"string",description:"Local filesystem path to a .png or .jpg/.jpeg image (max 10MB)."},width:{type:"number",description:"Optional display width in pixels."},height:{type:"number",description:"Optional display height in pixels."}},required:["documentId","imagePath"]}},{name:"larkbitable_list_tables",description:"List the tables in a Lark/Feishu Base (\u591A\u7EF4\u8868\u683C / bitable). Accepts a Base URL (a /base/\u2026 link, or a /wiki/\u2026 link whose node IS a Base) or a raw app token. Returns { ok, appToken, name, tables:[{ tableId, name }] } plus linkedTableId/linkedViewId when the URL pinned them. A Base is NOT a document \u2014 larkdoc_get cannot read one; use this and larkbitable_read_records instead.",input_schema:{type:"object",properties:{baseId:{type:"string",description:"Lark Base app token OR a full Base/wiki URL (/base/\u2026 or /wiki/\u2026)."}},required:["baseId"]}},{name:"larkbitable_read_records",description:"Read the rows of one table in a Lark/Feishu Base (\u591A\u7EF4\u8868\u683C / bitable) as flat text. Accepts a Base URL (/base/\u2026 or a /wiki/\u2026 link fronting a Base) or app token; the table and view are taken from the URL's table=/view= when present, and tableId may be omitted when the Base has exactly one table. Optionally restrict to viewId or fieldNames. Returns { ok, appToken, name, tableId, total, count, columns:[{name,type}], records:[{ recordId, fields:{ <column>: <text> } }], hasMore, nextPageToken }. Every cell is a STRING (dates as ISO). Defaults to 200 records; pass maxRecords (max 1000) and pageToken to page through a bigger table. A WIDE table (dozens of columns) hits a character budget before the row cap: the reply then carries truncated:true, hasMore:true and a note, and the fix is fieldNames, not paging. Column names are matched EXACTLY and real ones can carry a leading space or a ' (1)' suffix \u2014 an unknown name is refused with this table's actual column list, so read columns[] (or one small call) before naming any. TO ANSWER 'what changed, when, by whom': pass includeRowMeta:true and each record also carries createdTime/createdBy/lastModifiedTime/lastModifiedBy (ISO + display name) \u2014 row METADATA that exists even when the table declares no such column. This is the ONLY way to see a row's edit time; Lark exposes NO per-cell edit history to any API, so a field's previous VALUE is unavailable (only the Base's own web UI record history has it). Filter and sort SERVER-SIDE rather than pulling the table and counting: filter = { conjunction:'and'|'or', conditions:[{ fieldName, operator, value }] } with operator one of is/isNot/contains/doesNotContain/isEmpty/isNotEmpty/isGreater/isGreaterEqual/isLess/isLessEqual, and sort = [{ fieldName, desc }]. With a filter the reply carries filtered:true and `total` is the MATCH count, so one call with maxRecords:1 answers 'how many rows are X'. Date/datetime columns compare against value ['ExactDate','<epoch ms>'] (Lark also accepts 'Today', 'Yesterday', 'CurrentWeek'); everything else takes plain strings.",input_schema:{type:"object",properties:{baseId:{type:"string",description:"Lark Base app token OR a full Base/wiki URL (/base/\u2026 or /wiki/\u2026)."},tableId:{type:"string",description:"Table id (tbl\u2026). Optional when the URL carries table= or the Base has exactly one table; larkbitable_list_tables lists them."},viewId:{type:"string",description:"Optional view id (vew\u2026) \u2014 restricts the read to that view's rows and order."},fieldNames:{type:"array",items:{type:"string"},description:"Optional column names to return. Absent = every column."},includeRowMeta:{type:"boolean",description:"Also return each row's createdTime/createdBy/lastModifiedTime/lastModifiedBy. Use for any 'what changed / who edited / when' question \u2014 without it those are absent, and a row count cannot substitute for them. createdTime answers WHAT WAS ADDED; lastModifiedTime answers WHAT WAS EDITED \u2014 a row edited yesterday was not added yesterday."},filter:{type:"object",description:"Server-side filter, evaluated by Lark. { conjunction:'and'|'or', conditions:[{ fieldName, operator, value }] }.",properties:{conjunction:{type:"string",description:"'and' (default) or 'or'."},conditions:{type:"array",items:{type:"object",properties:{fieldName:{type:"string",description:"Exact column name (see columns[])."},operator:{type:"string",description:"is | isNot | contains | doesNotContain | isEmpty | isNotEmpty | isGreater | isGreaterEqual | isLess | isLessEqual."},value:{description:"String, number, or array of them. Omit for isEmpty/isNotEmpty. Date columns take ['ExactDate','<epoch ms>'] or 'Today'/'Yesterday'/'CurrentWeek'."}},required:["fieldName","operator"]}}},required:["conditions"]},sort:{type:"array",description:'Server-side sort, e.g. [{ fieldName: "\u6700\u540E\u66F4\u65B0\u65F6\u95F4", desc: true }].',items:{type:"object",properties:{fieldName:{type:"string",description:"Exact column name."},desc:{type:"boolean",description:"Descending when true."}},required:["fieldName"]}},maxRecords:{type:"number",description:"Max records to return (default 200, hard cap 1000)."},pageToken:{type:"string",description:"Resume token from a previous call's nextPageToken."}},required:["baseId"]}},{name:"larkdoc_list_comments",description:"List the comment threads on a Lark/Feishu document. Accepts a documentId or full doc URL. Returns { ok, comments:[{ commentId, replies:[{ replyId, author, text }] }] }. Use to read the thread you are replying to.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Lark doc id (docx token) OR a full Lark/Feishu doc URL."},fileType:{type:"string",description:"Drive file type \u2014 defaults to 'docx'. Only change for non-docx files (doc/sheet/bitable/file/slides)."}},required:["documentId"]}},{name:"larkdoc_add_comment",description:"Post a NEW top-level comment on a Lark/Feishu document. Accepts a documentId or full doc URL plus text. Returns { ok, documentId, commentId }.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Lark doc id (docx token) OR a full Lark/Feishu doc URL."},text:{type:"string",description:"The comment body (plain text)."},fileType:{type:"string",description:"Drive file type \u2014 defaults to 'docx'."}},required:["documentId","text"]}},{name:"larkdoc_reply_comment",description:"Reply INSIDE an existing comment thread on a Lark/Feishu document. Pass { documentId, commentId, text }. Use to answer a user who @mentioned Zibby in a doc comment (reply in the same commentId). Returns { ok, documentId, commentId, replyId }.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Lark doc id (docx token) OR a full Lark/Feishu doc URL."},commentId:{type:"string",description:"The comment_id of the thread to reply into (from larkdoc_list_comments or the webhook event)."},text:{type:"string",description:"The reply body (plain text)."},fileType:{type:"string",description:"Drive file type \u2014 defaults to 'docx'."}},required:["documentId","commentId","text"]}}]};import{existsSync as ju}from"fs";import{fileURLToPath as Pu}from"url";import{dirname as Cu,resolve as Uu}from"path";function Bu(){if(process.env.MCP_SKILL_PATH)return process.env.MCP_SKILL_PATH;let r=Cu(Pu(import.meta.url)),e=Uu(r,"..","bin","mcp-skill.mjs");return ju(e)?e:null}var Du=6e3*1e3,Rt=null,Mu=50,qu=50,Ju=40,Gu=31,Fu=200,Ku=31,Hu=50,ji=50,Pi=Object.freeze(["daily","month"]),Ci=Object.freeze(["zh","en","ja"]),zu=Object.freeze(["employee_id","employee_no"]),Wu=mr;async function Yu(){let{appId:r,appSecret:e,host:t}=await hr();if(Rt&&Rt.appId===r&&Rt.expiresAt>Date.now())return{token:Rt.token,host:t};let s=await(await w(`${t}/open-apis/auth/v3/tenant_access_token/internal`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({app_id:r,app_secret:e})},{kind:"api",what:"Lark tenant_access_token"})).json();if(s.code!==0)throw new Error(`Lark tenant_access_token failed: ${s.msg||s.code}`);return Rt={token:s.tenant_access_token,expiresAt:Date.now()+Du,appId:r},{token:s.tenant_access_token,host:t}}function Zu(r,e,t){let n=`Lark attendance API ${r} error ${e}: ${t||"(no message)"}`,s=r.includes("/attendance/v1/groups")?"attendance:rule:readonly (\u6253\u5361\u89C4\u5219/\u8003\u52E4\u7EC4 \u8BFB\u53D6)":r.includes("/contact/v3/users")?"contact:user.id:readonly (\u90AE\u7BB1 \u2192 \u7528\u6237 ID)":"attendance:task:readonly (\u6253\u5361\u7ED3\u679C/\u7EDF\u8BA1\u6570\u636E \u8BFB\u53D6)";return String(e)==="99991672"||/permission|scope|权限/i.test(String(t||""))?`${n}. The connected Lark app is missing the ${s} permission \u2014 add it in the Lark/Feishu developer console (\u5F00\u53D1\u8005\u540E\u53F0 > \u6743\u9650\u7BA1\u7406), then RE-PUBLISH a new app version so the scope takes effect.`:String(e)==="1220002"?`${n}. The tenant token was rejected \u2014 the Lark integration's app credentials look wrong; reconnect Lark in Integrations.`:String(e)==="1220001"?`${n}. Lark rejected the parameters \u2014 check the date range (yyyyMMdd, and within the documented span), stats_type, and that every user id matches the employeeType you passed.`:n}async function it(r,e,t){let{token:n,host:s}=await Yu(),i={method:r,headers:{Authorization:`Bearer ${n}`,"Content-Type":"application/json; charset=utf-8"}};r!=="GET"&&t!==void 0&&(i.body=JSON.stringify(t));let a=await(await w(`${s}${e}`,i,{kind:"api",what:`Lark Attendance ${r} ${e}`})).json();if(a.code!==0)throw new Error(Zu(e,a.code,a.msg));return a.data||{}}function Ui(r){if(r==null||r==="")return null;let e=String(r).trim().replace(/[-/.]/g,"");if(!/^\d{8}$/.test(e))return null;let t=Number(e.slice(0,4)),n=Number(e.slice(4,6)),s=Number(e.slice(6,8));if(t<1970||t>9999||n<1||n>12||s<1||s>31)return null;let i=new Date(Date.UTC(t,n-1,s));return i.getUTCFullYear()!==t||i.getUTCMonth()!==n-1||i.getUTCDate()!==s?null:Number(e)}function Vu(r,e){let t=n=>Date.UTC(Math.floor(n/1e4),Math.floor(n%1e4/100)-1,n%100);return Math.floor((t(e)-t(r))/864e5)+1}function gn(r,e){let t=Ui(r?.startDate??r?.start_date),n=Ui(r?.endDate??r?.end_date);if(!t||!n)return{error:"startDate and endDate are required, as yyyyMMdd (e.g. 20260401) or YYYY-MM-DD"};if(n<t)return{error:"endDate must not be earlier than startDate"};let s=Vu(t,n);return s>e?{error:`Lark caps this query at ${e} days; the requested range is ${s} days. Split it into consecutive windows and merge the results.`}:{startDate:t,endDate:n}}function Bi(r,e){let t=r?.userIds??r?.user_ids,n=(Array.isArray(t)?t:typeof t=="string"?[t]:[]).map(s=>String(s||"").trim()).filter(Boolean);return n.length?n.length>e?{error:`Lark accepts at most ${e} user ids per call; ${n.length} were passed. Split into batches.`}:{ids:n}:{error:"userIds is required \u2014 attendance data is personal, so this tool never returns a whole tenant. Get the ids from larkattendance_get_group (bind_user_ids) or larkattendance_resolve_users."}}function Et(r,e,t){let n=String(r||"").trim().toLowerCase();return e.includes(n)?n:t}function fr(r){return Et(r?.employeeType??r?.employee_type,zu,"employee_id")}function Xu(r){return{code:String(r?.code||""),title:String(r?.title||""),fields:(Array.isArray(r?.child_fields)?r.child_fields:[]).map(e=>({code:String(e?.code||""),title:String(e?.title||""),...e?.time_unit?{timeUnit:String(e.time_unit)}:{}}))}}function Qu(r,e){let n=(Array.isArray(r?.datas)?r.datas:[]).filter(s=>!e||e.has(String(s?.code||""))||e.has(String(s?.title||""))).map(s=>({code:String(s?.code||""),title:String(s?.title||""),value:s?.value===void 0||s?.value===null?"":String(s.value),...s?.duration_num?{durationNum:s.duration_num}:{},...Array.isArray(s?.features)&&s.features.length?{features:s.features}:{}}));return{userId:String(r?.user_id||""),name:String(r?.name||""),fields:n}}function em(r){return{userId:String(r?.user_id||""),name:String(r?.employee_name||""),day:r?.day??null,groupId:String(r?.group_id||""),shiftId:String(r?.shift_id||""),records:(Array.isArray(r?.records)?r.records:[]).map(e=>({checkInTime:e?.check_in_record?.check_time??null,checkInResult:String(e?.check_in_result||""),checkInResultSupplement:String(e?.check_in_result_supplement||""),checkInShiftTime:e?.check_in_shift_time??null,checkOutTime:e?.check_out_record?.check_time??null,checkOutResult:String(e?.check_out_result||""),checkOutResultSupplement:String(e?.check_out_result_supplement||""),checkOutShiftTime:e?.check_out_shift_time??null,...e?.task_shift_type===void 0?{}:{taskShiftType:e.task_shift_type}}))}}var Di={id:"lark-attendance",callsBackend:!0,serverName:"larkattendance",allowedTools:["mcp__larkattendance__*"],requiresIntegration:[I.LARK_DOCS,I.LARK],description:"Lark / Feishu attendance (\u8003\u52E4) \u2014 discover attendance groups and statistic fields, then read per-user work-hour / attendance statistics and clock-in records. Read-only.",envKeys:["PROJECT_API_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV",...Wu],promptFragment:"## Lark Attendance (\u8003\u52E4)\nRead-only access to this tenant's Lark/Feishu attendance data. NOTHING about the tenant's setup is known in advance \u2014 group names, what counts as \"\u5DE5\u65F6\", and the per-tenant \u81EA\u5B9A\u4E49\u5B57\u6BB5 all differ \u2014 so DISCOVER before you fetch:\n1. `larkattendance_list_groups` \u2192 the \u8003\u52E4\u7EC4 that exist.\n2. `larkattendance_get_group` \u2192 that group's member user ids (the scope for every data call).\n3. `larkattendance_list_stats_fields` \u2192 every statistic column available for the period, INCLUDING this tenant's \u81EA\u5B9A\u4E49\u5B57\u6BB5. Match the user's words (\"\u5DE5\u65F6\", \"\u51FA\u52E4\u5929\u6570\", \u2026) against the returned `title`s and pass the matching `code`s onward.\n4. `larkattendance_query_stats` (aggregated numbers) and/or `larkattendance_query_records` (raw clock-in/out).\n`larkattendance_query_stats` also needs an `operatorUserId` \u2014 the attendance admin the query runs AS. Lark rejects the call without one, and it selects the saved statistics view that decides which columns come back, so this skill refuses to invent it: if the prompt did not supply one, ASK for it rather than substituting a member id.\nIdentity mapping: attendance ids are the tenant `user_id` (employee_id), NOT the `open_id` the `lark` skill's `lark_lookup_user_by_email` returns \u2014 use `larkattendance_resolve_users` to turn emails into attendance ids. For chat/DM routing keep using the `lark` skill's tools.\nAttendance is personal HR data: every data tool requires an explicit `userIds` list, and there is no whole-tenant dump. Report the numbers as returned; do not invent a column the tenant does not have.\nThese tools return { ok:false, error } on failure \u2014 read the error, it names the missing permission or the range limit.",resolve(){let r=Bu();if(!r)return null;let e={};for(let t of this.envKeys)process.env[t]&&(e[t]=process.env[t]);return{type:"stdio",command:"node",args:[r,"../dist/larkAttendance.js","larkAttendanceSkill"],env:e,description:this.description}},async handleToolCall(r,e){try{switch(r){case"larkattendance_list_groups":{let t=Number(e?.pageSize??e?.page_size),n=Number.isFinite(t)&&t>0?Math.min(Math.floor(t),Mu):qu,s=new URLSearchParams({page_size:String(n)}),i=String(e?.pageToken??e?.page_token??"").trim();i&&s.set("page_token",i);let o=await it("GET",`/open-apis/attendance/v1/groups?${s.toString()}`),a=(Array.isArray(o?.group_list)?o.group_list:[]).map(c=>({groupId:String(c?.group_id||""),groupName:String(c?.group_name||"")}));return JSON.stringify({ok:!0,count:a.length,groups:a,hasMore:!!o?.has_more,...o?.has_more&&o?.page_token?{pageToken:String(o.page_token)}:{}})}case"larkattendance_get_group":{let t=String(e?.groupId??e?.group_id??"").trim();if(!t)return JSON.stringify({ok:!1,error:"groupId is required (from larkattendance_list_groups)"});let n=fr(e),s=new URLSearchParams({employee_type:n,dept_type:"open_id"}),i=await it("GET",`/open-apis/attendance/v1/groups/${encodeURIComponent(t)}?${s.toString()}`),o=a=>Array.isArray(a)?a.map(c=>String(c)):[];return JSON.stringify({ok:!0,groupId:String(i?.group_id||t),groupName:String(i?.group_name||""),employeeType:n,memberUserIds:o(i?.bind_user_ids),memberDeptIds:o(i?.bind_dept_ids),excludedUserIds:o(i?.except_user_ids),excludedDeptIds:o(i?.except_dept_ids),noPunchUserIds:o(i?.bind_default_user_ids),noPunchDeptIds:o(i?.bind_default_dept_ids),...Array.isArray(i?.group_leader_ids)?{leaderUserIds:o(i.group_leader_ids)}:{}})}case"larkattendance_list_stats_fields":{let t=gn(e,Ju);if(t.error)return JSON.stringify({ok:!1,error:t.error});let n=Et(e?.statsType??e?.stats_type,Pi,"month"),s=Et(e?.locale,Ci,"zh"),i=fr(e),a=(await it("POST",`/open-apis/attendance/v1/user_stats_fields/query?employee_type=${encodeURIComponent(i)}`,{locale:s,stats_type:n,start_date:t.startDate,end_date:t.endDate}))?.user_stats_field||{},c=(Array.isArray(a?.fields)?a.fields:[]).map(Xu);return JSON.stringify({ok:!0,statsType:n,locale:s,startDate:t.startDate,endDate:t.endDate,fieldGroupCount:c.length,fieldGroups:c})}case"larkattendance_query_stats":{let t=gn(e,Gu);if(t.error)return JSON.stringify({ok:!1,error:t.error});let n=Bi(e,Fu);if(n.error)return JSON.stringify({ok:!1,error:n.error});let s=Et(e?.statsType??e?.stats_type,Pi,"month"),i=Et(e?.locale,Ci,"zh"),o=fr(e),a=String(e?.operatorUserId??e?.user_id??"").trim();if(!a)return JSON.stringify({ok:!1,error:'operatorUserId is required by Lark (the API rejects this call with 1220001 "Need user_id"). It is the OPERATOR performing the query \u2014 a third id, not one of `userIds` and not `employeeType`. It must be a user whose saved attendance statistics view contains the columns you want (normally an attendance admin), because that view decides which columns Lark returns. This skill will NOT pick one for you: substituting an arbitrary member would silently return the wrong column set instead of failing. Ask the caller/prompt for the attendance admin\'s user id, or resolve their email with larkattendance_resolve_users.'});let c={locale:i,stats_type:s,start_date:t.startDate,end_date:t.endDate,user_ids:n.ids,user_id:a,need_history:e?.needHistory===!0,current_group_only:e?.currentGroupOnly===!0},l=Array.isArray(e?.fieldCodes)?e.fieldCodes:null,d=l?l.map(h=>String(h||"").trim()).filter(Boolean):[],p=d.length?new Set(d):null,u=await it("POST",`/open-apis/attendance/v1/user_stats_datas/query?employee_type=${encodeURIComponent(o)}`,c),m=(Array.isArray(u?.user_datas)?u.user_datas:[]).map(h=>Qu(h,p));return JSON.stringify({ok:!0,statsType:s,startDate:t.startDate,endDate:t.endDate,employeeType:o,userCount:m.length,users:m,invalidUserIds:Array.isArray(u?.invalid_user_list)?u.invalid_user_list.map(h=>String(h)):[]})}case"larkattendance_query_records":{let t=gn({startDate:e?.startDate??e?.checkDateFrom??e?.check_date_from,endDate:e?.endDate??e?.checkDateTo??e?.check_date_to},Ku);if(t.error)return JSON.stringify({ok:!1,error:t.error});let n=Bi(e,Hu);if(n.error)return JSON.stringify({ok:!1,error:n.error});let s=fr(e),i=new URLSearchParams({employee_type:s,ignore_invalid_users:"true"});e?.includeTerminatedUser===!0&&i.set("include_terminated_user","true");let o=await it("POST",`/open-apis/attendance/v1/user_tasks/query?${i.toString()}`,{user_ids:n.ids,check_date_from:t.startDate,check_date_to:t.endDate,need_overtime_result:e?.needOvertimeResult===!0}),a=(Array.isArray(o?.user_task_results)?o.user_task_results:[]).map(em);return JSON.stringify({ok:!0,startDate:t.startDate,endDate:t.endDate,employeeType:s,count:a.length,results:a,invalidUserIds:Array.isArray(o?.invalid_user_ids)?o.invalid_user_ids.map(c=>String(c)):[],unauthorizedUserIds:Array.isArray(o?.unauthorized_user_ids)?o.unauthorized_user_ids.map(c=>String(c)):[]})}case"larkattendance_resolve_users":{let t=e?.emails,n=(Array.isArray(t)?t:typeof t=="string"?[t]:[]).map(c=>String(c||"").trim()).filter(Boolean);if(!n.length)return JSON.stringify({ok:!1,error:"emails is required (a non-empty array of work email addresses)"});if(n.length>ji)return JSON.stringify({ok:!1,error:`At most ${ji} emails per call; ${n.length} were passed. Split into batches.`});let s=await it("POST","/open-apis/contact/v3/users/batch_get_id?user_id_type=user_id",{emails:n}),o=(Array.isArray(s?.user_list)?s.user_list:[]).filter(c=>c?.user_id).map(c=>({email:String(c?.email||""),userId:String(c.user_id)})),a=new Set(o.map(c=>c.email));return JSON.stringify({ok:!0,employeeType:"employee_id",users:o,notFound:n.filter(c=>!a.has(c))})}default:return JSON.stringify({ok:!1,error:`Unknown tool: ${r}`})}}catch(t){return JSON.stringify({ok:!1,error:t?.message||String(t)})}},tools:[{name:"larkattendance_list_groups",description:"DISCOVERY (start here). List this tenant's Lark/Feishu attendance groups (\u8003\u52E4\u7EC4) \u2014 their names are tenant-specific, so never assume one. Returns { ok, count, groups:[{ groupId, groupName }], hasMore, pageToken }. When hasMore is true, call again with pageToken to continue. Needs the attendance:rule:readonly permission on the connected Lark app.",input_schema:{type:"object",properties:{pageSize:{type:"number",description:"Groups per page, 1-50 (default 50)."},pageToken:{type:"string",description:"Cursor from a previous call's pageToken \u2014 omit for the first page."}}}},{name:"larkattendance_get_group",description:"DISCOVERY. Read one attendance group and, crucially, ITS MEMBERS \u2014 memberUserIds is the scope you pass as userIds to larkattendance_query_stats / larkattendance_query_records (a real group can hold hundreds, so expect to batch). Returns { ok, groupId, groupName, employeeType, memberUserIds, memberDeptIds, excludedUserIds, excludedDeptIds, noPunchUserIds, noPunchDeptIds }. THREE different exclusion lists, do not conflate them: excludedUserIds are removed from the group; noPunchUserIds are in the group but not required to clock in (they still have statistics). Members bound by DEPARTMENT appear in memberDeptIds, not memberUserIds \u2014 resolve those people another way (e.g. larkattendance_resolve_users from emails you already have). Lark does not reliably return the group leaders here, so leaderUserIds may be absent \u2014 never source the query operator id from this tool.",input_schema:{type:"object",properties:{groupId:{type:"string",description:"Attendance group id from larkattendance_list_groups."},employeeType:{type:"string",description:"Which id form to return members in: 'employee_id' (tenant user_id, the default and what every other tool here expects) or 'employee_no' (\u5DE5\u53F7)."}},required:["groupId"]}},{name:"larkattendance_list_stats_fields",description:'DISCOVERY \u2014 call this BEFORE larkattendance_query_stats. Lists every statistic column this tenant has for the period, grouped (e.g. \u57FA\u672C\u4FE1\u606F / \u51FA\u52E4\u7EDF\u8BA1 / \u5F02\u5E38\u7EDF\u8BA1 / \u6BCF\u65E5\u7EDF\u8BA1 / \u81EA\u5B9A\u4E49\u5B57\u6BB5). The \u81EA\u5B9A\u4E49\u5B57\u6BB5 group is tenant-defined and exists nowhere else, so this is the only way to learn its codes. Match the user\'s wording ("\u5DE5\u65F6", "\u51FA\u52E4\u5929\u6570", "\u8FDF\u5230\u6B21\u6570") against the returned `title`s and pass the matching `code`s as fieldCodes to larkattendance_query_stats. Returns { ok, statsType, locale, startDate, endDate, fieldGroups:[{ code, title, fields:[{ code, title, timeUnit }] }] }. Range is capped at 40 days.',input_schema:{type:"object",properties:{startDate:{type:"string",description:"Period start as yyyyMMdd (20260401) or YYYY-MM-DD."},endDate:{type:"string",description:"Period end, same format. At most 40 days after startDate."},statsType:{type:"string",description:"'month' (default) for period totals, or 'daily' for per-day columns."},locale:{type:"string",description:"Field-title language: 'zh' (default), 'en', or 'ja'."},employeeType:{type:"string",description:"'employee_id' (default) or 'employee_no'."}},required:["startDate","endDate"]}},{name:"larkattendance_query_stats",description:'The numbers: per-user attendance statistics (work hours, attendance days, exceptions, custom fields) for a period. Needs THREE separate things: userIds (whose data you want, max 200), operatorUserId (WHO IS ASKING \u2014 required by Lark, see below), and the date range (max 31 days). Attendance is personal data and there is no whole-tenant read; get ids from larkattendance_get_group or larkattendance_resolve_users. Returns { ok, statsType, startDate, endDate, users:[{ userId, name, fields:[{ code, title, value, durationNum? }] }], invalidUserIds }. Values are STRINGS exactly as Lark returns them \u2014 report them as-is, do not re-derive. EXPECT MANY COLUMNS: a one-month query returns roughly 40+ cells per person, because Lark mixes period totals (e.g. an "actual attendance hours" column, late/early counts) with identity columns (department, employee number) AND one column PER DAY of the range \u2014 so match on `title` rather than assuming a short list, and pass fieldCodes to trim. The exact set of columns follows the SAVED STATISTICS VIEW of the operatorUserId (\u8003\u52E4 > \u7EDF\u8BA1\u8BBE\u7F6E), so if a field that larkattendance_list_stats_fields advertises is missing from the result, it is switched off in that operator\'s view and an admin must enable it \u2014 or you are querying as the wrong operator.',input_schema:{type:"object",properties:{userIds:{type:"array",items:{type:"string"},description:"The people to report on, in the employeeType id form. Max 200. Required."},startDate:{type:"string",description:"Period start as yyyyMMdd (20260401) or YYYY-MM-DD."},endDate:{type:"string",description:"Period end, same format. At most 31 days after startDate."},statsType:{type:"string",description:"'month' (default) for period totals, or 'daily' for per-day rows."},fieldCodes:{type:"array",items:{type:"string"},description:"Optional filter \u2014 only these field codes/titles are returned. Omit to get every column."},locale:{type:"string",description:"Field-title language: 'zh' (default), 'en', or 'ja'."},employeeType:{type:"string",description:"'employee_id' (default) or 'employee_no' \u2014 must match the form of userIds."},operatorUserId:{type:"string",description:'REQUIRED. The user id of the OPERATOR performing the query \u2014 a third id, distinct from `userIds` (whose data you want) and from `employeeType` (the id form). Lark rejects the call without it (1220001 "Need user_id"). It should be an attendance admin whose saved statistics view contains the columns you need, because that view decides which columns come back. This skill will not pick one for you \u2014 get it from the caller/prompt, or from an email via larkattendance_resolve_users.'},needHistory:{type:"boolean",description:"Include transferred/departed staff (default false)."},currentGroupOnly:{type:"boolean",description:"Restrict to the user's current attendance group (default false)."}},required:["userIds","operatorUserId","startDate","endDate"]}},{name:"larkattendance_query_records",description:"The raw clock-in/out results (\u6253\u5361\u7ED3\u679C) per person per day \u2014 use when the aggregated statistics are not enough (e.g. to show which days were late, or the actual punch times). REQUIRES an explicit userIds list (max 50); range capped at 31 days. Returns { ok, results:[{ userId, name, day, groupId, shiftId, records:[{ checkInTime, checkInResult, checkOutTime, checkOutResult, checkInShiftTime, checkOutShiftTime }] }], invalidUserIds, unauthorizedUserIds }. Unknown ids are reported in invalidUserIds rather than failing the batch. Result values are Lark's own enums (Normal / Early / Late / Lack / \u2026).",input_schema:{type:"object",properties:{userIds:{type:"array",items:{type:"string"},description:"The people to read, in the employeeType id form. Max 50. Required."},startDate:{type:"string",description:"First day as yyyyMMdd (20260401) or YYYY-MM-DD."},endDate:{type:"string",description:"Last day, same format. At most 31 days after startDate."},employeeType:{type:"string",description:"'employee_id' (default) or 'employee_no' \u2014 must match the form of userIds."},needOvertimeResult:{type:"boolean",description:"Also return overtime shift records (default false)."},includeTerminatedUser:{type:"boolean",description:"Include departed employees (default false)."}},required:["userIds","startDate","endDate"]}},{name:"larkattendance_resolve_users",description:"Map work EMAIL addresses to the attendance user ids (tenant user_id = the attendance API's employee_id). Use this to line a report's people up with attendance rows. NOTE this is deliberately different from the `lark` skill's lark_lookup_user_by_email, which returns an open_id for chat/DM \u2014 the attendance endpoints reject open_ids. Max 50 emails per call. Returns { ok, employeeType:\"employee_id\", users:[{ email, userId }], notFound:[email] }. Needs the contact:user.id:readonly permission.",input_schema:{type:"object",properties:{emails:{type:"array",items:{type:"string"},description:"Work email addresses to resolve. Max 50."}},required:["emails"]}}]};var Mi={id:"chat_notify",description:"Chat notification meta-skill \u2014 routes to whichever messaging integration (Slack OR Lark) the user has configured for this project.",envKeys:[...B.envKeys||[],...H.envKeys||[]],get serverName(){if(process.env.SLACK_CHANNEL)return B.serverName;if(process.env.LARK_RECEIVE_ID)return H.serverName;if(process.env.SLACK_BOT_TOKEN)return B.serverName},get allowedTools(){return process.env.SLACK_CHANNEL?B.allowedTools||[]:process.env.LARK_RECEIVE_ID?H.allowedTools||[]:process.env.SLACK_BOT_TOKEN?B.allowedTools||[]:[]},promptFragment:`## Chat notifications (Slack OR Lark \u2014 at least one connected)
429
429
  You can post chat messages via:
430
430
  - slack_post_message (channel, text[, blocks]) \u2014 Slack. The \`channel\` is REQUIRED on every call.
431
431
  - lark_send_message (receive_id, text) \u2014 Lark.
package/dist/larkDocs.js CHANGED
@@ -1,3 +1,3 @@
1
- import{existsSync as ee,statSync as pe,readFileSync as ue}from"fs";import{fileURLToPath as me}from"url";import{basename as fe,dirname as ke,resolve as he}from"path";var w=Object.freeze({SENTRY:"sentry",JIRA:"jira",GITHUB:"github",GITLAB:"gitlab",SLACK:"slack",LARK:"lark",LARK_DOCS:"lark_docs",OPENAI_BILLING:"openai_billing",ANTHROPIC_BILLING:"anthropic_billing",CURSOR_ADMIN:"cursor_admin",NOTION:"notion",GOOGLE:"google",PLANE:"plane",LINEAR:"linear",VIKUNJA:"vikunja",FIGMA:"figma",HUBSPOT:"hubspot",OPEN_DESIGN:"open_design",LINKEDIN_PERSONAL:"linkedin_personal",LINKEDIN_BUSINESS:"linkedin_business",DISCORD:"discord"}),Fe=Object.freeze({sentry:{id:"sentry",name:"Sentry",connectPath:"/integrations?provider=sentry"},jira:{id:"jira",name:"Jira",connectPath:"/integrations?provider=jira"},github:{id:"github",name:"GitHub",connectPath:"/integrations?provider=github"},gitlab:{id:"gitlab",name:"GitLab",connectPath:"/integrations?provider=gitlab"},slack:{id:"slack",name:"Slack",connectPath:"/integrations?provider=slack"},lark:{id:"lark",name:"Lark Chat",connectPath:"/integrations?provider=lark"},lark_docs:{id:"lark_docs",name:"Lark App",connectPath:"/integrations?provider=lark_docs"},openai_billing:{id:"openai_billing",name:"OpenAI Admin",connectPath:"/integrations?provider=openai_billing"},anthropic_billing:{id:"anthropic_billing",name:"Anthropic Admin",connectPath:"/integrations?provider=anthropic_billing"},cursor_admin:{id:"cursor_admin",name:"Cursor Admin",connectPath:"/integrations?provider=cursor_admin"},notion:{id:"notion",name:"Notion",connectPath:"/integrations?provider=notion"},google:{id:"google",name:"Google Docs",connectPath:"/integrations?provider=google"},plane:{id:"plane",name:"Plane",connectPath:"/integrations?provider=plane"},linear:{id:"linear",name:"Linear",connectPath:"/integrations?provider=linear"},vikunja:{id:"vikunja",name:"Vikunja",connectPath:"/integrations?provider=vikunja"},figma:{id:"figma",name:"Figma",connectPath:"/integrations?provider=figma"},hubspot:{id:"hubspot",name:"HubSpot",connectPath:"/integrations?provider=hubspot"},open_design:{id:"open_design",name:"OpenDesign",connectPath:"/integrations?provider=open_design"},linkedin_personal:{id:"linkedin_personal",name:"LinkedIn (Personal)",connectPath:"/integrations?provider=linkedin_personal"},linkedin_business:{id:"linkedin_business",name:"LinkedIn (Business)",connectPath:"/integrations?provider=linkedin_business"},discord:{id:"discord",name:"Discord",connectPath:"/integrations?provider=discord"}});import{resolveIntegrationToken as K}from"@zibby/core/backend-client.js";var re="https://open.larksuite.com",J=Object.freeze([Object.freeze({appId:"LARK_DOCS_APP_ID",appSecret:"LARK_DOCS_APP_SECRET",host:"LARK_DOCS_HOST"}),Object.freeze({appId:"LARK_APP_ID",appSecret:"LARK_APP_SECRET",host:"LARK_HOST"})]),G=Object.freeze(J.flatMap(t=>[t.appId,t.appSecret,t.host]));function R(t){let e=String(t||"").trim().replace(/\/+$/,"");return e?/^https?:\/\//i.test(e)?e:`https://${e}`:re}function ie(t){let e=String(process.env[t.appId]||"").trim(),n=String(process.env[t.appSecret]||"").trim();return!e||!n?null:{appId:e,appSecret:n,host:R(process.env[t.host])}}async function z(){for(let t of J){let e=ie(t);if(e)return e}try{let t=await K(w.LARK_DOCS);return{appId:t?.appId,appSecret:t?.appSecret,host:R(t?.host)}}catch(t){if(!/unknown provider/i.test(String(t?.message||"")))throw t;let e=await K(w.LARK);return{appId:e?.appId,appSecret:e?.appSecret,host:R(e?.host)}}}var x={api:{knob:"SKILL_API_TIMEOUT_MS",fallback:3e4},transfer:{knob:"SKILL_TRANSFER_TIMEOUT_MS",fallback:12e4},job:{knob:"SKILL_JOB_TIMEOUT_MS",fallback:3e5}};function ae(t,e,n=process.env){let r=Number(n?.[t]);return Number.isFinite(r)&&r>0?Math.min(6e5,Math.max(1e3,Math.floor(r))):e}function se(t="api",e=process.env){let n=x[t]||x.api;return ae(n.knob,n.fallback,e)}function ce(t){return t?.name==="TimeoutError"||t?.name==="AbortError"}function de(t){try{return new URL(String(t?.url??t)).host||"unknown host"}catch{return"unknown host"}}function le(t,e){if(!t)return e;let n=new AbortController,r=i=>{n.signal.aborted||n.abort(i)},o=()=>r(t.reason),a=()=>r(e.reason);return n.signal.addEventListener("abort",()=>{t.removeEventListener("abort",o),e.removeEventListener("abort",a)},{once:!0}),t.aborted?r(t.reason):e.aborted?r(e.reason):(t.addEventListener("abort",o,{once:!0}),e.addEventListener("abort",a,{once:!0})),n.signal}async function O(t,e={},n={}){let r=n.kind||"api",o=(x[r]||x.api).knob,a=n.timeoutMs?Math.min(6e5,Math.max(1e3,Math.floor(n.timeoutMs))):se(r),i=e?.signal;if(i?.aborted)throw i.reason??new DOMException("This operation was aborted","AbortError");let s=AbortSignal.timeout(a),c=le(i,s);try{return await fetch(t,{...e,signal:c})}catch(d){throw s.aborted&&ce(d)?new Error(`${n.what||"request"} TIMED OUT after ${a}ms against ${de(t)} (${o})`):d}}function ye(){if(process.env.MCP_SKILL_PATH)return process.env.MCP_SKILL_PATH;let t=ke(me(import.meta.url)),e=he(t,"..","bin","mcp-skill.mjs");return ee(e)?e:null}var H=2e4,W=50,_e=50,be=20,we=500,ge=200,Ie=1e3,te=100,U=20,Se=3e4,Te=new Set([5,1001,1002]),Le=10*1024*1024,Ae=6e3*1e3,I=null,Ee=G;async function D(){let{appId:t,appSecret:e,host:n}=await z();if(I&&I.appId===t&&I.expiresAt>Date.now())return{token:I.token,host:n};let o=await(await O(`${n}/open-apis/auth/v3/tenant_access_token/internal`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({app_id:t,app_secret:e})},{kind:"api",what:"Lark tenant_access_token"})).json();if(o.code!==0)throw new Error(`Lark tenant_access_token failed: ${o.msg||o.code}`);return I={token:o.tenant_access_token,expiresAt:Date.now()+Ae,appId:t},{token:o.tenant_access_token,host:n}}async function m(t,e,n){let{token:r,host:o}=await D(),a={method:t,headers:{Authorization:`Bearer ${r}`,"Content-Type":"application/json; charset=utf-8"}};t!=="GET"&&n!==void 0&&(a.body=JSON.stringify(n));let s=await(await O(`${o}${e}`,a,{kind:"api",what:`Lark Docx ${t} ${e}`})).json();if(s.code!==0)throw new Error(`Lark Docx API ${e} error: ${s.msg||s.code}`);return{data:s.data||{},host:o}}function L(t,e){return String(t||"").includes("feishu")?`https://feishu.cn/docx/${e}`:`https://www.larksuite.com/docx/${e}`}function xe(t,e){return String(t||"").includes("feishu")?`https://feishu.cn/wiki/${e}`:`https://www.larksuite.com/wiki/${e}`}function Oe(t){let e=t.indexOf("?");if(e<0)return{};let n=t.slice(e+1).split("#")[0],r=new URLSearchParams(n),o={},a=r.get("table"),i=r.get("view");return a&&/^tbl[A-Za-z0-9]+$/.test(a)&&(o.tableId=a),i&&/^vew[A-Za-z0-9]+$/.test(i)&&(o.viewId=i),o}function y(t){if(!t||typeof t!="string")return null;let e=t.trim();if(!e)return null;let n=e.match(/\/(docx|wiki|base)\/([A-Za-z0-9]+)/);return n?{type:n[1],token:n[2],...Oe(e)}:/^[A-Za-z0-9]{10,}$/.test(e)?{type:"docx",token:e}:null}async function g(t){let e=typeof t=="string"?y(t):t;if(!e)throw new Error("A valid Lark doc id or URL is required");if(e.type==="docx")return e.token;let{data:n}=await m("GET",`/open-apis/wiki/v2/spaces/get_node?token=${encodeURIComponent(e.token)}`),r=n?.node||{};if(r.obj_type!=="docx"||!r.obj_token){let o=r.obj_type||"unknown";throw o==="bitable"?new Error("This Lark wiki link is a Base / \u591A\u7EF4\u8868\u683C (obj_type=bitable), not a document \u2014 read it with larkbitable_read_records (larkbitable_list_tables lists its tables)"):new Error(`Wiki node is not a docx document (obj_type=${o})`)}return r.obj_token}function v(t){let e=String(t??"").replace(/\r\n/g,`
1
+ import{existsSync as ee,statSync as pe,readFileSync as ue}from"fs";import{fileURLToPath as me}from"url";import{basename as fe,dirname as he,resolve as ke}from"path";var w=Object.freeze({SENTRY:"sentry",JIRA:"jira",GITHUB:"github",GITLAB:"gitlab",SLACK:"slack",LARK:"lark",LARK_DOCS:"lark_docs",OPENAI_BILLING:"openai_billing",ANTHROPIC_BILLING:"anthropic_billing",CURSOR_ADMIN:"cursor_admin",NOTION:"notion",GOOGLE:"google",PLANE:"plane",LINEAR:"linear",VIKUNJA:"vikunja",FIGMA:"figma",HUBSPOT:"hubspot",OPEN_DESIGN:"open_design",LINKEDIN_PERSONAL:"linkedin_personal",LINKEDIN_BUSINESS:"linkedin_business",DISCORD:"discord"}),Fe=Object.freeze({sentry:{id:"sentry",name:"Sentry",connectPath:"/integrations?provider=sentry"},jira:{id:"jira",name:"Jira",connectPath:"/integrations?provider=jira"},github:{id:"github",name:"GitHub",connectPath:"/integrations?provider=github"},gitlab:{id:"gitlab",name:"GitLab",connectPath:"/integrations?provider=gitlab"},slack:{id:"slack",name:"Slack",connectPath:"/integrations?provider=slack"},lark:{id:"lark",name:"Lark Chat",connectPath:"/integrations?provider=lark"},lark_docs:{id:"lark_docs",name:"Lark App",connectPath:"/integrations?provider=lark_docs"},openai_billing:{id:"openai_billing",name:"OpenAI Admin",connectPath:"/integrations?provider=openai_billing"},anthropic_billing:{id:"anthropic_billing",name:"Anthropic Admin",connectPath:"/integrations?provider=anthropic_billing"},cursor_admin:{id:"cursor_admin",name:"Cursor Admin",connectPath:"/integrations?provider=cursor_admin"},notion:{id:"notion",name:"Notion",connectPath:"/integrations?provider=notion"},google:{id:"google",name:"Google Docs",connectPath:"/integrations?provider=google"},plane:{id:"plane",name:"Plane",connectPath:"/integrations?provider=plane"},linear:{id:"linear",name:"Linear",connectPath:"/integrations?provider=linear"},vikunja:{id:"vikunja",name:"Vikunja",connectPath:"/integrations?provider=vikunja"},figma:{id:"figma",name:"Figma",connectPath:"/integrations?provider=figma"},hubspot:{id:"hubspot",name:"HubSpot",connectPath:"/integrations?provider=hubspot"},open_design:{id:"open_design",name:"OpenDesign",connectPath:"/integrations?provider=open_design"},linkedin_personal:{id:"linkedin_personal",name:"LinkedIn (Personal)",connectPath:"/integrations?provider=linkedin_personal"},linkedin_business:{id:"linkedin_business",name:"LinkedIn (Business)",connectPath:"/integrations?provider=linkedin_business"},discord:{id:"discord",name:"Discord",connectPath:"/integrations?provider=discord"}});import{resolveIntegrationToken as K}from"@zibby/core/backend-client.js";var re="https://open.larksuite.com",J=Object.freeze([Object.freeze({appId:"LARK_DOCS_APP_ID",appSecret:"LARK_DOCS_APP_SECRET",host:"LARK_DOCS_HOST"}),Object.freeze({appId:"LARK_APP_ID",appSecret:"LARK_APP_SECRET",host:"LARK_HOST"})]),G=Object.freeze(J.flatMap(t=>[t.appId,t.appSecret,t.host]));function R(t){let e=String(t||"").trim().replace(/\/+$/,"");return e?/^https?:\/\//i.test(e)?e:`https://${e}`:re}function ie(t){let e=String(process.env[t.appId]||"").trim(),n=String(process.env[t.appSecret]||"").trim();return!e||!n?null:{appId:e,appSecret:n,host:R(process.env[t.host])}}async function z(){for(let t of J){let e=ie(t);if(e)return e}try{let t=await K(w.LARK_DOCS);return{appId:t?.appId,appSecret:t?.appSecret,host:R(t?.host)}}catch(t){if(!/unknown provider/i.test(String(t?.message||"")))throw t;let e=await K(w.LARK);return{appId:e?.appId,appSecret:e?.appSecret,host:R(e?.host)}}}var O={api:{knob:"SKILL_API_TIMEOUT_MS",fallback:3e4},transfer:{knob:"SKILL_TRANSFER_TIMEOUT_MS",fallback:12e4},job:{knob:"SKILL_JOB_TIMEOUT_MS",fallback:3e5}};function ae(t,e,n=process.env){let r=Number(n?.[t]);return Number.isFinite(r)&&r>0?Math.min(6e5,Math.max(1e3,Math.floor(r))):e}function se(t="api",e=process.env){let n=O[t]||O.api;return ae(n.knob,n.fallback,e)}function ce(t){return t?.name==="TimeoutError"||t?.name==="AbortError"}function de(t){try{return new URL(String(t?.url??t)).host||"unknown host"}catch{return"unknown host"}}function le(t,e){if(!t)return e;let n=new AbortController,r=i=>{n.signal.aborted||n.abort(i)},o=()=>r(t.reason),a=()=>r(e.reason);return n.signal.addEventListener("abort",()=>{t.removeEventListener("abort",o),e.removeEventListener("abort",a)},{once:!0}),t.aborted?r(t.reason):e.aborted?r(e.reason):(t.addEventListener("abort",o,{once:!0}),e.addEventListener("abort",a,{once:!0})),n.signal}async function x(t,e={},n={}){let r=n.kind||"api",o=(O[r]||O.api).knob,a=n.timeoutMs?Math.min(6e5,Math.max(1e3,Math.floor(n.timeoutMs))):se(r),i=e?.signal;if(i?.aborted)throw i.reason??new DOMException("This operation was aborted","AbortError");let s=AbortSignal.timeout(a),c=le(i,s);try{return await fetch(t,{...e,signal:c})}catch(d){throw s.aborted&&ce(d)?new Error(`${n.what||"request"} TIMED OUT after ${a}ms against ${de(t)} (${o})`):d}}function ye(){if(process.env.MCP_SKILL_PATH)return process.env.MCP_SKILL_PATH;let t=he(me(import.meta.url)),e=ke(t,"..","bin","mcp-skill.mjs");return ee(e)?e:null}var H=2e4,W=50,_e=50,be=20,we=500,ge=200,Ie=1e3,te=100,U=20,Se=3e4,Te=new Set([5,1001,1002]),Le=10*1024*1024,Ae=6e3*1e3,I=null,Ee=G;async function D(){let{appId:t,appSecret:e,host:n}=await z();if(I&&I.appId===t&&I.expiresAt>Date.now())return{token:I.token,host:n};let o=await(await x(`${n}/open-apis/auth/v3/tenant_access_token/internal`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({app_id:t,app_secret:e})},{kind:"api",what:"Lark tenant_access_token"})).json();if(o.code!==0)throw new Error(`Lark tenant_access_token failed: ${o.msg||o.code}`);return I={token:o.tenant_access_token,expiresAt:Date.now()+Ae,appId:t},{token:o.tenant_access_token,host:n}}async function m(t,e,n){let{token:r,host:o}=await D(),a={method:t,headers:{Authorization:`Bearer ${r}`,"Content-Type":"application/json; charset=utf-8"}};t!=="GET"&&n!==void 0&&(a.body=JSON.stringify(n));let s=await(await x(`${o}${e}`,a,{kind:"api",what:`Lark Docx ${t} ${e}`})).json();if(s.code!==0)throw new Error(`Lark Docx API ${e} error: ${s.msg||s.code}`);return{data:s.data||{},host:o}}function L(t,e){return String(t||"").includes("feishu")?`https://feishu.cn/docx/${e}`:`https://www.larksuite.com/docx/${e}`}function Oe(t,e){return String(t||"").includes("feishu")?`https://feishu.cn/wiki/${e}`:`https://www.larksuite.com/wiki/${e}`}function xe(t){let e=t.indexOf("?");if(e<0)return{};let n=t.slice(e+1).split("#")[0],r=new URLSearchParams(n),o={},a=r.get("table"),i=r.get("view");return a&&/^tbl[A-Za-z0-9]+$/.test(a)&&(o.tableId=a),i&&/^vew[A-Za-z0-9]+$/.test(i)&&(o.viewId=i),o}function y(t){if(!t||typeof t!="string")return null;let e=t.trim();if(!e)return null;let n=e.match(/\/(docx|wiki|base)\/([A-Za-z0-9]+)/);return n?{type:n[1],token:n[2],...xe(e)}:/^[A-Za-z0-9]{10,}$/.test(e)?{type:"docx",token:e}:null}async function g(t){let e=typeof t=="string"?y(t):t;if(!e)throw new Error("A valid Lark doc id or URL is required");if(e.type==="docx")return e.token;let{data:n}=await m("GET",`/open-apis/wiki/v2/spaces/get_node?token=${encodeURIComponent(e.token)}`),r=n?.node||{};if(r.obj_type!=="docx"||!r.obj_token){let o=r.obj_type||"unknown";throw o==="bitable"?new Error("This Lark wiki link is a Base / \u591A\u7EF4\u8868\u683C (obj_type=bitable), not a document \u2014 read it with larkbitable_read_records (larkbitable_list_tables lists its tables)"):new Error(`Wiki node is not a docx document (obj_type=${o})`)}return r.obj_token}function v(t){let e=String(t??"").replace(/\r\n/g,`
2
2
  `),n=[];for(let r of e.split(`
3
- `)){let o=r.replace(/\s+$/,"");if(!o.trim())continue;let a=/^(#{1,3})\s+(.*)$/.exec(o),i=/^\s*[-*]\s+(.*)$/.exec(o),s=/^\s*\d+[.)]\s+(.*)$/.exec(o),c,d,l;if(a){let u=a[1].length;c=`heading${u}`,d=2+u,l=a[2]}else i?(c="bullet",d=12,l=i[1]):s?(c="ordered",d=13,l=s[1]):(c="text",d=2,l=o);n.push({block_type:d,[c]:{elements:[{text_run:{content:l}}],style:{}}})}return n}function A(t){let e=typeof t?.markdown=="string"?t.markdown:null,n=typeof t?.text=="string"?t.text:null;return e??n}async function P(t,e){let n;for(let r=0;r<e.length;r+=W){let o=e.slice(r,r+W);n=(await m("POST",`/open-apis/docx/v1/documents/${t}/blocks/${t}/children?document_revision_id=-1`,{children:o})).host}return n}function Ne(t){let e=typeof t=="string"?t.trim():"";if(!e)throw new Error("imagePath is required");if(!ee(e)||!pe(e).isFile())throw new Error(`imagePath not found (or not a file): ${e}`);if(!/\.(png|jpe?g)$/i.test(e))throw new Error("imagePath must be a .png or .jpg/.jpeg file");let n=ue(e);if(n.length>Le)throw new Error(`image is ${(n.length/(1024*1024)).toFixed(1)}MB \u2014 max 10MB`);return n}async function Re({fileName:t,parentNode:e,bytes:n}){let{token:r,host:o}=await D(),a=new FormData;a.set("file_name",t),a.set("parent_type","docx_image"),a.set("parent_node",e),a.set("size",String(n.length)),a.set("file",new Blob([n]),t);let s=await(await O(`${o}/open-apis/drive/v1/medias/upload_all`,{method:"POST",headers:{Authorization:`Bearer ${r}`},body:a},{kind:"transfer",what:"Lark Docx image upload"})).json();if(s.code!==0)throw new Error(`Lark media upload_all error: ${s.msg||s.code}`);let c=s.data?.file_token;if(!c)throw new Error("Lark media upload_all returned no file_token");return c}var ve="docx",Pe=50;function Y(t){return[{type:"text_run",text_run:{text:String(t??"")}}]}function Be(t){return Array.isArray(t)?t.map(e=>e?.text_run?.text??e?.docs_link?.url??(e?.person?`@${e.person.user_id||""}`:"")).join(""):""}function Me(t){let e=Array.isArray(t?.reply_list?.replies)?t.reply_list.replies:[];return{commentId:t?.comment_id||"",resolved:!!t?.is_solved,replies:e.map(n=>({replyId:n?.reply_id||"",author:n?.user_id||"",text:Be(n?.content?.elements),createTime:n?.create_time||""}))}}function B(t){return typeof t?.fileType=="string"&&t.fileType.trim()?t.fileType.trim():ve}function V(t,e,n,r){let a=String(t||"").includes("feishu")?"https://feishu.cn":"https://www.larksuite.com",i=new URLSearchParams;n&&i.set("table",n),r&&i.set("view",r);let s=i.toString();return`${a}/base/${e}${s?`?${s}`:""}`}async function X(t){let e=typeof t=="string"?y(t):t;if(!e)throw new Error("A valid Lark Base id or URL is required");let n={tableId:e.tableId||null,viewId:e.viewId||null};if(e.type!=="wiki")return{appToken:e.token,...n};let{data:r}=await m("GET",`/open-apis/wiki/v2/spaces/get_node?token=${encodeURIComponent(e.token)}`),o=r?.node||{};if(o.obj_type!=="bitable"||!o.obj_token)throw o.obj_type==="docx"?new Error("This Lark wiki link is a document (obj_type=docx), not a Base / \u591A\u7EF4\u8868\u683C \u2014 read it with larkdoc_get"):new Error(`Wiki node is not a Base / \u591A\u7EF4\u8868\u683C (obj_type=${o.obj_type||"unknown"})`);return{appToken:o.obj_token,...n}}function E(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return String(t);if(Array.isArray(t))return t.map(E).filter(e=>e!=="").join(", ");if(typeof t=="object"){let e=t;return typeof e.text=="string"&&e.text?e.text:typeof e.name=="string"&&e.name?e.name:typeof e.en_name=="string"&&e.en_name?e.en_name:typeof e.full_address=="string"&&e.full_address?e.full_address:typeof e.link=="string"&&e.link?e.link:e.value!==void 0?E(e.value):JSON.stringify(e)}return String(t)}function Ue(t,e){if(Te.has(Number(e))){let n=Number(Array.isArray(t)?t[0]:t);if(Number.isFinite(n)&&n>0)return new Date(n).toISOString()}return E(t)}async function ne(t){let e=[],n=null;for(let r=0;r<U;r++){let o=new URLSearchParams({page_size:String(te)});n&&o.set("page_token",n);let{data:a}=await m("GET",`/open-apis/bitable/v1/apps/${encodeURIComponent(t)}/tables?${o.toString()}`);for(let i of Array.isArray(a?.items)?a.items:[])e.push({tableId:String(i?.table_id||""),name:String(i?.name||"")});if(!a?.has_more||!a?.page_token)break;n=a.page_token}return e}async function De(t,e){let n=[],r=null;for(let o=0;o<U;o++){let a=new URLSearchParams({page_size:String(te)});r&&a.set("page_token",r);let{data:i}=await m("GET",`/open-apis/bitable/v1/apps/${encodeURIComponent(t)}/tables/${encodeURIComponent(e)}/fields?${a.toString()}`);for(let s of Array.isArray(i?.items)?i.items:[])n.push({name:String(s?.field_name||""),type:Number(s?.type)||0});if(!i?.has_more||!i?.page_token)break;r=i.page_token}return n}async function $e({appToken:t,tableId:e}){if(e)return e;let n=await ne(t);if(n.length===1)return n[0].tableId;if(!n.length)throw new Error("This Lark Base has no tables");let r=n.map(o=>`${o.name} (${o.tableId})`).join("; ");throw new Error(`tableId is required \u2014 this Base has ${n.length} tables: ${r}`)}var Z=new Set(["is","isNot","contains","doesNotContain","isEmpty","isNotEmpty","isGreater","isGreaterEqual","isLess","isLessEqual"]),Q=new Set(["isEmpty","isNotEmpty"]);function M(t,e,n){let r=new Set(t.map(i=>i.name)),o=e.filter(i=>!r.has(i));if(!o.length)return;let a=o.flatMap(i=>t.filter(s=>s.name.trim()===String(i).trim()).map(s=>JSON.stringify(s.name)));throw new Error(`unknown column(s) in ${n}: ${o.map(i=>JSON.stringify(i)).join(", ")}`+(a.length?` \u2014 did you mean ${a.join(", ")}? (names can carry spaces)`:"")+`. This table's columns: ${t.map(i=>JSON.stringify(i.name)).join(", ")}`)}function je({columns:t,viewId:e,fieldNames:n,filter:r,sort:o,includeRowMeta:a}){let i={};if(e&&(i.view_id=e),n?.length&&(M(t,n,"fieldNames"),i.field_names=n),r){let s=Array.isArray(r.conditions)?r.conditions:[];if(!s.length)throw new Error("filter.conditions must be a non-empty array");M(t,s.map(c=>c?.fieldName??c?.field_name),"filter"),i.filter={conjunction:r.conjunction==="or"?"or":"and",conditions:s.map(c=>{let d=String(c?.operator||"is");if(!Z.has(d))throw new Error(`unknown filter operator ${JSON.stringify(d)} \u2014 Lark accepts: ${[...Z].join(", ")}`);let l=c?.value,u=l==null?[]:(Array.isArray(l)?l:[l]).map(p=>String(p));if(!Q.has(d)&&!u.length)throw new Error(`filter operator ${JSON.stringify(d)} requires a value`);return{field_name:String(c?.fieldName??c?.field_name),operator:d,...Q.has(d)?{}:{value:u}}})}}return o?.length&&(M(t,o.map(s=>s?.fieldName??s?.field_name),"sort"),i.sort=o.map(s=>({field_name:String(s?.fieldName??s?.field_name),desc:!!s?.desc}))),a&&(i.automatic_fields=!0),i}function Ce(t){let e={},n=s=>{let c=Number(s);return!Number.isFinite(c)||c<=0?"":new Date(c<1e11?c*1e3:c).toISOString()},r=n(t?.created_time),o=n(t?.last_modified_time),a=E(t?.created_by),i=E(t?.last_modified_by);return r&&(e.createdTime=r),a&&(e.createdBy=a),o&&(e.lastModifiedTime=o),i&&(e.lastModifiedBy=i),e}async function qe({appToken:t,tableId:e,viewId:n,fieldNames:r,filter:o,sort:a,includeRowMeta:i,maxRecords:s,pageToken:c}){let d=await De(t,e),l=new Map(d.map(b=>[b.name,b.type])),u=je({columns:d,viewId:n,fieldNames:r,filter:o,sort:a,includeRowMeta:i}),p=[],h=0,k=c||null,f=!1,S=0,_=!1;for(let b=0;b<U;b++){let $=new URLSearchParams({page_size:String(Math.min(we,s))});k&&$.set("page_token",k);let{data:T}=await m("POST",`/open-apis/bitable/v1/apps/${encodeURIComponent(t)}/tables/${encodeURIComponent(e)}/records/search?${$.toString()}`,u);S=Number(T?.total)||S,f=!!T?.has_more,k=T?.page_token||null;for(let N of Array.isArray(T?.items)?T.items:[]){let j={};for(let[F,oe]of Object.entries(N?.fields||{}))j[F]=Ue(oe,l.get(F));let C={recordId:String(N?.record_id||""),...i?Ce(N):{},fields:j},q=JSON.stringify(C).length;if(p.length&&h+q>Se){_=!0;break}if(h+=q,p.push(C),p.length>=s)break}if(_||p.length>=s||!f||!k)break}return{columns:d,records:p,total:S,hasMore:f||_,nextPageToken:k||"",truncated:_,..._&&!k?{note:"stopped at the character budget, and these rows have no resume token \u2014 re-read with fieldNames set to the few columns you need (see columns[])."}:{}}}var et={id:"lark-docs",callsBackend:!0,serverName:"larkdocs",allowedTools:["mcp__larkdocs__*"],requiresIntegration:[w.LARK_DOCS,w.LARK],description:"Lark / Feishu Docs \u2014 read, create, append, and insert images into Lark documents (docx), and read Lark Bases (\u591A\u7EF4\u8868\u683C / bitable).",envKeys:["PROJECT_API_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV",...Ee],promptFragment:'## Lark Docs\nYou can read, create, and append Lark/Feishu documents (docx), read/post/reply to their comments, and READ Lark Bases (\u591A\u7EF4\u8868\u683C / bitable). This runs on the connected Lark Docs app (the chat app is used when no separate docs app is connected). Each `larkdoc_*` / `larkwiki_*` / `larkbitable_*` tool documents its own params and return shape in its tool description \u2014 they are not restated here.\nA Lark link is not always a document: a `/base/\u2026` link, or a `/wiki/\u2026` link carrying `?table=tbl\u2026`, is a Base \u2014 read it with `larkbitable_read_records`, NOT `larkdoc_get` (which refuses it, since a Base has no document body). Base reads need the `bitable:app:readonly` scope on the connected app.\nA Base question about CHANGE \u2014 what moved since yesterday, who edited a row, which rows are stale \u2014 is answered by `larkbitable_read_records` with `includeRowMeta:true` (per-row created/last-modified time and person) and a `filter`/`sort` on that timestamp, NOT by reading the whole table twice and diffing. Lark exposes no per-cell edit history to any API, so a field\'s PREVIOUS value cannot be fetched \u2014 say so rather than inferring it from snapshots.\n`createdTime` and `lastModifiedTime` are DIFFERENT QUESTIONS and must never stand in for one another: a row edited yesterday was not added yesterday. Rows APPEARING in a read you did not see before is not evidence they were created \u2014 a shorter earlier read (see `truncated`) explains it just as well. Answer "what was added" from `createdTime` ONLY, and if you did not ask for row metadata, say the read cannot tell rather than reasoning from row counts.\nThese tools return { ok:false, error } on failure \u2014 treat an unavailable Lark connection as "cannot read/deliver to Lark Docs" and continue rather than blocking the task.',resolve(){let t=ye();if(!t)return null;let e={};for(let n of this.envKeys)process.env[n]&&(e[n]=process.env[n]);return{type:"stdio",command:"node",args:[t,"../dist/larkDocs.js","larkDocsSkill"],env:e,description:this.description}},async handleToolCall(t,e){try{switch(t){case"larkdoc_get":{let n=e?.documentId||e?.url||e?.id,r=y(n);if(!r)return JSON.stringify({ok:!1,error:"A valid Lark doc id or URL is required"});let o=await g(r),a="";try{a=(await m("GET",`/open-apis/docx/v1/documents/${o}`)).data?.document?.title||""}catch{}let{data:i,host:s}=await m("GET",`/open-apis/docx/v1/documents/${o}/raw_content?lang=0`),c=String(i?.content||""),d=!1;return c.length>H&&(c=c.slice(0,H),d=!0),JSON.stringify({ok:!0,documentId:o,title:a,url:L(s,o),text:c,...d?{truncated:!0}:{}})}case"larkdoc_create":{let n=typeof e?.title=="string"&&e.title.trim()?e.title.trim():null;if(!n)return JSON.stringify({ok:!1,error:"title is required"});let r=typeof e?.wikiSpaceId=="string"&&e.wikiSpaceId.trim()?e.wikiSpaceId.trim():null;if(r){let c=typeof e?.parentNodeToken=="string"&&e.parentNodeToken.trim()?e.parentNodeToken.trim():null,{data:d,host:l}=await m("POST",`/open-apis/wiki/v2/spaces/${encodeURIComponent(r)}/nodes`,{obj_type:"docx",node_type:"origin",title:n,...c?{parent_node_token:c}:{}}),u=d?.node||{},p=u.obj_token;if(!p)return JSON.stringify({ok:!1,error:"Lark wiki node create returned no obj_token"});let h=A(e);if(h&&h.trim()){let k=v(h);k.length&&await P(p,k)}return JSON.stringify({ok:!0,documentId:p,title:n,wikiSpaceId:r,wikiNodeToken:u.node_token||"",url:u.node_token?xe(l,u.node_token):L(l,p)})}let{data:o,host:a}=await m("POST","/open-apis/docx/v1/documents",{title:n,...e?.folderToken?{folder_token:String(e.folderToken)}:{}}),i=o?.document?.document_id;if(!i)return JSON.stringify({ok:!1,error:"Lark Docs create returned no document_id"});let s=A(e);if(s&&s.trim()){let c=v(s);c.length&&await P(i,c)}return JSON.stringify({ok:!0,documentId:i,title:n,url:L(a,i)})}case"larkdoc_append":{let n=e?.documentId||e?.url||e?.id,r=y(n);if(!r)return JSON.stringify({ok:!1,error:"A valid Lark doc id or URL is required"});let o=A(e);if(!o||!o.trim())return JSON.stringify({ok:!1,error:"markdown or text content is required"});let a=await g(r),i=v(o);if(!i.length)return JSON.stringify({ok:!1,error:"no non-empty content to append"});let s=await P(a,i);return JSON.stringify({ok:!0,documentId:a,url:L(s,a)})}case"larkdoc_insert_image":{let n=e?.documentId||e?.url||e?.id,r=y(n);if(!r)return JSON.stringify({ok:!1,error:"A valid Lark doc id or URL is required"});if(!e?.imagePath||typeof e.imagePath!="string"||!e.imagePath.trim())return JSON.stringify({ok:!1,error:"imagePath is required"});let o=await g(r),a=e.imagePath.trim(),i=Ne(a),s=fe(a),d=(await m("POST",`/open-apis/docx/v1/documents/${o}/blocks/${o}/children?document_revision_id=-1`,{children:[{block_type:27,image:{}}]})).data?.children?.[0]?.block_id;if(!d)return JSON.stringify({ok:!1,error:"Lark image block create returned no block_id"});let l=await Re({fileName:s,parentNode:d,bytes:i}),u={token:l},p=Number(e?.width),h=Number(e?.height);Number.isFinite(p)&&p>0&&(u.width=Math.round(p)),Number.isFinite(h)&&h>0&&(u.height=Math.round(h));let{host:k}=await m("PATCH",`/open-apis/docx/v1/documents/${o}/blocks/${d}?document_revision_id=-1`,{replace_image:u});return JSON.stringify({ok:!0,documentId:o,blockId:d,fileToken:l,url:L(k,o)})}case"larkwiki_list_spaces":{let n=[],r=null;for(let o=0;o<be;o++){let a=new URLSearchParams({page_size:String(_e)});r&&a.set("page_token",r);let{data:i}=await m("GET",`/open-apis/wiki/v2/spaces?${a.toString()}`),s=Array.isArray(i?.items)?i.items:[];for(let c of s)n.push({spaceId:String(c?.space_id||""),name:String(c?.name||"")});if(!i?.has_more||!i?.page_token)break;r=i.page_token}return JSON.stringify({ok:!0,count:n.length,spaces:n})}case"larkbitable_list_tables":{let n=e?.baseId||e?.url||e?.documentId||e?.id,r=y(n);if(!r)return JSON.stringify({ok:!1,error:"A valid Lark Base id or URL is required"});let{appToken:o,tableId:a,viewId:i}=await X(r),s=await ne(o),c="",d="";try{let l=await m("GET",`/open-apis/bitable/v1/apps/${encodeURIComponent(o)}`);c=l.data?.app?.name||"",d=l.host}catch{}return JSON.stringify({ok:!0,appToken:o,name:c,...d?{url:V(d,o,a,i)}:{},...a?{linkedTableId:a}:{},...i?{linkedViewId:i}:{},count:s.length,tables:s})}case"larkbitable_read_records":{let n=e?.baseId||e?.url||e?.documentId||e?.id,r=y(n);if(!r)return JSON.stringify({ok:!1,error:"A valid Lark Base id or URL is required"});let o=await X(r),a=o.appToken,i=await $e({appToken:a,tableId:typeof e?.tableId=="string"&&e.tableId.trim()?e.tableId.trim():o.tableId}),s=typeof e?.viewId=="string"&&e.viewId.trim()?e.viewId.trim():o.viewId,c=Array.isArray(e?.fieldNames)?e.fieldNames.map(b=>String(b)).filter(Boolean):null,d=Number(e?.maxRecords),l=Number.isFinite(d)&&d>0?Math.min(Math.round(d),Ie):ge,u=typeof e?.pageToken=="string"&&e.pageToken.trim()?e.pageToken.trim():null,p=e?.filter&&typeof e.filter=="object"?e.filter:null,h=Array.isArray(e?.sort)&&e.sort.length?e.sort:null,k=!!e?.includeRowMeta,f=await qe({appToken:a,tableId:i,viewId:s,fieldNames:c,filter:p,sort:h,includeRowMeta:k,maxRecords:l,pageToken:u}),{host:S}=await D(),_="";try{_=(await m("GET",`/open-apis/bitable/v1/apps/${encodeURIComponent(a)}`)).data?.app?.name||""}catch{}return JSON.stringify({ok:!0,appToken:a,name:_,tableId:i,...s?{viewId:s}:{},url:V(S,a,i,s),total:f.total,...p?{filtered:!0}:{},count:f.records.length,columns:f.columns,records:f.records,hasMore:f.hasMore,...f.nextPageToken?{nextPageToken:f.nextPageToken}:{},...f.truncated?{truncated:!0}:{},...f.note?{note:f.note}:{}})}case"larkdoc_list_comments":{let n=e?.documentId||e?.url||e?.id||e?.fileToken,r=y(n);if(!r)return JSON.stringify({ok:!1,error:"A valid Lark doc id or URL is required"});let o=await g(r),a=B(e),i=new URLSearchParams({file_type:a,page_size:String(Pe)}),{data:s}=await m("GET",`/open-apis/drive/v1/files/${o}/comments?${i.toString()}`),d=(Array.isArray(s?.items)?s.items:[]).map(Me);return JSON.stringify({ok:!0,documentId:o,count:d.length,comments:d})}case"larkdoc_add_comment":{let n=e?.documentId||e?.url||e?.id||e?.fileToken,r=y(n);if(!r)return JSON.stringify({ok:!1,error:"A valid Lark doc id or URL is required"});let o=A({text:e?.text??e?.body});if(!o||!o.trim())return JSON.stringify({ok:!1,error:"text is required"});let a=await g(r),i=B(e),{data:s}=await m("POST",`/open-apis/drive/v1/files/${a}/comments?file_type=${encodeURIComponent(i)}`,{reply_list:{replies:[{content:{elements:Y(o)}}]}});return JSON.stringify({ok:!0,documentId:a,commentId:s?.comment_id||""})}case"larkdoc_reply_comment":{let n=e?.documentId||e?.url||e?.id||e?.fileToken,r=y(n);if(!r)return JSON.stringify({ok:!1,error:"A valid Lark doc id or URL is required"});let o=e?.commentId||e?.comment_id;if(!o)return JSON.stringify({ok:!1,error:"commentId is required"});let a=A({text:e?.text??e?.body});if(!a||!a.trim())return JSON.stringify({ok:!1,error:"text is required"});let i=await g(r),s=B(e),{data:c}=await m("POST",`/open-apis/drive/v1/files/${i}/comments/${encodeURIComponent(o)}/replies?file_type=${encodeURIComponent(s)}`,{content:{elements:Y(a)}});return JSON.stringify({ok:!0,documentId:i,commentId:String(o),replyId:c?.reply_id||""})}default:return JSON.stringify({ok:!1,error:`Unknown tool: ${t}`})}}catch(n){return JSON.stringify({ok:!1,error:n.message})}},tools:[{name:"larkdoc_get",description:"Read a Lark/Feishu document (docx) as plain text, for use as reference context. Accepts a raw doc id OR a full Lark doc URL (a /docx/ or /wiki/ link \u2014 wiki links are resolved to their backing docx). Returns { ok, documentId, title, url, text }. Text is truncated to ~20k chars.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Lark doc id (docx token) OR a full Lark/Feishu doc URL (/docx/\u2026 or /wiki/\u2026)."}},required:["documentId"]}},{name:"larkdoc_create",description:"Create a new Lark/Feishu document (docx) with a title and optional content (markdown: #/##/### headings, - bullets, 1. ordered; or plain text). Pass wikiSpaceId (from larkwiki_list_spaces) to create the doc INSIDE a wiki space instead of as a standalone doc \u2014 optionally nested under parentNodeToken. Returns { ok, documentId, url } (plus wikiNodeToken when created in a wiki) \u2014 share the url with the user.",input_schema:{type:"object",properties:{title:{type:"string",description:"Document title."},markdown:{type:"string",description:"Document body as markdown (preferred)."},text:{type:"string",description:"Document body as plain text (used when markdown is absent)."},folderToken:{type:"string",description:"Optional Lark drive folder token to create the doc in. Absent = the app root. Ignored when wikiSpaceId is set."},wikiSpaceId:{type:"string",description:"Optional wiki space id (from larkwiki_list_spaces) \u2014 create the doc as a node inside this wiki space."},parentNodeToken:{type:"string",description:"Optional wiki node token to nest the new doc under (only with wikiSpaceId). Absent = the space root."}},required:["title"]}},{name:"larkwiki_list_spaces",description:"List the Lark/Feishu wiki spaces visible to the connected app. Returns { ok, count, spaces:[{ spaceId, name }] }. Use to discover the wikiSpaceId for larkdoc_create.",input_schema:{type:"object",properties:{}}},{name:"larkdoc_append",description:"Append markdown/text content to the END of an existing Lark/Feishu document (docx). Accepts a documentId or a full Lark doc URL. Returns { ok, documentId, url }.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Lark doc id (docx token) OR a full Lark/Feishu doc URL."},markdown:{type:"string",description:"Content to append, as markdown (preferred)."},text:{type:"string",description:"Content to append, as plain text (used when markdown is absent)."}},required:["documentId"]}},{name:"larkdoc_insert_image",description:"Append a LOCAL image file (png/jpg, max 10MB) to the END of an existing Lark/Feishu document (docx). Accepts a documentId or full doc URL plus a local file path. Optional width/height in pixels. Returns { ok, documentId, blockId, fileToken, url }.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Lark doc id (docx token) OR a full Lark/Feishu doc URL."},imagePath:{type:"string",description:"Local filesystem path to a .png or .jpg/.jpeg image (max 10MB)."},width:{type:"number",description:"Optional display width in pixels."},height:{type:"number",description:"Optional display height in pixels."}},required:["documentId","imagePath"]}},{name:"larkbitable_list_tables",description:"List the tables in a Lark/Feishu Base (\u591A\u7EF4\u8868\u683C / bitable). Accepts a Base URL (a /base/\u2026 link, or a /wiki/\u2026 link whose node IS a Base) or a raw app token. Returns { ok, appToken, name, tables:[{ tableId, name }] } plus linkedTableId/linkedViewId when the URL pinned them. A Base is NOT a document \u2014 larkdoc_get cannot read one; use this and larkbitable_read_records instead.",input_schema:{type:"object",properties:{baseId:{type:"string",description:"Lark Base app token OR a full Base/wiki URL (/base/\u2026 or /wiki/\u2026)."}},required:["baseId"]}},{name:"larkbitable_read_records",description:"Read the rows of one table in a Lark/Feishu Base (\u591A\u7EF4\u8868\u683C / bitable) as flat text. Accepts a Base URL (/base/\u2026 or a /wiki/\u2026 link fronting a Base) or app token; the table and view are taken from the URL's table=/view= when present, and tableId may be omitted when the Base has exactly one table. Optionally restrict to viewId or fieldNames. Returns { ok, appToken, name, tableId, total, count, columns:[{name,type}], records:[{ recordId, fields:{ <column>: <text> } }], hasMore, nextPageToken }. Every cell is a STRING (dates as ISO). Defaults to 200 records; pass maxRecords (max 1000) and pageToken to page through a bigger table. A WIDE table (dozens of columns) hits a character budget before the row cap: the reply then carries truncated:true, hasMore:true and a note, and the fix is fieldNames, not paging. Column names are matched EXACTLY and real ones can carry a leading space or a ' (1)' suffix \u2014 an unknown name is refused with this table's actual column list, so read columns[] (or one small call) before naming any. TO ANSWER 'what changed, when, by whom': pass includeRowMeta:true and each record also carries createdTime/createdBy/lastModifiedTime/lastModifiedBy (ISO + display name) \u2014 row METADATA that exists even when the table declares no such column. This is the ONLY way to see a row's edit time; Lark exposes NO per-cell edit history to any API, so a field's previous VALUE is unavailable (only the Base's own web UI record history has it). Filter and sort SERVER-SIDE rather than pulling the table and counting: filter = { conjunction:'and'|'or', conditions:[{ fieldName, operator, value }] } with operator one of is/isNot/contains/doesNotContain/isEmpty/isNotEmpty/isGreater/isGreaterEqual/isLess/isLessEqual, and sort = [{ fieldName, desc }]. With a filter the reply carries filtered:true and `total` is the MATCH count, so one call with maxRecords:1 answers 'how many rows are X'. Date/datetime columns compare against value ['ExactDate','<epoch ms>'] (Lark also accepts 'Today', 'Yesterday', 'CurrentWeek'); everything else takes plain strings.",input_schema:{type:"object",properties:{baseId:{type:"string",description:"Lark Base app token OR a full Base/wiki URL (/base/\u2026 or /wiki/\u2026)."},tableId:{type:"string",description:"Table id (tbl\u2026). Optional when the URL carries table= or the Base has exactly one table; larkbitable_list_tables lists them."},viewId:{type:"string",description:"Optional view id (vew\u2026) \u2014 restricts the read to that view's rows and order."},fieldNames:{type:"array",items:{type:"string"},description:"Optional column names to return. Absent = every column."},includeRowMeta:{type:"boolean",description:"Also return each row's createdTime/createdBy/lastModifiedTime/lastModifiedBy. Use for any 'what changed / who edited / when' question \u2014 without it those are absent, and a row count cannot substitute for them. createdTime answers WHAT WAS ADDED; lastModifiedTime answers WHAT WAS EDITED \u2014 a row edited yesterday was not added yesterday."},filter:{type:"object",description:"Server-side filter, evaluated by Lark. { conjunction:'and'|'or', conditions:[{ fieldName, operator, value }] }.",properties:{conjunction:{type:"string",description:"'and' (default) or 'or'."},conditions:{type:"array",items:{type:"object",properties:{fieldName:{type:"string",description:"Exact column name (see columns[])."},operator:{type:"string",description:"is | isNot | contains | doesNotContain | isEmpty | isNotEmpty | isGreater | isGreaterEqual | isLess | isLessEqual."},value:{description:"String, number, or array of them. Omit for isEmpty/isNotEmpty. Date columns take ['ExactDate','<epoch ms>'] or 'Today'/'Yesterday'/'CurrentWeek'."}},required:["fieldName","operator"]}}},required:["conditions"]},sort:{type:"array",description:'Server-side sort, e.g. [{ fieldName: "\u6700\u540E\u66F4\u65B0\u65F6\u95F4", desc: true }].',items:{type:"object",properties:{fieldName:{type:"string",description:"Exact column name."},desc:{type:"boolean",description:"Descending when true."}},required:["fieldName"]}},maxRecords:{type:"number",description:"Max records to return (default 200, hard cap 1000)."},pageToken:{type:"string",description:"Resume token from a previous call's nextPageToken."}},required:["baseId"]}},{name:"larkdoc_list_comments",description:"List the comment threads on a Lark/Feishu document. Accepts a documentId or full doc URL. Returns { ok, comments:[{ commentId, replies:[{ replyId, author, text }] }] }. Use to read the thread you are replying to.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Lark doc id (docx token) OR a full Lark/Feishu doc URL."},fileType:{type:"string",description:"Drive file type \u2014 defaults to 'docx'. Only change for non-docx files (doc/sheet/bitable/file/slides)."}},required:["documentId"]}},{name:"larkdoc_add_comment",description:"Post a NEW top-level comment on a Lark/Feishu document. Accepts a documentId or full doc URL plus text. Returns { ok, documentId, commentId }.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Lark doc id (docx token) OR a full Lark/Feishu doc URL."},text:{type:"string",description:"The comment body (plain text)."},fileType:{type:"string",description:"Drive file type \u2014 defaults to 'docx'."}},required:["documentId","text"]}},{name:"larkdoc_reply_comment",description:"Reply INSIDE an existing comment thread on a Lark/Feishu document. Pass { documentId, commentId, text }. Use to answer a user who @mentioned Zibby in a doc comment (reply in the same commentId). Returns { ok, documentId, commentId, replyId }.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Lark doc id (docx token) OR a full Lark/Feishu doc URL."},commentId:{type:"string",description:"The comment_id of the thread to reply into (from larkdoc_list_comments or the webhook event)."},text:{type:"string",description:"The reply body (plain text)."},fileType:{type:"string",description:"Drive file type \u2014 defaults to 'docx'."}},required:["documentId","commentId","text"]}}]};function tt(){I=null}export{Ee as LARK_DOCS_APP_ENV_KEYS,tt as _resetLarkDocsTokenCache,V as baseWebUrl,Ce as bitableRowMeta,je as buildBitableSearchBody,L as docWebUrl,et as larkDocsSkill,v as markdownToLarkBlocks,y as parseLarkDocRef,xe as wikiWebUrl};
3
+ `)){let o=r.replace(/\s+$/,"");if(!o.trim())continue;let a=/^(#{1,3})\s+(.*)$/.exec(o),i=/^\s*[-*]\s+(.*)$/.exec(o),s=/^\s*\d+[.)]\s+(.*)$/.exec(o),c,d,l;if(a){let u=a[1].length;c=`heading${u}`,d=2+u,l=a[2]}else i?(c="bullet",d=12,l=i[1]):s?(c="ordered",d=13,l=s[1]):(c="text",d=2,l=o);n.push({block_type:d,[c]:{elements:[{text_run:{content:l}}],style:{}}})}return n}function A(t){let e=typeof t?.markdown=="string"?t.markdown:null,n=typeof t?.text=="string"?t.text:null;return e??n}async function P(t,e){let n;for(let r=0;r<e.length;r+=W){let o=e.slice(r,r+W);n=(await m("POST",`/open-apis/docx/v1/documents/${t}/blocks/${t}/children?document_revision_id=-1`,{children:o})).host}return n}function Ne(t){let e=typeof t=="string"?t.trim():"";if(!e)throw new Error("imagePath is required");if(!ee(e)||!pe(e).isFile())throw new Error(`imagePath not found (or not a file): ${e}`);if(!/\.(png|jpe?g)$/i.test(e))throw new Error("imagePath must be a .png or .jpg/.jpeg file");let n=ue(e);if(n.length>Le)throw new Error(`image is ${(n.length/(1024*1024)).toFixed(1)}MB \u2014 max 10MB`);return n}async function Re({fileName:t,parentNode:e,bytes:n}){let{token:r,host:o}=await D(),a=new FormData;a.set("file_name",t),a.set("parent_type","docx_image"),a.set("parent_node",e),a.set("size",String(n.length)),a.set("file",new Blob([n]),t);let s=await(await x(`${o}/open-apis/drive/v1/medias/upload_all`,{method:"POST",headers:{Authorization:`Bearer ${r}`},body:a},{kind:"transfer",what:"Lark Docx image upload"})).json();if(s.code!==0)throw new Error(`Lark media upload_all error: ${s.msg||s.code}`);let c=s.data?.file_token;if(!c)throw new Error("Lark media upload_all returned no file_token");return c}var ve="docx",Pe=50;function Y(t){return[{type:"text_run",text_run:{text:String(t??"")}}]}function Be(t){return Array.isArray(t)?t.map(e=>e?.text_run?.text??e?.docs_link?.url??(e?.person?`@${e.person.user_id||""}`:"")).join(""):""}function Me(t){let e=Array.isArray(t?.reply_list?.replies)?t.reply_list.replies:[];return{commentId:t?.comment_id||"",resolved:!!t?.is_solved,replies:e.map(n=>({replyId:n?.reply_id||"",author:n?.user_id||"",text:Be(n?.content?.elements),createTime:n?.create_time||""}))}}function B(t){return typeof t?.fileType=="string"&&t.fileType.trim()?t.fileType.trim():ve}function V(t,e,n,r){let a=String(t||"").includes("feishu")?"https://feishu.cn":"https://www.larksuite.com",i=new URLSearchParams;n&&i.set("table",n),r&&i.set("view",r);let s=i.toString();return`${a}/base/${e}${s?`?${s}`:""}`}async function X(t){let e=typeof t=="string"?y(t):t;if(!e)throw new Error("A valid Lark Base id or URL is required");let n={tableId:e.tableId||null,viewId:e.viewId||null};if(e.type!=="wiki")return{appToken:e.token,...n};let{data:r}=await m("GET",`/open-apis/wiki/v2/spaces/get_node?token=${encodeURIComponent(e.token)}`),o=r?.node||{};if(o.obj_type!=="bitable"||!o.obj_token)throw o.obj_type==="docx"?new Error("This Lark wiki link is a document (obj_type=docx), not a Base / \u591A\u7EF4\u8868\u683C \u2014 read it with larkdoc_get"):new Error(`Wiki node is not a Base / \u591A\u7EF4\u8868\u683C (obj_type=${o.obj_type||"unknown"})`);return{appToken:o.obj_token,...n}}function E(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return String(t);if(Array.isArray(t))return t.map(E).filter(e=>e!=="").join(", ");if(typeof t=="object"){let e=t;return typeof e.text=="string"&&e.text?e.text:typeof e.name=="string"&&e.name?e.name:typeof e.en_name=="string"&&e.en_name?e.en_name:typeof e.full_address=="string"&&e.full_address?e.full_address:typeof e.link=="string"&&e.link?e.link:e.value!==void 0?E(e.value):JSON.stringify(e)}return String(t)}function Ue(t,e){if(Te.has(Number(e))){let n=Number(Array.isArray(t)?t[0]:t);if(Number.isFinite(n)&&n>0)return new Date(n).toISOString()}return E(t)}async function ne(t){let e=[],n=null;for(let r=0;r<U;r++){let o=new URLSearchParams({page_size:String(te)});n&&o.set("page_token",n);let{data:a}=await m("GET",`/open-apis/bitable/v1/apps/${encodeURIComponent(t)}/tables?${o.toString()}`);for(let i of Array.isArray(a?.items)?a.items:[])e.push({tableId:String(i?.table_id||""),name:String(i?.name||"")});if(!a?.has_more||!a?.page_token)break;n=a.page_token}return e}async function De(t,e){let n=[],r=null;for(let o=0;o<U;o++){let a=new URLSearchParams({page_size:String(te)});r&&a.set("page_token",r);let{data:i}=await m("GET",`/open-apis/bitable/v1/apps/${encodeURIComponent(t)}/tables/${encodeURIComponent(e)}/fields?${a.toString()}`);for(let s of Array.isArray(i?.items)?i.items:[])n.push({name:String(s?.field_name||""),type:Number(s?.type)||0});if(!i?.has_more||!i?.page_token)break;r=i.page_token}return n}async function $e({appToken:t,tableId:e}){if(e)return e;let n=await ne(t);if(n.length===1)return n[0].tableId;if(!n.length)throw new Error("This Lark Base has no tables");let r=n.map(o=>`${o.name} (${o.tableId})`).join("; ");throw new Error(`tableId is required \u2014 this Base has ${n.length} tables: ${r}`)}var Z=new Set(["is","isNot","contains","doesNotContain","isEmpty","isNotEmpty","isGreater","isGreaterEqual","isLess","isLessEqual"]),Q=new Set(["isEmpty","isNotEmpty"]);function M(t,e,n){let r=new Set(t.map(i=>i.name)),o=e.filter(i=>!r.has(i));if(!o.length)return;let a=o.flatMap(i=>t.filter(s=>s.name.trim()===String(i).trim()).map(s=>JSON.stringify(s.name)));throw new Error(`unknown column(s) in ${n}: ${o.map(i=>JSON.stringify(i)).join(", ")}`+(a.length?` \u2014 did you mean ${a.join(", ")}? (names can carry spaces)`:"")+`. This table's columns: ${t.map(i=>JSON.stringify(i.name)).join(", ")}`)}function je({columns:t,viewId:e,fieldNames:n,filter:r,sort:o,includeRowMeta:a}){let i={};if(e&&(i.view_id=e),n?.length&&(M(t,n,"fieldNames"),i.field_names=n),r){let s=Array.isArray(r.conditions)?r.conditions:[];if(!s.length)throw new Error("filter.conditions must be a non-empty array");M(t,s.map(c=>c?.fieldName??c?.field_name),"filter"),i.filter={conjunction:r.conjunction==="or"?"or":"and",conditions:s.map(c=>{let d=String(c?.operator||"is");if(!Z.has(d))throw new Error(`unknown filter operator ${JSON.stringify(d)} \u2014 Lark accepts: ${[...Z].join(", ")}`);let l=c?.value,u=l==null?[]:(Array.isArray(l)?l:[l]).map(p=>String(p));if(!Q.has(d)&&!u.length)throw new Error(`filter operator ${JSON.stringify(d)} requires a value`);return{field_name:String(c?.fieldName??c?.field_name),operator:d,...Q.has(d)?{}:{value:u}}})}}return o?.length&&(M(t,o.map(s=>s?.fieldName??s?.field_name),"sort"),i.sort=o.map(s=>({field_name:String(s?.fieldName??s?.field_name),desc:!!s?.desc}))),a&&(i.automatic_fields=!0),i}function Ce(t){let e={},n=s=>{let c=Number(s);return!Number.isFinite(c)||c<=0?"":new Date(c<1e11?c*1e3:c).toISOString()},r=n(t?.created_time),o=n(t?.last_modified_time),a=E(t?.created_by),i=E(t?.last_modified_by);return r&&(e.createdTime=r),a&&(e.createdBy=a),o&&(e.lastModifiedTime=o),i&&(e.lastModifiedBy=i),e}async function qe({appToken:t,tableId:e,viewId:n,fieldNames:r,filter:o,sort:a,includeRowMeta:i,maxRecords:s,pageToken:c}){let d=await De(t,e),l=new Map(d.map(b=>[b.name,b.type])),u=je({columns:d,viewId:n,fieldNames:r,filter:o,sort:a,includeRowMeta:i}),p=[],k=0,h=c||null,f=!1,S=0,_=!1;for(let b=0;b<U;b++){let $=new URLSearchParams({page_size:String(Math.min(we,s))});h&&$.set("page_token",h);let{data:T}=await m("POST",`/open-apis/bitable/v1/apps/${encodeURIComponent(t)}/tables/${encodeURIComponent(e)}/records/search?${$.toString()}`,u);S=Number(T?.total)||S,f=!!T?.has_more,h=T?.page_token||null;for(let N of Array.isArray(T?.items)?T.items:[]){let j={};for(let[F,oe]of Object.entries(N?.fields||{}))j[F]=Ue(oe,l.get(F));let C={recordId:String(N?.record_id||""),...i?Ce(N):{},fields:j},q=JSON.stringify(C).length;if(p.length&&k+q>Se){_=!0;break}if(k+=q,p.push(C),p.length>=s)break}if(_||p.length>=s||!f||!h)break}return{columns:d,records:p,total:S,hasMore:f||_,nextPageToken:h||"",truncated:_,..._&&!h?{note:"stopped at the character budget, and these rows have no resume token \u2014 re-read with fieldNames set to the few columns you need (see columns[])."}:{}}}var et={id:"lark-docs",callsBackend:!0,serverName:"larkdocs",allowedTools:["mcp__larkdocs__*"],requiresIntegration:[w.LARK_DOCS,w.LARK],description:"Lark / Feishu Docs \u2014 read, create, append, and insert images into Lark documents (docx), and read Lark Bases (\u591A\u7EF4\u8868\u683C / bitable).",envKeys:["PROJECT_API_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV",...Ee],promptFragment:'## Lark Docs\nYou can read, create, and append Lark/Feishu documents (docx), read/post/reply to their comments, and READ Lark Bases (\u591A\u7EF4\u8868\u683C / bitable). This runs on the connected Lark Docs app (the chat app is used when no separate docs app is connected). Each `larkdoc_*` / `larkwiki_*` / `larkbitable_*` tool documents its own params and return shape in its tool description \u2014 they are not restated here.\nA Lark link is not always a document: a `/base/\u2026` link, or a `/wiki/\u2026` link carrying `?table=tbl\u2026`, is a Base \u2014 read it with `larkbitable_read_records`, NOT `larkdoc_get` (which refuses it, since a Base has no document body). Base reads need the `bitable:app:readonly` scope on the connected app.\nA Base question about CHANGE \u2014 what moved since yesterday, who edited a row, which rows are stale \u2014 is answered by `larkbitable_read_records` with `includeRowMeta:true` (per-row created/last-modified time and person) and a `filter`/`sort` on that timestamp, NOT by reading the whole table twice and diffing. Lark exposes no per-cell edit history to any API, so a field\'s PREVIOUS value cannot be fetched \u2014 say so rather than inferring it from snapshots.\n`createdTime` and `lastModifiedTime` are DIFFERENT QUESTIONS and must never stand in for one another: a row edited yesterday was not added yesterday. Rows APPEARING in a read you did not see before is not evidence they were created \u2014 a shorter earlier read (see `truncated`) explains it just as well. Answer "what was added" from `createdTime` ONLY, and if you did not ask for row metadata, say the read cannot tell rather than reasoning from row counts. A Base read is a SNAPSHOT OF NOW: every field holds only its CURRENT value, and `createdTime` says when the ROW appeared, never when it acquired that value. So a question about a PAST STATE of a mutable field has no answerable form here \u2014 only a PROXY (rows whose value is X today and whose `createdTime` precedes the date) that is wrong in BOTH directions: rows given the value after that date inflate it, and rows that held it then but were since changed away or deleted are absent from the read entirely and deflate it. Offer the proxy labelled as a proxy with both directions stated \u2014 never as the answer.\nThese tools return { ok:false, error } on failure \u2014 treat an unavailable Lark connection as "cannot read/deliver to Lark Docs" and continue rather than blocking the task.',resolve(){let t=ye();if(!t)return null;let e={};for(let n of this.envKeys)process.env[n]&&(e[n]=process.env[n]);return{type:"stdio",command:"node",args:[t,"../dist/larkDocs.js","larkDocsSkill"],env:e,description:this.description}},async handleToolCall(t,e){try{switch(t){case"larkdoc_get":{let n=e?.documentId||e?.url||e?.id,r=y(n);if(!r)return JSON.stringify({ok:!1,error:"A valid Lark doc id or URL is required"});let o=await g(r),a="";try{a=(await m("GET",`/open-apis/docx/v1/documents/${o}`)).data?.document?.title||""}catch{}let{data:i,host:s}=await m("GET",`/open-apis/docx/v1/documents/${o}/raw_content?lang=0`),c=String(i?.content||""),d=!1;return c.length>H&&(c=c.slice(0,H),d=!0),JSON.stringify({ok:!0,documentId:o,title:a,url:L(s,o),text:c,...d?{truncated:!0}:{}})}case"larkdoc_create":{let n=typeof e?.title=="string"&&e.title.trim()?e.title.trim():null;if(!n)return JSON.stringify({ok:!1,error:"title is required"});let r=typeof e?.wikiSpaceId=="string"&&e.wikiSpaceId.trim()?e.wikiSpaceId.trim():null;if(r){let c=typeof e?.parentNodeToken=="string"&&e.parentNodeToken.trim()?e.parentNodeToken.trim():null,{data:d,host:l}=await m("POST",`/open-apis/wiki/v2/spaces/${encodeURIComponent(r)}/nodes`,{obj_type:"docx",node_type:"origin",title:n,...c?{parent_node_token:c}:{}}),u=d?.node||{},p=u.obj_token;if(!p)return JSON.stringify({ok:!1,error:"Lark wiki node create returned no obj_token"});let k=A(e);if(k&&k.trim()){let h=v(k);h.length&&await P(p,h)}return JSON.stringify({ok:!0,documentId:p,title:n,wikiSpaceId:r,wikiNodeToken:u.node_token||"",url:u.node_token?Oe(l,u.node_token):L(l,p)})}let{data:o,host:a}=await m("POST","/open-apis/docx/v1/documents",{title:n,...e?.folderToken?{folder_token:String(e.folderToken)}:{}}),i=o?.document?.document_id;if(!i)return JSON.stringify({ok:!1,error:"Lark Docs create returned no document_id"});let s=A(e);if(s&&s.trim()){let c=v(s);c.length&&await P(i,c)}return JSON.stringify({ok:!0,documentId:i,title:n,url:L(a,i)})}case"larkdoc_append":{let n=e?.documentId||e?.url||e?.id,r=y(n);if(!r)return JSON.stringify({ok:!1,error:"A valid Lark doc id or URL is required"});let o=A(e);if(!o||!o.trim())return JSON.stringify({ok:!1,error:"markdown or text content is required"});let a=await g(r),i=v(o);if(!i.length)return JSON.stringify({ok:!1,error:"no non-empty content to append"});let s=await P(a,i);return JSON.stringify({ok:!0,documentId:a,url:L(s,a)})}case"larkdoc_insert_image":{let n=e?.documentId||e?.url||e?.id,r=y(n);if(!r)return JSON.stringify({ok:!1,error:"A valid Lark doc id or URL is required"});if(!e?.imagePath||typeof e.imagePath!="string"||!e.imagePath.trim())return JSON.stringify({ok:!1,error:"imagePath is required"});let o=await g(r),a=e.imagePath.trim(),i=Ne(a),s=fe(a),d=(await m("POST",`/open-apis/docx/v1/documents/${o}/blocks/${o}/children?document_revision_id=-1`,{children:[{block_type:27,image:{}}]})).data?.children?.[0]?.block_id;if(!d)return JSON.stringify({ok:!1,error:"Lark image block create returned no block_id"});let l=await Re({fileName:s,parentNode:d,bytes:i}),u={token:l},p=Number(e?.width),k=Number(e?.height);Number.isFinite(p)&&p>0&&(u.width=Math.round(p)),Number.isFinite(k)&&k>0&&(u.height=Math.round(k));let{host:h}=await m("PATCH",`/open-apis/docx/v1/documents/${o}/blocks/${d}?document_revision_id=-1`,{replace_image:u});return JSON.stringify({ok:!0,documentId:o,blockId:d,fileToken:l,url:L(h,o)})}case"larkwiki_list_spaces":{let n=[],r=null;for(let o=0;o<be;o++){let a=new URLSearchParams({page_size:String(_e)});r&&a.set("page_token",r);let{data:i}=await m("GET",`/open-apis/wiki/v2/spaces?${a.toString()}`),s=Array.isArray(i?.items)?i.items:[];for(let c of s)n.push({spaceId:String(c?.space_id||""),name:String(c?.name||"")});if(!i?.has_more||!i?.page_token)break;r=i.page_token}return JSON.stringify({ok:!0,count:n.length,spaces:n})}case"larkbitable_list_tables":{let n=e?.baseId||e?.url||e?.documentId||e?.id,r=y(n);if(!r)return JSON.stringify({ok:!1,error:"A valid Lark Base id or URL is required"});let{appToken:o,tableId:a,viewId:i}=await X(r),s=await ne(o),c="",d="";try{let l=await m("GET",`/open-apis/bitable/v1/apps/${encodeURIComponent(o)}`);c=l.data?.app?.name||"",d=l.host}catch{}return JSON.stringify({ok:!0,appToken:o,name:c,...d?{url:V(d,o,a,i)}:{},...a?{linkedTableId:a}:{},...i?{linkedViewId:i}:{},count:s.length,tables:s})}case"larkbitable_read_records":{let n=e?.baseId||e?.url||e?.documentId||e?.id,r=y(n);if(!r)return JSON.stringify({ok:!1,error:"A valid Lark Base id or URL is required"});let o=await X(r),a=o.appToken,i=await $e({appToken:a,tableId:typeof e?.tableId=="string"&&e.tableId.trim()?e.tableId.trim():o.tableId}),s=typeof e?.viewId=="string"&&e.viewId.trim()?e.viewId.trim():o.viewId,c=Array.isArray(e?.fieldNames)?e.fieldNames.map(b=>String(b)).filter(Boolean):null,d=Number(e?.maxRecords),l=Number.isFinite(d)&&d>0?Math.min(Math.round(d),Ie):ge,u=typeof e?.pageToken=="string"&&e.pageToken.trim()?e.pageToken.trim():null,p=e?.filter&&typeof e.filter=="object"?e.filter:null,k=Array.isArray(e?.sort)&&e.sort.length?e.sort:null,h=!!e?.includeRowMeta,f=await qe({appToken:a,tableId:i,viewId:s,fieldNames:c,filter:p,sort:k,includeRowMeta:h,maxRecords:l,pageToken:u}),{host:S}=await D(),_="";try{_=(await m("GET",`/open-apis/bitable/v1/apps/${encodeURIComponent(a)}`)).data?.app?.name||""}catch{}return JSON.stringify({ok:!0,appToken:a,name:_,tableId:i,...s?{viewId:s}:{},url:V(S,a,i,s),total:f.total,...p?{filtered:!0}:{},count:f.records.length,columns:f.columns,records:f.records,hasMore:f.hasMore,...f.nextPageToken?{nextPageToken:f.nextPageToken}:{},...f.truncated?{truncated:!0}:{},...f.note?{note:f.note}:{}})}case"larkdoc_list_comments":{let n=e?.documentId||e?.url||e?.id||e?.fileToken,r=y(n);if(!r)return JSON.stringify({ok:!1,error:"A valid Lark doc id or URL is required"});let o=await g(r),a=B(e),i=new URLSearchParams({file_type:a,page_size:String(Pe)}),{data:s}=await m("GET",`/open-apis/drive/v1/files/${o}/comments?${i.toString()}`),d=(Array.isArray(s?.items)?s.items:[]).map(Me);return JSON.stringify({ok:!0,documentId:o,count:d.length,comments:d})}case"larkdoc_add_comment":{let n=e?.documentId||e?.url||e?.id||e?.fileToken,r=y(n);if(!r)return JSON.stringify({ok:!1,error:"A valid Lark doc id or URL is required"});let o=A({text:e?.text??e?.body});if(!o||!o.trim())return JSON.stringify({ok:!1,error:"text is required"});let a=await g(r),i=B(e),{data:s}=await m("POST",`/open-apis/drive/v1/files/${a}/comments?file_type=${encodeURIComponent(i)}`,{reply_list:{replies:[{content:{elements:Y(o)}}]}});return JSON.stringify({ok:!0,documentId:a,commentId:s?.comment_id||""})}case"larkdoc_reply_comment":{let n=e?.documentId||e?.url||e?.id||e?.fileToken,r=y(n);if(!r)return JSON.stringify({ok:!1,error:"A valid Lark doc id or URL is required"});let o=e?.commentId||e?.comment_id;if(!o)return JSON.stringify({ok:!1,error:"commentId is required"});let a=A({text:e?.text??e?.body});if(!a||!a.trim())return JSON.stringify({ok:!1,error:"text is required"});let i=await g(r),s=B(e),{data:c}=await m("POST",`/open-apis/drive/v1/files/${i}/comments/${encodeURIComponent(o)}/replies?file_type=${encodeURIComponent(s)}`,{content:{elements:Y(a)}});return JSON.stringify({ok:!0,documentId:i,commentId:String(o),replyId:c?.reply_id||""})}default:return JSON.stringify({ok:!1,error:`Unknown tool: ${t}`})}}catch(n){return JSON.stringify({ok:!1,error:n.message})}},tools:[{name:"larkdoc_get",description:"Read a Lark/Feishu document (docx) as plain text, for use as reference context. Accepts a raw doc id OR a full Lark doc URL (a /docx/ or /wiki/ link \u2014 wiki links are resolved to their backing docx). Returns { ok, documentId, title, url, text }. Text is truncated to ~20k chars.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Lark doc id (docx token) OR a full Lark/Feishu doc URL (/docx/\u2026 or /wiki/\u2026)."}},required:["documentId"]}},{name:"larkdoc_create",description:"Create a new Lark/Feishu document (docx) with a title and optional content (markdown: #/##/### headings, - bullets, 1. ordered; or plain text). Pass wikiSpaceId (from larkwiki_list_spaces) to create the doc INSIDE a wiki space instead of as a standalone doc \u2014 optionally nested under parentNodeToken. Returns { ok, documentId, url } (plus wikiNodeToken when created in a wiki) \u2014 share the url with the user.",input_schema:{type:"object",properties:{title:{type:"string",description:"Document title."},markdown:{type:"string",description:"Document body as markdown (preferred)."},text:{type:"string",description:"Document body as plain text (used when markdown is absent)."},folderToken:{type:"string",description:"Optional Lark drive folder token to create the doc in. Absent = the app root. Ignored when wikiSpaceId is set."},wikiSpaceId:{type:"string",description:"Optional wiki space id (from larkwiki_list_spaces) \u2014 create the doc as a node inside this wiki space."},parentNodeToken:{type:"string",description:"Optional wiki node token to nest the new doc under (only with wikiSpaceId). Absent = the space root."}},required:["title"]}},{name:"larkwiki_list_spaces",description:"List the Lark/Feishu wiki spaces visible to the connected app. Returns { ok, count, spaces:[{ spaceId, name }] }. Use to discover the wikiSpaceId for larkdoc_create.",input_schema:{type:"object",properties:{}}},{name:"larkdoc_append",description:"Append markdown/text content to the END of an existing Lark/Feishu document (docx). Accepts a documentId or a full Lark doc URL. Returns { ok, documentId, url }.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Lark doc id (docx token) OR a full Lark/Feishu doc URL."},markdown:{type:"string",description:"Content to append, as markdown (preferred)."},text:{type:"string",description:"Content to append, as plain text (used when markdown is absent)."}},required:["documentId"]}},{name:"larkdoc_insert_image",description:"Append a LOCAL image file (png/jpg, max 10MB) to the END of an existing Lark/Feishu document (docx). Accepts a documentId or full doc URL plus a local file path. Optional width/height in pixels. Returns { ok, documentId, blockId, fileToken, url }.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Lark doc id (docx token) OR a full Lark/Feishu doc URL."},imagePath:{type:"string",description:"Local filesystem path to a .png or .jpg/.jpeg image (max 10MB)."},width:{type:"number",description:"Optional display width in pixels."},height:{type:"number",description:"Optional display height in pixels."}},required:["documentId","imagePath"]}},{name:"larkbitable_list_tables",description:"List the tables in a Lark/Feishu Base (\u591A\u7EF4\u8868\u683C / bitable). Accepts a Base URL (a /base/\u2026 link, or a /wiki/\u2026 link whose node IS a Base) or a raw app token. Returns { ok, appToken, name, tables:[{ tableId, name }] } plus linkedTableId/linkedViewId when the URL pinned them. A Base is NOT a document \u2014 larkdoc_get cannot read one; use this and larkbitable_read_records instead.",input_schema:{type:"object",properties:{baseId:{type:"string",description:"Lark Base app token OR a full Base/wiki URL (/base/\u2026 or /wiki/\u2026)."}},required:["baseId"]}},{name:"larkbitable_read_records",description:"Read the rows of one table in a Lark/Feishu Base (\u591A\u7EF4\u8868\u683C / bitable) as flat text. Accepts a Base URL (/base/\u2026 or a /wiki/\u2026 link fronting a Base) or app token; the table and view are taken from the URL's table=/view= when present, and tableId may be omitted when the Base has exactly one table. Optionally restrict to viewId or fieldNames. Returns { ok, appToken, name, tableId, total, count, columns:[{name,type}], records:[{ recordId, fields:{ <column>: <text> } }], hasMore, nextPageToken }. Every cell is a STRING (dates as ISO). Defaults to 200 records; pass maxRecords (max 1000) and pageToken to page through a bigger table. A WIDE table (dozens of columns) hits a character budget before the row cap: the reply then carries truncated:true, hasMore:true and a note, and the fix is fieldNames, not paging. Column names are matched EXACTLY and real ones can carry a leading space or a ' (1)' suffix \u2014 an unknown name is refused with this table's actual column list, so read columns[] (or one small call) before naming any. TO ANSWER 'what changed, when, by whom': pass includeRowMeta:true and each record also carries createdTime/createdBy/lastModifiedTime/lastModifiedBy (ISO + display name) \u2014 row METADATA that exists even when the table declares no such column. This is the ONLY way to see a row's edit time; Lark exposes NO per-cell edit history to any API, so a field's previous VALUE is unavailable (only the Base's own web UI record history has it). Filter and sort SERVER-SIDE rather than pulling the table and counting: filter = { conjunction:'and'|'or', conditions:[{ fieldName, operator, value }] } with operator one of is/isNot/contains/doesNotContain/isEmpty/isNotEmpty/isGreater/isGreaterEqual/isLess/isLessEqual, and sort = [{ fieldName, desc }]. With a filter the reply carries filtered:true and `total` is the MATCH count, so one call with maxRecords:1 answers 'how many rows are X'. Date/datetime columns compare against value ['ExactDate','<epoch ms>'] (Lark also accepts 'Today', 'Yesterday', 'CurrentWeek'); everything else takes plain strings.",input_schema:{type:"object",properties:{baseId:{type:"string",description:"Lark Base app token OR a full Base/wiki URL (/base/\u2026 or /wiki/\u2026)."},tableId:{type:"string",description:"Table id (tbl\u2026). Optional when the URL carries table= or the Base has exactly one table; larkbitable_list_tables lists them."},viewId:{type:"string",description:"Optional view id (vew\u2026) \u2014 restricts the read to that view's rows and order."},fieldNames:{type:"array",items:{type:"string"},description:"Optional column names to return. Absent = every column."},includeRowMeta:{type:"boolean",description:"Also return each row's createdTime/createdBy/lastModifiedTime/lastModifiedBy. Use for any 'what changed / who edited / when' question \u2014 without it those are absent, and a row count cannot substitute for them. createdTime answers WHAT WAS ADDED; lastModifiedTime answers WHAT WAS EDITED \u2014 a row edited yesterday was not added yesterday."},filter:{type:"object",description:"Server-side filter, evaluated by Lark. { conjunction:'and'|'or', conditions:[{ fieldName, operator, value }] }.",properties:{conjunction:{type:"string",description:"'and' (default) or 'or'."},conditions:{type:"array",items:{type:"object",properties:{fieldName:{type:"string",description:"Exact column name (see columns[])."},operator:{type:"string",description:"is | isNot | contains | doesNotContain | isEmpty | isNotEmpty | isGreater | isGreaterEqual | isLess | isLessEqual."},value:{description:"String, number, or array of them. Omit for isEmpty/isNotEmpty. Date columns take ['ExactDate','<epoch ms>'] or 'Today'/'Yesterday'/'CurrentWeek'."}},required:["fieldName","operator"]}}},required:["conditions"]},sort:{type:"array",description:'Server-side sort, e.g. [{ fieldName: "\u6700\u540E\u66F4\u65B0\u65F6\u95F4", desc: true }].',items:{type:"object",properties:{fieldName:{type:"string",description:"Exact column name."},desc:{type:"boolean",description:"Descending when true."}},required:["fieldName"]}},maxRecords:{type:"number",description:"Max records to return (default 200, hard cap 1000)."},pageToken:{type:"string",description:"Resume token from a previous call's nextPageToken."}},required:["baseId"]}},{name:"larkdoc_list_comments",description:"List the comment threads on a Lark/Feishu document. Accepts a documentId or full doc URL. Returns { ok, comments:[{ commentId, replies:[{ replyId, author, text }] }] }. Use to read the thread you are replying to.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Lark doc id (docx token) OR a full Lark/Feishu doc URL."},fileType:{type:"string",description:"Drive file type \u2014 defaults to 'docx'. Only change for non-docx files (doc/sheet/bitable/file/slides)."}},required:["documentId"]}},{name:"larkdoc_add_comment",description:"Post a NEW top-level comment on a Lark/Feishu document. Accepts a documentId or full doc URL plus text. Returns { ok, documentId, commentId }.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Lark doc id (docx token) OR a full Lark/Feishu doc URL."},text:{type:"string",description:"The comment body (plain text)."},fileType:{type:"string",description:"Drive file type \u2014 defaults to 'docx'."}},required:["documentId","text"]}},{name:"larkdoc_reply_comment",description:"Reply INSIDE an existing comment thread on a Lark/Feishu document. Pass { documentId, commentId, text }. Use to answer a user who @mentioned Zibby in a doc comment (reply in the same commentId). Returns { ok, documentId, commentId, replyId }.",input_schema:{type:"object",properties:{documentId:{type:"string",description:"Lark doc id (docx token) OR a full Lark/Feishu doc URL."},commentId:{type:"string",description:"The comment_id of the thread to reply into (from larkdoc_list_comments or the webhook event)."},text:{type:"string",description:"The reply body (plain text)."},fileType:{type:"string",description:"Drive file type \u2014 defaults to 'docx'."}},required:["documentId","commentId","text"]}}]};function tt(){I=null}export{Ee as LARK_DOCS_APP_ENV_KEYS,tt as _resetLarkDocsTokenCache,V as baseWebUrl,Ce as bitableRowMeta,je as buildBitableSearchBody,L as docWebUrl,et as larkDocsSkill,v as markdownToLarkBlocks,y as parseLarkDocRef,Oe as wikiWebUrl};
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zibby/skills",
3
- "version": "2.0.18",
3
+ "version": "2.0.19",
4
4
  "description": "Built-in skill definitions for the Zibby agent-workflow framework",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -117,10 +117,10 @@
117
117
  "dependencies": {
118
118
  "@modelcontextprotocol/sdk": "^1.29.0",
119
119
  "@resvg/resvg-js": "^2.6.2",
120
- "@zibby/agent-workflow": "2.0.18",
120
+ "@zibby/agent-workflow": "2.0.19",
121
121
  "@zibby/bin-oxlint": "^1.73.0",
122
122
  "@zibby/bin-semgrep": "^1.169.0",
123
- "@zibby/skill-ids": "2.0.18",
123
+ "@zibby/skill-ids": "2.0.19",
124
124
  "echarts": "^6.1.0",
125
125
  "mem0ai": "npm:@zibby/mem0ai@^3.0.5",
126
126
  "zod": "^3.23.0 || ^4.0.0"
@@ -135,7 +135,7 @@
135
135
  }
136
136
  },
137
137
  "optionalDependencies": {
138
- "@zibby/mcp-browser": "2.0.18",
138
+ "@zibby/mcp-browser": "2.0.19",
139
139
  "@zibby/mcp-memory": "*"
140
140
  },
141
141
  "devDependencies": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zibby/skills",
3
- "version": "2.0.18",
3
+ "version": "2.0.19",
4
4
  "description": "Built-in skill definitions for the Zibby agent-workflow framework",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -117,10 +117,10 @@
117
117
  "dependencies": {
118
118
  "@modelcontextprotocol/sdk": "^1.29.0",
119
119
  "@resvg/resvg-js": "^2.6.2",
120
- "@zibby/agent-workflow": "2.0.18",
120
+ "@zibby/agent-workflow": "2.0.19",
121
121
  "@zibby/bin-oxlint": "^1.73.0",
122
122
  "@zibby/bin-semgrep": "^1.169.0",
123
- "@zibby/skill-ids": "2.0.18",
123
+ "@zibby/skill-ids": "2.0.19",
124
124
  "echarts": "^6.1.0",
125
125
  "mem0ai": "npm:@zibby/mem0ai@^3.0.5",
126
126
  "zod": "^3.23.0 || ^4.0.0"
@@ -135,7 +135,7 @@
135
135
  }
136
136
  },
137
137
  "optionalDependencies": {
138
- "@zibby/mcp-browser": "2.0.18",
138
+ "@zibby/mcp-browser": "2.0.19",
139
139
  "@zibby/mcp-memory": "*"
140
140
  },
141
141
  "devDependencies": {