@zibby/skills 0.1.60 → 0.1.63
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 +102 -102
- package/dist/linkedin.d.ts +36 -0
- package/dist/linkedin.js +4 -4
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/linkedin.d.ts
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* INJECTED-TOKEN fast path (Zibby Copilot per-turn credential injection).
|
|
3
|
+
*
|
|
4
|
+
* The Zibby Copilot is ONE shared Slack/Lark bot backed by ONE project's
|
|
5
|
+
* PROJECT_API_TOKEN. resolveIntegrationToken(provider) authenticates with that
|
|
6
|
+
* PAT, so the account is inferred SERVER-SIDE from the PAT — meaning the shared
|
|
7
|
+
* bot would only ever see the PROJECT OWNER's LinkedIn, never the (different)
|
|
8
|
+
* person who actually sent the Slack message.
|
|
9
|
+
*
|
|
10
|
+
* To let the bot post to the VERIFIED SENDER's OWN LinkedIn, the copilot-runtime
|
|
11
|
+
* (a TRUSTED backend service that already KMS-decrypts tenants' creds directly)
|
|
12
|
+
* resolves the EMAIL-VERIFIED Slack sender's OWN linkedin_personal token straight
|
|
13
|
+
* from the encrypted store and injects it into THIS turn's env
|
|
14
|
+
* (ZIBBY_INJECTED_LINKEDIN_TOKEN / ZIBBY_INJECTED_LINKEDIN_MEMBER_ID, set +
|
|
15
|
+
* restored per-turn — never persisted, never cross-turn). When present it takes
|
|
16
|
+
* PRECEDENCE over resolveIntegrationToken so the post is attributed to the sender.
|
|
17
|
+
*
|
|
18
|
+
* SCOPED to linkedin_personal ONLY (member-profile publishing). It is NEVER used
|
|
19
|
+
* for the business/org provider (company Pages) or any other integration. Absent
|
|
20
|
+
* (no injection) → this returns null and the normal PAT chokepoint runs
|
|
21
|
+
* unchanged (self-host + cloud), so the Fargate linkedin-post workflow, which
|
|
22
|
+
* never sets these vars, is byte-for-byte unaffected.
|
|
23
|
+
*/
|
|
24
|
+
export function injectedPersonalToken(): {
|
|
25
|
+
token: string;
|
|
26
|
+
memberId: string;
|
|
27
|
+
};
|
|
1
28
|
/**
|
|
2
29
|
* Extract a numeric organization id from a urn (urn:li:organization:123) or a
|
|
3
30
|
* bare numeric id. Returns the id string, or null if none can be found.
|
|
@@ -52,6 +79,7 @@ export namespace linkedinSkill {
|
|
|
52
79
|
organizationUrn?: undefined;
|
|
53
80
|
text?: undefined;
|
|
54
81
|
visibility?: undefined;
|
|
82
|
+
dry_run?: undefined;
|
|
55
83
|
};
|
|
56
84
|
required?: undefined;
|
|
57
85
|
};
|
|
@@ -78,6 +106,10 @@ export namespace linkedinSkill {
|
|
|
78
106
|
enum: string[];
|
|
79
107
|
description: string;
|
|
80
108
|
};
|
|
109
|
+
dry_run: {
|
|
110
|
+
type: string;
|
|
111
|
+
description: string;
|
|
112
|
+
};
|
|
81
113
|
};
|
|
82
114
|
required: string[];
|
|
83
115
|
};
|
|
@@ -96,6 +128,10 @@ export namespace linkedinSkill {
|
|
|
96
128
|
enum: string[];
|
|
97
129
|
description: string;
|
|
98
130
|
};
|
|
131
|
+
dry_run: {
|
|
132
|
+
type: string;
|
|
133
|
+
description: string;
|
|
134
|
+
};
|
|
99
135
|
organizationId?: undefined;
|
|
100
136
|
organizationUrn?: undefined;
|
|
101
137
|
};
|
package/dist/linkedin.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import{existsSync as
|
|
1
|
+
import{existsSync as b}from"fs";import{fileURLToPath as k}from"url";import{dirname as L,resolve as _}from"path";import{resolveIntegrationToken as g,clearTokenCache as w}from"@zibby/core/backend-client.js";function S(){if(process.env.MCP_SKILL_PATH)return process.env.MCP_SKILL_PATH;let t=L(k(import.meta.url)),e=_(t,"..","bin","mcp-skill.mjs");return b(e)?e:null}function I(){let t=String(process.env.ZIBBY_INJECTED_LINKEDIN_TOKEN||"").trim();if(!t)return null;let e=String(process.env.ZIBBY_INJECTED_LINKEDIN_MEMBER_ID||"").trim();return{token:t,memberId:e}}var T="202506",v="https://api.linkedin.com";function f(t){if(t==null)return null;let e=String(t).trim();if(!e)return null;let i=e.match(/urn:li:organization:(\d+)/i);return i?i[1]:/^\d+$/.test(e)?e:null}function m(t){if(!t)return"";if(t.localizedName)return String(t.localizedName);let e=t.name&&t.name.localized;if(e&&typeof e=="object"){let i=Object.values(e)[0];if(i)return String(i)}return""}function p(t,e){return t?typeof t.get=="function"?t.get(e):t[e]||t[e.toLowerCase()]||null:null}async function c(t,e={},i="linkedin_business"){let l=async()=>{let n;if(i==="linkedin_personal"){let u=I();u&&(n=u.token)}if(n||({token:n}=await g(i)),typeof n!="string"||!n)throw new Error("LinkedIn is not connected: no access token available. Connect LinkedIn in Integrations.");let o=t.startsWith("https://")?t:`${v}${t}`,r=await fetch(o,{method:e.method||"GET",headers:{Authorization:`Bearer ${n}`,"LinkedIn-Version":T,"X-Restli-Protocol-Version":"2.0.0",Accept:"application/json",...e.body?{"Content-Type":"application/json"}:{},...e.headers},body:e.body?JSON.stringify(e.body):void 0});if(!r.ok){let u=await r.text().catch(()=>"");throw new Error(`LinkedIn API ${r.status}: ${u.slice(0,300)}`)}let d=await r.text().catch(()=>""),s={};if(d&&d.trim())try{s=JSON.parse(d)}catch{s={raw:d}}return{status:r.status,headers:r.headers,body:s}};try{return await l()}catch(n){let o=String(n?.message||n||"").toLowerCase();if(!(o.includes("token")||o.includes("401")||o.includes("unauthorized")))throw n;return w(i),l()}}var E={id:"linkedin",serverName:"linkedin",allowedTools:["mcp__linkedin__*"],envKeys:[],description:"LinkedIn \u2014 (business) list admin Organizations + create DRAFT posts on a company Page, and (personal) PUBLISH a post to your own member profile feed",promptFragment:`## LinkedIn (connected)
|
|
2
2
|
You can post to LinkedIn two ways \u2014 an ORG company Page (business) and your own member PROFILE (personal). Each path needs its own LinkedIn integration connected.
|
|
3
3
|
|
|
4
4
|
Business (company Page) \u2014 DRAFT only:
|
|
5
5
|
- linkedin_list_organizations: List the Organizations (company Pages) the member ADMINISTERS. Returns [{ id, urn, name, vanityName }]. Call this first to choose the author org. (needs LinkedIn Business connected)
|
|
6
|
-
- linkedin_create_draft_post: Create a DRAFT post on a company Page. Pass organizationId (or organizationUrn) + text (the post commentary). The post is created in DRAFT state (never published automatically) so a human reviews and publishes it in LinkedIn. Returns { postUrn }. (needs LinkedIn Business connected)
|
|
6
|
+
- linkedin_create_draft_post: Create a DRAFT post on a company Page. Pass organizationId (or organizationUrn) + text (the post commentary). The post is created in DRAFT state (never published automatically) so a human reviews and publishes it in LinkedIn. Returns { postUrn }. Set dry_run:true to VALIDATE which LinkedIn account/profile the post would go to (and preview the text) WITHOUT posting \u2014 nothing is published; returns { dryRun, target, wouldPostAs, textPreview }. (needs LinkedIn Business connected)
|
|
7
7
|
|
|
8
8
|
Personal (your own profile) \u2014 PUBLISHES IMMEDIATELY:
|
|
9
|
-
- linkedin_publish_post: PUBLISH a post to your OWN member profile feed. Pass text (the post body) + optional visibility ('PUBLIC' default, or 'CONNECTIONS'). UNLIKE the org draft tool, this PUBLISHES the post immediately \u2014 LinkedIn has no DRAFT state for member profiles, so there is no human review step. Returns { postUrn }. (needs LinkedIn Personal connected)
|
|
9
|
+
- linkedin_publish_post: PUBLISH a post to your OWN member profile feed. Pass text (the post body) + optional visibility ('PUBLIC' default, or 'CONNECTIONS'). UNLIKE the org draft tool, this PUBLISHES the post immediately \u2014 LinkedIn has no DRAFT state for member profiles, so there is no human review step. Returns { postUrn }. Set dry_run:true to VALIDATE which member profile the post would publish to (and preview the text) WITHOUT publishing \u2014 nothing is posted; returns { dryRun, target, wouldPostAs, textPreview }. Use this to confirm the identity before an approved publish. (needs LinkedIn Personal connected)
|
|
10
10
|
|
|
11
11
|
Notes:
|
|
12
12
|
- Org-page posts are ALWAYS created as a DRAFT; personal-profile posts are ALWAYS published live \u2014 choose the tool accordingly.
|
|
13
13
|
- PUBLISHING IS OUTWARD-FACING AND IRREVERSIBLE. linkedin_publish_post posts live to a real audience the instant you call it \u2014 there is no undo and no draft state for member profiles. Only call it after the human has EXPLICITLY approved publishing THIS specific text in THIS conversation. If you wrote or edited the text, show it and get an explicit "yes, publish" first; if you are unsure, HOLD/draft rather than publish. The org draft tool (linkedin_create_draft_post) is the safe default when a human still needs to review.
|
|
14
|
-
- If the relevant LinkedIn integration is not connected these tools return { ok:false, error }; treat that as "LinkedIn unavailable" and continue.`,resolve(){let t=
|
|
14
|
+
- If the relevant LinkedIn integration is not connected these tools return { ok:false, error }; treat that as "LinkedIn unavailable" and continue.`,resolve(){let t=S();return t?{type:"stdio",command:"node",args:[t,"../dist/linkedin.js","linkedinSkill"],env:{},description:this.description,alwaysLoad:!0}:{command:null,args:[],env:{},description:this.description}},async handleToolCall(t,e){try{switch(t){case"linkedin_list_organizations":{let{body:i}=await c("/rest/organizationAcls?q=roleAssignee&role=ADMINISTRATOR&state=APPROVED",{},"linkedin_business"),l=Array.isArray(i.elements)?i.elements:[],n=[];for(let o of l){let r=o.organizationalTarget||o["organizationalTarget~"]||o.organization,d=f(r);if(!d)continue;let s="",u="";try{let a=(await c(`/rest/organizations/${d}`,{},"linkedin_business")).body;s=m(a),u=a.vanityName||""}catch{}n.push({id:d,urn:`urn:li:organization:${d}`,name:s,vanityName:u})}return JSON.stringify({ok:!0,count:n.length,organizations:n})}case"linkedin_create_draft_post":{let i=e?.organizationUrn||e?.organizationId||e?.organization||e?.orgId,l=f(i);if(!l)return JSON.stringify({ok:!1,error:"A valid organizationId or organizationUrn (urn:li:organization:{id}) is required"});let n=e?.text;if(typeof n!="string"||!n.trim())return JSON.stringify({ok:!1,error:"text (the post commentary) is required"});let o=e?.visibility?String(e.visibility).toUpperCase():"PUBLIC",r=`urn:li:organization:${l}`;if(e?.dry_run===!0)try{let a="";try{let h=(await c(`/rest/organizations/${l}`,{},"linkedin_business")).body;a=m(h)}catch{}return JSON.stringify({dryRun:!0,target:"organization",wouldPostAs:{name:a,id:l,urn:r},visibility:o,textPreview:n,note:"DRY RUN \u2014 nothing was posted"})}catch(a){return JSON.stringify({dryRun:!0,ok:!1,error:a.message})}let s=await c("/rest/posts",{method:"POST",body:{author:r,commentary:n,visibility:o,distribution:{feedDistribution:"MAIN_FEED",targetEntities:[],thirdPartyDistributionChannels:[]},lifecycleState:"DRAFT",isReshareDisabledByAuthor:!1}},"linkedin_business"),u=p(s.headers,"x-restli-id")||p(s.headers,"x-linkedin-id")||s.body?.id||null;return JSON.stringify({ok:!0,postUrn:u,author:r,lifecycleState:"DRAFT",visibility:o,status:s.status})}case"linkedin_publish_post":{let i=I(),l=i?i.memberId:(await g("linkedin_personal")).memberId;if(!l)return JSON.stringify({ok:!1,error:"LinkedIn (personal) not connected or member id unavailable \u2014 reconnect LinkedIn Personal"});let n=e?.text;if(typeof n!="string"||!n.trim())return JSON.stringify({ok:!1,error:"text (the post body) is required"});let o=e?.visibility?String(e.visibility).toUpperCase():"PUBLIC",r=`urn:li:person:${l}`;if(e?.dry_run===!0)try{let a=(await c("/v2/userinfo",{},"linkedin_personal")).body,h=a?.name||[a?.given_name,a?.family_name].filter(Boolean).join(" ")||"",y=a?.sub?String(a.sub):l;return JSON.stringify({dryRun:!0,target:"member",wouldPostAs:{name:h,id:y,urn:`urn:li:person:${y}`},visibility:o,textPreview:n,note:"DRY RUN \u2014 nothing was posted"})}catch(a){return JSON.stringify({dryRun:!0,ok:!1,error:a.message})}let s=await c("/rest/posts",{method:"POST",body:{author:r,commentary:n,visibility:o,distribution:{feedDistribution:"MAIN_FEED",targetEntities:[],thirdPartyDistributionChannels:[]},lifecycleState:"PUBLISHED",isReshareDisabledByAuthor:!1}},"linkedin_personal"),u=p(s.headers,"x-restli-id")||p(s.headers,"x-linkedin-id")||s.body?.id||null;return JSON.stringify({ok:!0,postUrn:u,author:r,lifecycleState:"PUBLISHED",visibility:o,status:s.status})}default:return JSON.stringify({ok:!1,error:`Unknown tool: ${t}`})}}catch(i){return JSON.stringify({ok:!1,error:i.message})}},tools:[{name:"linkedin_list_organizations",description:"List the LinkedIn Organizations (company Pages) the authenticated member ADMINISTERS. Returns [{ id, urn, name, vanityName }]. Call this first to choose the author org for a draft post.",input_schema:{type:"object",properties:{}}},{name:"linkedin_create_draft_post",description:"Create a DRAFT post on a LinkedIn Organization (company Page). The post is created in DRAFT state (never published automatically) so a human can review and publish it in LinkedIn. Returns the created post URN. Set dry_run:true to VALIDATE which LinkedIn account/profile the post would go to (and preview the text) WITHOUT posting \u2014 nothing is published.",input_schema:{type:"object",properties:{organizationId:{type:"string",description:'The numeric organization id (e.g. "12345"). Alternative to organizationUrn.'},organizationUrn:{type:"string",description:'The organization URN, e.g. "urn:li:organization:12345". Alternative to organizationId.'},text:{type:"string",description:"The post commentary (the body text of the post)."},visibility:{type:"string",enum:["PUBLIC"],description:"Post visibility. Defaults to PUBLIC."},dry_run:{type:"boolean",description:"Set dry_run:true to VALIDATE which LinkedIn account/profile the post would go to (and preview the text) WITHOUT posting \u2014 nothing is published. Returns { dryRun, target, wouldPostAs, visibility, textPreview }. Defaults to false."}},required:["text"]}},{name:"linkedin_publish_post",description:"PUBLISH a post to the authenticated member's OWN LinkedIn profile feed (personal). UNLIKE linkedin_create_draft_post (which only drafts on a company Page), this PUBLISHES the post IMMEDIATELY \u2014 LinkedIn has no DRAFT state for member profiles, so there is no human review step. Returns the created post URN. Requires the LinkedIn Personal integration connected. Set dry_run:true to VALIDATE which LinkedIn account/profile the post would go to (and preview the text) WITHOUT posting \u2014 nothing is published.",input_schema:{type:"object",properties:{text:{type:"string",description:"The post body (the commentary text of the post)."},visibility:{type:"string",enum:["PUBLIC","CONNECTIONS"],description:"Post visibility: PUBLIC (anyone) or CONNECTIONS (your connections only). Defaults to PUBLIC."},dry_run:{type:"boolean",description:"Set dry_run:true to VALIDATE which LinkedIn account/profile the post would go to (and preview the text) WITHOUT posting \u2014 nothing is published. Returns { dryRun, target, wouldPostAs, visibility, textPreview }. Defaults to false."}},required:["text"]}}]};export{I as injectedPersonalToken,c as linkedinApi,E as linkedinSkill,f as parseOrgId};
|
package/dist/package.json
CHANGED