publishport-opencli 1.0.12 → 1.0.14

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.
Files changed (112) hide show
  1. package/cli-manifest.json +1083 -181
  2. package/clis/antigravity/send.js +1 -1
  3. package/clis/bilibili/comment.js +1 -1
  4. package/clis/bilibili/dm-send.js +1 -1
  5. package/clis/bilibili/dynamic-post.js +1 -1
  6. package/clis/bilibili/upload.js +1 -1
  7. package/clis/bluesky/post.js +1 -1
  8. package/clis/boss/batchgreet.js +1 -1
  9. package/clis/boss/greet.js +1 -1
  10. package/clis/boss/send.js +1 -1
  11. package/clis/chatgpt/ask.js +1 -1
  12. package/clis/chatgpt/image.js +1 -1
  13. package/clis/chatgpt/send.js +1 -1
  14. package/clis/chatgpt-app/ask.js +1 -1
  15. package/clis/chatgpt-app/send.js +1 -1
  16. package/clis/chatwise/ask.js +1 -1
  17. package/clis/chatwise/send.js +1 -1
  18. package/clis/claude/ask.js +1 -1
  19. package/clis/claude/send.js +1 -1
  20. package/clis/codex/ask.js +1 -1
  21. package/clis/codex/send.js +1 -1
  22. package/clis/cursor/ask.js +1 -1
  23. package/clis/cursor/composer.js +1 -1
  24. package/clis/cursor/send.js +1 -1
  25. package/clis/deepseek/ask.js +1 -1
  26. package/clis/deepseek/send.js +1 -1
  27. package/clis/discord-web/send.js +1 -1
  28. package/clis/doubao/ask.js +1 -1
  29. package/clis/doubao-app/ask.js +1 -1
  30. package/clis/doubao-app/send.js +1 -1
  31. package/clis/douyin/draft.js +1 -1
  32. package/clis/douyin/publish.js +1 -1
  33. package/clis/douyin/update.js +1 -1
  34. package/clis/facebook/comment.js +1 -1
  35. package/clis/facebook/post.js +1 -1
  36. package/clis/gemini/ask.js +1 -1
  37. package/clis/gemini/deep-research.js +1 -1
  38. package/clis/gemini/image.js +2 -2
  39. package/clis/grok/ask.js +1 -1
  40. package/clis/grok/image.js +1 -1
  41. package/clis/grok/send.js +1 -1
  42. package/clis/hackernews/comment.js +1 -1
  43. package/clis/hackernews/submit.js +1 -1
  44. package/clis/instagram/comment.js +1 -1
  45. package/clis/instagram/note.js +1 -1
  46. package/clis/instagram/post.js +1 -1
  47. package/clis/instagram/reel.js +1 -1
  48. package/clis/jike/comment.js +1 -1
  49. package/clis/jike/create.js +1 -1
  50. package/clis/jike/repost.js +1 -1
  51. package/clis/jimeng/generate.js +1 -1
  52. package/clis/kimi/chat.js +1 -1
  53. package/clis/kuaishou/publish.js +1 -1
  54. package/clis/linkedin/comment.js +1 -1
  55. package/clis/linkedin/create.js +1 -1
  56. package/clis/linkedin/reply.js +1 -1
  57. package/clis/linkedin/repost.js +1 -1
  58. package/clis/linkedin/safe-send.js +1 -1
  59. package/clis/linkedin/salesnav-message.js +2 -2
  60. package/clis/note/publish.js +1 -1
  61. package/clis/notebooklm/add-source.js +1 -1
  62. package/clis/notebooklm/write-note.js +1 -1
  63. package/clis/nowcoder/comment.js +1 -1
  64. package/clis/nowcoder/dm-send.js +1 -1
  65. package/clis/nowcoder/post.js +1 -1
  66. package/clis/pinterest/publish.js +1 -1
  67. package/clis/qoder/quest.js +1 -1
  68. package/clis/quora/comment.js +1 -1
  69. package/clis/qwen/ask.js +1 -1
  70. package/clis/qwen/image.js +1 -1
  71. package/clis/qwen/send.js +1 -1
  72. package/clis/reddit/comment.js +1 -1
  73. package/clis/reddit/reply.js +1 -1
  74. package/clis/reddit/submit.js +1 -1
  75. package/clis/slock/message-send.js +1 -1
  76. package/clis/substack/publish.js +1 -1
  77. package/clis/suno/generate.js +1 -1
  78. package/clis/telegram/send.js +1 -1
  79. package/clis/threads/post.js +1 -1
  80. package/clis/tiktok/comment.js +1 -1
  81. package/clis/trae-cn/ask.js +1 -1
  82. package/clis/trae-cn/new.js +1 -1
  83. package/clis/twitter/dm-send.js +1 -1
  84. package/clis/twitter/post.js +1 -1
  85. package/clis/twitter/quote.js +1 -1
  86. package/clis/twitter/reply-dm.js +1 -1
  87. package/clis/twitter/schedule.js +1 -1
  88. package/clis/vk/post.js +1 -1
  89. package/clis/wechat-channels/publish.js +1 -1
  90. package/clis/weixin/create-draft.js +1 -1
  91. package/clis/xianyu/chat.js +1 -1
  92. package/clis/xianyu/manage.js +1 -1
  93. package/clis/xianyu/order.js +1 -1
  94. package/clis/xianyu/publish.js +1 -1
  95. package/clis/xianyu/reply.js +1 -1
  96. package/clis/yollomi/background.js +1 -1
  97. package/clis/yollomi/edit.js +1 -1
  98. package/clis/yollomi/generate.js +1 -1
  99. package/clis/yollomi/video.js +1 -1
  100. package/clis/youtube/publish.js +1 -1
  101. package/clis/yuanbao/ask.js +1 -1
  102. package/clis/yuanbao/send.js +1 -1
  103. package/dist/src/build-manifest.js +3 -3
  104. package/dist/src/cli-argv-preprocess.d.ts +1 -0
  105. package/dist/src/cli-argv-preprocess.js +1 -1
  106. package/dist/src/execution.d.ts +8 -0
  107. package/dist/src/execution.js +2 -2
  108. package/dist/src/manifest-types.d.ts +8 -0
  109. package/dist/src/registry.d.ts +21 -0
  110. package/dist/src/registry.js +1 -1
  111. package/dist/src/serialization.js +2 -2
  112. package/package.json +1 -1
@@ -1 +1 @@
1
- import{cli as I,Strategy as h}from"@jackwener/opencli/registry";import{ArgumentError as x}from"@jackwener/opencli/errors";import{GEMINI_DEEP_RESEARCH_DEFAULT_CONFIRM_LABELS as C,GEMINI_DEEP_RESEARCH_DEFAULT_TOOL_LABELS as F,GEMINI_APP_URL as S,GEMINI_DOMAIN as T,getCurrentGeminiUrl as H,getLatestGeminiAssistantResponse as D,readGeminiSnapshot as E,resolveGeminiLabels as O,selectGeminiTool as Y,sendGeminiMessage as _,startNewGeminiChat as G,waitForGeminiSubmission as M,waitForGeminiConfirmButton as Z}from"./utils.js";function $(j){try{const q=new URL(j);return q.origin+q.pathname.replace(/\/+$/,"")===S}catch{return!1}}function N(j){const q=/\bresearching(?:\s+websites?)?\b|research in progress|working on your research|正在研究|研究中/i.test(j),J=/\bstart(?:\s+deep)?\s+research\b|begin\s+research|generate(?:\s+deep)?\s+research\s+plan|开始研究|开始深度研究|开始调研|生成研究计划|生成调研计划|try again without deep research/i.test(j);return{isResearching:q,waitingForStart:J}}export const deepResearchCommand=I({site:"gemini",name:"deep-research",access:"write",description:"Start a Gemini Deep Research run and confirm it",domain:T,strategy:h.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,defaultFormat:"plain",args:[{name:"prompt",positional:!0,required:!0,help:"Prompt to send"},{name:"timeout",type:"int",required:!1,help:"Max seconds for the overall command (default: 180; confirm-wait clamps internally to 6-20s)",default:180},{name:"tool",required:!1,help:"Override tool label (default: Deep Research)"},{name:"confirm",required:!1,help:"Override confirm button label (default: Start research)"}],columns:["status","url"],func:async(j,q)=>{const{prompt:J,timeout:z}=q;if(!Number.isInteger(z)||z<1)throw new x("--timeout must be a positive integer (seconds)");const y=Math.min(Math.max(z,6),20);await G(j);const K=O(q.tool,F),P=O(q.confirm,C);if(!await Y(j,K))return[{status:"tool-not-found",url:await H(j)}];let Q=await E(j);await _(j,J);let U=await M(j,Q,y);if(!U){await Y(j,K);Q=await E(j);await _(j,J);U=await M(j,Q,y)}if(!U)return[{status:"submit-not-found",url:await H(j)}];const A=await Z(j,P,z);let v=await H(j);if(A&&!$(v))return[{status:"started",url:v}];{if($(v)){await Y(j,K);const X=await Z(j,P,z);v=await H(j);if(X&&!$(v))return[{status:"started",url:v}]}let B=await D(j),{isResearching:V,waitingForStart:W}=N(B);if(!V&&W){const X=Array.from(new Set([...P,...C]));if(await Z(j,X,Math.min(z,8))){v=await H(j);B=await D(j);({isResearching:V,waitingForStart:W}=N(B))}}if(V&&!W)return[{status:"started",url:v}];return[{status:"confirm-not-found",url:v}]}}});
1
+ import{cli as I,Strategy as h}from"@jackwener/opencli/registry";import{ArgumentError as x}from"@jackwener/opencli/errors";import{GEMINI_DEEP_RESEARCH_DEFAULT_CONFIRM_LABELS as C,GEMINI_DEEP_RESEARCH_DEFAULT_TOOL_LABELS as F,GEMINI_APP_URL as S,GEMINI_DOMAIN as T,getCurrentGeminiUrl as H,getLatestGeminiAssistantResponse as D,readGeminiSnapshot as E,resolveGeminiLabels as O,selectGeminiTool as Y,sendGeminiMessage as _,startNewGeminiChat as G,waitForGeminiSubmission as M,waitForGeminiConfirmButton as Z}from"./utils.js";function $(j){try{const q=new URL(j);return q.origin+q.pathname.replace(/\/+$/,"")===S}catch{return!1}}function N(j){const q=/\bresearching(?:\s+websites?)?\b|research in progress|working on your research|正在研究|研究中/i.test(j),J=/\bstart(?:\s+deep)?\s+research\b|begin\s+research|generate(?:\s+deep)?\s+research\s+plan|开始研究|开始深度研究|开始调研|生成研究计划|生成调研计划|try again without deep research/i.test(j);return{isResearching:q,waitingForStart:J}}export const deepResearchCommand=I({site:"gemini",name:"deep-research",access:"write",description:"Start a Gemini Deep Research run and confirm it",domain:T,strategy:h.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,defaultFormat:"plain",args:[{name:"prompt",fromFile:!0,positional:!0,required:!0,help:"Prompt to send"},{name:"timeout",type:"int",required:!1,help:"Max seconds for the overall command (default: 180; confirm-wait clamps internally to 6-20s)",default:180},{name:"tool",required:!1,help:"Override tool label (default: Deep Research)"},{name:"confirm",required:!1,help:"Override confirm button label (default: Start research)"}],columns:["status","url"],func:async(j,q)=>{const{prompt:J,timeout:z}=q;if(!Number.isInteger(z)||z<1)throw new x("--timeout must be a positive integer (seconds)");const y=Math.min(Math.max(z,6),20);await G(j);const K=O(q.tool,F),P=O(q.confirm,C);if(!await Y(j,K))return[{status:"tool-not-found",url:await H(j)}];let Q=await E(j);await _(j,J);let U=await M(j,Q,y);if(!U){await Y(j,K);Q=await E(j);await _(j,J);U=await M(j,Q,y)}if(!U)return[{status:"submit-not-found",url:await H(j)}];const A=await Z(j,P,z);let v=await H(j);if(A&&!$(v))return[{status:"started",url:v}];{if($(v)){await Y(j,K);const X=await Z(j,P,z);v=await H(j);if(X&&!$(v))return[{status:"started",url:v}]}let B=await D(j),{isResearching:V,waitingForStart:W}=N(B);if(!V&&W){const X=Array.from(new Set([...P,...C]));if(await Z(j,X,Math.min(z,8))){v=await H(j);B=await D(j);({isResearching:V,waitingForStart:W}=N(B))}}if(V&&!W)return[{status:"started",url:v}];return[{status:"confirm-not-found",url:v}]}}});
@@ -1,3 +1,3 @@
1
- import*as f from"node:os";import*as p from"node:path";import{cli as S,Strategy as k}from"@jackwener/opencli/registry";import{saveBase64ToFile as v}from"@jackwener/opencli/utils";import{ArgumentError as F}from"@jackwener/opencli/errors";import{GEMINI_DOMAIN as D,exportGeminiImages as M,getGeminiVisibleImageUrls as j,sendGeminiMessage as z,startNewGeminiChat as C,waitForGeminiImages as N}from"./utils.js";function O(e){if(e.includes("png"))return".png";if(e.includes("webp"))return".webp";if(e.includes("gif"))return".gif";return".jpg"}function q(e){if(typeof e==="boolean")return e;const t=String(e??"").trim().toLowerCase();return t==="true"||t==="1"||t==="yes"||t==="on"}function B(e){const t=f.homedir();return e.startsWith(t)?`~${e.slice(t.length)}`:e}function E(e,t){const i=[];if(t.ratio)i.push(`aspect ratio ${t.ratio}`);if(t.style)i.push(`style ${t.style}`);if(i.length===0)return e;return`${e}
1
+ import*as f from"node:os";import*as p from"node:path";import{cli as S,Strategy as F}from"@jackwener/opencli/registry";import{saveBase64ToFile as k}from"@jackwener/opencli/utils";import{ArgumentError as v}from"@jackwener/opencli/errors";import{GEMINI_DOMAIN as D,exportGeminiImages as M,getGeminiVisibleImageUrls as j,sendGeminiMessage as z,startNewGeminiChat as C,waitForGeminiImages as N}from"./utils.js";function O(e){if(e.includes("png"))return".png";if(e.includes("webp"))return".webp";if(e.includes("gif"))return".gif";return".jpg"}function q(e){if(typeof e==="boolean")return e;const t=String(e??"").trim().toLowerCase();return t==="true"||t==="1"||t==="yes"||t==="on"}function B(e){const t=f.homedir();return e.startsWith(t)?`~${e.slice(t.length)}`:e}function E(e,t){const i=[];if(t.ratio)i.push(`aspect ratio ${t.ratio}`);if(t.style)i.push(`style ${t.style}`);if(i.length===0)return e;return`${e}
2
2
 
3
- Image requirements: ${i.join(", ")}.`}function R(e){const t=e.trim();return new Set(["1:1","16:9","9:16","4:3","3:4","3:2","2:3"]).has(t)?t:"1:1"}async function U(e){const t=await e.evaluate("window.location.href").catch(()=>"");return typeof t==="string"&&t?t:"https://gemini.google.com/app"}export const imageCommand=S({site:"gemini",name:"image",access:"write",description:"Generate images with Gemini web and save them locally",domain:D,strategy:k.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,defaultFormat:"plain",args:[{name:"prompt",positional:!0,required:!0,help:"Image prompt to send to Gemini"},{name:"rt",default:"1:1",help:"Ratio shorthand for aspect ratio (1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3)"},{name:"st",default:"",help:"Style shorthand, e.g. anime, icon, watercolor"},{name:"op",default:"~/tmp/gemini-images",help:"Output directory shorthand"},{name:"sd",type:"boolean",default:!1,help:"Skip download shorthand; only show Gemini page link"},{name:"timeout",type:"int",required:!1,default:240,help:"Max seconds for the overall command (default: 240)"}],columns:["status","file","link"],func:async(e,t)=>{const i=t.prompt,d=R(String(t.rt??"1:1")),h=String(t.st??"").trim(),g=t.op||p.join(f.homedir(),"tmp","gemini-images"),s=t.timeout;if(!Number.isInteger(s)||s<1)throw new F("--timeout must be a positive integer (seconds)");const w=!0,a=t.sd,y=a===""||a===!0||q(a),$=E(i,{ratio:d,style:h||void 0});if(w)await C(e);const b=await j(e);await z(e,$);const l=await N(e,b,s),n=await U(e);if(!l.length)return[{status:"⚠️ no-images",file:"\uD83D\uDCC1 -",link:`\uD83D\uDD17 ${n}`}];if(y)return[{status:"\uD83C\uDFA8 generated",file:"\uD83D\uDCC1 -",link:`\uD83D\uDD17 ${n}`}];const o=await M(e,l);if(!o.length)return[{status:"⚠️ export-failed",file:"\uD83D\uDCC1 -",link:`\uD83D\uDD17 ${n}`}];const G=Date.now(),m=[];for(let r=0;r<o.length;r+=1){const u=o[r],I=u.dataUrl.replace(/^data:[^;]+;base64,/,""),x=o.length>1?`_${r+1}`:"",c=p.join(g,`gemini_${G}${x}${O(u.mimeType)}`);await v(I,c);m.push({status:"✅ saved",file:`\uD83D\uDCC1 ${B(c)}`,link:`\uD83D\uDD17 ${n}`})}return m}});
3
+ Image requirements: ${i.join(", ")}.`}function R(e){const t=e.trim();return new Set(["1:1","16:9","9:16","4:3","3:4","3:2","2:3"]).has(t)?t:"1:1"}async function U(e){const t=await e.evaluate("window.location.href").catch(()=>"");return typeof t==="string"&&t?t:"https://gemini.google.com/app"}export const imageCommand=S({site:"gemini",name:"image",access:"write",description:"Generate images with Gemini web and save them locally",domain:D,strategy:F.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,defaultFormat:"plain",args:[{name:"prompt",fromFile:!0,positional:!0,required:!0,help:"Image prompt to send to Gemini"},{name:"rt",default:"1:1",help:"Ratio shorthand for aspect ratio (1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3)"},{name:"st",default:"",help:"Style shorthand, e.g. anime, icon, watercolor"},{name:"op",default:"~/tmp/gemini-images",help:"Output directory shorthand"},{name:"sd",type:"boolean",default:!1,help:"Skip download shorthand; only show Gemini page link"},{name:"timeout",type:"int",required:!1,default:240,help:"Max seconds for the overall command (default: 240)"}],columns:["status","file","link"],func:async(e,t)=>{const i=t.prompt,d=R(String(t.rt??"1:1")),h=String(t.st??"").trim(),g=t.op||p.join(f.homedir(),"tmp","gemini-images"),s=t.timeout;if(!Number.isInteger(s)||s<1)throw new v("--timeout must be a positive integer (seconds)");const w=!0,a=t.sd,y=a===""||a===!0||q(a),$=E(i,{ratio:d,style:h||void 0});if(w)await C(e);const b=await j(e);await z(e,$);const l=await N(e,b,s),n=await U(e);if(!l.length)return[{status:"⚠️ no-images",file:"\uD83D\uDCC1 -",link:`\uD83D\uDD17 ${n}`}];if(y)return[{status:"\uD83C\uDFA8 generated",file:"\uD83D\uDCC1 -",link:`\uD83D\uDD17 ${n}`}];const o=await M(e,l);if(!o.length)return[{status:"⚠️ export-failed",file:"\uD83D\uDCC1 -",link:`\uD83D\uDD17 ${n}`}];const G=Date.now(),m=[];for(let r=0;r<o.length;r+=1){const u=o[r],I=u.dataUrl.replace(/^data:[^;]+;base64,/,""),x=o.length>1?`_${r+1}`:"",c=p.join(g,`gemini_${G}${x}${O(u.mimeType)}`);await k(I,c);m.push({status:"✅ saved",file:`\uD83D\uDCC1 ${B(c)}`,link:`\uD83D\uDD17 ${n}`})}return m}});
package/clis/grok/ask.js CHANGED
@@ -1 +1 @@
1
- import{cli as X,Strategy as Y}from"@jackwener/opencli/registry";import{CommandExecutionError as Z,TimeoutError as $}from"@jackwener/opencli/errors";import{authRequired as q,ensureOnGrok as z,getMessageBubbles as G,isLoggedIn as H,normalizeBooleanFlag as _,sendMessage as x,startNewChat as y,waitForAnswer as C}from"./utils.js";const F="Likely login/auth/challenge/session issue in the existing grok.com browser session.";async function Q(f){const v=await G(f);for(let D=v.length-1;D>=0;D-=1)if(v[D].role==="Assistant")return v[D].id;return""}export const askCommand=X({site:"grok",name:"ask",access:"write",description:"Send a message to Grok and get response",domain:"grok.com",strategy:Y.COOKIE,browser:!0,siteSession:"persistent",args:[{name:"prompt",positional:!0,type:"string",required:!0,help:"Prompt to send to Grok"},{name:"timeout",type:"int",default:120,help:"Max seconds to wait for response (default: 120)"},{name:"new",type:"boolean",default:!1,help:"Start a new chat before sending (default: false)"}],columns:["response"],func:async(f,v)=>{const D=v.prompt,P=v.timeout||120;if(_(v.new))await y(f);else await z(f);if(!await H(f))throw q();const U=await Q(f),J=await x(f,D);if(!J||!J.ok){const V=J?.reason||"Unable to send the prompt to Grok.",W=J?.detail?` ${J.detail}`:"";throw new Z(`${V}${W}`,F)}const K=await C(f,D,P,U);if(K.status==="ok")return[{response:K.assistant.text}];if(K.status==="partial"&&K.assistant)return[{response:K.assistant.text}];throw new $("grok ask response",P)}}),__test__={getBaselineLastAssistantId:Q};
1
+ import{cli as X,Strategy as Y}from"@jackwener/opencli/registry";import{CommandExecutionError as Z,TimeoutError as $}from"@jackwener/opencli/errors";import{authRequired as q,ensureOnGrok as z,getMessageBubbles as G,isLoggedIn as H,normalizeBooleanFlag as _,sendMessage as x,startNewChat as y,waitForAnswer as C}from"./utils.js";const F="Likely login/auth/challenge/session issue in the existing grok.com browser session.";async function Q(f){const v=await G(f);for(let D=v.length-1;D>=0;D-=1)if(v[D].role==="Assistant")return v[D].id;return""}export const askCommand=X({site:"grok",name:"ask",access:"write",description:"Send a message to Grok and get response",domain:"grok.com",strategy:Y.COOKIE,browser:!0,siteSession:"persistent",args:[{name:"prompt",fromFile:!0,positional:!0,type:"string",required:!0,help:"Prompt to send to Grok"},{name:"timeout",type:"int",default:120,help:"Max seconds to wait for response (default: 120)"},{name:"new",type:"boolean",default:!1,help:"Start a new chat before sending (default: false)"}],columns:["response"],func:async(f,v)=>{const D=v.prompt,P=v.timeout||120;if(_(v.new))await y(f);else await z(f);if(!await H(f))throw q();const U=await Q(f),J=await x(f,D);if(!J||!J.ok){const V=J?.reason||"Unable to send the prompt to Grok.",W=J?.detail?` ${J.detail}`:"";throw new Z(`${V}${W}`,F)}const K=await C(f,D,P,U);if(K.status==="ok")return[{response:K.assistant.text}];if(K.status==="partial"&&K.assistant)return[{response:K.assistant.text}];throw new $("grok ask response",P)}}),__test__={getBaselineLastAssistantId:Q};
@@ -116,4 +116,4 @@ import*as L from"node:fs";import*as k from"node:path";import*as I from"node:cryp
116
116
  for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
117
117
  return { ok: true, base64: btoa(binary), contentType: blob.type || 'image/jpeg' };
118
118
  } catch (e) { return { ok: false, error: e && e.message || String(e) }; }
119
- })()`)}async function v(q,Q,W){L.mkdirSync(W,{recursive:!0});const X=[];for(const Y of Q){const Z=await p(q,Y.src);if(!Z||!Z.ok){const M=Z?.error?`: ${Z.error}`:"";throw new x(`Failed to download grok image ${Y.src}${M}`,"assets.grok.com download requires the live grok.com browser session — verify the tab is logged in and try again.")}const H=k.join(W,B(Y.src,Z.contentType));L.writeFileSync(H,Buffer.from(Z.base64||"","base64"));X.push({...Y,path:H})}return X}function K(q,Q=""){return{url:q.src,width:q.w,height:q.h,path:Q}}export const imageCommand=f({site:"grok",name:"image",description:"Generate images on grok.com and return image URLs",access:"write",domain:"grok.com",strategy:w.COOKIE,browser:!0,siteSession:"persistent",args:[{name:"prompt",positional:!0,type:"string",required:!0,help:"Image generation prompt"},{name:"timeout",type:"int",default:240,help:"Max seconds to wait for the image (default: 240)"},{name:"new",type:"boolean",default:!1,help:"Start a new chat before sending (default: false)"},{name:"count",type:"int",default:1,help:"Minimum images to wait for before returning (default: 1)"},{name:"out",type:"string",default:"",help:"Directory to save downloaded images (uses browser session to bypass auth)"}],columns:["url","width","height","path"],func:async(q,Q)=>{const W=Q.prompt,X=(Q.timeout||240)*1000,Y=P(Q.new),Z=T(Q.count,1,"count"),H=(Q.out||"").toString().trim();if(Y){await q.goto(O);await q.wait(2);await c(q);await q.wait(2)}else if(!await D(q)){await q.goto(O);await q.wait(3)}const M=(await y(q)).length,N=await d(q,W);if(!N||!N.ok)throw new x(`Grok composer rejected the prompt: ${JSON.stringify(N)}`,h);const C=Date.now();let V="",U=0,j=[];while(Date.now()-C<X){await q.wait(3);const A=await y(q),$=R(A,M);if($.length>=Z){const J=F($);if(J===V){U+=1;if(U>=2){if(H)return(await v(q,$,H)).map((G)=>K(G,G.path));return $.map((_)=>K(_))}}else{U=0;V=J;j=$}}}if(j.length){if(H)return(await v(q,j,H)).map(($)=>K($,$.path));return j.map((A)=>K(A))}throw new b("grok image generation",Math.round(X/1000))}}),__test__={normalizeBooleanFlag:P,normalizePositiveInteger:T,isOnGrok:D,dedupeBySrc:E,imagesSignature:F,extFromContentType:z,buildFilename:B,pickLatestImageCandidate:R};
119
+ })()`)}async function v(q,Q,W){L.mkdirSync(W,{recursive:!0});const X=[];for(const Y of Q){const Z=await p(q,Y.src);if(!Z||!Z.ok){const M=Z?.error?`: ${Z.error}`:"";throw new x(`Failed to download grok image ${Y.src}${M}`,"assets.grok.com download requires the live grok.com browser session — verify the tab is logged in and try again.")}const H=k.join(W,B(Y.src,Z.contentType));L.writeFileSync(H,Buffer.from(Z.base64||"","base64"));X.push({...Y,path:H})}return X}function K(q,Q=""){return{url:q.src,width:q.w,height:q.h,path:Q}}export const imageCommand=f({site:"grok",name:"image",description:"Generate images on grok.com and return image URLs",access:"write",domain:"grok.com",strategy:w.COOKIE,browser:!0,siteSession:"persistent",args:[{name:"prompt",fromFile:!0,positional:!0,type:"string",required:!0,help:"Image generation prompt"},{name:"timeout",type:"int",default:240,help:"Max seconds to wait for the image (default: 240)"},{name:"new",type:"boolean",default:!1,help:"Start a new chat before sending (default: false)"},{name:"count",type:"int",default:1,help:"Minimum images to wait for before returning (default: 1)"},{name:"out",type:"string",default:"",help:"Directory to save downloaded images (uses browser session to bypass auth)"}],columns:["url","width","height","path"],func:async(q,Q)=>{const W=Q.prompt,X=(Q.timeout||240)*1000,Y=P(Q.new),Z=T(Q.count,1,"count"),H=(Q.out||"").toString().trim();if(Y){await q.goto(O);await q.wait(2);await c(q);await q.wait(2)}else if(!await D(q)){await q.goto(O);await q.wait(3)}const M=(await y(q)).length,N=await d(q,W);if(!N||!N.ok)throw new x(`Grok composer rejected the prompt: ${JSON.stringify(N)}`,h);const C=Date.now();let V="",U=0,j=[];while(Date.now()-C<X){await q.wait(3);const A=await y(q),$=R(A,M);if($.length>=Z){const J=F($);if(J===V){U+=1;if(U>=2){if(H)return(await v(q,$,H)).map((G)=>K(G,G.path));return $.map((_)=>K(_))}}else{U=0;V=J;j=$}}}if(j.length){if(H)return(await v(q,j,H)).map(($)=>K($,$.path));return j.map((A)=>K(A))}throw new b("grok image generation",Math.round(X/1000))}}),__test__={normalizeBooleanFlag:P,normalizePositiveInteger:T,isOnGrok:D,dedupeBySrc:E,imagesSignature:F,extFromContentType:z,buildFilename:B,pickLatestImageCandidate:R};
package/clis/grok/send.js CHANGED
@@ -1 +1 @@
1
- import{cli as Q,Strategy as T}from"@jackwener/opencli/registry";import{ArgumentError as U,CommandExecutionError as V}from"@jackwener/opencli/errors";import{GROK_DOMAIN as W,authRequired as X,ensureOnGrok as Y,isLoggedIn as Z,normalizeBooleanFlag as $,sendMessage as q,startNewChat as z}from"./utils.js";Q({site:"grok",name:"send",access:"write",description:"Fire-and-forget: send a prompt to Grok without waiting for the reply",domain:W,strategy:T.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,args:[{name:"prompt",required:!0,positional:!0,help:"Prompt to send to Grok"},{name:"new",type:"boolean",default:!1,help:"Start a new chat before sending"}],columns:["Status","Prompt"],func:async(b,H)=>{const v=String(H.prompt||"").trim();if(!v)throw new U("prompt","is required");const P=$(H.new,!1);await Y(b);if(P)await z(b);const J=await q(b,v);if(!J?.ok){if(!await Z(b))throw X();throw new V(J?.reason||"Failed to send Grok prompt")}return[{Status:"sent",Prompt:v}]}});
1
+ import{cli as Q,Strategy as T}from"@jackwener/opencli/registry";import{ArgumentError as U,CommandExecutionError as V}from"@jackwener/opencli/errors";import{GROK_DOMAIN as W,authRequired as X,ensureOnGrok as Y,isLoggedIn as Z,normalizeBooleanFlag as $,sendMessage as q,startNewChat as z}from"./utils.js";Q({site:"grok",name:"send",access:"write",description:"Fire-and-forget: send a prompt to Grok without waiting for the reply",domain:W,strategy:T.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,args:[{name:"prompt",fromFile:!0,required:!0,positional:!0,help:"Prompt to send to Grok"},{name:"new",type:"boolean",default:!1,help:"Start a new chat before sending"}],columns:["Status","Prompt"],func:async(b,H)=>{const v=String(H.prompt||"").trim();if(!v)throw new U("prompt","is required");const P=$(H.new,!1);await Y(b);if(P)await z(b);const J=await q(b,v);if(!J?.ok){if(!await Z(b))throw X();throw new V(J?.reason||"Failed to send Grok prompt")}return[{Status:"sent",Prompt:v}]}});
@@ -1,4 +1,4 @@
1
- import{ArgumentError as Q,AuthRequiredError as T,CommandExecutionError as L}from"@jackwener/opencli/errors";import{cli as U,Strategy as V}from"@jackwener/opencli/registry";const M="https://news.ycombinator.com";export function normalizeItemId(F){const J=String(F||"").trim();if(!J)throw new Q("Item id is required.","Pass an HN item id (story or comment), e.g. 48811591.");if(/^\d+$/.test(J))return J;const B=J.match(/[?&]id=(\d+)/);if(B)return B[1];throw new Q(`Invalid HN item reference: ${JSON.stringify(J)}`,"Pass a numeric id or an item?id=… URL.")}U({site:"hackernews",name:"comment",access:"write",description:"Post a comment on a Hacker News item. Pass the parent id (a story to comment on it, or a comment to reply). Contribute a substantive, on-topic comment — HN flags low-effort or off-topic replies.",domain:"news.ycombinator.com",strategy:V.COOKIE,browser:!0,args:[{name:"parent",type:"string",required:!0,positional:!0,help:"Parent item id (story or comment) to reply to"},{name:"text",type:"string",required:!0,positional:!0,help:"Comment text (HN markup: blank line = paragraph)"}],columns:["status","id","url","parent","message"],func:async(F,J)=>{if(!F)throw new L("Browser session required");const B=normalizeItemId(J.parent),P=String(J.text??"");if(!P.trim())throw new Q("Comment text cannot be empty.");await F.goto(`${M}/item?id=${B}`);await F.wait(1);const D=await F.evaluate(`(async () => {
1
+ import{ArgumentError as Q,AuthRequiredError as T,CommandExecutionError as L}from"@jackwener/opencli/errors";import{cli as U,Strategy as V}from"@jackwener/opencli/registry";const M="https://news.ycombinator.com";export function normalizeItemId(F){const J=String(F||"").trim();if(!J)throw new Q("Item id is required.","Pass an HN item id (story or comment), e.g. 48811591.");if(/^\d+$/.test(J))return J;const B=J.match(/[?&]id=(\d+)/);if(B)return B[1];throw new Q(`Invalid HN item reference: ${JSON.stringify(J)}`,"Pass a numeric id or an item?id=… URL.")}U({site:"hackernews",name:"comment",access:"write",description:"Post a comment on a Hacker News item. Pass the parent id (a story to comment on it, or a comment to reply). Contribute a substantive, on-topic comment — HN flags low-effort or off-topic replies.",domain:"news.ycombinator.com",strategy:V.COOKIE,browser:!0,args:[{name:"parent",type:"string",required:!0,positional:!0,help:"Parent item id (story or comment) to reply to"},{name:"text",fromFile:!0,type:"string",required:!0,positional:!0,help:"Comment text (HN markup: blank line = paragraph)"}],columns:["status","id","url","parent","message"],func:async(F,J)=>{if(!F)throw new L("Browser session required");const B=normalizeItemId(J.parent),P=String(J.text??"");if(!P.trim())throw new Q("Comment text cannot be empty.");await F.goto(`${M}/item?id=${B}`);await F.wait(1);const D=await F.evaluate(`(async () => {
2
2
  try {
3
3
  if (/\\/login/.test(location.pathname)) return { kind: 'auth', detail: 'redirected to /login' };
4
4
  // The reply form posts to "comment" (relative). Match it by the hidden
@@ -1,4 +1,4 @@
1
- import{ArgumentError as F,AuthRequiredError as Q,CommandExecutionError as L}from"@jackwener/opencli/errors";import{cli as T,Strategy as V}from"@jackwener/opencli/registry";const M="https://news.ycombinator.com";T({site:"hackernews",name:"submit",access:"write",description:"Submit a story to Hacker News. Link post by default (--url), or an Ask/text post with --text. HN gates submissions on account age/karma and dedupes URLs — rejections are surfaced verbatim. Post genuinely worthwhile content; low-effort submissions hurt the account.",domain:"news.ycombinator.com",strategy:V.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!0,positional:!0,help:"Story title (max 80 chars)"},{name:"url",type:"string",help:"External URL for a link post (mutually exclusive with --text)"},{name:"text",type:"string",help:"Body text for an Ask HN / text post (mutually exclusive with --url)"}],columns:["status","id","url","title","message"],func:async(v,D)=>{if(!v)throw new L("Browser session required");const z=String(D.title??"").trim();if(!z)throw new F("Title is required.");if(z.length>80)throw new F("HN titles must be at most 80 characters.");const B=typeof D.url==="string"?D.url.trim():"",K=typeof D.text==="string"?D.text:"";if(B&&K)throw new F("Pass either --url (link post) or --text (text post), not both.");if(!B&&!K)throw new F("A story needs either --url or --text.","Give a link to submit, or --text for an Ask HN post.");if(B)try{new URL(B)}catch{throw new F(`Invalid --url: ${B}`)}await v.goto(`${M}/submit`);await v.wait(1);const f=await v.evaluate(`(async () => {
1
+ import{ArgumentError as F,AuthRequiredError as Q,CommandExecutionError as L}from"@jackwener/opencli/errors";import{cli as T,Strategy as V}from"@jackwener/opencli/registry";const M="https://news.ycombinator.com";T({site:"hackernews",name:"submit",access:"write",description:"Submit a story to Hacker News. Link post by default (--url), or an Ask/text post with --text. HN gates submissions on account age/karma and dedupes URLs — rejections are surfaced verbatim. Post genuinely worthwhile content; low-effort submissions hurt the account.",domain:"news.ycombinator.com",strategy:V.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!0,positional:!0,help:"Story title (max 80 chars)"},{name:"url",type:"string",help:"External URL for a link post (mutually exclusive with --text)"},{name:"text",fromFile:!0,type:"string",help:"Body text for an Ask HN / text post (mutually exclusive with --url)"}],columns:["status","id","url","title","message"],func:async(v,D)=>{if(!v)throw new L("Browser session required");const z=String(D.title??"").trim();if(!z)throw new F("Title is required.");if(z.length>80)throw new F("HN titles must be at most 80 characters.");const B=typeof D.url==="string"?D.url.trim():"",K=typeof D.text==="string"?D.text:"";if(B&&K)throw new F("Pass either --url (link post) or --text (text post), not both.");if(!B&&!K)throw new F("A story needs either --url or --text.","Give a link to submit, or --text for an Ask HN post.");if(B)try{new URL(B)}catch{throw new F(`Invalid --url: ${B}`)}await v.goto(`${M}/submit`);await v.wait(1);const f=await v.evaluate(`(async () => {
2
2
  try {
3
3
  // The submit form only renders for a signed-in user; if HN bounced us
4
4
  // to /login the form (and its fnid) will be absent.
@@ -1,4 +1,4 @@
1
- import{cli as e}from"@jackwener/opencli/registry";e({site:"instagram",name:"comment",access:"write",description:"Comment on an Instagram post",domain:"www.instagram.com",args:[{name:"username",required:!0,positional:!0,help:"Username of the post author"},{name:"text",required:!0,positional:!0,help:"Comment text"},{name:"index",type:"int",default:1,help:"Post index (1 = most recent)"}],columns:["status","user","text"],pipeline:[{navigate:"https://www.instagram.com"},{evaluate:`(async () => {
1
+ import{cli as e}from"@jackwener/opencli/registry";e({site:"instagram",name:"comment",access:"write",description:"Comment on an Instagram post",domain:"www.instagram.com",args:[{name:"username",required:!0,positional:!0,help:"Username of the post author"},{name:"text",fromFile:!0,required:!0,positional:!0,help:"Comment text"},{name:"index",type:"int",default:1,help:"Post index (1 = most recent)"}],columns:["status","user","text"],pipeline:[{navigate:"https://www.instagram.com"},{evaluate:`(async () => {
2
2
  const username = \${{ args.username | json }};
3
3
  const commentText = \${{ args.text | json }};
4
4
  const idx = \${{ args.index }} - 1;
@@ -159,4 +159,4 @@ import{ArgumentError as s,CommandExecutionError as r}from"@jackwener/opencli/err
159
159
  text: normalizedText || text,
160
160
  };
161
161
  })()
162
- `}d({site:"instagram",name:"note",access:"write",description:"Publish a text Instagram note",domain:"www.instagram.com",strategy:p.UI,browser:!0,args:[{name:"content",positional:!0,required:!0,help:"Note text (max 60 characters)"},{name:"timeout",type:"int",required:!1,default:120,help:"Max seconds for the overall command (default: 120)"}],columns:["status","detail","noteId"],validateArgs:u,func:async(t,e)=>{const o=m(t),c=f(e);await o.goto(l);await o.wait({time:2});const n=await o.evaluate(y(c));if(!n?.ok)throw new r(`Instagram note publish failed at ${String(n?.stage||"unknown")}: ${String(n?.text||"unknown error")}`);return b(String(n.noteId||""))}});
162
+ `}d({site:"instagram",name:"note",access:"write",description:"Publish a text Instagram note",domain:"www.instagram.com",strategy:p.UI,browser:!0,args:[{name:"content",fromFile:!0,positional:!0,required:!0,help:"Note text (max 60 characters)"},{name:"timeout",type:"int",required:!1,default:120,help:"Max seconds for the overall command (default: 120)"}],columns:["status","detail","noteId"],validateArgs:u,func:async(t,e)=>{const o=m(t),c=f(e);await o.goto(l);await o.wait({time:2});const n=await o.evaluate(y(c));if(!n?.ok)throw new r(`Instagram note publish failed at ${String(n?.stage||"unknown")}: ${String(n?.text||"unknown error")}`);return b(String(n.noteId||""))}});
@@ -668,4 +668,4 @@ import*as P from"node:fs";import*as S from"node:path";import{cli as r,Strategy a
668
668
 
669
669
  return { ok: hrefs.length > 0, hrefs };
670
670
  })()
671
- `);return Array.isArray(G?.hrefs)?G.hrefs.filter(Boolean):[]}async function vj(j){const G=await s(j);if(!G)return null;const K=await o(j,G);if(!K)return null;try{await j.goto(K);await j.wait({time:3});return new Set(await n(j))}catch{return null}}async function h(j,G){const K=await j.getCurrentUrl?.();if(K&&/\/p\//.test(K))return K;const N=await s(j),O=await o(j,N);if(!O)return"";await j.goto(O);await j.wait({time:4});for(let Q=0;Q<8;Q++){const X=(await n(j)).find((_)=>!G.has(_))||"";if(X)return new URL(X,J).toString();if(Q<7)await j.wait({time:1})}return""}r({site:"instagram",name:"post",access:"write",description:"Post an Instagram feed image or mixed-media carousel",domain:"www.instagram.com",strategy:p.UI,browser:!0,args:[{name:"media",required:!1,valueRequired:!0,help:`Comma-separated media paths (images/videos, up to ${x})`},{name:"content",positional:!0,required:!1,help:"Caption text"},{name:"timeout",type:"int",required:!1,default:300,help:"Max seconds for the overall command (default: 300)"}],columns:["status","detail","url"],validateArgs:$j,func:async(j,G)=>{const K=Yj(j),N=_j(G),O=String(G.content??"").trim(),Q=await vj(K),Y=Q!==null,X=Q??new Set,_=Mj(N),q=Dj(N),V=Fj(N),L=Wj(N),B=Rj(N),F=yj(N),b=hj(N),z=process.env.OPENCLI_INSTAGRAM_CAPTURE==="1",C=[],f=[],u=async()=>{if(!z)return;await a(K)},U=async()=>{if(!z)return;const Z=await t(K);if(Z.data.length)C.push(...Z.data);if(Z.errors.length)f.push(...Z.errors)};try{try{return await Hj({page:K,mediaItems:N,content:O,existingPostPaths:X,canDiffProfile:Y})}catch(Z){if(Z instanceof W)throw Z;if(Kj(Z)){let $="",E=!1;if(Y)try{$=await h(K,X);E=!0}catch{}if($)return R(N,$);if(!E){const i=Z instanceof Error?Z.message:String(Z);throw new T(`Instagram publish outcome uncertain: ${i}`,"The post may already be live. Do NOT rerun this command blindly — check the profile for a new post first; retrying on a false failure creates a duplicate post.")}}if(!qj(Z))throw Z;try{return await Jj({page:K,mediaItems:N,content:O,existingPostPaths:X,canDiffProfile:Y,commandAttemptBudget:_,preUploadDelaySeconds:q,uploadAttemptBudget:V,previewProbeWindowSeconds:L,finalPreviewWaitSeconds:B,preShareDelaySeconds:F,inlineUploadRetryBudget:b,installProtocolCapture:u,drainProtocolCapture:U,forceFreshStart:!0})}catch($){if($ instanceof W)throw $;if($ instanceof T)throw new T($.message,zj(Z,$));throw $}}}finally{if(z){try{await U()}catch{}P.writeFileSync(Xj,JSON.stringify({data:C,errors:f},null,2))}}}});
671
+ `);return Array.isArray(G?.hrefs)?G.hrefs.filter(Boolean):[]}async function vj(j){const G=await s(j);if(!G)return null;const K=await o(j,G);if(!K)return null;try{await j.goto(K);await j.wait({time:3});return new Set(await n(j))}catch{return null}}async function h(j,G){const K=await j.getCurrentUrl?.();if(K&&/\/p\//.test(K))return K;const N=await s(j),O=await o(j,N);if(!O)return"";await j.goto(O);await j.wait({time:4});for(let Q=0;Q<8;Q++){const X=(await n(j)).find((_)=>!G.has(_))||"";if(X)return new URL(X,J).toString();if(Q<7)await j.wait({time:1})}return""}r({site:"instagram",name:"post",access:"write",description:"Post an Instagram feed image or mixed-media carousel",domain:"www.instagram.com",strategy:p.UI,browser:!0,args:[{name:"media",required:!1,valueRequired:!0,help:`Comma-separated media paths (images/videos, up to ${x})`},{name:"content",fromFile:!0,positional:!0,required:!1,help:"Caption text"},{name:"timeout",type:"int",required:!1,default:300,help:"Max seconds for the overall command (default: 300)"}],columns:["status","detail","url"],validateArgs:$j,func:async(j,G)=>{const K=Yj(j),N=_j(G),O=String(G.content??"").trim(),Q=await vj(K),Y=Q!==null,X=Q??new Set,_=Mj(N),q=Dj(N),V=Fj(N),L=Wj(N),B=Rj(N),F=yj(N),b=hj(N),z=process.env.OPENCLI_INSTAGRAM_CAPTURE==="1",C=[],f=[],u=async()=>{if(!z)return;await a(K)},U=async()=>{if(!z)return;const Z=await t(K);if(Z.data.length)C.push(...Z.data);if(Z.errors.length)f.push(...Z.errors)};try{try{return await Hj({page:K,mediaItems:N,content:O,existingPostPaths:X,canDiffProfile:Y})}catch(Z){if(Z instanceof W)throw Z;if(Kj(Z)){let $="",E=!1;if(Y)try{$=await h(K,X);E=!0}catch{}if($)return R(N,$);if(!E){const i=Z instanceof Error?Z.message:String(Z);throw new T(`Instagram publish outcome uncertain: ${i}`,"The post may already be live. Do NOT rerun this command blindly — check the profile for a new post first; retrying on a false failure creates a duplicate post.")}}if(!qj(Z))throw Z;try{return await Jj({page:K,mediaItems:N,content:O,existingPostPaths:X,canDiffProfile:Y,commandAttemptBudget:_,preUploadDelaySeconds:q,uploadAttemptBudget:V,previewProbeWindowSeconds:L,finalPreviewWaitSeconds:B,preShareDelaySeconds:F,inlineUploadRetryBudget:b,installProtocolCapture:u,drainProtocolCapture:U,forceFreshStart:!0})}catch($){if($ instanceof W)throw $;if($ instanceof T)throw new T($.message,zj(Z,$));throw $}}}finally{if(z){try{await U()}catch{}P.writeFileSync(Xj,JSON.stringify({data:C,errors:f},null,2))}}}});
@@ -449,4 +449,4 @@ import*as L from"node:fs";import*as E from"node:os";import*as F from"node:path";
449
449
  .filter((href, index, arr) => arr.indexOf(href) === index);
450
450
  return { hrefs };
451
451
  })()
452
- `);return Array.isArray(z?.hrefs)?z.hrefs.filter(Boolean):[]}async function Yj(j){const z=await _(j);if(!z)return new Set;const B=await w(j,z);if(!B)return new Set;try{await j.goto(B);await j.wait({time:3});return new Set(await M(j))}catch{return new Set}}async function Zj(j,z){const B=await j.getCurrentUrl?.();if(B&&/\/reel\//.test(B))return B;const Q=await _(j),Y=await w(j,Q);if(!Y)return"";await j.goto(Y);await j.wait({time:4});for(let q=0;q<8;q+=1){const y=await M(j),K=y.find(($)=>$.includes("/reel/")&&!z.has($))||y.find(($)=>!z.has($))||"";if(K)return new URL(K,H).toString();if(q<7)await j.wait({time:1})}return""}b({site:"instagram",name:"reel",access:"write",description:"Post an Instagram reel video",domain:"www.instagram.com",strategy:I.UI,browser:!0,args:[{name:"video",required:!1,valueRequired:!0,help:"Path to a single .mp4 video file"},{name:"content",positional:!0,required:!1,help:"Caption text"},{name:"timeout",type:"int",required:!1,default:C,help:`Max seconds for the overall command (default: ${C})`}],columns:["status","detail","url"],validateArgs:P,func:async(j,z)=>{const B=m(j),Q=u(z.video),Y=String(z.content??"").trim(),q=o(Q),y=async(K,$=new Set)=>{if(typeof K.startNetworkCapture==="function")await K.startNetworkCapture("/rupload_igvideo/|/api/v1/|/reel/|/clips/|/media/|/configure|/upload");await v(K,!0);await K.wait({time:2});await l(K);await s(K);await K.wait({time:2});const X=await i(K);let V=!1,J;for(const N of X)try{await g(K,q.uploadPath,N);if(await p(K,N)===0)throw new Z("Instagram reel upload failed","The selected reel input never received the video file");await r(K,10);V=!0;break}catch(T){J=T}if(!V)throw J instanceof Error?J:new Z("Instagram reel preview did not appear after upload");await t(K,["OK"],"any");await D(K,["Next","下一步"],"media");await O(K,"edit",20);await D(K,["Next","下一步"],"media");await O(K,"composer",20);if(Y){await zj(K,Y);await Bj(K,Y)}await D(K,["Share","分享"],"caption");const k=await Qj(K)||await Zj(K,$);return n(k)};try{if(!process.env.VITEST){const $=async()=>{const X=new A(`site:instagram-reel-${Date.now()}`);try{return await y(X,new Set)}finally{await X.closeWindow?.()}};try{return await $()}catch(X){if(!d(X))throw X;return await $()}}const K=await Yj(B);return await y(B,K)}finally{if(q.cleanupPath)L.rmSync(q.cleanupPath,{force:!0})}}});
452
+ `);return Array.isArray(z?.hrefs)?z.hrefs.filter(Boolean):[]}async function Yj(j){const z=await _(j);if(!z)return new Set;const B=await w(j,z);if(!B)return new Set;try{await j.goto(B);await j.wait({time:3});return new Set(await M(j))}catch{return new Set}}async function Zj(j,z){const B=await j.getCurrentUrl?.();if(B&&/\/reel\//.test(B))return B;const Q=await _(j),Y=await w(j,Q);if(!Y)return"";await j.goto(Y);await j.wait({time:4});for(let q=0;q<8;q+=1){const y=await M(j),K=y.find(($)=>$.includes("/reel/")&&!z.has($))||y.find(($)=>!z.has($))||"";if(K)return new URL(K,H).toString();if(q<7)await j.wait({time:1})}return""}b({site:"instagram",name:"reel",access:"write",description:"Post an Instagram reel video",domain:"www.instagram.com",strategy:I.UI,browser:!0,args:[{name:"video",required:!1,valueRequired:!0,help:"Path to a single .mp4 video file"},{name:"content",fromFile:!0,positional:!0,required:!1,help:"Caption text"},{name:"timeout",type:"int",required:!1,default:C,help:`Max seconds for the overall command (default: ${C})`}],columns:["status","detail","url"],validateArgs:P,func:async(j,z)=>{const B=m(j),Q=u(z.video),Y=String(z.content??"").trim(),q=o(Q),y=async(K,$=new Set)=>{if(typeof K.startNetworkCapture==="function")await K.startNetworkCapture("/rupload_igvideo/|/api/v1/|/reel/|/clips/|/media/|/configure|/upload");await v(K,!0);await K.wait({time:2});await l(K);await s(K);await K.wait({time:2});const X=await i(K);let V=!1,J;for(const N of X)try{await g(K,q.uploadPath,N);if(await p(K,N)===0)throw new Z("Instagram reel upload failed","The selected reel input never received the video file");await r(K,10);V=!0;break}catch(T){J=T}if(!V)throw J instanceof Error?J:new Z("Instagram reel preview did not appear after upload");await t(K,["OK"],"any");await D(K,["Next","下一步"],"media");await O(K,"edit",20);await D(K,["Next","下一步"],"media");await O(K,"composer",20);if(Y){await zj(K,Y);await Bj(K,Y)}await D(K,["Share","分享"],"caption");const k=await Qj(K)||await Zj(K,$);return n(k)};try{if(!process.env.VITEST){const $=async()=>{const X=new A(`site:instagram-reel-${Date.now()}`);try{return await y(X,new Set)}finally{await X.closeWindow?.()}};try{return await $()}catch(X){if(!d(X))throw X;return await $()}}const K=await Yj(B);return await y(B,K)}finally{if(q.cleanupPath)L.rmSync(q.cleanupPath,{force:!0})}}});
@@ -1,4 +1,4 @@
1
- import{CommandExecutionError as A}from"@jackwener/opencli/errors";import{cli as D,Strategy as F}from"@jackwener/opencli/registry";D({site:"jike",name:"comment",access:"write",description:"评论即刻帖子",domain:"web.okjike.com",strategy:F.UI,browser:!0,args:[{name:"id",type:"string",required:!0,positional:!0,help:"帖子 ID"},{name:"text",type:"string",required:!0,positional:!0,help:"评论内容"}],columns:["status","message"],func:async(f,v)=>{await f.goto(`https://web.okjike.com/originalPost/${v.id}`);for(let h=0;h<30;h++){if(await f.evaluate(`(() => {
1
+ import{CommandExecutionError as A}from"@jackwener/opencli/errors";import{cli as D,Strategy as F}from"@jackwener/opencli/registry";D({site:"jike",name:"comment",access:"write",description:"评论即刻帖子",domain:"web.okjike.com",strategy:F.UI,browser:!0,args:[{name:"id",type:"string",required:!0,positional:!0,help:"帖子 ID"},{name:"text",fromFile:!0,type:"string",required:!0,positional:!0,help:"评论内容"}],columns:["status","message"],func:async(f,v)=>{await f.goto(`https://web.okjike.com/originalPost/${v.id}`);for(let h=0;h<30;h++){if(await f.evaluate(`(() => {
2
2
  const editor =
3
3
  document.querySelector('[class*="_comment_"] [contenteditable="true"]') ||
4
4
  document.querySelector('[contenteditable="true"]');
@@ -1,4 +1,4 @@
1
- import{CommandExecutionError as z}from"@jackwener/opencli/errors";import{cli as D,Strategy as F}from"@jackwener/opencli/registry";D({site:"jike",name:"create",access:"write",description:"发布即刻动态",domain:"web.okjike.com",strategy:F.UI,browser:!0,args:[{name:"text",type:"string",required:!0,positional:!0,help:"动态正文内容"}],columns:["status","message"],func:async(f,A)=>{await f.goto("https://web.okjike.com");for(let h=0;h<30;h++){if(await f.evaluate(`(() => {
1
+ import{CommandExecutionError as z}from"@jackwener/opencli/errors";import{cli as D,Strategy as F}from"@jackwener/opencli/registry";D({site:"jike",name:"create",access:"write",description:"发布即刻动态",domain:"web.okjike.com",strategy:F.UI,browser:!0,args:[{name:"text",fromFile:!0,type:"string",required:!0,positional:!0,help:"动态正文内容"}],columns:["status","message"],func:async(f,A)=>{await f.goto("https://web.okjike.com");for(let h=0;h<30;h++){if(await f.evaluate(`(() => {
2
2
  const form = document.querySelector('[class*="_postForm_"]');
3
3
  const editor = form
4
4
  ? form.querySelector('[contenteditable="true"]')
@@ -1,4 +1,4 @@
1
- import{CommandExecutionError as v}from"@jackwener/opencli/errors";import{cli as G,Strategy as H}from"@jackwener/opencli/registry";G({site:"jike",name:"repost",access:"write",description:"转发即刻帖子",domain:"web.okjike.com",strategy:H.UI,browser:!0,args:[{name:"id",type:"string",required:!0,positional:!0,help:"帖子 ID"},{name:"text",positional:!0,type:"string",required:!1,help:"转发附言(可选)"}],columns:["status","message"],func:async(b,z)=>{await b.goto(`https://web.okjike.com/originalPost/${z.id}`);for(let h=0;h<30;h++){if(await b.evaluate(`(() => {
1
+ import{CommandExecutionError as v}from"@jackwener/opencli/errors";import{cli as G,Strategy as H}from"@jackwener/opencli/registry";G({site:"jike",name:"repost",access:"write",description:"转发即刻帖子",domain:"web.okjike.com",strategy:H.UI,browser:!0,args:[{name:"id",type:"string",required:!0,positional:!0,help:"帖子 ID"},{name:"text",fromFile:!0,positional:!0,type:"string",required:!1,help:"转发附言(可选)"}],columns:["status","message"],func:async(b,z)=>{await b.goto(`https://web.okjike.com/originalPost/${z.id}`);for(let h=0;h<30;h++){if(await b.evaluate(`(() => {
2
2
  const actions = document.querySelector('[class*="_actions_"]');
3
3
  if (!actions) return false;
4
4
  const children = Array.from(actions.children).filter(c => c.offsetHeight > 0);
@@ -56,5 +56,5 @@ import{cli as h,Strategy as G}from"@jackwener/opencli/registry";import{CommandEx
56
56
  }
57
57
  if (!done.length && failMsg) return { state: 'failed', error: failMsg };
58
58
  return { state: 'pending', got: done.length, total };
59
- })()`;h({site:"jimeng",name:"generate",access:"write",description:"即梦AI 文生图 — 输入 prompt 生成图片",domain:"jimeng.jianying.com",strategy:G.COOKIE,browser:!0,navigateBefore:!1,siteSession:"persistent",args:[{name:"prompt",type:"string",required:!0,positional:!0,help:"图片描述 prompt"},{name:"wait",type:"int",default:60,help:"等待生成完成的最长秒数(每 3 秒轮询一次,超时会返回 pending 并提示用 history 回查)"}],columns:["status","prompt","model","image_count","image_urls"],func:async(q,K)=>{const z=String(K.prompt??"").trim();if(!z)throw new F("prompt is required");const Q=Number(K.wait??60);await q.goto(M);await q.wait(3);const v=await q.evaluate(N(z));if(!v?.ok){const $={editor_not_found:"找不到 prompt 输入框(页面结构可能已变)",button_not_found:"找不到生成按钮(页面结构可能已变)",button_disabled:"生成按钮不可点击(可能未登录、额度用尽或 prompt 为空)"}[v?.error]||`提交失败:${v?.error||"unknown"}`;throw new F($)}const X=Array.isArray(v.existing)?v.existing:[],Z=Date.now()+Q*1000;let f=null;while(Date.now()<Z){await q.wait(3);f=await q.evaluate(j(X));if(f?.state==="success")return[{status:"success",prompt:z.slice(0,80),model:f.model||"",image_count:f.urls.length,image_urls:f.urls.join(`
59
+ })()`;h({site:"jimeng",name:"generate",access:"write",description:"即梦AI 文生图 — 输入 prompt 生成图片",domain:"jimeng.jianying.com",strategy:G.COOKIE,browser:!0,navigateBefore:!1,siteSession:"persistent",args:[{name:"prompt",fromFile:!0,type:"string",required:!0,positional:!0,help:"图片描述 prompt"},{name:"wait",type:"int",default:60,help:"等待生成完成的最长秒数(每 3 秒轮询一次,超时会返回 pending 并提示用 history 回查)"}],columns:["status","prompt","model","image_count","image_urls"],func:async(q,K)=>{const z=String(K.prompt??"").trim();if(!z)throw new F("prompt is required");const Q=Number(K.wait??60);await q.goto(M);await q.wait(3);const v=await q.evaluate(N(z));if(!v?.ok){const $={editor_not_found:"找不到 prompt 输入框(页面结构可能已变)",button_not_found:"找不到生成按钮(页面结构可能已变)",button_disabled:"生成按钮不可点击(可能未登录、额度用尽或 prompt 为空)"}[v?.error]||`提交失败:${v?.error||"unknown"}`;throw new F($)}const X=Array.isArray(v.existing)?v.existing:[],Z=Date.now()+Q*1000;let f=null;while(Date.now()<Z){await q.wait(3);f=await q.evaluate(j(X));if(f?.state==="success")return[{status:"success",prompt:z.slice(0,80),model:f.model||"",image_count:f.urls.length,image_urls:f.urls.join(`
60
60
  `)}];if(f?.state==="failed")throw new F(`即梦生成失败:${f.error}`)}return[{status:"pending",prompt:z.slice(0,80),model:f&&f.model||"",image_count:f&&f.got||0,image_urls:`生成仍在进行(已 ${Q}s);稍后用 \`ppcli jimeng history\` 回查结果`}]}});
package/clis/kimi/chat.js CHANGED
@@ -74,4 +74,4 @@ import{cli as W,Strategy as X}from"@jackwener/opencli/registry";import{ArgumentE
74
74
  const rows = Array.from(document.querySelectorAll('.chat-content-list .chat-content-item, .message-list > *, .segment'))
75
75
  .filter((row) => /user|sent-by-user|me-/i.test(String(row.className || '')));
76
76
  return rows.slice(before).some((row) => normalize(row.innerText || row.textContent).includes(prompt));
77
- })()`))return{prompt:F};await q.wait(0.2)}throw new $("Kimi message submission was not verified","The prompt was injected and Send was clicked, but no new user turn containing that prompt appeared.")}W({site:"kimi",name:"send",access:"write",description:"Send a message in the current Kimi chat (fire-and-forget; does not wait for reply).",domain:Y,strategy:X.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,args:[{name:"text",positional:!0,required:!0,help:"Message text"}],columns:Z,func:async(q,z)=>{const F=await O(q,z?.text);return[{Status:"sent",Length:String(F.prompt.length)}]}});W({site:"kimi",name:"copy-message",access:"write",description:"Return the text of the last assistant message. Pass --conv <id> to navigate to a specific chat first.",domain:Y,strategy:X.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,args:[{name:"conv",required:!1,help:"Chat id or URL (navigates there before reading)"},{name:"click-button",type:"boolean",default:!1,help:"Also click in-UI Copy button (writes to clipboard)"}],columns:Z,func:async(q,z)=>{await B(q,z?.conv);const G=(await b(q)).filter((P)=>P.role==="Assistant");if(!G.length)throw new D("kimi copy-message","No assistant message visible.");const Q=G[G.length-1];if(z?.["click-button"]===!0||z?.["click-button"]==="true"){const P=await q.evaluate(V("Copy"));if(!P?.ok)throw new $(P?.reason||"Copy button not visible","")}return[{Field:"Length",Value:String((Q.text||"").length)+" chars"},{Field:"ClipboardClicked",Value:z?.["click-button"]===!0||z?.["click-button"]==="true"?"yes":"no"},{Field:"Text",Value:Q.text||""}]}});W({site:"kimi",name:"regenerate",access:"write",description:"Click Refresh (Kimi's regenerate button) on the last assistant message. Pass --conv <id> to target a specific chat.",domain:Y,strategy:X.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,args:[{name:"conv",required:!1,help:"Chat id or URL"}],columns:Z,func:async(q,z)=>{await B(q,z?.conv);const F=await q.evaluate(V("Refresh"));if(!F?.ok)throw new $(F?.reason||"Refresh button not visible","");return[{Status:"regenerated"}]}});W({site:"kimi",name:"react",access:"write",description:"Like or dislike the last assistant message. Pass --conv <id> to target a specific chat.",domain:Y,strategy:X.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,args:[{name:"kind",positional:!0,required:!0,help:"like or dislike"},{name:"conv",required:!1,help:"Chat id or URL"}],columns:Z,func:async(q,z)=>{const F=String(z?.kind||"").trim().toLowerCase();if(F!=="like"&&F!=="dislike")throw new j("kind",'must be "like" or "dislike"');await B(q,z?.conv);const G=F==="like"?"Like":"Dislike",Q=await q.evaluate(V(G));if(!Q?.ok)throw new $(Q?.reason||`${F} button not visible`,"");return[{Status:"clicked",Reaction:F}]}});W({site:"kimi",name:"share",access:"write",description:"Click Share on the last assistant message (opens Kimi's share dialog). Pass --conv <id> to target a specific chat.",domain:Y,strategy:X.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,args:[{name:"conv",required:!1,help:"Chat id or URL"}],columns:Z,func:async(q,z)=>{await B(q,z?.conv);const F=await q.evaluate(V("Share_a"));if(!F?.ok)throw new $(F?.reason||"Share button not visible","");await q.wait(1);return[{Status:"share dialog opened"}]}});W({site:"kimi",name:"ask",access:"write",description:"Send a message and wait up to --timeout seconds for the assistant reply (best-effort: polls for turn count to grow + stabilize).",domain:Y,strategy:X.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,args:[{name:"text",positional:!0,required:!0,help:"Prompt text"},{name:"timeout",type:"int",required:!1,default:120}],columns:Z,func:async(q,z)=>{const F=String(z?.text||"").trim();if(!F)throw new j("text","is required");const G=Number.isInteger(z?.timeout)&&z.timeout>0?z.timeout:120;await J(q);const Q=(await b(q)).filter((x)=>x.role==="Assistant").length;await O(q,F);const P=Date.now(),f=P+G*1000;let h="",H=0;while(Date.now()<f){await q.wait(1.5);const R=(await b(q).catch(()=>[])).filter((y)=>y.role==="Assistant");if(R.length<=Q)continue;const U=R[R.length-1]?.text||"";if(U&&U===h)H++;else{h=U;H=0}if(h&&H>=2)break}const T=Math.round((Date.now()-P)/1000);if(!h)throw new E("kimi ask",G,"No new Kimi assistant reply was visible before the timeout.");return[{Status:"reply-received",Length:String(F.length),WaitedSeconds:String(T),ReplyPreview:h.slice(0,300)}]}});
77
+ })()`))return{prompt:F};await q.wait(0.2)}throw new $("Kimi message submission was not verified","The prompt was injected and Send was clicked, but no new user turn containing that prompt appeared.")}W({site:"kimi",name:"send",access:"write",description:"Send a message in the current Kimi chat (fire-and-forget; does not wait for reply).",domain:Y,strategy:X.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,args:[{name:"text",fromFile:!0,positional:!0,required:!0,help:"Message text"}],columns:Z,func:async(q,z)=>{const F=await O(q,z?.text);return[{Status:"sent",Length:String(F.prompt.length)}]}});W({site:"kimi",name:"copy-message",access:"write",description:"Return the text of the last assistant message. Pass --conv <id> to navigate to a specific chat first.",domain:Y,strategy:X.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,args:[{name:"conv",required:!1,help:"Chat id or URL (navigates there before reading)"},{name:"click-button",type:"boolean",default:!1,help:"Also click in-UI Copy button (writes to clipboard)"}],columns:Z,func:async(q,z)=>{await B(q,z?.conv);const G=(await b(q)).filter((P)=>P.role==="Assistant");if(!G.length)throw new D("kimi copy-message","No assistant message visible.");const Q=G[G.length-1];if(z?.["click-button"]===!0||z?.["click-button"]==="true"){const P=await q.evaluate(V("Copy"));if(!P?.ok)throw new $(P?.reason||"Copy button not visible","")}return[{Field:"Length",Value:String((Q.text||"").length)+" chars"},{Field:"ClipboardClicked",Value:z?.["click-button"]===!0||z?.["click-button"]==="true"?"yes":"no"},{Field:"Text",Value:Q.text||""}]}});W({site:"kimi",name:"regenerate",access:"write",description:"Click Refresh (Kimi's regenerate button) on the last assistant message. Pass --conv <id> to target a specific chat.",domain:Y,strategy:X.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,args:[{name:"conv",required:!1,help:"Chat id or URL"}],columns:Z,func:async(q,z)=>{await B(q,z?.conv);const F=await q.evaluate(V("Refresh"));if(!F?.ok)throw new $(F?.reason||"Refresh button not visible","");return[{Status:"regenerated"}]}});W({site:"kimi",name:"react",access:"write",description:"Like or dislike the last assistant message. Pass --conv <id> to target a specific chat.",domain:Y,strategy:X.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,args:[{name:"kind",positional:!0,required:!0,help:"like or dislike"},{name:"conv",required:!1,help:"Chat id or URL"}],columns:Z,func:async(q,z)=>{const F=String(z?.kind||"").trim().toLowerCase();if(F!=="like"&&F!=="dislike")throw new j("kind",'must be "like" or "dislike"');await B(q,z?.conv);const G=F==="like"?"Like":"Dislike",Q=await q.evaluate(V(G));if(!Q?.ok)throw new $(Q?.reason||`${F} button not visible`,"");return[{Status:"clicked",Reaction:F}]}});W({site:"kimi",name:"share",access:"write",description:"Click Share on the last assistant message (opens Kimi's share dialog). Pass --conv <id> to target a specific chat.",domain:Y,strategy:X.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,args:[{name:"conv",required:!1,help:"Chat id or URL"}],columns:Z,func:async(q,z)=>{await B(q,z?.conv);const F=await q.evaluate(V("Share_a"));if(!F?.ok)throw new $(F?.reason||"Share button not visible","");await q.wait(1);return[{Status:"share dialog opened"}]}});W({site:"kimi",name:"ask",access:"write",description:"Send a message and wait up to --timeout seconds for the assistant reply (best-effort: polls for turn count to grow + stabilize).",domain:Y,strategy:X.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,args:[{name:"text",fromFile:!0,positional:!0,required:!0,help:"Prompt text"},{name:"timeout",type:"int",required:!1,default:120}],columns:Z,func:async(q,z)=>{const F=String(z?.text||"").trim();if(!F)throw new j("text","is required");const G=Number.isInteger(z?.timeout)&&z.timeout>0?z.timeout:120;await J(q);const Q=(await b(q)).filter((x)=>x.role==="Assistant").length;await O(q,F);const P=Date.now(),f=P+G*1000;let h="",H=0;while(Date.now()<f){await q.wait(1.5);const R=(await b(q).catch(()=>[])).filter((y)=>y.role==="Assistant");if(R.length<=Q)continue;const U=R[R.length-1]?.text||"";if(U&&U===h)H++;else{h=U;H=0}if(h&&H>=2)break}const T=Math.round((Date.now()-P)/1000);if(!h)throw new E("kimi ask",G,"No new Kimi assistant reply was visible before the timeout.");return[{Status:"reply-received",Length:String(F.length),WaitedSeconds:String(T),ReplyPreview:h.slice(0,300)}]}});
@@ -42,7 +42,7 @@ import{cli as V,Strategy as v}from"@jackwener/opencli/registry";import{ArgumentE
42
42
  first.click();
43
43
  return true;
44
44
  })()
45
- `))))throw new H("找不到快手「封面设置」入口");if(!await A(q,'div[role="document"].ant-modal',30000))throw new H("封面设置弹窗未出现");await M(q,"上传封面",{exact:!0,timeoutMs:1e4});await q.wait({time:0.5});await Z(q,['div[role="document"].ant-modal input[type="file"]','input[type="file"]'],[z]);await q.wait({time:1});await M(q,"确认",{exact:!0,timeoutMs:1e4});const J=Date.now()+30000;while(Date.now()<J){if(!Boolean(W(await q.evaluate(`(() => { const m = document.querySelector('div[role="document"].ant-modal'); return !!(m && m.getBoundingClientRect().width > 0); })()`))))break;await q.wait({time:0.5})}}V({site:"kuaishou",name:"publish",access:"write",description:"发布视频到快手(一条命令完成上传+填写+发布;可选封面/定时/话题)",domain:"cp.kuaishou.com",strategy:v.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"video",required:!0,positional:!0,help:"视频文件路径"},{name:"title",required:!0,help:"作品标题(无 --desc 时作为描述)"},{name:"desc",default:"",help:"视频描述(默认用标题)"},{name:"tags",default:"",help:"话题标签,逗号分隔,最多取前 3 个(不含 #)"},{name:"cover",default:"",help:"封面图片路径(可选)"},{name:"schedule",default:"",help:"定时发布时间(ISO8601 或 Unix 秒;不填即立即发布)"}],columns:["status","title","url"],func:async(q,z)=>{const G=String(z.title??"").trim();if(!G)throw new O("快手视频上传时,title 是必须的");const J=_(z.video),Q=z.cover?U(z.cover):"",X=String(z.desc||"").trim()||G,R=z.tags?String(z.tags).split(",").map((K)=>K.trim()).filter(Boolean):[],Y=C(z.schedule);await q.goto(D);if(!await j(q,"article/publish/video",30000))throw new H("未能进入快手上传页(登录态可能失效,请先 ppcli kuaishou login)");if(!await A(q,S,15000))throw new H("快手上传按钮未出现");await Z(q,N,[J]);await q.wait({time:2});if(await I(q,"我知道了",{exact:!0}))await M(q,"我知道了",{exact:!0});await q.evaluate(`
45
+ `))))throw new H("找不到快手「封面设置」入口");if(!await A(q,'div[role="document"].ant-modal',30000))throw new H("封面设置弹窗未出现");await M(q,"上传封面",{exact:!0,timeoutMs:1e4});await q.wait({time:0.5});await Z(q,['div[role="document"].ant-modal input[type="file"]','input[type="file"]'],[z]);await q.wait({time:1});await M(q,"确认",{exact:!0,timeoutMs:1e4});const J=Date.now()+30000;while(Date.now()<J){if(!Boolean(W(await q.evaluate(`(() => { const m = document.querySelector('div[role="document"].ant-modal'); return !!(m && m.getBoundingClientRect().width > 0); })()`))))break;await q.wait({time:0.5})}}V({site:"kuaishou",name:"publish",access:"write",description:"发布视频到快手(一条命令完成上传+填写+发布;可选封面/定时/话题)",domain:"cp.kuaishou.com",strategy:v.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"video",required:!0,positional:!0,help:"视频文件路径"},{name:"title",required:!0,help:"作品标题(无 --desc 时作为描述)"},{name:"desc",fromFile:!0,default:"",help:"视频描述(默认用标题)"},{name:"tags",default:"",help:"话题标签,逗号分隔,最多取前 3 个(不含 #)"},{name:"cover",default:"",help:"封面图片路径(可选)"},{name:"schedule",default:"",help:"定时发布时间(ISO8601 或 Unix 秒;不填即立即发布)"}],columns:["status","title","url"],func:async(q,z)=>{const G=String(z.title??"").trim();if(!G)throw new O("快手视频上传时,title 是必须的");const J=_(z.video),Q=z.cover?U(z.cover):"",X=String(z.desc||"").trim()||G,R=z.tags?String(z.tags).split(",").map((K)=>K.trim()).filter(Boolean):[],Y=C(z.schedule);await q.goto(D);if(!await j(q,"article/publish/video",30000))throw new H("未能进入快手上传页(登录态可能失效,请先 ppcli kuaishou login)");if(!await A(q,S,15000))throw new H("快手上传按钮未出现");await Z(q,N,[J]);await q.wait({time:2});if(await I(q,"我知道了",{exact:!0}))await M(q,"我知道了",{exact:!0});await q.evaluate(`
46
46
  (() => {
47
47
  const tip = document.querySelector('div[id^="react-joyride-step"] div[role="alertdialog"]');
48
48
  if (!tip) return false;
@@ -1 +1 @@
1
- import{cli as Z,Strategy as $}from"@jackwener/opencli/registry";import{ArgumentError as Q,AuthRequiredError as b,CommandExecutionError as V}from"@jackwener/opencli/errors";import{assertLinkedInAuthenticated as q,LINKEDIN_DOMAIN as W,unwrapEvaluateResult as v}from"./shared.js";import{buildCommentScript as z,NORM_COMMENT_DECORATION as K,normalizeThreadUrn as k}from"./social-api.js";const P=1250;Z({site:"linkedin",name:"comment",access:"write",description:"Post a top-level comment on a LinkedIn post via the Voyager API. Dry-run by default; pass --send true to actually publish. Takes a post URL or activity/ugcPost urn.",domain:W,strategy:$.COOKIE,browser:!0,args:[{name:"post",type:"string",required:!0,positional:!0,help:"Post to comment on: a LinkedIn post URL (…/feed/update/… or …/posts/…) or a raw urn:li:activity:… / urn:li:ugcPost:… urn."},{name:"text",type:"string",required:!0,help:`Comment body (max ${P} chars).`},{name:"send",type:"bool",required:!1,default:!1,help:"Actually post the comment. Default false = dry-run (validates the session only; nothing is posted)."}],columns:["status","post_urn","comment_urn","text_chars"],func:async(F,G)=>{if(!F)throw new V("Browser session required for linkedin comment");const J=k(G?.post),B=String(G?.text??"").replace(/ /g," ").trim();if(!B)throw new Q("--text is required");if(B.length>P)throw new Q(`--text must be ${P} characters or fewer (got ${B.length})`);const Y=G?.send===!0||G?.send==="true";await F.goto(`https://www.linkedin.com/feed/update/${J}/`);await F.wait(2);if(!Y){await q(F,"LinkedIn comment");return[{status:"dry_run",post_urn:J,comment_urn:"(not posted — pass --send true to publish)",text_chars:B.length}]}const f=v(await F.evaluate(z({threadUrn:J,text:B,decorationId:K})));if(!f?.ok){if(f?.stage==="auth"||f?.httpStatus===401||f?.httpStatus===403)throw new b(W,`LinkedIn comment requires an active signed-in LinkedIn browser session (${f?.error||"auth"}).`);throw new V(`LinkedIn comment failed: stage=${f?.stage||"unknown"} http=${f?.httpStatus||"?"} ${f?.error||""}`.trim())}return[{status:"commented",post_urn:J,comment_urn:f.commentUrn||"",text_chars:B.length}]}});
1
+ import{cli as Z,Strategy as $}from"@jackwener/opencli/registry";import{ArgumentError as Q,AuthRequiredError as b,CommandExecutionError as V}from"@jackwener/opencli/errors";import{assertLinkedInAuthenticated as q,LINKEDIN_DOMAIN as W,unwrapEvaluateResult as v}from"./shared.js";import{buildCommentScript as z,NORM_COMMENT_DECORATION as K,normalizeThreadUrn as k}from"./social-api.js";const P=1250;Z({site:"linkedin",name:"comment",access:"write",description:"Post a top-level comment on a LinkedIn post via the Voyager API. Dry-run by default; pass --send true to actually publish. Takes a post URL or activity/ugcPost urn.",domain:W,strategy:$.COOKIE,browser:!0,args:[{name:"post",type:"string",required:!0,positional:!0,help:"Post to comment on: a LinkedIn post URL (…/feed/update/… or …/posts/…) or a raw urn:li:activity:… / urn:li:ugcPost:… urn."},{name:"text",fromFile:!0,type:"string",required:!0,help:`Comment body (max ${P} chars).`},{name:"send",type:"bool",required:!1,default:!1,help:"Actually post the comment. Default false = dry-run (validates the session only; nothing is posted)."}],columns:["status","post_urn","comment_urn","text_chars"],func:async(F,G)=>{if(!F)throw new V("Browser session required for linkedin comment");const J=k(G?.post),B=String(G?.text??"").replace(/ /g," ").trim();if(!B)throw new Q("--text is required");if(B.length>P)throw new Q(`--text must be ${P} characters or fewer (got ${B.length})`);const Y=G?.send===!0||G?.send==="true";await F.goto(`https://www.linkedin.com/feed/update/${J}/`);await F.wait(2);if(!Y){await q(F,"LinkedIn comment");return[{status:"dry_run",post_urn:J,comment_urn:"(not posted — pass --send true to publish)",text_chars:B.length}]}const f=v(await F.evaluate(z({threadUrn:J,text:B,decorationId:K})));if(!f?.ok){if(f?.stage==="auth"||f?.httpStatus===401||f?.httpStatus===403)throw new b(W,`LinkedIn comment requires an active signed-in LinkedIn browser session (${f?.error||"auth"}).`);throw new V(`LinkedIn comment failed: stage=${f?.stage||"unknown"} http=${f?.httpStatus||"?"} ${f?.error||""}`.trim())}return[{status:"commented",post_urn:J,comment_urn:f.commentUrn||"",text_chars:B.length}]}});
@@ -110,4 +110,4 @@ import{cli as j,Strategy as G}from"@jackwener/opencli/registry";import{ArgumentE
110
110
  const d = await res.json();
111
111
  const mini = d && d.miniProfile;
112
112
  return { ok: Boolean(mini && mini.publicIdentifier), public_id: mini ? String(mini.publicIdentifier || '') : '' };
113
- })()`}j({site:"linkedin",name:"create",access:"write",description:"Publish a post to your LinkedIn feed via the Voyager API. Supports plain text plus images and visibility scope. Dry-run by default; pass --send true to actually publish.",domain:q,strategy:G.COOKIE,browser:!0,args:[{name:"content",type:"string",required:!0,positional:!0,help:`Post body text (max ${x} chars). Emojis and line breaks allowed.`},{name:"image",type:"string",required:!1,help:`Up to ${B} images: comma-separated local file paths and/or http(s) URLs. Local paths ≤10MB each (jpg/png/gif).`},{name:"visibility",type:"string",required:!1,default:"anyone",help:"Who can see the post: anyone (public, default) or connections (1st-degree only)."},{name:"send",type:"bool",required:!1,default:!1,help:"Actually publish. Default false = dry-run (verifies the signed-in session only; no post is created)."}],columns:["status","content_chars","images","visibility","activity_urn"],func:async(Q,J)=>{if(!Q)throw new Y("Browser session required for linkedin create");const z=String(J?.content??"").replace(/ /g," ").trim();if(!z)throw new Z("content is required");if(z.length>x)throw new Z(`content must be ${x} characters or fewer (got ${z.length})`);const $=L(J?.visibility),W=R(J?.image);if(W.length>B)throw new Z(`最多 ${B} 张图片(收到 ${W.length})`);const f=await X(W),H=J?.send===!0||J?.send==="true";await Q.goto("https://www.linkedin.com/feed/");await Q.wait(3);const U=k(await Q.evaluate(T()));if(!U?.ok)throw new V(q,`LinkedIn create requires an active signed-in LinkedIn browser session (${U?.error||"probe_failed"}).`);if(!H)return[{status:"dry_run",content_chars:z.length,images:f.length,visibility:$.label,activity_urn:"(not published — pass --send true to publish)"}];const F=k(await Q.evaluate(O({content:z,images:f,visibleToConnectionsOnly:$.visibleToConnectionsOnly})));if(!F?.ok){if(F?.stage==="auth"||F?.httpStatus===401||F?.httpStatus===403)throw new V(q,"LinkedIn create: session rejected by Voyager API — re-login in the desktop client.");const K=[`stage=${F?.stage||"unknown"}`,F?.index!=null?`image#${F.index}`:"",F?.httpStatus?`http=${F.httpStatus}`:"",F?.error?`error=${F.error}`:"",F?.raw?`raw=${F.raw}`:""].filter(Boolean).join(" ");throw new Y(`LinkedIn create failed: ${K}`)}return[{status:"published",content_chars:z.length,images:F.images??f.length,visibility:$.label,activity_urn:F.activityUrn||F.updateUrn||""}]}});
113
+ })()`}j({site:"linkedin",name:"create",access:"write",description:"Publish a post to your LinkedIn feed via the Voyager API. Supports plain text plus images and visibility scope. Dry-run by default; pass --send true to actually publish.",domain:q,strategy:G.COOKIE,browser:!0,args:[{name:"content",fromFile:!0,type:"string",required:!0,positional:!0,help:`Post body text (max ${x} chars). Emojis and line breaks allowed.`},{name:"image",type:"string",required:!1,help:`Up to ${B} images: comma-separated local file paths and/or http(s) URLs. Local paths ≤10MB each (jpg/png/gif).`},{name:"visibility",type:"string",required:!1,default:"anyone",help:"Who can see the post: anyone (public, default) or connections (1st-degree only)."},{name:"send",type:"bool",required:!1,default:!1,help:"Actually publish. Default false = dry-run (verifies the signed-in session only; no post is created)."}],columns:["status","content_chars","images","visibility","activity_urn"],func:async(Q,J)=>{if(!Q)throw new Y("Browser session required for linkedin create");const z=String(J?.content??"").replace(/ /g," ").trim();if(!z)throw new Z("content is required");if(z.length>x)throw new Z(`content must be ${x} characters or fewer (got ${z.length})`);const $=L(J?.visibility),W=R(J?.image);if(W.length>B)throw new Z(`最多 ${B} 张图片(收到 ${W.length})`);const f=await X(W),H=J?.send===!0||J?.send==="true";await Q.goto("https://www.linkedin.com/feed/");await Q.wait(3);const U=k(await Q.evaluate(T()));if(!U?.ok)throw new V(q,`LinkedIn create requires an active signed-in LinkedIn browser session (${U?.error||"probe_failed"}).`);if(!H)return[{status:"dry_run",content_chars:z.length,images:f.length,visibility:$.label,activity_urn:"(not published — pass --send true to publish)"}];const F=k(await Q.evaluate(O({content:z,images:f,visibleToConnectionsOnly:$.visibleToConnectionsOnly})));if(!F?.ok){if(F?.stage==="auth"||F?.httpStatus===401||F?.httpStatus===403)throw new V(q,"LinkedIn create: session rejected by Voyager API — re-login in the desktop client.");const K=[`stage=${F?.stage||"unknown"}`,F?.index!=null?`image#${F.index}`:"",F?.httpStatus?`http=${F.httpStatus}`:"",F?.error?`error=${F.error}`:"",F?.raw?`raw=${F.raw}`:""].filter(Boolean).join(" ");throw new Y(`LinkedIn create failed: ${K}`)}return[{status:"published",content_chars:z.length,images:F.images??f.length,visibility:$.label,activity_urn:F.activityUrn||F.updateUrn||""}]}});
@@ -1 +1 @@
1
- import{cli as b,Strategy as q}from"@jackwener/opencli/registry";import{ArgumentError as W,AuthRequiredError as v,CommandExecutionError as Y}from"@jackwener/opencli/errors";import{assertLinkedInAuthenticated as z,LINKEDIN_DOMAIN as Z,unwrapEvaluateResult as K}from"./shared.js";import{buildCommentScript as k,NORM_COMMENT_DECORATION as w,normalizeCommentUrn as y}from"./social-api.js";const Q=1250;b({site:"linkedin",name:"reply",access:"write",description:"Reply to a comment on a LinkedIn post via the Voyager API. Dry-run by default; pass --send true to actually publish. The comment urn comes from `linkedin post-comments`.",domain:Z,strategy:q.COOKIE,browser:!0,args:[{name:"comment",type:"string",required:!0,positional:!0,help:"The comment to reply to: a comment_urn from `linkedin post-comments` (urn:li:comment:(activity:…,…) or urn:li:fsd_comment:(…))."},{name:"text",type:"string",required:!0,help:`Reply body (max ${Q} chars).`},{name:"send",type:"bool",required:!1,default:!1,help:"Actually post the reply. Default false = dry-run (validates the session only; nothing is posted)."}],columns:["status","parent_comment_urn","reply_urn","text_chars"],func:async(F,G)=>{if(!F)throw new Y("Browser session required for linkedin reply");const J=y(G?.comment),B=String(G?.text??"").replace(/ /g," ").trim();if(!B)throw new W("--text is required");if(B.length>Q)throw new W(`--text must be ${Q} characters or fewer (got ${B.length})`);const $=G?.send===!0||G?.send==="true",P=J.match(/urn:li:comment:\((activity|ugcPost):(\d+),/),V=P?`urn:li:${P[1]}:${P[2]}`:"";await F.goto(V?`https://www.linkedin.com/feed/update/${V}/`:"https://www.linkedin.com/feed/");await F.wait(2);if(!$){await z(F,"LinkedIn reply");return[{status:"dry_run",parent_comment_urn:J,reply_urn:"(not posted — pass --send true to publish)",text_chars:B.length}]}const f=K(await F.evaluate(k({threadUrn:J,text:B,decorationId:w})));if(!f?.ok){if(f?.stage==="auth"||f?.httpStatus===401||f?.httpStatus===403)throw new v(Z,`LinkedIn reply requires an active signed-in LinkedIn browser session (${f?.error||"auth"}).`);throw new Y(`LinkedIn reply failed: stage=${f?.stage||"unknown"} http=${f?.httpStatus||"?"} ${f?.error||""}`.trim())}return[{status:"replied",parent_comment_urn:J,reply_urn:f.commentUrn||"",text_chars:B.length}]}});
1
+ import{cli as b,Strategy as q}from"@jackwener/opencli/registry";import{ArgumentError as W,AuthRequiredError as v,CommandExecutionError as Y}from"@jackwener/opencli/errors";import{assertLinkedInAuthenticated as z,LINKEDIN_DOMAIN as Z,unwrapEvaluateResult as K}from"./shared.js";import{buildCommentScript as k,NORM_COMMENT_DECORATION as w,normalizeCommentUrn as y}from"./social-api.js";const Q=1250;b({site:"linkedin",name:"reply",access:"write",description:"Reply to a comment on a LinkedIn post via the Voyager API. Dry-run by default; pass --send true to actually publish. The comment urn comes from `linkedin post-comments`.",domain:Z,strategy:q.COOKIE,browser:!0,args:[{name:"comment",type:"string",required:!0,positional:!0,help:"The comment to reply to: a comment_urn from `linkedin post-comments` (urn:li:comment:(activity:…,…) or urn:li:fsd_comment:(…))."},{name:"text",fromFile:!0,type:"string",required:!0,help:`Reply body (max ${Q} chars).`},{name:"send",type:"bool",required:!1,default:!1,help:"Actually post the reply. Default false = dry-run (validates the session only; nothing is posted)."}],columns:["status","parent_comment_urn","reply_urn","text_chars"],func:async(F,G)=>{if(!F)throw new Y("Browser session required for linkedin reply");const J=y(G?.comment),B=String(G?.text??"").replace(/ /g," ").trim();if(!B)throw new W("--text is required");if(B.length>Q)throw new W(`--text must be ${Q} characters or fewer (got ${B.length})`);const $=G?.send===!0||G?.send==="true",P=J.match(/urn:li:comment:\((activity|ugcPost):(\d+),/),V=P?`urn:li:${P[1]}:${P[2]}`:"";await F.goto(V?`https://www.linkedin.com/feed/update/${V}/`:"https://www.linkedin.com/feed/");await F.wait(2);if(!$){await z(F,"LinkedIn reply");return[{status:"dry_run",parent_comment_urn:J,reply_urn:"(not posted — pass --send true to publish)",text_chars:B.length}]}const f=K(await F.evaluate(k({threadUrn:J,text:B,decorationId:w})));if(!f?.ok){if(f?.stage==="auth"||f?.httpStatus===401||f?.httpStatus===403)throw new v(Z,`LinkedIn reply requires an active signed-in LinkedIn browser session (${f?.error||"auth"}).`);throw new Y(`LinkedIn reply failed: stage=${f?.stage||"unknown"} http=${f?.httpStatus||"?"} ${f?.error||""}`.trim())}return[{status:"replied",parent_comment_urn:J,reply_urn:f.commentUrn||"",text_chars:B.length}]}});
@@ -1 +1 @@
1
- import{cli as $,Strategy as b}from"@jackwener/opencli/registry";import{ArgumentError as z,AuthRequiredError as P,CommandExecutionError as W}from"@jackwener/opencli/errors";import{assertLinkedInAuthenticated as Q,LINKEDIN_DOMAIN as Y,unwrapEvaluateResult as q}from"./shared.js";import{buildRepostScript as x,normalizeThreadUrn as K,REPOST_CREATE_QID as k}from"./social-api.js";const J=3000;$({site:"linkedin",name:"repost",access:"write",description:"Repost (reshare) a LinkedIn post to your own feed via the Voyager API. Plain repost by default; pass --text to quote-repost with your commentary. Dry-run unless --send true.",domain:Y,strategy:b.COOKIE,browser:!0,args:[{name:"post",type:"string",required:!0,positional:!0,help:"Post to repost: a LinkedIn post URL (…/feed/update/… or …/posts/…) or a raw urn:li:activity:… / urn:li:ugcPost:… urn."},{name:"text",type:"string",required:!1,help:`Optional commentary to quote-repost with (max ${J} chars). Omit for a plain repost.`},{name:"send",type:"bool",required:!1,default:!1,help:"Actually repost. Default false = dry-run (validates the session only; nothing is posted)."}],columns:["status","shared_urn","repost_urn","quote"],func:async(B,F)=>{if(!B)throw new W("Browser session required for linkedin repost");const V=K(F?.post),G=String(F?.text??"").replace(/ /g," ").trim();if(G.length>J)throw new z(`--text must be ${J} characters or fewer (got ${G.length})`);const Z=F?.send===!0||F?.send==="true";await B.goto("https://www.linkedin.com/feed/");await B.wait(2);if(!Z){await Q(B,"LinkedIn repost");return[{status:"dry_run",shared_urn:V,repost_urn:"(not posted — pass --send true to repost)",quote:G?"yes":""}]}const f=q(await B.evaluate(x({activityUrn:V,commentary:G,repostQid:k})));if(!f?.ok){if(f?.stage==="auth"||f?.httpStatus===401||f?.httpStatus===403)throw new P(Y,`LinkedIn repost requires an active signed-in LinkedIn browser session (${f?.error||"auth"}).`);throw new W(`LinkedIn repost failed: stage=${f?.stage||"unknown"} http=${f?.httpStatus||"?"} ${f?.error||""}`.trim())}return[{status:"reposted",shared_urn:f.shared_urn||"",repost_urn:f.repost_urn||"",quote:f.quote?"yes":""}]}});
1
+ import{cli as $,Strategy as b}from"@jackwener/opencli/registry";import{ArgumentError as z,AuthRequiredError as P,CommandExecutionError as W}from"@jackwener/opencli/errors";import{assertLinkedInAuthenticated as Q,LINKEDIN_DOMAIN as Y,unwrapEvaluateResult as q}from"./shared.js";import{buildRepostScript as x,normalizeThreadUrn as K,REPOST_CREATE_QID as k}from"./social-api.js";const J=3000;$({site:"linkedin",name:"repost",access:"write",description:"Repost (reshare) a LinkedIn post to your own feed via the Voyager API. Plain repost by default; pass --text to quote-repost with your commentary. Dry-run unless --send true.",domain:Y,strategy:b.COOKIE,browser:!0,args:[{name:"post",type:"string",required:!0,positional:!0,help:"Post to repost: a LinkedIn post URL (…/feed/update/… or …/posts/…) or a raw urn:li:activity:… / urn:li:ugcPost:… urn."},{name:"text",fromFile:!0,type:"string",required:!1,help:`Optional commentary to quote-repost with (max ${J} chars). Omit for a plain repost.`},{name:"send",type:"bool",required:!1,default:!1,help:"Actually repost. Default false = dry-run (validates the session only; nothing is posted)."}],columns:["status","shared_urn","repost_urn","quote"],func:async(B,F)=>{if(!B)throw new W("Browser session required for linkedin repost");const V=K(F?.post),G=String(F?.text??"").replace(/ /g," ").trim();if(G.length>J)throw new z(`--text must be ${J} characters or fewer (got ${G.length})`);const Z=F?.send===!0||F?.send==="true";await B.goto("https://www.linkedin.com/feed/");await B.wait(2);if(!Z){await Q(B,"LinkedIn repost");return[{status:"dry_run",shared_urn:V,repost_urn:"(not posted — pass --send true to repost)",quote:G?"yes":""}]}const f=q(await B.evaluate(x({activityUrn:V,commentary:G,repostQid:k})));if(!f?.ok){if(f?.stage==="auth"||f?.httpStatus===401||f?.httpStatus===403)throw new P(Y,`LinkedIn repost requires an active signed-in LinkedIn browser session (${f?.error||"auth"}).`);throw new W(`LinkedIn repost failed: stage=${f?.stage||"unknown"} http=${f?.httpStatus||"?"} ${f?.error||""}`.trim())}return[{status:"reposted",shared_urn:f.shared_urn||"",repost_urn:f.repost_urn||"",quote:f.quote?"yes":""}]}});
@@ -151,5 +151,5 @@ import{cli as U,Strategy as v}from"@jackwener/opencli/registry";import{ArgumentE
151
151
  if (send.disabled || send.getAttribute('aria-disabled') === 'true') return { ok: false, error: 'send_button_disabled', sent: false };
152
152
  send.click();
153
153
  return { ok: true, sent: true };
154
- })()`}async function F(j){const G=Y(await j.evaluate(h())),D=O(G?.latestMessageText||"");return{...G||{},latestMessageText:D,latestMessageHash:D?w(D):""}}async function m(j,G){if(!C(G)){await j.insertText(G);return{method:"insert_text",renderedBreaks:0}}const D=Y(await j.evaluate(k(G)));if(D?.ok)return{method:D.method||"dom_multiline",renderedBreaks:Number(D.renderedBreaks||0)};await j.insertText(G);return{method:"insert_text_fallback",renderedBreaks:0}}U({site:"linkedin",name:"safe-send",access:"write",description:"Fail-closed LinkedIn message sender that verifies exact thread, recipient, and latest message before filling/sending",domain:P,strategy:v.UI,browser:!0,args:[{name:"thread-url",required:!0,help:"Exact LinkedIn messaging thread URL to open and verify"},{name:"expected-name",required:!0,help:"Expected visible recipient name in the active thread header"},{name:"message",required:!0,help:"Message body to send or dry-run"},{name:"expected-last-text",help:"Substring expected in the currently visible latest conversation context"},{name:"expected-last-hash",help:"SHA-256 hash of expected latest visible message text"},{name:"send",type:"bool",default:!1,help:"Actually click Send. Default is dry-run verification only."},{name:"screenshot",type:"bool",default:!1,help:"Capture a screenshot during verification"}],columns:["status","recipient","reason","thread_url","message_chars","screenshot"],func:async(j,G)=>{if(!j)throw new X("Browser session required for linkedin safe-send");const D=b(S(G,"thread-url","--thread-url"),"--thread-url"),J=S(G,"expected-name","--expected-name"),V=L(G,"message","--message");await j.goto("https://www.linkedin.com/messaging/");await j.wait(4);await j.goto(D);await j.wait(12);let K=await F(j);const Z=O(G["expected-last-text"]);for(let $=0;Z&&$<6&&!z(K.bodyText,Z);$+=1){await j.wait(2);K=await F(j)}const Q=A(K,{expectedName:J,threadUrl:D,expectedLastText:G["expected-last-text"],expectedLastHash:G["expected-last-hash"]});if(Q.blockReason==="auth_required")throw new f(P,"LinkedIn safe-send requires an active signed-in LinkedIn browser session.");if(!Q.ok){const $=[`Expected ${Q.expectedValue}; actual ${Q.actualValue||"not_visible"} at ${Q.observedUrl||"url_not_available"}`,`Observed headers: ${(K.headerNames||[]).join(" | ")||"no_visible_headers"}`,`Title: ${K.title||"title_not_available"}`,`Body: ${O(K.bodyText||"").slice(0,500)}`].join(`
154
+ })()`}async function F(j){const G=Y(await j.evaluate(h())),D=O(G?.latestMessageText||"");return{...G||{},latestMessageText:D,latestMessageHash:D?w(D):""}}async function m(j,G){if(!C(G)){await j.insertText(G);return{method:"insert_text",renderedBreaks:0}}const D=Y(await j.evaluate(k(G)));if(D?.ok)return{method:D.method||"dom_multiline",renderedBreaks:Number(D.renderedBreaks||0)};await j.insertText(G);return{method:"insert_text_fallback",renderedBreaks:0}}U({site:"linkedin",name:"safe-send",access:"write",description:"Fail-closed LinkedIn message sender that verifies exact thread, recipient, and latest message before filling/sending",domain:P,strategy:v.UI,browser:!0,args:[{name:"thread-url",required:!0,help:"Exact LinkedIn messaging thread URL to open and verify"},{name:"expected-name",required:!0,help:"Expected visible recipient name in the active thread header"},{name:"message",fromFile:!0,required:!0,help:"Message body to send or dry-run"},{name:"expected-last-text",help:"Substring expected in the currently visible latest conversation context"},{name:"expected-last-hash",help:"SHA-256 hash of expected latest visible message text"},{name:"send",type:"bool",default:!1,help:"Actually click Send. Default is dry-run verification only."},{name:"screenshot",type:"bool",default:!1,help:"Capture a screenshot during verification"}],columns:["status","recipient","reason","thread_url","message_chars","screenshot"],func:async(j,G)=>{if(!j)throw new X("Browser session required for linkedin safe-send");const D=b(S(G,"thread-url","--thread-url"),"--thread-url"),J=S(G,"expected-name","--expected-name"),V=L(G,"message","--message");await j.goto("https://www.linkedin.com/messaging/");await j.wait(4);await j.goto(D);await j.wait(12);let K=await F(j);const Z=O(G["expected-last-text"]);for(let $=0;Z&&$<6&&!z(K.bodyText,Z);$+=1){await j.wait(2);K=await F(j)}const Q=A(K,{expectedName:J,threadUrl:D,expectedLastText:G["expected-last-text"],expectedLastHash:G["expected-last-hash"]});if(Q.blockReason==="auth_required")throw new f(P,"LinkedIn safe-send requires an active signed-in LinkedIn browser session.");if(!Q.ok){const $=[`Expected ${Q.expectedValue}; actual ${Q.actualValue||"not_visible"} at ${Q.observedUrl||"url_not_available"}`,`Observed headers: ${(K.headerNames||[]).join(" | ")||"no_visible_headers"}`,`Title: ${K.title||"title_not_available"}`,`Body: ${O(K.bodyText||"").slice(0,500)}`].join(`
155
155
  `);throw new X(`LinkedIn safe-send blocked: ${Q.blockReason}`,$)}let W="";if(G.screenshot&&typeof j.screenshot==="function")W=await j.screenshot({fullPage:!1});if(!G.send)return[{status:"verified_dry_run",recipient:Q.actualValue,reason:Q.blockReason,thread_url:Q.observedUrl,message_chars:V.length,screenshot:W?"captured":""}];const H=Y(await j.evaluate(x()));if(!H?.ok)throw new X(`LinkedIn safe-send blocked: ${H?.error||"composer_focus_failed"}`);await m(j,V);await j.wait(0.6+Math.random()*0.8);const I=Y(await j.evaluate(i()));if(!I?.ok||O(I.composerText)!==O(V))throw new X("LinkedIn safe-send blocked: composer_text_mismatch",`Composer text did not exactly match intended message for ${J}.`);const E=await F(j),R=A(E,{expectedName:J,threadUrl:D,expectedLastText:G["expected-last-text"],expectedLastHash:G["expected-last-hash"]});if(!R.ok)throw new X(`LinkedIn safe-send blocked after fill: ${R.blockReason}`);const M=Y(await j.evaluate(n()));if(!M?.ok||!M.sent)throw new X(`LinkedIn safe-send blocked: ${M?.error||"send_click_failed"}`);await j.wait(0.8+Math.random()*1.2);return[{status:"sent",recipient:Q.actualValue,reason:Q.blockReason,thread_url:Q.observedUrl,message_chars:V.length,screenshot:W?"captured":""}]}});export const __test__={normalizeWhitespace:O,unwrapEvaluateResult:Y,normalizeName:_,canonicalizeLinkedInThreadUrl:B,hashText:w,hasLineBreaks:C,requireRawStringArg:L,buildFillComposerMultilineScript:k,assessThreadSafety:A};
@@ -1,4 +1,4 @@
1
- import{cli as E,Strategy as q}from"@jackwener/opencli/registry";import{ArgumentError as l,AuthRequiredError as b,CommandExecutionError as u}from"@jackwener/opencli/errors";const w="www.linkedin.com",J="https://www.linkedin.com/sales/",B="(entityUrn,objectUrn,firstName,lastName,fullName,headline,degree,inmailRestriction,memberBadges,defaultPosition)",k="https://www.linkedin.com/sales-api/salesApiCredits?q=findCreditGrant&creditGrantType=LSS_INMAIL",D="https://www.linkedin.com/sales-api/salesApiMessageActions?action=createMessage";function c(e){return String(e??"").replace(/[\u00a0\u202f]/g," ").replace(/\s+/g," ").trim()}function R(e,t,n=t){const r=c(e[t]);if(!r)throw new l(`${n} is required`);return r}function f(e){if(e&&typeof e==="object"&&"data"in e&&"session"in e)return e.data;return e}function G(e){const t=String(e||"").toLowerCase();return t==="linkedin.com"||t.endsWith(".linkedin.com")}function g(e){const t=c(e),n=t.match(/^urn:li:fs_salesProfile:\(([^,()]+),([^,()]+),([^,()]+)\)$/);if(!n)return null;if(!m(n[1],n[2],n[3]))return null;return{profileId:n[1],authType:n[2],authToken:n[3],entityUrn:t}}function m(e,t,n){return[e,t,n].every((r)=>{const i=c(r).toLowerCase();return i&&i!=="undefined"&&i!=="null"&&i!=="not_available"})}function N({profileId:e,authType:t,authToken:n}){return`https://www.linkedin.com/sales/lead/${encodeURIComponent(e)},${encodeURIComponent(t)},${encodeURIComponent(n)}`}function v(e){const t=c(e),n=g(t);if(n)return n;try{const r=new URL(t);if(r.protocol!=="https:"||r.username||r.password||r.port||!G(r.hostname))return null;const i=r.pathname.match(/^\/sales\/lead\/([^,/]+),([^,/]+),([^/]+)\/?$/i);if(i){const a=decodeURIComponent(i[1]),s=decodeURIComponent(i[2]),d=decodeURIComponent(i[3]);if(!m(a,s,d))return null;return{profileId:a,authType:s,authToken:d,entityUrn:`urn:li:fs_salesProfile:(${a},${s},${d})`}}const o=r.pathname.match(/^\/in\/([^/]+)\/?$/i);if(o)return{profileId:decodeURIComponent(o[1]),authType:"",authToken:"",entityUrn:""}}catch{return null}return null}function F(e){return encodeURIComponent(e).replace(/\(/g,"%28").replace(/\)/g,"%29")}function U(e){if(!e?.profileId||!e?.authType||!e?.authToken)return"";return`https://www.linkedin.com/sales-api/salesApiProfiles/${`(profileId:${e.profileId},authType:${e.authType},authToken:${e.authToken})`}?decoration=${F(B)}`}function H(){const e=new Uint8Array(8);if(globalThis.crypto?.getRandomValues)globalThis.crypto.getRandomValues(e);else for(let t=0;t<e.length;t+=1)e[t]=Math.floor(Math.random()*256);return Array.from(e,(t)=>t.toString(16).padStart(2,"0")).join("")}function T({recipientUrn:e,subject:t,body:n,trackingId:r=H(),copyToCrm:i=!1}){const o=c(e);if(!g(o))throw new l("--recipient must resolve to a Sales Navigator lead urn");const a=c(t),s=String(n??"").trim();if(!a)throw new l("--subject is required");if(!s)throw new l("--body is required");if(a.length>200)throw new l("--subject must be 200 characters or fewer");if(s.length>1900)throw new l("--body must be 1900 characters or fewer");return{createMessageRequest:{recipients:[o],subject:a,body:s,copyToCrm:Boolean(i),trackingId:r}}}function _(e){const n=(Array.isArray(e?.elements)?e.elements:[]).find((o)=>o?.type==="LSS_INMAIL"&&Number.isInteger(o.value));if(n)return n.value;const r=[],i=(o)=>{if(o===null||o===void 0)return;if(typeof o==="number"&&Number.isFinite(o))r.push(o);if(Array.isArray(o))o.forEach(i);else if(typeof o==="object"){for(const[a,s]of Object.entries(o))if(/remaining|available|balance|value/i.test(a)&&typeof s==="number")r.unshift(s);else if(!/^count$|^start$|^id$/i.test(a))i(s)}};i(e);return r.find((o)=>Number.isInteger(o)&&o>=0)??null}function p(e,t,n={}){return String.raw`(async () => {
1
+ import{cli as E,Strategy as q}from"@jackwener/opencli/registry";import{ArgumentError as l,AuthRequiredError as b,CommandExecutionError as u}from"@jackwener/opencli/errors";const w="www.linkedin.com",J="https://www.linkedin.com/sales/",B="(entityUrn,objectUrn,firstName,lastName,fullName,headline,degree,inmailRestriction,memberBadges,defaultPosition)",k="https://www.linkedin.com/sales-api/salesApiCredits?q=findCreditGrant&creditGrantType=LSS_INMAIL",D="https://www.linkedin.com/sales-api/salesApiMessageActions?action=createMessage";function c(e){return String(e??"").replace(/[\u00a0\u202f]/g," ").replace(/\s+/g," ").trim()}function R(e,t,n=t){const r=c(e[t]);if(!r)throw new l(`${n} is required`);return r}function f(e){if(e&&typeof e==="object"&&"data"in e&&"session"in e)return e.data;return e}function F(e){const t=String(e||"").toLowerCase();return t==="linkedin.com"||t.endsWith(".linkedin.com")}function g(e){const t=c(e),n=t.match(/^urn:li:fs_salesProfile:\(([^,()]+),([^,()]+),([^,()]+)\)$/);if(!n)return null;if(!m(n[1],n[2],n[3]))return null;return{profileId:n[1],authType:n[2],authToken:n[3],entityUrn:t}}function m(e,t,n){return[e,t,n].every((r)=>{const i=c(r).toLowerCase();return i&&i!=="undefined"&&i!=="null"&&i!=="not_available"})}function N({profileId:e,authType:t,authToken:n}){return`https://www.linkedin.com/sales/lead/${encodeURIComponent(e)},${encodeURIComponent(t)},${encodeURIComponent(n)}`}function v(e){const t=c(e),n=g(t);if(n)return n;try{const r=new URL(t);if(r.protocol!=="https:"||r.username||r.password||r.port||!F(r.hostname))return null;const i=r.pathname.match(/^\/sales\/lead\/([^,/]+),([^,/]+),([^/]+)\/?$/i);if(i){const a=decodeURIComponent(i[1]),s=decodeURIComponent(i[2]),d=decodeURIComponent(i[3]);if(!m(a,s,d))return null;return{profileId:a,authType:s,authToken:d,entityUrn:`urn:li:fs_salesProfile:(${a},${s},${d})`}}const o=r.pathname.match(/^\/in\/([^/]+)\/?$/i);if(o)return{profileId:decodeURIComponent(o[1]),authType:"",authToken:"",entityUrn:""}}catch{return null}return null}function G(e){return encodeURIComponent(e).replace(/\(/g,"%28").replace(/\)/g,"%29")}function U(e){if(!e?.profileId||!e?.authType||!e?.authToken)return"";return`https://www.linkedin.com/sales-api/salesApiProfiles/${`(profileId:${e.profileId},authType:${e.authType},authToken:${e.authToken})`}?decoration=${G(B)}`}function H(){const e=new Uint8Array(8);if(globalThis.crypto?.getRandomValues)globalThis.crypto.getRandomValues(e);else for(let t=0;t<e.length;t+=1)e[t]=Math.floor(Math.random()*256);return Array.from(e,(t)=>t.toString(16).padStart(2,"0")).join("")}function T({recipientUrn:e,subject:t,body:n,trackingId:r=H(),copyToCrm:i=!1}){const o=c(e);if(!g(o))throw new l("--recipient must resolve to a Sales Navigator lead urn");const a=c(t),s=String(n??"").trim();if(!a)throw new l("--subject is required");if(!s)throw new l("--body is required");if(a.length>200)throw new l("--subject must be 200 characters or fewer");if(s.length>1900)throw new l("--body must be 1900 characters or fewer");return{createMessageRequest:{recipients:[o],subject:a,body:s,copyToCrm:Boolean(i),trackingId:r}}}function _(e){const n=(Array.isArray(e?.elements)?e.elements:[]).find((o)=>o?.type==="LSS_INMAIL"&&Number.isInteger(o.value));if(n)return n.value;const r=[],i=(o)=>{if(o===null||o===void 0)return;if(typeof o==="number"&&Number.isFinite(o))r.push(o);if(Array.isArray(o))o.forEach(i);else if(typeof o==="object"){for(const[a,s]of Object.entries(o))if(/remaining|available|balance|value/i.test(a)&&typeof s==="number")r.unshift(s);else if(!/^count$|^start$|^id$/i.test(a))i(s)}};i(e);return r.find((o)=>Number.isInteger(o)&&o>=0)??null}function p(e,t,n={}){return String.raw`(async () => {
2
2
  const headers = {
3
3
  'csrf-token': ${JSON.stringify(t)},
4
4
  'x-restli-protocol-version': '2.0.0',
@@ -30,4 +30,4 @@ import{cli as E,Strategy as q}from"@jackwener/opencli/registry";import{ArgumentE
30
30
  .slice(-20);
31
31
  return { href, text: text.slice(0, 1000), resourceUrns };
32
32
  })()`)),i=String(r?.href||"").match(/\/sales\/lead\/([^,/]+),([^,/]+),([^/?#]+)/i);if(i&&m(i[1],i[2],i[3]))return{profileId:decodeURIComponent(i[1]),authType:decodeURIComponent(i[2]),authToken:decodeURIComponent(i[3]),entityUrn:`urn:li:fs_salesProfile:(${decodeURIComponent(i[1])},${decodeURIComponent(i[2])},${decodeURIComponent(i[3])})`};for(const o of r?.resourceUrns||[]){const a=String(o).match(/profileId:([^,)]+),authType:([^,)]+),authToken:([^,)]+)\)/);if(a&&a[1]===t.profileId)return{profileId:a[1],authType:a[2],authToken:a[3],entityUrn:`urn:li:fs_salesProfile:(${a[1]},${a[2]},${a[3]})`}}throw new u("Could not resolve Sales Navigator auth token for recipient",`Observed URL: ${r?.href||"url_not_available"}
33
- Body: ${c(r?.text||"").slice(0,500)}`)}function $(e){const t=e?.data||e||{},n=t.defaultPosition||(Array.isArray(t.positions)?t.positions.find((r)=>r.current)||t.positions[0]:{})||{};return{recipient:c(t.fullName||[t.firstName,t.lastName].filter(Boolean).join(" ")),title:c(n.title||t.headline||""),company:c(n.companyName||n.company?.name||""),degree:c(t.degree||""),inmail_restriction:c(t.inmailRestriction||""),open_link:Boolean(t.memberBadges?.openLink)}}function j(e){const t=$(e);if(!t.recipient)throw new u("Sales Navigator profile lookup returned malformed profile data","missing_recipient_name");return t}E({site:"linkedin",name:"salesnav-message",access:"write",description:"Send or dry-run a LinkedIn Sales Navigator InMail to a lead using the Sales Navigator messaging API",domain:w,strategy:q.UI,browser:!0,args:[{name:"recipient",type:"string",required:!0,positional:!0,help:"Sales Navigator lead URL, LinkedIn /in/ URL from salesnav-search, or urn:li:fs_salesProfile:(...)"},{name:"subject",type:"string",required:!0,help:"InMail subject"},{name:"body",type:"string",required:!0,help:"InMail body"},{name:"send",type:"bool",default:!1,help:"Actually send the InMail. Default is dry-run validation only."},{name:"copy-to-crm",type:"bool",default:!1,help:"Set Sales Navigator copyToCrm on the message request"}],columns:["status","recipient","title","company","credits_remaining","credits_before","credits_after","sent_in_salesnav","message_chars","subject_chars","recipient_urn","degree","inmail_restriction","open_link"],func:async(e,t)=>{if(!e)throw new u("Browser session required for linkedin salesnav-message");const n=R(t,"recipient","--recipient"),r=R(t,"subject","--subject"),i=String(t.body??"").trim();if(!i)throw new l("--body is required");await e.goto(J);await e.wait(4);const o=await z(e),a=await V(e,v(n),o);let s={recipient:"",title:"",company:"",degree:"",inmail_restriction:"",open_link:!1};const d=U(a);if(d){const O=h(f(await e.evaluate(p(d,o))),"LinkedIn Sales Navigator profile API");s=j(O.json)}if(s.inmail_restriction&&s.inmail_restriction!=="NO_RESTRICTION")throw new u("Sales Navigator InMail blocked by recipient restriction",s.inmail_restriction);const C=h(f(await e.evaluate(p(k,o))),"LinkedIn Sales Navigator credits API"),y=_(C?.json),L=T({recipientUrn:a.entityUrn,subject:r,body:i,copyToCrm:t["copy-to-crm"]});if(!t.send)return[{status:"validated_dry_run",recipient:s.recipient,title:s.title,company:s.company,credits_remaining:y,credits_before:y,credits_after:"",sent_in_salesnav:!1,message_chars:i.length,subject_chars:r.length,recipient_urn:a.entityUrn,degree:s.degree,inmail_restriction:s.inmail_restriction,open_link:s.open_link}];const P=h(f(await e.evaluate(p(D,o,{method:"POST",accept:"application/vnd.linkedin.normalized+json+2.1",body:L}))),"LinkedIn Sales Navigator message API",{requireJson:!1});await e.wait(3);const M=h(f(await e.evaluate(p(k,o))),"LinkedIn Sales Navigator credits API after send"),I=_(M?.json);await e.goto(N(a));await e.wait(6);const x=f(await e.evaluate('document.body ? document.body.innerText : ""')),S=A(x,s.recipient);if(!S)throw new u("Sales Navigator post-send verification failed","Sent activity was not found on the Sales Navigator lead page.");return[{status:"sent",recipient:s.recipient,title:s.title,company:s.company,credits_remaining:I,credits_before:y,credits_after:I,sent_in_salesnav:S,message_chars:i.length,subject_chars:r.length,recipient_urn:a.entityUrn,degree:s.degree,inmail_restriction:s.inmail_restriction,open_link:s.open_link}]}});export const __test__={normalizeWhitespace:c,parseSalesProfileUrn:g,isResolvedSalesProfileParts:m,parseRecipient:v,salesLeadUrlFromParts:N,profileApiUrl:U,buildCreateMessagePayload:T,extractRemainingCredits:_,profileSummary:$,requireProfileSummary:j,salesPageShowsSentMessage:A};
33
+ Body: ${c(r?.text||"").slice(0,500)}`)}function $(e){const t=e?.data||e||{},n=t.defaultPosition||(Array.isArray(t.positions)?t.positions.find((r)=>r.current)||t.positions[0]:{})||{};return{recipient:c(t.fullName||[t.firstName,t.lastName].filter(Boolean).join(" ")),title:c(n.title||t.headline||""),company:c(n.companyName||n.company?.name||""),degree:c(t.degree||""),inmail_restriction:c(t.inmailRestriction||""),open_link:Boolean(t.memberBadges?.openLink)}}function j(e){const t=$(e);if(!t.recipient)throw new u("Sales Navigator profile lookup returned malformed profile data","missing_recipient_name");return t}E({site:"linkedin",name:"salesnav-message",access:"write",description:"Send or dry-run a LinkedIn Sales Navigator InMail to a lead using the Sales Navigator messaging API",domain:w,strategy:q.UI,browser:!0,args:[{name:"recipient",type:"string",required:!0,positional:!0,help:"Sales Navigator lead URL, LinkedIn /in/ URL from salesnav-search, or urn:li:fs_salesProfile:(...)"},{name:"subject",type:"string",required:!0,help:"InMail subject"},{name:"body",fromFile:!0,type:"string",required:!0,help:"InMail body"},{name:"send",type:"bool",default:!1,help:"Actually send the InMail. Default is dry-run validation only."},{name:"copy-to-crm",type:"bool",default:!1,help:"Set Sales Navigator copyToCrm on the message request"}],columns:["status","recipient","title","company","credits_remaining","credits_before","credits_after","sent_in_salesnav","message_chars","subject_chars","recipient_urn","degree","inmail_restriction","open_link"],func:async(e,t)=>{if(!e)throw new u("Browser session required for linkedin salesnav-message");const n=R(t,"recipient","--recipient"),r=R(t,"subject","--subject"),i=String(t.body??"").trim();if(!i)throw new l("--body is required");await e.goto(J);await e.wait(4);const o=await z(e),a=await V(e,v(n),o);let s={recipient:"",title:"",company:"",degree:"",inmail_restriction:"",open_link:!1};const d=U(a);if(d){const O=h(f(await e.evaluate(p(d,o))),"LinkedIn Sales Navigator profile API");s=j(O.json)}if(s.inmail_restriction&&s.inmail_restriction!=="NO_RESTRICTION")throw new u("Sales Navigator InMail blocked by recipient restriction",s.inmail_restriction);const C=h(f(await e.evaluate(p(k,o))),"LinkedIn Sales Navigator credits API"),y=_(C?.json),L=T({recipientUrn:a.entityUrn,subject:r,body:i,copyToCrm:t["copy-to-crm"]});if(!t.send)return[{status:"validated_dry_run",recipient:s.recipient,title:s.title,company:s.company,credits_remaining:y,credits_before:y,credits_after:"",sent_in_salesnav:!1,message_chars:i.length,subject_chars:r.length,recipient_urn:a.entityUrn,degree:s.degree,inmail_restriction:s.inmail_restriction,open_link:s.open_link}];const P=h(f(await e.evaluate(p(D,o,{method:"POST",accept:"application/vnd.linkedin.normalized+json+2.1",body:L}))),"LinkedIn Sales Navigator message API",{requireJson:!1});await e.wait(3);const M=h(f(await e.evaluate(p(k,o))),"LinkedIn Sales Navigator credits API after send"),I=_(M?.json);await e.goto(N(a));await e.wait(6);const x=f(await e.evaluate('document.body ? document.body.innerText : ""')),S=A(x,s.recipient);if(!S)throw new u("Sales Navigator post-send verification failed","Sent activity was not found on the Sales Navigator lead page.");return[{status:"sent",recipient:s.recipient,title:s.title,company:s.company,credits_remaining:I,credits_before:y,credits_after:I,sent_in_salesnav:S,message_chars:i.length,subject_chars:r.length,recipient_urn:a.entityUrn,degree:s.degree,inmail_restriction:s.inmail_restriction,open_link:s.open_link}]}});export const __test__={normalizeWhitespace:c,parseSalesProfileUrn:g,isResolvedSalesProfileParts:m,parseRecipient:v,salesLeadUrlFromParts:N,profileApiUrl:U,buildCreateMessagePayload:T,extractRemainingCredits:_,profileSummary:$,requireProfileSummary:j,salesPageShowsSentMessage:A};
@@ -14,4 +14,4 @@ import*as K from"node:fs";import*as W from"node:path";import{cli as x,Strategy a
14
14
  const res = await fetch(${JSON.stringify(V)}, { method: 'PUT', body: new Blob([arr]) });
15
15
  return { ok: res.ok, status: res.status };
16
16
  } catch (e) { return { ok: false, status: 0, text: String(e && e.message || e) }; }
17
- })()`),O=P(Y);if(!O||!O.ok)throw new M(`note image upload PUT failed (best-effort): HTTP ${O?O.status:"?"}`);return q}x({site:"note",name:"publish",access:"write",description:"Publish a text note to note.com (or save a draft with --draft)",domain:"note.com",strategy:D.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!0,positional:!0,help:"Note title"},{name:"body",type:"string",help:"Note body text (blank lines split paragraphs)"},{name:"tags",type:"string",help:"Hashtags, comma-separated (# optional)"},{name:"magazine",type:"string",help:"Magazine id(s) to file under, comma-separated"},{name:"images",type:"string",help:"(best-effort) in-body images: local paths and/or urls"},{name:"draft",type:"boolean",default:!1,help:"Save the draft but do not publish"}],columns:["status","note_key","url"],func:async(G,J)=>{if(!G)throw new M("Browser session required for note publish");const Z=String(J.title??"").trim();if(!Z)throw new v("note publish: title is required");const Q=h(J.tags),_=b(J.magazine),L=u(J.images);await G.goto("https://note.com/");const N=k(await G.getCookies({url:"https://note.com"})),V=await z(G,{url:"https://note.com/api/v2/current_user"}),q=V.data&&(V.data.data||V.data);if(V.status===401||V.status===403||!q||!q.urlname)throw new f("note.com",`Not logged in (current_user HTTP ${V.status})`);const R=String(q.urlname),Y=await z(G,{url:"https://note.com/api/v1/text_notes",method:"POST",headers:S(N),body:JSON.stringify({template_key:null})}),O=Y.data&&(Y.data.data||Y.data);if(!Y.ok||!O||O.id==null)throw new M(`note create-draft failed: HTTP ${Y.status} ${Y.text}`);const j=O.id;let $=O.key?String(O.key):"";const F=[];for(const A of L)F.push(A.kind==="url"?A.value:await U(G,A.value,N));const C=E(w(J.body),F),T=await z(G,{url:`https://note.com/api/v1/text_notes/draft_save?id=${encodeURIComponent(j)}&is_temp_saved=true`,method:"POST",headers:S(N),body:JSON.stringify(c({title:Z,bodyHtml:C}))});if(!T.ok)throw new M(`note draft_save failed: HTTP ${T.status} ${T.text}`);if(J.draft)return{status:"draft",note_key:$,url:$?H(R,$):`https://note.com/notes/${j}/edit`};const B=await z(G,{url:`https://note.com/api/v1/text_notes/${encodeURIComponent(j)}`,method:"PUT",headers:S(N),body:JSON.stringify(y({title:Z,bodyHtml:C,hashtags:Q,magazineIds:_}))});if(!B.ok)throw new M(`note publish failed: HTTP ${B.status} ${B.text}`);const X=B.data&&(B.data.data||B.data);if(X&&X.note_key)$=String(X.note_key);const I=X&&X.public_url||($?H(R,$):`https://note.com/${R}`);return{status:"published",note_key:$,url:I}}});export const __test__={uploadInlineImage:U};
17
+ })()`),O=P(Y);if(!O||!O.ok)throw new M(`note image upload PUT failed (best-effort): HTTP ${O?O.status:"?"}`);return q}x({site:"note",name:"publish",access:"write",description:"Publish a text note to note.com (or save a draft with --draft)",domain:"note.com",strategy:D.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!0,positional:!0,help:"Note title"},{name:"body",fromFile:!0,type:"string",help:"Note body text (blank lines split paragraphs)"},{name:"tags",type:"string",help:"Hashtags, comma-separated (# optional)"},{name:"magazine",type:"string",help:"Magazine id(s) to file under, comma-separated"},{name:"images",type:"string",help:"(best-effort) in-body images: local paths and/or urls"},{name:"draft",type:"boolean",default:!1,help:"Save the draft but do not publish"}],columns:["status","note_key","url"],func:async(G,J)=>{if(!G)throw new M("Browser session required for note publish");const Z=String(J.title??"").trim();if(!Z)throw new v("note publish: title is required");const Q=h(J.tags),_=b(J.magazine),L=u(J.images);await G.goto("https://note.com/");const N=k(await G.getCookies({url:"https://note.com"})),V=await z(G,{url:"https://note.com/api/v2/current_user"}),q=V.data&&(V.data.data||V.data);if(V.status===401||V.status===403||!q||!q.urlname)throw new f("note.com",`Not logged in (current_user HTTP ${V.status})`);const R=String(q.urlname),Y=await z(G,{url:"https://note.com/api/v1/text_notes",method:"POST",headers:S(N),body:JSON.stringify({template_key:null})}),O=Y.data&&(Y.data.data||Y.data);if(!Y.ok||!O||O.id==null)throw new M(`note create-draft failed: HTTP ${Y.status} ${Y.text}`);const j=O.id;let $=O.key?String(O.key):"";const F=[];for(const A of L)F.push(A.kind==="url"?A.value:await U(G,A.value,N));const C=E(w(J.body),F),T=await z(G,{url:`https://note.com/api/v1/text_notes/draft_save?id=${encodeURIComponent(j)}&is_temp_saved=true`,method:"POST",headers:S(N),body:JSON.stringify(c({title:Z,bodyHtml:C}))});if(!T.ok)throw new M(`note draft_save failed: HTTP ${T.status} ${T.text}`);if(J.draft)return{status:"draft",note_key:$,url:$?H(R,$):`https://note.com/notes/${j}/edit`};const B=await z(G,{url:`https://note.com/api/v1/text_notes/${encodeURIComponent(j)}`,method:"PUT",headers:S(N),body:JSON.stringify(y({title:Z,bodyHtml:C,hashtags:Q,magazineIds:_}))});if(!B.ok)throw new M(`note publish failed: HTTP ${B.status} ${B.text}`);const X=B.data&&(B.data.data||B.data);if(X&&X.note_key)$=String(X.note_key);const I=X&&X.public_url||($?H(R,$):`https://note.com/${R}`);return{status:"published",note_key:$,url:I}}});export const __test__={uploadInlineImage:U};
@@ -43,4 +43,4 @@ import*as B from"node:fs";import*as L from"node:path";import{cli as U,Strategy a
43
43
  } catch (e) {
44
44
  return { error: 'upload exception: ' + ((e && e.message) || String(e)) };
45
45
  }
46
- })()`,y=await G.evaluate(Y),H=y&&typeof y==="object"&&"data"in y&&"session"in y?y.data:y;if(!H||H.error)throw new j("NotebookLM file upload failed: "+(H?.error||"unknown error"));return H}U({site:x,name:"add-source",access:"write",description:"Add a URL, text, or local file source to an existing NotebookLM notebook",domain:h,strategy:O.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"notebook",positional:!0,required:!0,help:"Notebook id from `notebooklm list` or full notebook URL"},{name:"url",help:"Source URL to add (http/https). Pass exactly one of --url, --content, --file."},{name:"content",help:"Raw text content to add as a Text source (max 10 MB)."},{name:"file",help:`Local file path to upload as a source (max ${X} bytes; pdf / txt / md / html / docx / etc.). Uses Google Drive's 3-step resumable upload protocol.`},{name:"title",help:'Title for the text source (default "Text Source"). Ignored for --url and --file.'},{name:"mime-type",help:"Override the auto-detected MIME type when --file is given."},{name:"execute",type:"boolean",help:"Actually add the remote source to the NotebookLM notebook"}],columns:["notebook_id","source_id","kind","identifier","notebook_url"],func:async(G,Q)=>{const V=_(String(Q.notebook??"")),W=parseSourceUrl(Q.url),Z=parseSourceText(Q.content),$=typeof Q.file==="string"&&Q.file.trim()?Q.file.trim():"",J=[W?"url":"",Z!==null?"text":"",$?"file":""].filter(Boolean);if(J.length===0)throw new q("Pass exactly one of --url <url>, --content <text>, or --file <path>");if(J.length>1)throw new q("Pass exactly one of --url, --content, --file (got: "+J.join(" + ")+")");S(Q.execute,"add a NotebookLM source");const K=parseSourceTitle(Q.title,"Text Source");await T(G);await f(G);if($){const z=readFileForUpload($),N=inferMimeType(z.filename,Q["mime-type"]),R=await C(G,b,buildRegisterFileSourceArgs(V,z.filename)),D=parseAddSourceResult(R.result,[V]);if(!D)throw new j("NotebookLM AddFileSource (o4cbdc) RPC returned no source id; cannot start file upload.");await I(G,V,D,z.filename,z.base64,z.size);await M(G,V,D,"add-source --file");return[{notebook_id:V,source_id:D,kind:"file",identifier:`${z.filename} (${N}, ${z.size} bytes)`,notebook_url:P(V)}]}const Y=W?buildAddSourceFromUrlArgs(V,W):buildAddSourceFromTextArgs(V,K,Z),y=await C(G,A,Y),H=parseAddSourceResult(y.result,[V]);if(!H)throw new j("NotebookLM AddSources RPC returned no source id; verify the input reaches the NotebookLM backend.");await M(G,V,H,`add-source --${W?"url":"content"}`);return[{notebook_id:V,source_id:H,kind:W?"url":"text",identifier:W||K,notebook_url:P(V)}]}});export const __test__={parseSourceUrl,parseSourceText,parseSourceTitle,inferMimeType,buildAddSourceFromUrlArgs,buildAddSourceFromTextArgs,buildRegisterFileSourceArgs,readFileForUpload,parseAddSourceResult};
46
+ })()`,y=await G.evaluate(Y),H=y&&typeof y==="object"&&"data"in y&&"session"in y?y.data:y;if(!H||H.error)throw new j("NotebookLM file upload failed: "+(H?.error||"unknown error"));return H}U({site:x,name:"add-source",access:"write",description:"Add a URL, text, or local file source to an existing NotebookLM notebook",domain:h,strategy:O.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"notebook",positional:!0,required:!0,help:"Notebook id from `notebooklm list` or full notebook URL"},{name:"url",help:"Source URL to add (http/https). Pass exactly one of --url, --content, --file."},{name:"content",fromFile:"content-file",help:"Raw text content to add as a Text source (max 10 MB)."},{name:"file",help:`Local file path to upload as a source (max ${X} bytes; pdf / txt / md / html / docx / etc.). Uses Google Drive's 3-step resumable upload protocol.`},{name:"title",help:'Title for the text source (default "Text Source"). Ignored for --url and --file.'},{name:"mime-type",help:"Override the auto-detected MIME type when --file is given."},{name:"execute",type:"boolean",help:"Actually add the remote source to the NotebookLM notebook"}],columns:["notebook_id","source_id","kind","identifier","notebook_url"],func:async(G,Q)=>{const V=_(String(Q.notebook??"")),W=parseSourceUrl(Q.url),Z=parseSourceText(Q.content),$=typeof Q.file==="string"&&Q.file.trim()?Q.file.trim():"",J=[W?"url":"",Z!==null?"text":"",$?"file":""].filter(Boolean);if(J.length===0)throw new q("Pass exactly one of --url <url>, --content <text>, or --file <path>");if(J.length>1)throw new q("Pass exactly one of --url, --content, --file (got: "+J.join(" + ")+")");S(Q.execute,"add a NotebookLM source");const K=parseSourceTitle(Q.title,"Text Source");await T(G);await f(G);if($){const z=readFileForUpload($),N=inferMimeType(z.filename,Q["mime-type"]),R=await C(G,b,buildRegisterFileSourceArgs(V,z.filename)),D=parseAddSourceResult(R.result,[V]);if(!D)throw new j("NotebookLM AddFileSource (o4cbdc) RPC returned no source id; cannot start file upload.");await I(G,V,D,z.filename,z.base64,z.size);await M(G,V,D,"add-source --file");return[{notebook_id:V,source_id:D,kind:"file",identifier:`${z.filename} (${N}, ${z.size} bytes)`,notebook_url:P(V)}]}const Y=W?buildAddSourceFromUrlArgs(V,W):buildAddSourceFromTextArgs(V,K,Z),y=await C(G,A,Y),H=parseAddSourceResult(y.result,[V]);if(!H)throw new j("NotebookLM AddSources RPC returned no source id; verify the input reaches the NotebookLM backend.");await M(G,V,H,`add-source --${W?"url":"content"}`);return[{notebook_id:V,source_id:H,kind:W?"url":"text",identifier:W||K,notebook_url:P(V)}]}});export const __test__={parseSourceUrl,parseSourceText,parseSourceTitle,inferMimeType,buildAddSourceFromUrlArgs,buildAddSourceFromTextArgs,buildRegisterFileSourceArgs,readFileForUpload,parseAddSourceResult};
@@ -1 +1 @@
1
- import{cli as m,Strategy as N}from"@jackwener/opencli/registry";import{ArgumentError as s,CommandExecutionError as b}from"@jackwener/opencli/errors";import{NOTEBOOKLM_DOMAIN as d,NOTEBOOKLM_SITE as h}from"./shared.js";import{callNotebooklmRpc as c}from"./rpc.js";import{buildNotebooklmNotebookUrl as p,ensureNotebooklmHome as _,parseNotebooklmNotebookTarget as k,requireNotebooklmExecute as w,requireNotebooklmSession as E}from"./utils.js";const O="CYK0Xb",T="cYAfTb",l=200,u=1e6,f=/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;export function parseNoteTitle(t){const e=String(t??"").trim();if(!e)throw new s("--title is required");if(e.length>l)throw new s(`--title must be at most ${l} characters (got ${e.length})`);return e}export function parseNoteContent(t){const e=String(t??"");if(!e)throw new s("--content is required");if(e.length>u)throw new s(`--content exceeds ${u} characters; split into smaller notes.`);return e}export function buildCreateNoteShellArgs(t){return[t,"",[1],null,"New Note",null,[2]]}export function buildMutateNoteArgs(t,e,n,r){return[t,e,[[[n,r,[],0]]],[2]]}function g(t){return new Set(t.map((e)=>String(e??"").toLowerCase()).filter(Boolean))}export function parseNoteIdFromResult(t,e=[]){const n=g(e);if(typeof t==="string")return f.test(t)&&!n.has(t.toLowerCase())?t:"";const r=[t];while(r.length){const o=r.shift();if(typeof o==="string"){if(f.test(o)&&!n.has(o.toLowerCase()))return o;continue}if(Array.isArray(o))for(const i of o)r.push(i);else if(o&&typeof o==="object")for(const i of Object.values(o))r.push(i)}return""}m({site:h,name:"write-note",access:"write",description:"Create a Studio note in an existing NotebookLM notebook with the given title and Markdown content",domain:d,strategy:N.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"notebook",positional:!0,required:!0,help:"Notebook id from `notebooklm list` or full notebook URL"},{name:"title",required:!0,help:"Note title (1-200 chars)"},{name:"content",required:!0,help:"Note body as Markdown"},{name:"execute",type:"boolean",help:"Actually create the remote NotebookLM note"}],columns:["notebook_id","note_id","title","notebook_url"],func:async(t,e)=>{const n=k(String(e.notebook??"")),r=parseNoteTitle(e.title),o=parseNoteContent(e.content);w(e.execute,"create a NotebookLM note");await _(t);await E(t);const i=await c(t,O,buildCreateNoteShellArgs(n)),a=parseNoteIdFromResult(i.result,[n]);if(!a)throw new b("NotebookLM CreateNote RPC returned no note id");await c(t,T,buildMutateNoteArgs(n,a,o,r));return[{notebook_id:n,note_id:a,title:r,notebook_url:p(n)}]}});export const __test__={parseNoteTitle,parseNoteContent,buildCreateNoteShellArgs,buildMutateNoteArgs,parseNoteIdFromResult};
1
+ import{cli as m,Strategy as N}from"@jackwener/opencli/registry";import{ArgumentError as s,CommandExecutionError as b}from"@jackwener/opencli/errors";import{NOTEBOOKLM_DOMAIN as d,NOTEBOOKLM_SITE as h}from"./shared.js";import{callNotebooklmRpc as c}from"./rpc.js";import{buildNotebooklmNotebookUrl as p,ensureNotebooklmHome as _,parseNotebooklmNotebookTarget as k,requireNotebooklmExecute as w,requireNotebooklmSession as E}from"./utils.js";const O="CYK0Xb",T="cYAfTb",l=200,u=1e6,f=/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;export function parseNoteTitle(t){const e=String(t??"").trim();if(!e)throw new s("--title is required");if(e.length>l)throw new s(`--title must be at most ${l} characters (got ${e.length})`);return e}export function parseNoteContent(t){const e=String(t??"");if(!e)throw new s("--content is required");if(e.length>u)throw new s(`--content exceeds ${u} characters; split into smaller notes.`);return e}export function buildCreateNoteShellArgs(t){return[t,"",[1],null,"New Note",null,[2]]}export function buildMutateNoteArgs(t,e,n,r){return[t,e,[[[n,r,[],0]]],[2]]}function g(t){return new Set(t.map((e)=>String(e??"").toLowerCase()).filter(Boolean))}export function parseNoteIdFromResult(t,e=[]){const n=g(e);if(typeof t==="string")return f.test(t)&&!n.has(t.toLowerCase())?t:"";const r=[t];while(r.length){const o=r.shift();if(typeof o==="string"){if(f.test(o)&&!n.has(o.toLowerCase()))return o;continue}if(Array.isArray(o))for(const i of o)r.push(i);else if(o&&typeof o==="object")for(const i of Object.values(o))r.push(i)}return""}m({site:h,name:"write-note",access:"write",description:"Create a Studio note in an existing NotebookLM notebook with the given title and Markdown content",domain:d,strategy:N.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"notebook",positional:!0,required:!0,help:"Notebook id from `notebooklm list` or full notebook URL"},{name:"title",required:!0,help:"Note title (1-200 chars)"},{name:"content",fromFile:!0,required:!0,help:"Note body as Markdown"},{name:"execute",type:"boolean",help:"Actually create the remote NotebookLM note"}],columns:["notebook_id","note_id","title","notebook_url"],func:async(t,e)=>{const n=k(String(e.notebook??"")),r=parseNoteTitle(e.title),o=parseNoteContent(e.content);w(e.execute,"create a NotebookLM note");await _(t);await E(t);const i=await c(t,O,buildCreateNoteShellArgs(n)),a=parseNoteIdFromResult(i.result,[n]);if(!a)throw new b("NotebookLM CreateNote RPC returned no note id");await c(t,T,buildMutateNoteArgs(n,a,o,r));return[{notebook_id:n,note_id:a,title:r,notebook_url:p(n)}]}});export const __test__={parseNoteTitle,parseNoteContent,buildCreateNoteShellArgs,buildMutateNoteArgs,parseNoteIdFromResult};
@@ -1 +1 @@
1
- import{cli as m,Strategy as s}from"@jackwener/opencli/registry";import{ArgumentError as d}from"@jackwener/opencli/errors";import{WWW_BASE as c,postForm as u,requireOk as a,resolveMomentId as p}from"./utils.js";import{assertLiteralContent as l}from"../_shared/content-guard.js";m({site:"nowcoder",name:"comment",access:"write",description:"评论帖子;配 --to-user/--to-comment 可定向回复某条评论",domain:"www.nowcoder.com",strategy:s.COOKIE,args:[{name:"id",positional:!0,required:!0,help:"帖子 id(数字 momentId / UUID / 链接均可)"},{name:"content",positional:!0,required:!0,help:"评论内容"},{name:"to-user",type:"str",default:"0",help:"帖主/对方 userId(定向回复时必传)"},{name:"to-comment",type:"str",default:"0",help:"定向回复的评论 id(inbox 的 reply_comment_id)"}],columns:["post_id","kind","status"],func:async(n,t)=>{const r=await p(n,t.id),e=String(t.content??"").trim();l(e,{label:"评论内容"});if(!e)throw new d("评论内容不能为空");const i=String(t["to-user"]??"0").trim()||"0",o=String(t["to-comment"]??"0").trim()||"0";a(await u(n,`${c}/comment/create?_=${Date.now()}`,{entityType:"74",entityId:r,commentContent:e,contentV2:JSON.stringify({pureText:e,imgs:[]}),toFeed:"false",toUserId:i,toCommentId:o,entityOwnerId:i,isAnonymousFlag:"false"}),"评论");return[{post_id:r,kind:o!=="0"?`reply-to-${o}`:"comment",status:"ok"}]}});
1
+ import{cli as m,Strategy as s}from"@jackwener/opencli/registry";import{ArgumentError as d}from"@jackwener/opencli/errors";import{WWW_BASE as c,postForm as u,requireOk as a,resolveMomentId as l}from"./utils.js";import{assertLiteralContent as p}from"../_shared/content-guard.js";m({site:"nowcoder",name:"comment",access:"write",description:"评论帖子;配 --to-user/--to-comment 可定向回复某条评论",domain:"www.nowcoder.com",strategy:s.COOKIE,args:[{name:"id",positional:!0,required:!0,help:"帖子 id(数字 momentId / UUID / 链接均可)"},{name:"content",fromFile:!0,positional:!0,required:!0,help:"评论内容"},{name:"to-user",type:"str",default:"0",help:"帖主/对方 userId(定向回复时必传)"},{name:"to-comment",type:"str",default:"0",help:"定向回复的评论 id(inbox 的 reply_comment_id)"}],columns:["post_id","kind","status"],func:async(n,t)=>{const r=await l(n,t.id),e=String(t.content??"").trim();p(e,{label:"评论内容"});if(!e)throw new d("评论内容不能为空");const i=String(t["to-user"]??"0").trim()||"0",o=String(t["to-comment"]??"0").trim()||"0";a(await u(n,`${c}/comment/create?_=${Date.now()}`,{entityType:"74",entityId:r,commentContent:e,contentV2:JSON.stringify({pureText:e,imgs:[]}),toFeed:"false",toUserId:i,toCommentId:o,entityOwnerId:i,isAnonymousFlag:"false"}),"评论");return[{post_id:r,kind:o!=="0"?`reply-to-${o}`:"comment",status:"ok"}]}});
@@ -1 +1 @@
1
- import{cli as r,Strategy as i}from"@jackwener/opencli/registry";import{ArgumentError as s}from"@jackwener/opencli/errors";import{GW_BASE as a,postJson as c,requireOk as d}from"./utils.js";import{assertLiteralContent as m}from"../_shared/content-guard.js";r({site:"nowcoder",name:"dm-send",access:"write",description:"发私信(conversation 为 dm-list 的 conversation_id)",domain:"www.nowcoder.com",strategy:i.COOKIE,args:[{name:"conversation",positional:!0,required:!0,help:"会话 id(dm-list 的 conversation_id,格式 <我uid>_<对方uid>)"},{name:"content",positional:!0,required:!0,help:"私信内容"}],columns:["conversation_id","status"],func:async(n,o)=>{const e=String(o.conversation??"").trim(),t=String(o.content??"").trim();m(t,{label:"私信内容"});if(!t)throw new s("私信内容不能为空");d(await c(n,`${a}/msg/send-msg`,{body:{content:t,sourceType:1200,conversationId:e}}),"私信发送");return[{conversation_id:e,status:"sent"}]}});
1
+ import{cli as r,Strategy as i}from"@jackwener/opencli/registry";import{ArgumentError as s}from"@jackwener/opencli/errors";import{GW_BASE as a,postJson as c,requireOk as m}from"./utils.js";import{assertLiteralContent as d}from"../_shared/content-guard.js";r({site:"nowcoder",name:"dm-send",access:"write",description:"发私信(conversation 为 dm-list 的 conversation_id)",domain:"www.nowcoder.com",strategy:i.COOKIE,args:[{name:"conversation",positional:!0,required:!0,help:"会话 id(dm-list 的 conversation_id,格式 <我uid>_<对方uid>)"},{name:"content",fromFile:!0,positional:!0,required:!0,help:"私信内容"}],columns:["conversation_id","status"],func:async(n,o)=>{const e=String(o.conversation??"").trim(),t=String(o.content??"").trim();d(t,{label:"私信内容"});if(!t)throw new s("私信内容不能为空");m(await c(n,`${a}/msg/send-msg`,{body:{content:t,sourceType:1200,conversationId:e}}),"私信发送");return[{conversation_id:e,status:"sent"}]}});
@@ -1 +1 @@
1
- import{cli as i,Strategy as a}from"@jackwener/opencli/registry";import{ArgumentError as c,CommandExecutionError as m}from"@jackwener/opencli/errors";import{GW_BASE as l,postJson as s,requireOk as d}from"./utils.js";import{assertLiteralContent as p}from"../_shared/content-guard.js";i({site:"nowcoder",name:"post",access:"write",description:"发布动态(实名发帖,高风险写操作,务必低频且内容有价值)",domain:"www.nowcoder.com",strategy:a.COOKIE,args:[{name:"content",positional:!0,required:!0,help:"正文内容"},{name:"title",type:"str",default:"",help:"标题(可选)"}],columns:["moment_id","content_id","url"],func:async(o,n)=>{const e=String(n.content??"").trim();p(e,{label:"正文内容"});if(!e)throw new c("发帖正文不能为空");const r=d(await s(o,`${l}/moment/create`,{title:String(n.title??""),content:e,type:0,isAnonymousFlag:!1,publicEntrance:"发动态",publicEntrancePage:"首页"}),"发帖"),t=r.data??{};if(!t.id)throw new m(`牛客发帖响应缺少 momentId: ${JSON.stringify(r).slice(0,200)}`);return[{moment_id:t.id,content_id:t.result??"",url:t.result?`https://www.nowcoder.com/feed/main/detail/${t.result}`:""}]}});
1
+ import{cli as i,Strategy as a}from"@jackwener/opencli/registry";import{ArgumentError as c,CommandExecutionError as m}from"@jackwener/opencli/errors";import{GW_BASE as l,postJson as s,requireOk as d}from"./utils.js";import{assertLiteralContent as p}from"../_shared/content-guard.js";i({site:"nowcoder",name:"post",access:"write",description:"发布动态(实名发帖,高风险写操作,务必低频且内容有价值)",domain:"www.nowcoder.com",strategy:a.COOKIE,args:[{name:"content",fromFile:!0,positional:!0,required:!0,help:"正文内容"},{name:"title",type:"str",default:"",help:"标题(可选)"}],columns:["moment_id","content_id","url"],func:async(o,n)=>{const e=String(n.content??"").trim();p(e,{label:"正文内容"});if(!e)throw new c("发帖正文不能为空");const r=d(await s(o,`${l}/moment/create`,{title:String(n.title??""),content:e,type:0,isAnonymousFlag:!1,publicEntrance:"发动态",publicEntrancePage:"首页"}),"发帖"),t=r.data??{};if(!t.id)throw new m(`牛客发帖响应缺少 momentId: ${JSON.stringify(r).slice(0,200)}`);return[{moment_id:t.id,content_id:t.result??"",url:t.result?`https://www.nowcoder.com/feed/main/detail/${t.result}`:""}]}});
@@ -10,4 +10,4 @@ import*as g from"node:fs";import*as u from"node:path";import{cli as U,Strategy a
10
10
  let data = null; try { data = JSON.parse(text); } catch {}
11
11
  return { ok: res.ok, status: res.status, data, text: text.slice(0, 300) };
12
12
  } catch (e) { return { ok: false, status: 0, data: null, text: String(e && e.message || e) }; }
13
- })()`),e=a&&typeof a==="object"&&"session"in a&&"data"in a?a.data:a,n=e&&e.data&&(e.data.image_url||e.data.url||e.data.success&&e.data.success.image_url);if(!e||!e.ok||!n)throw new b(`Pinterest image upload failed (best-effort): HTTP ${e?e.status:"?"} ${e?e.text:""}. Prefer --image-url.`);return n}U({site:"pinterest",name:"pin",access:"write",description:"Create a pin on a board with an optional destination link",domain:"www.pinterest.com",strategy:O.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!0,positional:!0,help:"Pin title"},{name:"image-url",type:"string",help:"Image URL to pin (required unless --image)"},{name:"image",type:"string",help:"(best-effort) local image file to upload"},{name:"board",type:"string",required:!0,help:"Target board name"},{name:"description",type:"string",help:"Pin description"},{name:"link",type:"string",help:"Destination/landing URL (defaults to the image URL)"},{name:"create-board",type:"boolean",default:!1,help:"Create the board if it does not exist"}],columns:["status","pin_id","url"],func:async(r,t)=>{if(!r)throw new b("Browser session required for pinterest pin");const s=String(t.title??"").trim();if(!s)throw new o("pin title is required");const i=String(t.board??"").trim();if(!i)throw new o("--board is required");const l=t["image-url"]?String(t["image-url"]):"",c=t.image?String(t.image):"";if(!l&&!c)throw new o("provide --image-url (or --image local file)");const p=await _(r),d=T(await r.getCookies({url:"https://www.pinterest.com"})),a=await N(r,{resource:"BoardsResource",sourceUrl:`/${p}/boards/`,options:R(p),label:"boards"}),e=Array.isArray(a)?a:[];let n=$(e,i);if(!n){if(!t["create-board"]){const P=e.map((x)=>x.name).join(", ")||"(none)";throw new o(`Board "${i}" not found. Available: ${P} (use --create-board to create it)`)}n=await h(r,{resource:"BoardResource",sourceUrl:`/${p}/boards/`,options:C(i,{}),csrf:d,label:"board-create"})}const f=l||await w(r,c,d),y=I({boardId:String(n.id),imageUrl:f,title:s,description:t.description,link:t.link}),S=await h(r,{resource:"PinResource",sourceUrl:`/pin/find/?url=${encodeURIComponent(f)}`,options:y,csrf:d,label:"pin"}),m=String(S.id??"");return{status:"published",pin_id:m,url:m?`https://www.pinterest.com/pin/${m}/`:""}}});export const __test__={uploadLocalImage:w};export{pageFetch};
13
+ })()`),e=a&&typeof a==="object"&&"session"in a&&"data"in a?a.data:a,n=e&&e.data&&(e.data.image_url||e.data.url||e.data.success&&e.data.success.image_url);if(!e||!e.ok||!n)throw new b(`Pinterest image upload failed (best-effort): HTTP ${e?e.status:"?"} ${e?e.text:""}. Prefer --image-url.`);return n}U({site:"pinterest",name:"pin",access:"write",description:"Create a pin on a board with an optional destination link",domain:"www.pinterest.com",strategy:O.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!0,positional:!0,help:"Pin title"},{name:"image-url",type:"string",help:"Image URL to pin (required unless --image)"},{name:"image",type:"string",help:"(best-effort) local image file to upload"},{name:"board",type:"string",required:!0,help:"Target board name"},{name:"description",fromFile:!0,type:"string",help:"Pin description"},{name:"link",type:"string",help:"Destination/landing URL (defaults to the image URL)"},{name:"create-board",type:"boolean",default:!1,help:"Create the board if it does not exist"}],columns:["status","pin_id","url"],func:async(r,t)=>{if(!r)throw new b("Browser session required for pinterest pin");const s=String(t.title??"").trim();if(!s)throw new o("pin title is required");const i=String(t.board??"").trim();if(!i)throw new o("--board is required");const l=t["image-url"]?String(t["image-url"]):"",c=t.image?String(t.image):"";if(!l&&!c)throw new o("provide --image-url (or --image local file)");const p=await _(r),d=T(await r.getCookies({url:"https://www.pinterest.com"})),a=await N(r,{resource:"BoardsResource",sourceUrl:`/${p}/boards/`,options:R(p),label:"boards"}),e=Array.isArray(a)?a:[];let n=$(e,i);if(!n){if(!t["create-board"]){const P=e.map((x)=>x.name).join(", ")||"(none)";throw new o(`Board "${i}" not found. Available: ${P} (use --create-board to create it)`)}n=await h(r,{resource:"BoardResource",sourceUrl:`/${p}/boards/`,options:C(i,{}),csrf:d,label:"board-create"})}const f=l||await w(r,c,d),y=I({boardId:String(n.id),imageUrl:f,title:s,description:t.description,link:t.link}),S=await h(r,{resource:"PinResource",sourceUrl:`/pin/find/?url=${encodeURIComponent(f)}`,options:y,csrf:d,label:"pin"}),m=String(S.id??"");return{status:"published",pin_id:m,url:m?`https://www.pinterest.com/pin/${m}/`:""}}});export const __test__={uploadLocalImage:w};export{pageFetch};
@@ -1 +1 @@
1
- import{cli as F,Strategy as I}from"@jackwener/opencli/registry";import{ArgumentError as j,CommandExecutionError as Y,TimeoutError as A}from"@jackwener/opencli/errors";import{buildQoderInjectTextScript as G,clickByTextScript as J,clickFirstScript as B,evaluateQoder as H,parsePositiveInt as R,QODER_MESSAGE_COUNT_JS as $,qoderResponseAfterScript as w}from"./_utils.js";async function y(z,K,L=5000){const W=Math.max(1,Math.ceil(L/500));for(let V=0;V<W;V++){await z.wait(0.5);const X=await H(z,$);if(Number(X)>Number(K))return X}return K}F({site:"qoder",name:"new",access:"write",description:'Start a new Qoder Quest (conversation). Clicks the "New Quest" button in the sidebar (or its ⌘N variant).',domain:"localhost",strategy:I.UI,browser:!0,args:[],columns:["Status"],func:async(z)=>{const K=await H(z,J(["New Quest"]));if(!K?.ok)throw new Y(K?.reason||"New Quest button not found","");await z.wait(0.5);return[{Status:"started"}]}});F({site:"qoder",name:"send",access:"write",description:'Type text into the Qoder composer and click "Send message" (fire-and-forget).',domain:"localhost",strategy:I.UI,browser:!0,args:[{name:"text",positional:!0,required:!0,help:"Text to send"}],columns:["Status","Length"],func:async(z,K)=>{const L=String(K?.text||"").trim();if(!L)throw new j("text is required");const W=await H(z,$),V=await H(z,G(L));if(!V?.ok)throw new Y(V?.reason||"composer type failed","");await z.wait(0.3);if(!(await H(z,B(['button[aria-label="Send message"]','button[title="Send message"]'])))?.ok){if(!(await H(z,J(["Send message","Send","发送"])))?.ok)throw new Y("Send button not found","")}const N=await y(z,W);if(Number(N)<=Number(W))throw new Y("Qoder send did not create a new visible message row");return[{Status:"sent",Length:String(L.length)}]}});F({site:"qoder",name:"ask",access:"write",description:"Send a prompt to Qoder and wait up to --timeout seconds for the reply (best-effort: polls for the chat turn count to grow + stabilize).",domain:"localhost",strategy:I.UI,browser:!0,args:[{name:"text",positional:!0,required:!0,help:"Prompt text"},{name:"timeout",type:"int",required:!1,default:120,help:"Max seconds to wait"}],columns:["Role","Text","WaitedSeconds"],func:async(z,K)=>{const L=String(K?.text||"").trim();if(!L)throw new j("text is required");const W=R(K?.timeout,120,"--timeout"),V=await H(z,$),X=await H(z,G(L));if(!X?.ok)throw new Y(X?.reason||"composer type failed","");await z.wait(0.3);if(!(await H(z,B(['button[aria-label="Send message"]','button[title="Send message"]'])))?.ok){if(!(await H(z,J(["Send message","Send","发送"])))?.ok)throw new Y("Send button not found","")}const q=Date.now(),M=q+W*1000;let P=V,h=0,Z=null;while(Date.now()<M){await new Promise((O)=>setTimeout(O,1500));const D=await H(z,$);if(D!==P){P=D;h=0}else h++;if(P>V&&h>=6){Z=await H(z,w(V,L));if(Z?.text)break}}const U=Math.round((Date.now()-q)/1000);if(!Z?.text)throw new A("Qoder response",W,"Confirm Qoder sent the prompt and finished generating, then retry with a larger --timeout.");return[{Role:"User",Text:L,WaitedSeconds:String(U)},{Role:Z.role||"Assistant",Text:String(Z.text).slice(0,1200),WaitedSeconds:String(U)}]}});
1
+ import{cli as F,Strategy as I}from"@jackwener/opencli/registry";import{ArgumentError as j,CommandExecutionError as Y,TimeoutError as A}from"@jackwener/opencli/errors";import{buildQoderInjectTextScript as G,clickByTextScript as J,clickFirstScript as B,evaluateQoder as H,parsePositiveInt as R,QODER_MESSAGE_COUNT_JS as $,qoderResponseAfterScript as w}from"./_utils.js";async function y(z,K,L=5000){const W=Math.max(1,Math.ceil(L/500));for(let V=0;V<W;V++){await z.wait(0.5);const X=await H(z,$);if(Number(X)>Number(K))return X}return K}F({site:"qoder",name:"new",access:"write",description:'Start a new Qoder Quest (conversation). Clicks the "New Quest" button in the sidebar (or its ⌘N variant).',domain:"localhost",strategy:I.UI,browser:!0,args:[],columns:["Status"],func:async(z)=>{const K=await H(z,J(["New Quest"]));if(!K?.ok)throw new Y(K?.reason||"New Quest button not found","");await z.wait(0.5);return[{Status:"started"}]}});F({site:"qoder",name:"send",access:"write",description:'Type text into the Qoder composer and click "Send message" (fire-and-forget).',domain:"localhost",strategy:I.UI,browser:!0,args:[{name:"text",fromFile:!0,positional:!0,required:!0,help:"Text to send"}],columns:["Status","Length"],func:async(z,K)=>{const L=String(K?.text||"").trim();if(!L)throw new j("text is required");const W=await H(z,$),V=await H(z,G(L));if(!V?.ok)throw new Y(V?.reason||"composer type failed","");await z.wait(0.3);if(!(await H(z,B(['button[aria-label="Send message"]','button[title="Send message"]'])))?.ok){if(!(await H(z,J(["Send message","Send","发送"])))?.ok)throw new Y("Send button not found","")}const N=await y(z,W);if(Number(N)<=Number(W))throw new Y("Qoder send did not create a new visible message row");return[{Status:"sent",Length:String(L.length)}]}});F({site:"qoder",name:"ask",access:"write",description:"Send a prompt to Qoder and wait up to --timeout seconds for the reply (best-effort: polls for the chat turn count to grow + stabilize).",domain:"localhost",strategy:I.UI,browser:!0,args:[{name:"text",fromFile:!0,positional:!0,required:!0,help:"Prompt text"},{name:"timeout",type:"int",required:!1,default:120,help:"Max seconds to wait"}],columns:["Role","Text","WaitedSeconds"],func:async(z,K)=>{const L=String(K?.text||"").trim();if(!L)throw new j("text is required");const W=R(K?.timeout,120,"--timeout"),V=await H(z,$),X=await H(z,G(L));if(!X?.ok)throw new Y(X?.reason||"composer type failed","");await z.wait(0.3);if(!(await H(z,B(['button[aria-label="Send message"]','button[title="Send message"]'])))?.ok){if(!(await H(z,J(["Send message","Send","发送"])))?.ok)throw new Y("Send button not found","")}const q=Date.now(),M=q+W*1000;let P=V,h=0,Z=null;while(Date.now()<M){await new Promise((O)=>setTimeout(O,1500));const D=await H(z,$);if(D!==P){P=D;h=0}else h++;if(P>V&&h>=6){Z=await H(z,w(V,L));if(Z?.text)break}}const U=Math.round((Date.now()-q)/1000);if(!Z?.text)throw new A("Qoder response",W,"Confirm Qoder sent the prompt and finished generating, then retry with a larger --timeout.");return[{Role:"User",Text:L,WaitedSeconds:String(U)},{Role:Z.role||"Assistant",Text:String(Z.text).slice(0,1200),WaitedSeconds:String(U)}]}});
@@ -1,4 +1,4 @@
1
- import{ArgumentError as m,CommandExecutionError as e}from"@jackwener/opencli/errors";import{cli as c,Strategy as l}from"@jackwener/opencli/registry";import{assertLiteralContent as d}from"../_shared/content-guard.js";import{insertEditorText as w,normalizeQuoraUrl as b,openQuoraPage as f,readEditorTextJs as p,requireExecute as y}from"./shared.js";c({site:"quora",name:"comment",access:"write",description:"Post a top-level comment on a Quora answer through the visible editor.",domain:"www.quora.com",strategy:l.UI,browser:!0,args:[{name:"url",type:"string",required:!0,positional:!0,help:"Quora answer URL"},{name:"text",type:"string",required:!0,positional:!0,help:"Comment text"},{name:"execute",type:"boolean",help:"Actually post the comment"}],columns:["status","outcome","targetType","target","message"],func:async(t,r)=>{y(r);const a=b(r.url,{allowProfile:!1,allowQuestion:!1,allowAnswer:!0}),o=String(r.text??"");if(!o.trim())throw new m("Comment text cannot be empty");d(o,{label:"comment text"});await f(t,a.url,"comment");if(!await t.evaluate(`(() => {
1
+ import{ArgumentError as m,CommandExecutionError as e}from"@jackwener/opencli/errors";import{cli as c,Strategy as l}from"@jackwener/opencli/registry";import{assertLiteralContent as d}from"../_shared/content-guard.js";import{insertEditorText as w,normalizeQuoraUrl as b,openQuoraPage as f,readEditorTextJs as p,requireExecute as y}from"./shared.js";c({site:"quora",name:"comment",access:"write",description:"Post a top-level comment on a Quora answer through the visible editor.",domain:"www.quora.com",strategy:l.UI,browser:!0,args:[{name:"url",type:"string",required:!0,positional:!0,help:"Quora answer URL"},{name:"text",fromFile:!0,type:"string",required:!0,positional:!0,help:"Comment text"},{name:"execute",type:"boolean",help:"Actually post the comment"}],columns:["status","outcome","targetType","target","message"],func:async(t,r)=>{y(r);const a=b(r.url,{allowProfile:!1,allowQuestion:!1,allowAnswer:!0}),o=String(r.text??"");if(!o.trim())throw new m("Comment text cannot be empty");d(o,{label:"comment text"});await f(t,a.url,"comment");if(!await t.evaluate(`(() => {
2
2
  const button = document.querySelector('#mainContent .dom_annotate_answer_action_bar_comment button')
3
3
  || Array.from(document.querySelectorAll('#mainContent button[aria-label]'))
4
4
  .find((item) => /comment|评论|comentario|commentaire|kommentar/i.test(item.getAttribute('aria-label') || ''));