publishport-opencli 1.0.12 → 1.0.13

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 +980 -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 +5 -0
  109. package/dist/src/registry.d.ts +15 -0
  110. package/dist/src/registry.js +1 -1
  111. package/dist/src/serialization.js +2 -2
  112. package/package.json +1 -1
package/clis/qwen/ask.js CHANGED
@@ -1 +1 @@
1
- import{cli as Q,Strategy as j}from"@jackwener/opencli/registry";import{ArgumentError as $,CommandExecutionError as f,TimeoutError as L}from"@jackwener/opencli/errors";import{QIANWEN_DOMAIN as R,QIANWEN_URL as A,authRequired as D,bubbleHtmlToMarkdown as y,dismissLoginModal as G,ensureOnQianwen as z,getMessageBubbles as N,hasLoginGate as C,normalizeBooleanFlag as X,sendMessage as I,setFeatureToggle as H,startNewChat as T,waitForAnswer as M}from"./utils.js";Q({site:"qwen",name:"ask",access:"write",description:"Send a prompt to Qianwen and return the assistant reply",domain:R,strategy:j.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,defaultFormat:"plain",args:[{name:"prompt",required:!0,positional:!0,help:"Prompt to send to Qianwen"},{name:"timeout",type:"int",default:120,help:"Max seconds to wait for the response"},{name:"new",type:"boolean",default:!1,help:"Start a new chat before sending"},{name:"think",type:"boolean",default:!1,help:"Enable 深度思考 (DeepThink)"},{name:"research",type:"boolean",default:!1,help:"Enable 深度研究 (DeepResearch)"},{name:"markdown",type:"boolean",default:!1,help:"Emit assistant reply as markdown"}],columns:["Role","Text"],func:async(J,v)=>{const P=String(v.prompt||"").trim();if(!P)throw new $("prompt is required");const K=Number(v.timeout??120);if(!Number.isInteger(K)||K<=0)throw new $("timeout must be a positive integer");const U=X(v.new,!1),W=X(v.think,!1),_=X(v.research,!1),q=X(v.markdown,!1);await z(J);await G(J);if(U){await T(J);await G(J)}if(W)await H(J,"think",!0);if(_)await H(J,"research",!0);const x=await S(J),Z=await I(J,P);if(!Z?.ok){if(await C(J))throw D();throw new f(Z?.reason||"Failed to send Qianwen prompt")}const Y=await M(J,P,K,x);if(Y.status==="auth_required")throw D();if(Y.status==="timeout")throw new L("qianwen ask",K,"No Qianwen reply observed before timeout. Retry with --timeout increased.");const V=Y.assistant;if(!V)throw new f("No assistant reply found in Qianwen chat.");const O=q&&V.html?y(V.html)||V.text:V.text;return[{Role:"User",Text:P},{Role:"Assistant",Text:O}]}});async function S(J){const v=await N(J),P=v.length?v[v.length-1].id:"";let K="";for(let U=v.length-1;U>=0;U-=1)if(v[U].role==="Assistant"){K=v[U].id;break}return{lastBubbleId:P,lastAssistantId:K}}export const __test__={getBaselineChatAnchor:S};
1
+ import{cli as Q,Strategy as j}from"@jackwener/opencli/registry";import{ArgumentError as $,CommandExecutionError as f,TimeoutError as L}from"@jackwener/opencli/errors";import{QIANWEN_DOMAIN as R,QIANWEN_URL as A,authRequired as D,bubbleHtmlToMarkdown as y,dismissLoginModal as G,ensureOnQianwen as z,getMessageBubbles as N,hasLoginGate as C,normalizeBooleanFlag as X,sendMessage as I,setFeatureToggle as H,startNewChat as T,waitForAnswer as M}from"./utils.js";Q({site:"qwen",name:"ask",access:"write",description:"Send a prompt to Qianwen and return the assistant reply",domain:R,strategy:j.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,defaultFormat:"plain",args:[{name:"prompt",fromFile:!0,required:!0,positional:!0,help:"Prompt to send to Qianwen"},{name:"timeout",type:"int",default:120,help:"Max seconds to wait for the response"},{name:"new",type:"boolean",default:!1,help:"Start a new chat before sending"},{name:"think",type:"boolean",default:!1,help:"Enable 深度思考 (DeepThink)"},{name:"research",type:"boolean",default:!1,help:"Enable 深度研究 (DeepResearch)"},{name:"markdown",type:"boolean",default:!1,help:"Emit assistant reply as markdown"}],columns:["Role","Text"],func:async(J,v)=>{const P=String(v.prompt||"").trim();if(!P)throw new $("prompt is required");const K=Number(v.timeout??120);if(!Number.isInteger(K)||K<=0)throw new $("timeout must be a positive integer");const U=X(v.new,!1),W=X(v.think,!1),_=X(v.research,!1),q=X(v.markdown,!1);await z(J);await G(J);if(U){await T(J);await G(J)}if(W)await H(J,"think",!0);if(_)await H(J,"research",!0);const x=await S(J),Z=await I(J,P);if(!Z?.ok){if(await C(J))throw D();throw new f(Z?.reason||"Failed to send Qianwen prompt")}const Y=await M(J,P,K,x);if(Y.status==="auth_required")throw D();if(Y.status==="timeout")throw new L("qianwen ask",K,"No Qianwen reply observed before timeout. Retry with --timeout increased.");const V=Y.assistant;if(!V)throw new f("No assistant reply found in Qianwen chat.");const O=q&&V.html?y(V.html)||V.text:V.text;return[{Role:"User",Text:P},{Role:"Assistant",Text:O}]}});async function S(J){const v=await N(J),P=v.length?v[v.length-1].id:"";let K="";for(let U=v.length-1;U>=0;U-=1)if(v[U].role==="Assistant"){K=v[U].id;break}return{lastBubbleId:P,lastAssistantId:K}}export const __test__={getBaselineChatAnchor:S};
@@ -28,4 +28,4 @@ import*as S from"node:os";import*as F from"node:path";import{cli as P,Strategy a
28
28
  } catch (error) {
29
29
  return { ok: false, status: 0, error: String(error?.message || error) };
30
30
  }
31
- })()`)}P({site:"qwen",name:"image",access:"write",description:"Generate images with Qianwen (AI生图) and save them locally",domain:A,strategy:U.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,defaultFormat:"plain",args:[{name:"prompt",required:!0,positional:!0,help:"Image prompt to send"},{name:"op",default:"~/Pictures/qianwen",help:"Output directory"},{name:"new",type:"boolean",default:!0,help:"Start a new chat before generating (default: true)"},{name:"sd",type:"boolean",default:!1,help:"Skip download; only show the Qianwen link"},{name:"timeout",type:"int",default:180,help:"Max seconds to wait for the image response"}],columns:["Status","File","Link"],func:async(H,J)=>{const v=String(J.prompt||"").trim();if(!v)throw new j("prompt is required");const _=String(J.op||"~/Pictures/qianwen").replace(/^~\//,`${S.homedir()}/`),X=x(J.new,!0),V=x(J.sd,!1),K=Number(J.timeout??180);if(!Number.isInteger(K)||K<=0)throw new j("timeout must be a positive integer");await I(H);await N(H);if(X){await w(H);await N(H)}await c(H,"image",!0);await H.wait(0.5);const $=await h(H,v);if(!$?.ok){if(await T(H))throw L();throw new B($?.reason||"Failed to send Qianwen image prompt")}let W="";for(let Y=0;Y<5;Y+=1){await H.wait(1);const Z=[...await k(H)].reverse().find((Q)=>Q.role==="Assistant");if(Z){W=Z.id;break}}const q=await n(H,W,K),C=await H.evaluate("window.location.href").catch(()=>"https://www.qianwen.com/");if(q.status==="auth_required")throw L();if(q.status==="timeout")throw new f("qianwen image",K,"No generated images observed before timeout.");const z=q.urls;if(V)return[{Status:"\uD83C\uDFA8 generated",File:null,Link:C}];const M=Date.now(),O=[];for(let Y=0;Y<z.length;Y+=1){const D=z[Y],Z=await u(H,D);if(!Z?.ok)throw new B(`Failed to fetch generated Qianwen image ${Y+1}: status=${Z?.status||"?"}`);const Q=z.length>1?`_${Y+1}`:"",R=m(Z.mime),G=F.join(_,`qianwen_${M}${Q}${R}`);await b(Z.base64,G);O.push({Status:"✅ saved",File:d(G),Link:C})}if(!O.length)throw new E("qwen image","No generated images were available to download.");return O}});
31
+ })()`)}P({site:"qwen",name:"image",access:"write",description:"Generate images with Qianwen (AI生图) and save them locally",domain:A,strategy:U.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,defaultFormat:"plain",args:[{name:"prompt",fromFile:!0,required:!0,positional:!0,help:"Image prompt to send"},{name:"op",default:"~/Pictures/qianwen",help:"Output directory"},{name:"new",type:"boolean",default:!0,help:"Start a new chat before generating (default: true)"},{name:"sd",type:"boolean",default:!1,help:"Skip download; only show the Qianwen link"},{name:"timeout",type:"int",default:180,help:"Max seconds to wait for the image response"}],columns:["Status","File","Link"],func:async(H,J)=>{const v=String(J.prompt||"").trim();if(!v)throw new j("prompt is required");const _=String(J.op||"~/Pictures/qianwen").replace(/^~\//,`${S.homedir()}/`),X=x(J.new,!0),V=x(J.sd,!1),K=Number(J.timeout??180);if(!Number.isInteger(K)||K<=0)throw new j("timeout must be a positive integer");await I(H);await N(H);if(X){await w(H);await N(H)}await c(H,"image",!0);await H.wait(0.5);const $=await h(H,v);if(!$?.ok){if(await T(H))throw L();throw new B($?.reason||"Failed to send Qianwen image prompt")}let W="";for(let Y=0;Y<5;Y+=1){await H.wait(1);const Z=[...await k(H)].reverse().find((Q)=>Q.role==="Assistant");if(Z){W=Z.id;break}}const q=await n(H,W,K),C=await H.evaluate("window.location.href").catch(()=>"https://www.qianwen.com/");if(q.status==="auth_required")throw L();if(q.status==="timeout")throw new f("qianwen image",K,"No generated images observed before timeout.");const z=q.urls;if(V)return[{Status:"\uD83C\uDFA8 generated",File:null,Link:C}];const M=Date.now(),O=[];for(let Y=0;Y<z.length;Y+=1){const D=z[Y],Z=await u(H,D);if(!Z?.ok)throw new B(`Failed to fetch generated Qianwen image ${Y+1}: status=${Z?.status||"?"}`);const Q=z.length>1?`_${Y+1}`:"",R=m(Z.mime),G=F.join(_,`qianwen_${M}${Q}${R}`);await b(Z.base64,G);O.push({Status:"✅ saved",File:d(G),Link:C})}if(!O.length)throw new E("qwen image","No generated images were available to download.");return O}});
package/clis/qwen/send.js CHANGED
@@ -1 +1 @@
1
- import{cli as f,Strategy as h}from"@jackwener/opencli/registry";import{ArgumentError as p,CommandExecutionError as u}from"@jackwener/opencli/errors";import{QIANWEN_DOMAIN as w,authRequired as d,dismissLoginModal as o,ensureOnQianwen as c,hasLoginGate as b,normalizeBooleanFlag as n,sendMessage as y,setFeatureToggle as i,startNewChat as E}from"./utils.js";f({site:"qwen",name:"send",access:"write",description:"Fire-and-forget: send a prompt to Qianwen without waiting for the reply",domain:w,strategy:h.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,args:[{name:"prompt",required:!0,positional:!0,help:"Prompt to send to Qianwen"},{name:"new",type:"boolean",default:!1,help:"Start a new chat before sending"},{name:"think",type:"boolean",default:!1,help:"Enable 深度思考 (DeepThink)"},{name:"research",type:"boolean",default:!1,help:"Enable 深度研究 (DeepResearch)"}],columns:["Status","Prompt"],func:async(e,t)=>{const a=String(t.prompt||"").trim();if(!a)throw new p("prompt is required");const s=n(t.new,!1),l=n(t.think,!1),m=n(t.research,!1);await c(e);await o(e);if(s){await E(e);await o(e)}if(l)await i(e,"think",!0);if(m)await i(e,"research",!0);const r=await y(e,a);if(!r?.ok){if(await b(e))throw d();throw new u(r?.reason||"Failed to send Qianwen prompt")}return[{Status:"sent",Prompt:a}]}});
1
+ import{cli as f,Strategy as u}from"@jackwener/opencli/registry";import{ArgumentError as h,CommandExecutionError as p}from"@jackwener/opencli/errors";import{QIANWEN_DOMAIN as w,authRequired as d,dismissLoginModal as o,ensureOnQianwen as c,hasLoginGate as b,normalizeBooleanFlag as a,sendMessage as y,setFeatureToggle as i,startNewChat as E}from"./utils.js";f({site:"qwen",name:"send",access:"write",description:"Fire-and-forget: send a prompt to Qianwen without waiting for the reply",domain:w,strategy:u.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,args:[{name:"prompt",fromFile:!0,required:!0,positional:!0,help:"Prompt to send to Qianwen"},{name:"new",type:"boolean",default:!1,help:"Start a new chat before sending"},{name:"think",type:"boolean",default:!1,help:"Enable 深度思考 (DeepThink)"},{name:"research",type:"boolean",default:!1,help:"Enable 深度研究 (DeepResearch)"}],columns:["Status","Prompt"],func:async(e,t)=>{const r=String(t.prompt||"").trim();if(!r)throw new h("prompt is required");const s=a(t.new,!1),l=a(t.think,!1),m=a(t.research,!1);await c(e);await o(e);if(s){await E(e);await o(e)}if(l)await i(e,"think",!0);if(m)await i(e,"research",!0);const n=await y(e,r);if(!n?.ok){if(await b(e))throw d();throw new p(n?.reason||"Failed to send Qianwen prompt")}return[{Status:"sent",Prompt:r}]}});
@@ -1,4 +1,4 @@
1
- import{CommandExecutionError as o}from"@jackwener/opencli/errors";import{cli as r,Strategy as a}from"@jackwener/opencli/registry";r({site:"reddit",name:"comment",access:"write",description:"Post a comment on a Reddit post",domain:"reddit.com",strategy:a.COOKIE,browser:!0,args:[{name:"post-id",type:"string",required:!0,positional:!0,help:"Post ID (e.g. 1abc123) or fullname (t3_xxx)"},{name:"text",type:"string",required:!0,positional:!0,help:"Comment text"}],columns:["status","message"],func:async(t,e)=>{if(!t)throw new o("Browser session required");await t.goto("https://www.reddit.com");const s=await t.evaluate(`(async () => {
1
+ import{CommandExecutionError as o}from"@jackwener/opencli/errors";import{cli as r,Strategy as a}from"@jackwener/opencli/registry";r({site:"reddit",name:"comment",access:"write",description:"Post a comment on a Reddit post",domain:"reddit.com",strategy:a.COOKIE,browser:!0,args:[{name:"post-id",type:"string",required:!0,positional:!0,help:"Post ID (e.g. 1abc123) or fullname (t3_xxx)"},{name:"text",fromFile:!0,type:"string",required:!0,positional:!0,help:"Comment text"}],columns:["status","message"],func:async(t,e)=>{if(!t)throw new o("Browser session required");await t.goto("https://www.reddit.com");const s=await t.evaluate(`(async () => {
2
2
  try {
3
3
  let postId = ${JSON.stringify(e["post-id"])};
4
4
  const urlMatch = postId.match(/comments\\/([a-z0-9]+)/);
@@ -1,4 +1,4 @@
1
- import{ArgumentError as J,AuthRequiredError as Y,CommandExecutionError as L}from"@jackwener/opencli/errors";import{cli as Z,Strategy as $}from"@jackwener/opencli/registry";const F=/^[a-z0-9]+$/i;function W(H){const b=String(H||"").trim();if(!F.test(b))throw new J("Comment ID must be a Reddit comment id, t1_ fullname, or reddit.com comment URL.","Use a bare comment id like okf3s7u, a fullname like t1_okf3s7u, or a full Reddit comment URL.");return b.toLowerCase()}export function normalizeRedditCommentFullname(H){const b=String(H||"").trim();if(!b)throw new J("Comment ID is required.","Use a bare comment id like okf3s7u, a fullname like t1_okf3s7u, or a full Reddit comment URL.");const Q=b.match(/^t1_([a-z0-9]+)$/i);if(Q)return`t1_${W(Q[1])}`;if(/^https?:\/\//i.test(b)){let K;try{K=new URL(b)}catch{throw new J(`Invalid Reddit comment URL: ${b}`)}const G=K.hostname.toLowerCase();if(K.protocol!=="https:"||G!=="reddit.com"&&!G.endsWith(".reddit.com"))throw new J("Comment URL must be an https reddit.com URL.","Use a URL like https://www.reddit.com/r/sub/comments/post/title/okf3s7u/");const U=K.pathname.split("/").filter(Boolean),X=U.indexOf("comments"),V=X+3;if(X<0||U.length<=V)throw new J("Comment URL must include the target comment id.","Use a URL like https://www.reddit.com/r/sub/comments/post/title/okf3s7u/");if(U.length!==V+1)throw new J("Comment URL must end at the target comment id.","Remove extra path segments after the comment id.");return`t1_${W(U[V])}`}if(b.includes("/")||b.startsWith("t3_"))throw new J("Comment ID must be a Reddit comment id, t1_ fullname, or reddit.com comment URL.","Use a bare comment id like okf3s7u, a fullname like t1_okf3s7u, or a full Reddit comment URL.");return`t1_${W(b)}`}export function requireReplyText(H){const b=String(H||"");if(!b.trim())throw new J("Reply text is required.","Pass non-empty text to post as the Reddit reply.");return b}Z({site:"reddit",name:"reply",access:"write",description:"Reply to a Reddit comment",domain:"reddit.com",strategy:$.COOKIE,browser:!0,args:[{name:"comment-id",type:"string",required:!0,positional:!0,help:"Comment ID (e.g. okf3s7u) or fullname (t1_xxx)"},{name:"text",type:"string",required:!0,positional:!0,help:"Reply text"}],columns:["status","message"],func:async(H,b)=>{const Q=normalizeRedditCommentFullname(b["comment-id"]),K=requireReplyText(b.text);await H.goto("https://www.reddit.com");const G=await H.evaluate(`(async () => {
1
+ import{ArgumentError as J,AuthRequiredError as Y,CommandExecutionError as L}from"@jackwener/opencli/errors";import{cli as Z,Strategy as $}from"@jackwener/opencli/registry";const F=/^[a-z0-9]+$/i;function W(H){const b=String(H||"").trim();if(!F.test(b))throw new J("Comment ID must be a Reddit comment id, t1_ fullname, or reddit.com comment URL.","Use a bare comment id like okf3s7u, a fullname like t1_okf3s7u, or a full Reddit comment URL.");return b.toLowerCase()}export function normalizeRedditCommentFullname(H){const b=String(H||"").trim();if(!b)throw new J("Comment ID is required.","Use a bare comment id like okf3s7u, a fullname like t1_okf3s7u, or a full Reddit comment URL.");const Q=b.match(/^t1_([a-z0-9]+)$/i);if(Q)return`t1_${W(Q[1])}`;if(/^https?:\/\//i.test(b)){let K;try{K=new URL(b)}catch{throw new J(`Invalid Reddit comment URL: ${b}`)}const G=K.hostname.toLowerCase();if(K.protocol!=="https:"||G!=="reddit.com"&&!G.endsWith(".reddit.com"))throw new J("Comment URL must be an https reddit.com URL.","Use a URL like https://www.reddit.com/r/sub/comments/post/title/okf3s7u/");const U=K.pathname.split("/").filter(Boolean),X=U.indexOf("comments"),V=X+3;if(X<0||U.length<=V)throw new J("Comment URL must include the target comment id.","Use a URL like https://www.reddit.com/r/sub/comments/post/title/okf3s7u/");if(U.length!==V+1)throw new J("Comment URL must end at the target comment id.","Remove extra path segments after the comment id.");return`t1_${W(U[V])}`}if(b.includes("/")||b.startsWith("t3_"))throw new J("Comment ID must be a Reddit comment id, t1_ fullname, or reddit.com comment URL.","Use a bare comment id like okf3s7u, a fullname like t1_okf3s7u, or a full Reddit comment URL.");return`t1_${W(b)}`}export function requireReplyText(H){const b=String(H||"");if(!b.trim())throw new J("Reply text is required.","Pass non-empty text to post as the Reddit reply.");return b}Z({site:"reddit",name:"reply",access:"write",description:"Reply to a Reddit comment",domain:"reddit.com",strategy:$.COOKIE,browser:!0,args:[{name:"comment-id",type:"string",required:!0,positional:!0,help:"Comment ID (e.g. okf3s7u) or fullname (t1_xxx)"},{name:"text",fromFile:!0,type:"string",required:!0,positional:!0,help:"Reply text"}],columns:["status","message"],func:async(H,b)=>{const Q=normalizeRedditCommentFullname(b["comment-id"]),K=requireReplyText(b.text);await H.goto("https://www.reddit.com");const G=await H.evaluate(`(async () => {
2
2
  try {
3
3
  const fullname = ${JSON.stringify(Q)};
4
4
  const text = ${JSON.stringify(K)};
@@ -1,4 +1,4 @@
1
- import{ArgumentError as H,AuthRequiredError as V,CommandExecutionError as P}from"@jackwener/opencli/errors";import{cli as W,Strategy as X}from"@jackwener/opencli/registry";const Y=/^[A-Za-z0-9][A-Za-z0-9_]{1,20}$/;export function normalizeSubreddit(M){let F=String(M||"").trim();if(!F)throw new H("Subreddit is required.",'Pass a subreddit like "test" or "r/test".');if(/^https?:\/\//i.test(F)){let N;try{N=new URL(F)}catch{throw new H(`Invalid subreddit URL: ${F}`)}const J=N.pathname.split("/").filter(Boolean),K=J.indexOf("r");if(K<0||J.length<=K+1)throw new H("Subreddit URL must contain /r/<name>.");F=J[K+1]}F=F.replace(/^\/?r\//i,"").replace(/^\/+/,"");if(!Y.test(F))throw new H(`Invalid subreddit name: ${JSON.stringify(F)}`,'Use the bare name, e.g. "test" (2–21 chars, letters/digits/underscore).');return F}W({site:"reddit",name:"submit",access:"write",description:"Submit a new post to a subreddit. Text post by default (--text); pass --url for a link post. Note: many subreddits gate submissions behind karma/age requirements or flair — a rejection is surfaced verbatim.",domain:"reddit.com",strategy:X.COOKIE,browser:!0,args:[{name:"subreddit",type:"string",required:!0,positional:!0,help:'Target subreddit, e.g. "test" or "r/test"'},{name:"title",type:"string",required:!0,positional:!0,help:"Post title (max 300 chars)"},{name:"text",type:"string",help:"Body text (markdown) for a self/text post"},{name:"url",type:"string",help:"External URL for a link post (mutually exclusive with --text)"},{name:"flair-id",type:"string",help:"Flair template id, if the subreddit requires one"},{name:"flair-text",type:"string",help:"Flair text to accompany --flair-id"},{name:"nsfw",type:"bool",default:!1,help:"Mark the post as NSFW"},{name:"spoiler",type:"bool",default:!1,help:"Mark the post as a spoiler"},{name:"sendreplies",type:"bool",default:!0,help:"Receive inbox replies for this post (default: true)"}],columns:["status","url","id","message"],func:async(M,F)=>{if(!M)throw new P("Browser session required");const N=normalizeSubreddit(F.subreddit),J=String(F.title??"").trim();if(!J)throw new H("Title is required.");if(J.length>300)throw new H("Title must be at most 300 characters.");const K=typeof F.text==="string"?F.text:"",O=typeof F.url==="string"?F.url.trim():"";if(O&&K)throw new H("Pass either --text (self post) or --url (link post), not both.");const Q=O?"link":"self";if(Q==="link")try{new URL(O)}catch{throw new H(`Invalid --url: ${O}`)}await M.goto("https://www.reddit.com");const G=await M.evaluate(`(async () => {
1
+ import{ArgumentError as H,AuthRequiredError as V,CommandExecutionError as P}from"@jackwener/opencli/errors";import{cli as W,Strategy as X}from"@jackwener/opencli/registry";const Y=/^[A-Za-z0-9][A-Za-z0-9_]{1,20}$/;export function normalizeSubreddit(M){let F=String(M||"").trim();if(!F)throw new H("Subreddit is required.",'Pass a subreddit like "test" or "r/test".');if(/^https?:\/\//i.test(F)){let N;try{N=new URL(F)}catch{throw new H(`Invalid subreddit URL: ${F}`)}const J=N.pathname.split("/").filter(Boolean),K=J.indexOf("r");if(K<0||J.length<=K+1)throw new H("Subreddit URL must contain /r/<name>.");F=J[K+1]}F=F.replace(/^\/?r\//i,"").replace(/^\/+/,"");if(!Y.test(F))throw new H(`Invalid subreddit name: ${JSON.stringify(F)}`,'Use the bare name, e.g. "test" (2–21 chars, letters/digits/underscore).');return F}W({site:"reddit",name:"submit",access:"write",description:"Submit a new post to a subreddit. Text post by default (--text); pass --url for a link post. Note: many subreddits gate submissions behind karma/age requirements or flair — a rejection is surfaced verbatim.",domain:"reddit.com",strategy:X.COOKIE,browser:!0,args:[{name:"subreddit",type:"string",required:!0,positional:!0,help:'Target subreddit, e.g. "test" or "r/test"'},{name:"title",type:"string",required:!0,positional:!0,help:"Post title (max 300 chars)"},{name:"text",fromFile:!0,type:"string",help:"Body text (markdown) for a self/text post"},{name:"url",type:"string",help:"External URL for a link post (mutually exclusive with --text)"},{name:"flair-id",type:"string",help:"Flair template id, if the subreddit requires one"},{name:"flair-text",type:"string",help:"Flair text to accompany --flair-id"},{name:"nsfw",type:"bool",default:!1,help:"Mark the post as NSFW"},{name:"spoiler",type:"bool",default:!1,help:"Mark the post as a spoiler"},{name:"sendreplies",type:"bool",default:!0,help:"Receive inbox replies for this post (default: true)"}],columns:["status","url","id","message"],func:async(M,F)=>{if(!M)throw new P("Browser session required");const N=normalizeSubreddit(F.subreddit),J=String(F.title??"").trim();if(!J)throw new H("Title is required.");if(J.length>300)throw new H("Title must be at most 300 characters.");const K=typeof F.text==="string"?F.text:"",O=typeof F.url==="string"?F.url.trim():"";if(O&&K)throw new H("Pass either --text (self post) or --url (link post), not both.");const Q=O?"link":"self";if(Q==="link")try{new URL(O)}catch{throw new H(`Invalid --url: ${O}`)}await M.goto("https://www.reddit.com");const G=await M.evaluate(`(async () => {
2
2
  try {
3
3
  const meRes = await fetch('/api/me.json', { credentials: 'include' });
4
4
  if (meRes.status === 401 || meRes.status === 403) {
@@ -1,4 +1,4 @@
1
- import{cli as D,Strategy as T}from"@jackwener/opencli/registry";import{ArgumentError as y,CommandExecutionError as R}from"@jackwener/opencli/errors";import{authHeadersFragment as j,resolveShortIdFragment as B}from"./in-page.js";import{dispatchEvaluateResult as U}from"./errors.js";import{SLOCK_SITE as h,SLOCK_DOMAIN as K,SLOCK_HOME_URL as C,SLOCK_API_BASE as W}from"./shared.js";import{UUID_RE as H,classifyTarget as L}from"./resolve.js";D({site:h,name:"message-send",access:"write",description:"Send a message to a channel, DM, or thread (content sent verbatim)",domain:K,strategy:T.COOKIE,browser:!0,siteSession:"persistent",args:[{name:"target",positional:!0,required:!0,help:'"#channel", "#channel:msgIdOrShort", "dm:@name", "dm:<uuid>", or channel UUID'},{name:"content",positional:!0,required:!0,help:"Message body (sent verbatim, no marker)"},{name:"dry-run",type:"bool",default:!1,help:"Print the planned payload without sending"},{name:"as-task",type:"bool",default:!1,help:"Create the message as a task (asTask)"},{name:"attach",help:"Comma-separated attachmentId UUIDs (upload separately first)"},{name:"server",help:"Override active server (slug or id)"}],columns:["target","channelId","content","result","messageId"],func:async(f,Q)=>{const q=String(Q.target??"").trim();if(!q)throw new y("target required");const Y=String(Q.content??""),V=!!Q["as-task"],Z=String(Q.attach??"").split(",").map((G)=>G.trim()).filter(Boolean);for(const G of Z)if(!H.test(G))throw new y(`--attach expects attachmentId UUIDs; "${G}" is not a UUID`);let $;try{$=L(q)}catch(G){throw new y(G.message)}const X={asTask:V,attachmentIds:Z};if(Q["dry-run"])return[{target:q,channelId:"(not resolved in dry-run)",content:Y,result:V?"dry-run (asTask)":"dry-run",messageId:null}];await f.goto(C);const J=P(q,Y,$,Q.server,X),N=await f.evaluate(`(async () => { ${J} })()`),z=U(N)[0]??{},b=z.id??z.messageId;if(!b)throw new R("Slock message-send succeeded without returning a message id; refusing to report a sent row.");return[{target:q,channelId:z.channelId??"",content:Y,result:"sent",messageId:b}]}});function P(f,Q,q,Y,V={}){const Z=Y??null,$=JSON.stringify(Q),X=[];if(V.asTask)X.push("asTask: true");if(Array.isArray(V.attachmentIds)&&V.attachmentIds.length)X.push(`attachmentIds: ${JSON.stringify(V.attachmentIds)}`);const J=X.length?", "+X.join(", "):"",N=j({serverScoped:!0,serverIdOverride:Z}),F=`
1
+ import{cli as D,Strategy as T}from"@jackwener/opencli/registry";import{ArgumentError as y,CommandExecutionError as R}from"@jackwener/opencli/errors";import{authHeadersFragment as j,resolveShortIdFragment as B}from"./in-page.js";import{dispatchEvaluateResult as U}from"./errors.js";import{SLOCK_SITE as h,SLOCK_DOMAIN as K,SLOCK_HOME_URL as C,SLOCK_API_BASE as W}from"./shared.js";import{UUID_RE as H,classifyTarget as L}from"./resolve.js";D({site:h,name:"message-send",access:"write",description:"Send a message to a channel, DM, or thread (content sent verbatim)",domain:K,strategy:T.COOKIE,browser:!0,siteSession:"persistent",args:[{name:"target",positional:!0,required:!0,help:'"#channel", "#channel:msgIdOrShort", "dm:@name", "dm:<uuid>", or channel UUID'},{name:"content",fromFile:!0,positional:!0,required:!0,help:"Message body (sent verbatim, no marker)"},{name:"dry-run",type:"bool",default:!1,help:"Print the planned payload without sending"},{name:"as-task",type:"bool",default:!1,help:"Create the message as a task (asTask)"},{name:"attach",help:"Comma-separated attachmentId UUIDs (upload separately first)"},{name:"server",help:"Override active server (slug or id)"}],columns:["target","channelId","content","result","messageId"],func:async(f,Q)=>{const q=String(Q.target??"").trim();if(!q)throw new y("target required");const Y=String(Q.content??""),V=!!Q["as-task"],Z=String(Q.attach??"").split(",").map((G)=>G.trim()).filter(Boolean);for(const G of Z)if(!H.test(G))throw new y(`--attach expects attachmentId UUIDs; "${G}" is not a UUID`);let $;try{$=L(q)}catch(G){throw new y(G.message)}const X={asTask:V,attachmentIds:Z};if(Q["dry-run"])return[{target:q,channelId:"(not resolved in dry-run)",content:Y,result:V?"dry-run (asTask)":"dry-run",messageId:null}];await f.goto(C);const J=P(q,Y,$,Q.server,X),N=await f.evaluate(`(async () => { ${J} })()`),z=U(N)[0]??{},b=z.id??z.messageId;if(!b)throw new R("Slock message-send succeeded without returning a message id; refusing to report a sent row.");return[{target:q,channelId:z.channelId??"",content:Y,result:"sent",messageId:b}]}});function P(f,Q,q,Y,V={}){const Z=Y??null,$=JSON.stringify(Q),X=[];if(V.asTask)X.push("asTask: true");if(Array.isArray(V.attachmentIds)&&V.attachmentIds.length)X.push(`attachmentIds: ${JSON.stringify(V.attachmentIds)}`);const J=X.length?", "+X.join(", "):"",N=j({serverScoped:!0,serverIdOverride:Z}),F=`
2
2
  if (!channelId) return { kind: 'http', status: 500, where:'/messages (target resolved without channelId)' };
3
3
  const mres = await fetch('${W}/messages', { method:'POST', credentials:'include', headers, body: JSON.stringify({ channelId, content: ${$}${J} }) });
4
4
  if (!mres.ok) return { kind: mres.status===401?'auth':'http', status: mres.status, where:'/messages' };
@@ -7,4 +7,4 @@ import*as T from"node:fs";import*as A from"node:path";import{cli as f,Strategy a
7
7
  let data = null; try { data = JSON.parse(text); } catch {}
8
8
  return { ok: res.ok, status: res.status, data, text: text.slice(0, 300) };
9
9
  } catch (e) { return { ok: false, status: 0, data: null, text: String(e && e.message || e) }; }
10
- })()`;return d(await z.evaluate(J))}function F(z){return l(z).map((G)=>{if(G.kind==="url")return G;const H=A.resolve(G.value),L=A.extname(H).toLowerCase();if(!P[L])throw new N(`Unsupported image "${L}" (jpg/png/gif/webp): ${G.value}`);const O=T.statSync(H,{throwIfNoEntry:!1});if(!O||!O.isFile())throw new N(`Not a file: ${H}`);return{kind:"path",value:H,mime:P[L]}})}f({site:"substack",name:"publish",access:"write",description:"Publish a post to your Substack publication (or save a draft with --draft)",domain:"substack.com",strategy:E.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!0,positional:!0,help:"Post title"},{name:"body",type:"string",help:"Post body text (blank lines split paragraphs)"},{name:"subtitle",type:"string",help:"Optional subtitle"},{name:"images",type:"string",help:"Images: comma-separated local paths and/or http(s) urls"},{name:"audience",type:"string",default:"everyone",help:"Visibility: everyone | only_paid"},{name:"section",type:"string",help:"Section name to file the post under"},{name:"send",type:"boolean",default:!1,help:"Also email subscribers on publish"},{name:"draft",type:"boolean",default:!1,help:"Create the draft but do not publish"}],columns:["status","publication","draft_id","url"],func:async(z,G)=>{if(!z)throw new $("Browser session required for substack publish");const H=String(G.title??"").trim();if(!H)throw new N("substack publish: title is required");const L=k(G.audience),O=F(G.images);await z.goto("https://substack.com/");const J=await V(z,{url:"https://substack.com/api/v1/user/profile/self"});if(J.status===401||J.status===403||!J.data||J.data.id==null)throw new w("substack.com",`Not logged in (profile/self HTTP ${J.status})`);const{host:K,userId:M,publication:S}=b(J.data);await z.goto(`${K}/`);const U=[];for(const Q of O){let B=Q.value;if(Q.kind==="path"){const I=T.readFileSync(Q.value).toString("base64"),Z=await V(z,{url:`${K}/api/v1/image`,method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`image=${encodeURIComponent(h(I,Q.mime))}`});if(!Z.ok||!Z.data?.url)throw new $(`Substack image upload failed: HTTP ${Z.status} ${Z.text}`);B=Z.data.url}U.push(y(B))}const R=[...m(G.body).map(D),...U];if(!R.length)R.push(D(""));const C=u(R);let X=null;if(G.section){const Q=await V(z,{url:`${K}/api/v1/subscriptions`});X=i(c(Q.data),G.section)}const x=n({title:H,subtitle:G.subtitle,bodyDoc:C,userId:M,audience:L,sectionId:X}),W=await V(z,{url:`${K}/api/v1/drafts`,method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(x)});if(!W.ok||W.data?.id==null)throw new $(`Substack create-draft failed: HTTP ${W.status} ${W.text}`);const Y=W.data.id,_=`${K}/publish/post/${Y}`;if(G.draft)return{status:"draft",publication:S,draft_id:String(Y),url:_};const j=await V(z,{url:`${K}/api/v1/drafts/${Y}/prepublish`});if(!j.ok)throw new $(`Substack prepublish failed: HTTP ${j.status} ${j.text}`);const q=await V(z,{url:`${K}/api/v1/drafts/${Y}/publish`,method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({send:!!G.send,share_automatically:!1})});if(!q.ok)throw new $(`Substack publish failed: HTTP ${q.status} ${q.text}`);const v=q.data?.slug;return{status:"published",publication:S,draft_id:String(Y),url:v?`${K}/p/${v}`:_}}});export const __test__={resolveImageEntries:F,EXT_MIME:P};
10
+ })()`;return d(await z.evaluate(J))}function F(z){return l(z).map((G)=>{if(G.kind==="url")return G;const H=A.resolve(G.value),L=A.extname(H).toLowerCase();if(!P[L])throw new N(`Unsupported image "${L}" (jpg/png/gif/webp): ${G.value}`);const O=T.statSync(H,{throwIfNoEntry:!1});if(!O||!O.isFile())throw new N(`Not a file: ${H}`);return{kind:"path",value:H,mime:P[L]}})}f({site:"substack",name:"publish",access:"write",description:"Publish a post to your Substack publication (or save a draft with --draft)",domain:"substack.com",strategy:E.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!0,positional:!0,help:"Post title"},{name:"body",fromFile:!0,type:"string",help:"Post body text (blank lines split paragraphs)"},{name:"subtitle",type:"string",help:"Optional subtitle"},{name:"images",type:"string",help:"Images: comma-separated local paths and/or http(s) urls"},{name:"audience",type:"string",default:"everyone",help:"Visibility: everyone | only_paid"},{name:"section",type:"string",help:"Section name to file the post under"},{name:"send",type:"boolean",default:!1,help:"Also email subscribers on publish"},{name:"draft",type:"boolean",default:!1,help:"Create the draft but do not publish"}],columns:["status","publication","draft_id","url"],func:async(z,G)=>{if(!z)throw new $("Browser session required for substack publish");const H=String(G.title??"").trim();if(!H)throw new N("substack publish: title is required");const L=k(G.audience),O=F(G.images);await z.goto("https://substack.com/");const J=await V(z,{url:"https://substack.com/api/v1/user/profile/self"});if(J.status===401||J.status===403||!J.data||J.data.id==null)throw new w("substack.com",`Not logged in (profile/self HTTP ${J.status})`);const{host:K,userId:M,publication:S}=b(J.data);await z.goto(`${K}/`);const U=[];for(const Q of O){let B=Q.value;if(Q.kind==="path"){const I=T.readFileSync(Q.value).toString("base64"),Z=await V(z,{url:`${K}/api/v1/image`,method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`image=${encodeURIComponent(h(I,Q.mime))}`});if(!Z.ok||!Z.data?.url)throw new $(`Substack image upload failed: HTTP ${Z.status} ${Z.text}`);B=Z.data.url}U.push(y(B))}const R=[...m(G.body).map(D),...U];if(!R.length)R.push(D(""));const C=u(R);let X=null;if(G.section){const Q=await V(z,{url:`${K}/api/v1/subscriptions`});X=i(c(Q.data),G.section)}const x=n({title:H,subtitle:G.subtitle,bodyDoc:C,userId:M,audience:L,sectionId:X}),W=await V(z,{url:`${K}/api/v1/drafts`,method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(x)});if(!W.ok||W.data?.id==null)throw new $(`Substack create-draft failed: HTTP ${W.status} ${W.text}`);const Y=W.data.id,_=`${K}/publish/post/${Y}`;if(G.draft)return{status:"draft",publication:S,draft_id:String(Y),url:_};const j=await V(z,{url:`${K}/api/v1/drafts/${Y}/prepublish`});if(!j.ok)throw new $(`Substack prepublish failed: HTTP ${j.status} ${j.text}`);const q=await V(z,{url:`${K}/api/v1/drafts/${Y}/publish`,method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({send:!!G.send,share_automatically:!1})});if(!q.ok)throw new $(`Substack publish failed: HTTP ${q.status} ${q.text}`);const v=q.data?.slug;return{status:"published",publication:S,draft_id:String(Y),url:v?`${K}/p/${v}`:_}}});export const __test__={resolveImageEntries:F,EXT_MIME:P};
@@ -1,4 +1,4 @@
1
- import{cli as c,Strategy as n}from"@jackwener/opencli/registry";import{ArgumentError as z,CommandExecutionError as V}from"@jackwener/opencli/errors";import{DEFAULT_SUNO_MODEL as y,SUNO_DOMAIN as t,SUNO_MODELS as v,SUNO_URL as Z,checkSunoCaptcha as o,clampSlider as _,downloadSunoClip as i,ensureSunoSession as e,normalizeBooleanFlag as A,parseFormats as a,pollSunoClips as s,requirePositiveInt as r,resolveSunoOutputDir as HH,submitSunoGeneration as JH}from"./utils.js";import*as C from"node:crypto";import*as KH from"node:os";function QH(K){if(!K)return"-";const J=KH.homedir();return K.startsWith(J)?`~${K.slice(J.length)}`:K}export const generateCommand=c({site:"suno",name:"generate",access:"write",description:"Generate music with Suno (V5.5 chirp-fenix by default) and download clips locally",domain:t,strategy:n.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,defaultFormat:"plain",args:[{name:"prompt",positional:!0,required:!1,help:"Simple-mode description (ignored when --lyrics is provided)"},{name:"lyrics",help:"Custom-mode lyrics (with [Verse]/[Chorus] metatags). Triggers Custom mode."},{name:"tags",help:"Custom-mode style tags (genre, BPM, instruments...). Used with --lyrics."},{name:"negative-tags",help:'Custom-mode style exclusions (e.g. "no vocals, no autotune"). Used with --lyrics.'},{name:"title",help:"Song title (default: auto-derived from prompt)"},{name:"instrumental",type:"boolean",default:!1,help:"No vocals"},{name:"model",help:`Model id: ${v.join(", ")}. Default: ${y}`},{name:"weirdness",help:"Creative weirdness slider (0..1). Default: 0.5"},{name:"style-weight",help:"Style adherence slider (0..1). Default: 0.5"},{name:"formats",help:"Comma-separated download formats: mp3, m4a, wav, video, cover, metadata. Default: mp3,metadata"},{name:"op",help:"Output directory (default: ~/Music/suno)"},{name:"timeout",type:"int",default:300,help:"Max seconds to wait for clips to finish (default: 300)"},{name:"sd",type:"boolean",default:!1,help:"Skip download; only print clip ids and Suno URLs"},{name:"confirm-paid",type:"boolean",default:!1,help:"Required to allow paid downloads (wav). Without it, paid formats are skipped with a warning."}],columns:["status","clip","title","files","link"],func:async(K,J)=>{const B=J.lyrics?String(J.lyrics):"",M=J.tags?String(J.tags):"",D=J["negative-tags"]?String(J["negative-tags"]):"",R=J.prompt?String(J.prompt):"",P=J.title?String(J.title):"",T=J.model?String(J.model).trim():y;if(!v.includes(T))throw new z(`Unsupported --model "${T}"`,`Choices: ${v.join(", ")}`);const $=B.trim()!=="";if(!$&&!R.trim())throw new z("Either provide a Simple-mode prompt as the positional argument, or pass --lyrics for Custom mode.",`Examples:
1
+ import{cli as c,Strategy as n}from"@jackwener/opencli/registry";import{ArgumentError as z,CommandExecutionError as V}from"@jackwener/opencli/errors";import{DEFAULT_SUNO_MODEL as y,SUNO_DOMAIN as t,SUNO_MODELS as v,SUNO_URL as Z,checkSunoCaptcha as o,clampSlider as _,downloadSunoClip as i,ensureSunoSession as e,normalizeBooleanFlag as A,parseFormats as a,pollSunoClips as s,requirePositiveInt as r,resolveSunoOutputDir as HH,submitSunoGeneration as JH}from"./utils.js";import*as C from"node:crypto";import*as KH from"node:os";function QH(K){if(!K)return"-";const J=KH.homedir();return K.startsWith(J)?`~${K.slice(J.length)}`:K}export const generateCommand=c({site:"suno",name:"generate",access:"write",description:"Generate music with Suno (V5.5 chirp-fenix by default) and download clips locally",domain:t,strategy:n.COOKIE,browser:!0,siteSession:"persistent",navigateBefore:!1,defaultFormat:"plain",args:[{name:"prompt",fromFile:!0,positional:!0,required:!1,help:"Simple-mode description (ignored when --lyrics is provided)"},{name:"lyrics",help:"Custom-mode lyrics (with [Verse]/[Chorus] metatags). Triggers Custom mode."},{name:"tags",help:"Custom-mode style tags (genre, BPM, instruments...). Used with --lyrics."},{name:"negative-tags",help:'Custom-mode style exclusions (e.g. "no vocals, no autotune"). Used with --lyrics.'},{name:"title",help:"Song title (default: auto-derived from prompt)"},{name:"instrumental",type:"boolean",default:!1,help:"No vocals"},{name:"model",help:`Model id: ${v.join(", ")}. Default: ${y}`},{name:"weirdness",help:"Creative weirdness slider (0..1). Default: 0.5"},{name:"style-weight",help:"Style adherence slider (0..1). Default: 0.5"},{name:"formats",help:"Comma-separated download formats: mp3, m4a, wav, video, cover, metadata. Default: mp3,metadata"},{name:"op",help:"Output directory (default: ~/Music/suno)"},{name:"timeout",type:"int",default:300,help:"Max seconds to wait for clips to finish (default: 300)"},{name:"sd",type:"boolean",default:!1,help:"Skip download; only print clip ids and Suno URLs"},{name:"confirm-paid",type:"boolean",default:!1,help:"Required to allow paid downloads (wav). Without it, paid formats are skipped with a warning."}],columns:["status","clip","title","files","link"],func:async(K,J)=>{const B=J.lyrics?String(J.lyrics):"",M=J.tags?String(J.tags):"",D=J["negative-tags"]?String(J["negative-tags"]):"",R=J.prompt?String(J.prompt):"",P=J.title?String(J.title):"",T=J.model?String(J.model).trim():y;if(!v.includes(T))throw new z(`Unsupported --model "${T}"`,`Choices: ${v.join(", ")}`);const $=B.trim()!=="";if(!$&&!R.trim())throw new z("Either provide a Simple-mode prompt as the positional argument, or pass --lyrics for Custom mode.",`Examples:
2
2
  ppcli suno generate "lo-fi study beat, 80 bpm"
3
3
  ppcli suno generate --lyrics "[Verse]\\n..." --tags "synthwave, 120 bpm"`);if(!$&&(M||D))throw new z("--tags and --negative-tags only apply in Custom mode (alongside --lyrics).");const f=a(J.formats),h=A(J["confirm-paid"]),I=A(J.sd),O=new Set(["wav"]),b=[],L=f.filter((H)=>{if(O.has(H)&&!h){b.push(H);return!1}return!0});if(!I&&!L.length)throw new z("All requested formats require --confirm-paid true","Add --confirm-paid true or include a free format such as mp3 or metadata.");const U=HH(J.op),E=r(J.timeout,"--timeout"),S=A(J.instrumental),u=_(J.weirdness,"--weirdness",0.5),m=_(J["style-weight"],"--style-weight",0.5),d=(P||($?M||B.split(`
4
4
  `)[0]:R)).replace(/\s+/g," ").trim().slice(0,60)||"Untitled",X=await e(K);if(!X.planId)throw new V(`Suno generation needs a resolved plan id for the user_tier field, but billing/info did not surface one for this account (subscription_type=${X.planKey}). Verify the account is active at ${Z}/account, then retry.`);const G=X.deviceId,W=await o(K,G);if(!W?.ok)throw new V(`Suno captcha pre-flight failed${W?.status?` (HTTP ${W.status})`:""}.`,`Open ${Z}/create in Chrome and verify the account is ready, then retry.`);if(W?.required)throw new V("Suno requires a CAPTCHA challenge for this account/IP right now.",`Open ${Z}/create in Chrome, solve a Create challenge once, then retry.`);if(X.totalCreditsAvailable<10){const H=X.breakdown;throw new V(`Suno generation needs ~10 credits; you have ${X.totalCreditsAvailable} (monthly ${H.monthlyRemaining}/${H.monthlyLimit} + packs ${H.purchasedPacks} + leftover ${H.pack}). Top up at ${Z}/account.`)}const k=C.randomUUID(),p=C.randomUUID(),N=await JH(K,{mode:$?"custom":"simple",model:T,title:d,lyrics:B,tags:M,negativeTags:D,description:R,makeInstrumental:S,weirdness:u,styleWeight:m,userTier:X.planId,createSessionToken:p,transactionUuid:k,deviceId:G});if(!Array.isArray(N.clips))throw new V("Suno generation returned malformed clips payload.");const j=N.clips.map((H)=>H?.id);if(j.some((H)=>!H))throw new V("Suno generation returned malformed clip identity.");if(!j.length)throw new V("Suno accepted the request but returned no clip ids.");const x=await s(K,j,E,G);if(!x.filter((H)=>H.status==="complete").length){const H=x.map((Y)=>`${Y.id.slice(0,8)}:${Y.status}`).join(", ");throw new V(`All Suno clips failed (${H}). Open ${Z}/song/${j[0]} to inspect.`)}const q=[];for(const H of x){const Y=`${Z}/song/${H.id}`;if(H.status!=="complete"){q.push({status:`❌ ${H.status}`,clip:H.id.slice(0,8),title:H.title||"(untitled)",files:"-",link:`\uD83D\uDD17 ${Y}`});continue}if(I){q.push({status:"\uD83C\uDFB5 generated",clip:H.id.slice(0,8),title:H.title||"(untitled)",files:"\uD83D\uDCC1 -",link:`\uD83D\uDD17 ${Y}`});continue}const F=await i(K,H,U,L,G);if(!F.written.some((Q)=>Q.ok))throw new V(`Suno download wrote no files for clip ${H.id}`);const w=F.written.map((Q)=>Q.ok?`${Q.format}:${QH(Q.file)}`:`${Q.format}:✗(${Q.reason})`).join(" | "),g=b.length?` | skipped(needs --confirm-paid):${b.join(",")}`:"",l=F.written.some((Q)=>!Q.ok);q.push({status:l?"⚠ partial":"✅ saved",clip:H.id.slice(0,8),title:H.title||"(untitled)",files:`\uD83D\uDCC1 ${w}${g}`,link:`\uD83D\uDD17 ${Y}`})}return q}});
@@ -8,7 +8,7 @@ import{ArgumentError as X,AuthRequiredError as G,CommandExecutionError as Z}from
8
8
  })()`)}async function W(b){return b.evaluate(`(() => {
9
9
  const ed = document.querySelector(${JSON.stringify(Y)});
10
10
  return ed ? (ed.textContent || '').replace(/\\uFEFF/g, '') : null;
11
- })()`)}j({site:"telegram",name:"send",access:"write",description:'Send a text message to a Telegram chat via web.telegram.org/k/. Pass a channel @username / t.me link, or "saved" for your own Saved Messages. You must have permission to post in the target.',domain:"web.telegram.org",strategy:v.UI,browser:!0,args:[{name:"peer",type:"string",required:!0,positional:!0,help:'Target: channel @username, t.me/<name> link, or "saved" (your Saved Messages)'},{name:"text",type:"string",required:!0,positional:!0,help:"Message text (multi-line allowed)"}],columns:["status","peer","message"],func:async(b,z)=>{if(!b)throw new Z("Browser session required for telegram send");const J=resolvePeer(z.peer),F=String(z.text??"");y(F,{label:"消息内容"});if(!F.trim())throw new X("消息内容不能为空。");await b.goto(J.url);let V=!1;for(let H=0;H<B;H++){const q=await U(b);if(q==="auth")throw new G("web.telegram.org","Telegram Web 停在登录页 —— 未登录。先 `ppcli telegram login`。");if(q==="ready"){V=!0;break}if(q==="no-chat"&&J.savedMessages)await I(b);await b.wait(1)}if(!V)throw new Z(`未能在 ${J.label} 打开消息输入框 —— 会话可能不存在、你没有发帖权限,或页面未加载完。`);await b.click(Y);await b.pressKey("Control+A");await b.pressKey("Delete");const Q=F.split(`
11
+ })()`)}j({site:"telegram",name:"send",access:"write",description:'Send a text message to a Telegram chat via web.telegram.org/k/. Pass a channel @username / t.me link, or "saved" for your own Saved Messages. You must have permission to post in the target.',domain:"web.telegram.org",strategy:v.UI,browser:!0,args:[{name:"peer",type:"string",required:!0,positional:!0,help:'Target: channel @username, t.me/<name> link, or "saved" (your Saved Messages)'},{name:"text",fromFile:!0,type:"string",required:!0,positional:!0,help:"Message text (multi-line allowed)"}],columns:["status","peer","message"],func:async(b,z)=>{if(!b)throw new Z("Browser session required for telegram send");const J=resolvePeer(z.peer),F=String(z.text??"");y(F,{label:"消息内容"});if(!F.trim())throw new X("消息内容不能为空。");await b.goto(J.url);let V=!1;for(let H=0;H<B;H++){const q=await U(b);if(q==="auth")throw new G("web.telegram.org","Telegram Web 停在登录页 —— 未登录。先 `ppcli telegram login`。");if(q==="ready"){V=!0;break}if(q==="no-chat"&&J.savedMessages)await I(b);await b.wait(1)}if(!V)throw new Z(`未能在 ${J.label} 打开消息输入框 —— 会话可能不存在、你没有发帖权限,或页面未加载完。`);await b.click(Y);await b.pressKey("Control+A");await b.pressKey("Delete");const Q=F.split(`
12
12
  `);for(let H=0;H<Q.length;H++){if(H>0)await b.pressKey("Shift+Enter");if(Q[H].length)await b.typeText(Y,Q[H])}const $=await W(b),f=(H)=>String(H||"").replace(/\s+/g," ").trim();if($===null||f($)!==f(F))throw new Z(`发送前组文失败(输入框内容为 ${JSON.stringify(($||"").slice(0,60))})。`);if(!(await b.evaluate(`(() => {
13
13
  const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
14
14
  const btn = Array.from(document.querySelectorAll(${JSON.stringify(M)})).find(visible);
@@ -130,4 +130,4 @@ import*as D from"node:fs";import*as v from"node:path";import{cli as V,Strategy a
130
130
  const done = !box && !dialogOpen;
131
131
  return { ok: done && !failed, failed, settled: done };
132
132
  })()
133
- `}async function L(z){const K=Math.ceil(h/y);let Q=0;for(let W=0;W<K;W++){const Y=await z.evaluate(buildPublishStatusProbeJs());if(Y?.failed)return{ok:!1,message:"Threads post could not be published (share failed)."};if(Y?.ok)return{ok:!0,message:"Threads post published successfully."};if(Y?.settled){Q+=1;if(Q>=3)return{ok:!0,message:"Threads post published successfully."}}else Q=0;if(W<K-1)await z.wait(y/1000)}return{ok:!1,message:"Threads post was not confirmed sent; verify via readback before retrying."}}V({site:"threads",name:"post",access:"write",description:"Post a new thread (text, optional single image) on Threads",domain:"www.threads.com",strategy:H.UI,browser:!0,args:[{name:"text",type:"string",required:!0,positional:!0,help:"The text content of the thread"},{name:"image",type:"string",required:!1,valueRequired:!0,help:"Optional single image path (jpg/jpeg/png/webp/gif)"}],columns:["status","message","text","url"],func:async(z,K)=>{const Q=w(z),W=K.image?T(K.image):null,Y=String(K.text??"");B(Y,{label:"串文正文"});if(!Y.trim()&&!W)throw new q("Thread text is required.","Provide the post text as the positional argument");await Q.goto(k);await Q.wait(2);await c(Q);await Q.wait(1);if(W)await d(Q,W);const Z=await b(Q,Y);if(!Z?.ok)throw new G(Z?.message??"Could not type Threads post text.");await Q.wait(1);const N=await I(Q);if(!N?.ok)throw new G(N?.message??"Threads post button not found.");const X=await L(Q);if(!X?.ok)throw new G(X?.message??"Threads post was not confirmed sent; verify via readback before retrying.");return[{status:"success",message:X.message,text:Y,url:""}]}});
133
+ `}async function L(z){const K=Math.ceil(h/y);let Q=0;for(let W=0;W<K;W++){const Y=await z.evaluate(buildPublishStatusProbeJs());if(Y?.failed)return{ok:!1,message:"Threads post could not be published (share failed)."};if(Y?.ok)return{ok:!0,message:"Threads post published successfully."};if(Y?.settled){Q+=1;if(Q>=3)return{ok:!0,message:"Threads post published successfully."}}else Q=0;if(W<K-1)await z.wait(y/1000)}return{ok:!1,message:"Threads post was not confirmed sent; verify via readback before retrying."}}V({site:"threads",name:"post",access:"write",description:"Post a new thread (text, optional single image) on Threads",domain:"www.threads.com",strategy:H.UI,browser:!0,args:[{name:"text",fromFile:!0,type:"string",required:!0,positional:!0,help:"The text content of the thread"},{name:"image",type:"string",required:!1,valueRequired:!0,help:"Optional single image path (jpg/jpeg/png/webp/gif)"}],columns:["status","message","text","url"],func:async(z,K)=>{const Q=w(z),W=K.image?T(K.image):null,Y=String(K.text??"");B(Y,{label:"串文正文"});if(!Y.trim()&&!W)throw new q("Thread text is required.","Provide the post text as the positional argument");await Q.goto(k);await Q.wait(2);await c(Q);await Q.wait(1);if(W)await d(Q,W);const Z=await b(Q,Y);if(!Z?.ok)throw new G(Z?.message??"Could not type Threads post text.");await Q.wait(1);const N=await I(Q);if(!N?.ok)throw new G(N?.message??"Threads post button not found.");const X=await L(Q);if(!X?.ok)throw new G(X?.message??"Threads post was not confirmed sent; verify via readback before retrying.");return[{status:"success",message:X.message,text:Y,url:""}]}});
@@ -66,4 +66,4 @@ import{cli as c,Strategy as m}from"@jackwener/opencli/registry";import{BROWSER_H
66
66
  result: 'posted',
67
67
  }];
68
68
  })()
69
- `}async function w(e,r){const{url:o}=p(r.url),s=T(r.text),i=(n)=>h(n,{authMessage:"TikTok requires login to post comments",failureMessage:`Failed to post comment on ${o}`,retryableHint:d.commentFailure});let t;try{await e.goto(o,{waitUntil:"load",settleMs:6000});t=await e.evaluate(a(s))}catch(n){i(n)}if(!Array.isArray(t)||t.length===0)i(Error(`STATE_VERIFY_FAIL: comment returned no row for ${o}`));return t}export const commentCommand=c({site:"tiktok",name:"comment",access:"write",description:"Post a comment on a TikTok video",domain:"www.tiktok.com",strategy:m.COOKIE,browser:!0,args:[{name:"url",required:!0,positional:!0,help:"TikTok video URL (https://www.tiktok.com/@user/video/<id>)"},{name:"text",required:!0,positional:!0,help:"Comment text (≤150 chars)"}],columns:["url","text","result"],func:w}),__test__={buildCommentScript:a};
69
+ `}async function w(e,r){const{url:o}=p(r.url),s=T(r.text),i=(n)=>h(n,{authMessage:"TikTok requires login to post comments",failureMessage:`Failed to post comment on ${o}`,retryableHint:d.commentFailure});let t;try{await e.goto(o,{waitUntil:"load",settleMs:6000});t=await e.evaluate(a(s))}catch(n){i(n)}if(!Array.isArray(t)||t.length===0)i(Error(`STATE_VERIFY_FAIL: comment returned no row for ${o}`));return t}export const commentCommand=c({site:"tiktok",name:"comment",access:"write",description:"Post a comment on a TikTok video",domain:"www.tiktok.com",strategy:m.COOKIE,browser:!0,args:[{name:"url",required:!0,positional:!0,help:"TikTok video URL (https://www.tiktok.com/@user/video/<id>)"},{name:"text",fromFile:!0,required:!0,positional:!0,help:"Comment text (≤150 chars)"}],columns:["url","text","result"],func:w}),__test__={buildCommentScript:a};
@@ -1 +1 @@
1
- import{cli as w,Strategy as C}from"@jackwener/opencli/registry";import{TimeoutError as c}from"@jackwener/opencli/errors";import{activityScript as y,approveTraePrompts as g,isActivityComplete as A,isLikelyBlockingActivity as d,latestAssistantScript as m,normalizeApprovalKinds as b,normalizeMaxChars as k,normalizeTimeout as P,sendTraePrompt as I}from"./utils.js";function N(e){return e===!0||String(e).toLowerCase()==="true"}export const askCommand=w({site:"trae-cn",name:"ask",access:"write",description:"Send a prompt to Trae CN, wait for the assistant result without treating approval cards as final, and return it",example:'OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk ppcli trae-cn ask "请只回复 OK" --timeout 120 -f json',domain:"localhost",strategy:C.UI,browser:!0,args:[{name:"text",required:!0,positional:!0,help:"Prompt to send into Trae CN"},{name:"timeout",type:"int",required:!1,help:"Max seconds to wait for response (default: 60)",default:60},{name:"max-chars",type:"int",required:!1,help:"Max chars to return from the assistant response; 0 returns full text (default: 12000)",default:12000},{name:"auto-approve",type:"boolean",required:!1,help:"While waiting, approve visible terminal/delete prompts, including high-risk terminal confirmation layers (default: false)",default:!1},{name:"approve-kinds",type:"string",required:!1,help:"Comma-separated approval categories for --auto-approve: terminal,delete,keep,all (default: terminal,delete; keep is not default)",default:"terminal,delete"}],columns:["Role","Text","TextChars","Truncated","TurnIndex","MessageId"],func:async(e,r)=>{const o=P(r.timeout,60),f=k(r["max-chars"],12000),h=N(r["auto-approve"]),v=b(r["approve-kinds"]),i=await e.evaluate(m(0)),T=[i?.MessageId||"",i?.TurnIndex||"",i?.Text||""].join("|");await I(e,r.text);const x=Date.now()+o*1000;let s="",l=0,t=null,a=null;while(Date.now()<x){await e.wait(2);if(h)await g(e,v,{click:!0,limit:1,maxChars:600});t=await e.evaluate(m(f));a=await e.evaluate(y(600));const n=t?.Text?.trim()||"";if(!n)continue;if([t?.MessageId||"",t?.TurnIndex||"",t?.Text||""].join("|")===T)continue;if(n!==s){s=n;l=Date.now();continue}const p=Date.now()-l,u=d(a);if(A(a)&&!u&&p>=1000)return[t];if(!u&&p>=3000)return[t]}if(t?.Text){if(d(a))throw new c("trae-cn ask",o,`Current status=${a?.Status??""}, approvalPending=${a?.ApprovalPending??""}, approvalKind=${a?.ApprovalKind??""}, approvalButton=${a?.ApprovalButton??""}. Use "ppcli trae-cn targets -f json" to find the waiting target, then run "OPENCLI_CDP_TARGET=<target> ppcli trae-cn watch --stream true --duration 300" or "ppcli trae-cn approve".`);return[t]}throw new c("trae-cn ask",o,"No Trae CN response was visible before the timeout. The agent may still be generating.")}});
1
+ import{cli as w,Strategy as C}from"@jackwener/opencli/registry";import{TimeoutError as c}from"@jackwener/opencli/errors";import{activityScript as y,approveTraePrompts as g,isActivityComplete as A,isLikelyBlockingActivity as m,latestAssistantScript as d,normalizeApprovalKinds as b,normalizeMaxChars as k,normalizeTimeout as P,sendTraePrompt as I}from"./utils.js";function N(e){return e===!0||String(e).toLowerCase()==="true"}export const askCommand=w({site:"trae-cn",name:"ask",access:"write",description:"Send a prompt to Trae CN, wait for the assistant result without treating approval cards as final, and return it",example:'OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk ppcli trae-cn ask "请只回复 OK" --timeout 120 -f json',domain:"localhost",strategy:C.UI,browser:!0,args:[{name:"text",fromFile:!0,required:!0,positional:!0,help:"Prompt to send into Trae CN"},{name:"timeout",type:"int",required:!1,help:"Max seconds to wait for response (default: 60)",default:60},{name:"max-chars",type:"int",required:!1,help:"Max chars to return from the assistant response; 0 returns full text (default: 12000)",default:12000},{name:"auto-approve",type:"boolean",required:!1,help:"While waiting, approve visible terminal/delete prompts, including high-risk terminal confirmation layers (default: false)",default:!1},{name:"approve-kinds",type:"string",required:!1,help:"Comma-separated approval categories for --auto-approve: terminal,delete,keep,all (default: terminal,delete; keep is not default)",default:"terminal,delete"}],columns:["Role","Text","TextChars","Truncated","TurnIndex","MessageId"],func:async(e,r)=>{const n=P(r.timeout,60),f=k(r["max-chars"],12000),h=N(r["auto-approve"]),v=b(r["approve-kinds"]),i=await e.evaluate(d(0)),T=[i?.MessageId||"",i?.TurnIndex||"",i?.Text||""].join("|");await I(e,r.text);const x=Date.now()+n*1000;let s="",l=0,t=null,a=null;while(Date.now()<x){await e.wait(2);if(h)await g(e,v,{click:!0,limit:1,maxChars:600});t=await e.evaluate(d(f));a=await e.evaluate(y(600));const o=t?.Text?.trim()||"";if(!o)continue;if([t?.MessageId||"",t?.TurnIndex||"",t?.Text||""].join("|")===T)continue;if(o!==s){s=o;l=Date.now();continue}const p=Date.now()-l,u=m(a);if(A(a)&&!u&&p>=1000)return[t];if(!u&&p>=3000)return[t]}if(t?.Text){if(m(a))throw new c("trae-cn ask",n,`Current status=${a?.Status??""}, approvalPending=${a?.ApprovalPending??""}, approvalKind=${a?.ApprovalKind??""}, approvalButton=${a?.ApprovalButton??""}. Use "ppcli trae-cn targets -f json" to find the waiting target, then run "OPENCLI_CDP_TARGET=<target> ppcli trae-cn watch --stream true --duration 300" or "ppcli trae-cn approve".`);return[t]}throw new c("trae-cn ask",n,"No Trae CN response was visible before the timeout. The agent may still be generating.")}});
@@ -1 +1 @@
1
- import{cli as c,Strategy as u}from"@jackwener/opencli/registry";import{CommandExecutionError as p,selectorError as d}from"@jackwener/opencli/errors";import{clickNewTaskScript as l,currentTaskStateScript as s,ensurePrompt as f,normalizeTimeout as w,sendTraePrompt as h}from"./utils.js";export const newCommand=c({site:"trae-cn",name:"new",access:"write",description:"Start a new Trae CN task in the current workspace, optionally sending the first prompt",example:'OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk ppcli trae-cn new "请执行你的任务" -f json',domain:"localhost",strategy:u.UI,browser:!0,args:[{name:"prompt",required:!1,positional:!0,help:"Optional first prompt to send after creating the task"},{name:"timeout",type:"int",required:!1,help:"Max seconds to wait for a fresh task composer (default: 10)",default:10}],columns:["Status","Action","Workspace","Model","Agent","FreshTaskConfirmed","TurnsBeforeSubmit","Turns","ComposerReady","SubmitMode"],func:async(t,r)=>{const n=w(r.timeout,10),o=await t.evaluate(l());if(!o?.ok)throw d("Trae CN new task button");const i=Date.now();let e=null;while(Date.now()-i<n*1000){await t.wait(0.5);e=await t.evaluate(s());if(e?.composerReady&&e.turns===0)break}if(!e?.composerReady||e.turns!==0)throw new p(`Clicked Trae CN new task via ${o.method}; fresh empty composer was not confirmed`,`Observed composerReady=${e?.composerReady?"yes":"no"}, turns=${e?.turns??"unavailable"}. Verify the current window is a Trae CN chat workspace and retry.`);const m=e.turns;let a="";if(r.prompt!==void 0&&r.prompt!==null&&String(r.prompt).trim()){a=(await h(t,f(r.prompt))).mode;await t.wait(0.5);e=await t.evaluate(s())}return[{Status:"Success",Action:r.prompt?`New task created via ${o.method}; prompt submitted`:`New task created via ${o.method}`,Workspace:e?.workspace||"",Model:e?.model||"",Agent:e?.agent||"",FreshTaskConfirmed:"yes",TurnsBeforeSubmit:m,Turns:e?.turns??0,ComposerReady:e?.composerReady?"yes":"no",SubmitMode:a}]}});
1
+ import{cli as c,Strategy as u}from"@jackwener/opencli/registry";import{CommandExecutionError as p,selectorError as d}from"@jackwener/opencli/errors";import{clickNewTaskScript as l,currentTaskStateScript as s,ensurePrompt as f,normalizeTimeout as w,sendTraePrompt as h}from"./utils.js";export const newCommand=c({site:"trae-cn",name:"new",access:"write",description:"Start a new Trae CN task in the current workspace, optionally sending the first prompt",example:'OPENCLI_CDP_ENDPOINT=http://127.0.0.1:39240 OPENCLI_CDP_TARGET=talk ppcli trae-cn new "请执行你的任务" -f json',domain:"localhost",strategy:u.UI,browser:!0,args:[{name:"prompt",fromFile:!0,required:!1,positional:!0,help:"Optional first prompt to send after creating the task"},{name:"timeout",type:"int",required:!1,help:"Max seconds to wait for a fresh task composer (default: 10)",default:10}],columns:["Status","Action","Workspace","Model","Agent","FreshTaskConfirmed","TurnsBeforeSubmit","Turns","ComposerReady","SubmitMode"],func:async(t,r)=>{const n=w(r.timeout,10),o=await t.evaluate(l());if(!o?.ok)throw d("Trae CN new task button");const i=Date.now();let e=null;while(Date.now()-i<n*1000){await t.wait(0.5);e=await t.evaluate(s());if(e?.composerReady&&e.turns===0)break}if(!e?.composerReady||e.turns!==0)throw new p(`Clicked Trae CN new task via ${o.method}; fresh empty composer was not confirmed`,`Observed composerReady=${e?.composerReady?"yes":"no"}, turns=${e?.turns??"unavailable"}. Verify the current window is a Trae CN chat workspace and retry.`);const m=e.turns;let a="";if(r.prompt!==void 0&&r.prompt!==null&&String(r.prompt).trim()){a=(await h(t,f(r.prompt))).mode;await t.wait(0.5);e=await t.evaluate(s())}return[{Status:"Success",Action:r.prompt?`New task created via ${o.method}; prompt submitted`:`New task created via ${o.method}`,Workspace:e?.workspace||"",Model:e?.model||"",Agent:e?.agent||"",FreshTaskConfirmed:"yes",TurnsBeforeSubmit:m,Turns:e?.turns??0,ComposerReady:e?.composerReady?"yes":"no",SubmitMode:a}]}});
@@ -1,4 +1,4 @@
1
- import{cli as $,Strategy as q}from"@jackwener/opencli/registry";import{ArgumentError as L,CommandExecutionError as W}from"@jackwener/opencli/errors";import{DM_SEND_QUERY as N,buildQuery as P,getDmAuth as R,resolveTwitterUserId as T,shapeDmMessages as Y}from"./dm-shared.js";import{assertLiteralContent as A}from"../_shared/content-guard.js";$({site:"twitter",name:"dm-send",access:"write",description:"Send a direct message to one user (by @screen_name, numeric user id, or conversation id from dm-list)",domain:"x.com",strategy:q.COOKIE,browser:!0,args:[{name:"user",type:"string",required:!0,positional:!0,help:"Recipient: @screen_name, numeric user id, or a conversation id like 123-456 from `dm-list`"},{name:"text",type:"string",required:!0,positional:!0,help:"Message text to send"},{name:"reply-to",type:"string",required:!1,help:"Message id to reply to (from `dm-read`)"}],columns:["id","conversation_id","text","time","status"],func:async(G,B)=>{const H=String(B.text??"").trim();A(H,{label:"私信内容"});if(!H)throw new L("Message text must not be empty");const{ownId:X,headers:K}=await R(G),J=String(B.user??"").trim();let F;if(/^\d+-\d+$/.test(J))F=J;else F=`${await T(G,J,K)}-${X}`;const O={cards_platform:"Web-12",conversation_id:F,dm_users:!1,include_cards:1,include_quote_count:!0,recipient_ids:!1,text:H,audio_only_media_attachment:!1};if(B["reply-to"])O.reply_to_dm_id=String(B["reply-to"]).trim();const Z="https://x.com/i/api/1.1/dm/new2.json?"+P(N),f=await G.evaluate(`async () => {
1
+ import{cli as $,Strategy as q}from"@jackwener/opencli/registry";import{ArgumentError as L,CommandExecutionError as W}from"@jackwener/opencli/errors";import{DM_SEND_QUERY as N,buildQuery as P,getDmAuth as R,resolveTwitterUserId as T,shapeDmMessages as Y}from"./dm-shared.js";import{assertLiteralContent as A}from"../_shared/content-guard.js";$({site:"twitter",name:"dm-send",access:"write",description:"Send a direct message to one user (by @screen_name, numeric user id, or conversation id from dm-list)",domain:"x.com",strategy:q.COOKIE,browser:!0,args:[{name:"user",type:"string",required:!0,positional:!0,help:"Recipient: @screen_name, numeric user id, or a conversation id like 123-456 from `dm-list`"},{name:"text",fromFile:!0,type:"string",required:!0,positional:!0,help:"Message text to send"},{name:"reply-to",type:"string",required:!1,help:"Message id to reply to (from `dm-read`)"}],columns:["id","conversation_id","text","time","status"],func:async(G,B)=>{const H=String(B.text??"").trim();A(H,{label:"私信内容"});if(!H)throw new L("Message text must not be empty");const{ownId:X,headers:K}=await R(G),J=String(B.user??"").trim();let F;if(/^\d+-\d+$/.test(J))F=J;else F=`${await T(G,J,K)}-${X}`;const O={cards_platform:"Web-12",conversation_id:F,dm_users:!1,include_cards:1,include_quote_count:!0,recipient_ids:!1,text:H,audio_only_media_attachment:!1};if(B["reply-to"])O.reply_to_dm_id=String(B["reply-to"]).trim();const Z="https://x.com/i/api/1.1/dm/new2.json?"+P(N),f=await G.evaluate(`async () => {
2
2
  const body = ${JSON.stringify(O)};
3
3
  body.request_id = crypto.randomUUID();
4
4
  const r = await fetch(${JSON.stringify(Z)}, {
@@ -128,4 +128,4 @@ import*as R from"node:fs";import*as J from"node:path";import{cli as h,Strategy a
128
128
  let bodyJson = null;
129
129
  try { bodyJson = JSON.parse(bodyText); } catch {}
130
130
  return { ok: r.ok, httpStatus: r.status, bodyJson, bodyText };
131
- }`));if(W?.httpStatus===401||W?.httpStatus===403)throw new j("x.com",`Twitter CreateTweet returned HTTP ${W.httpStatus}`);if(!W?.ok){const G=k(W?.bodyJson);return{ok:!1,message:U("CreateTweet",W?.httpStatus??0,G||void 0)}}const X=g(W.bodyJson);if(!X){const G=k(W.bodyJson);return{ok:!1,message:G?`CreateTweet failed: ${G}`:"CreateTweet returned no created tweet id."}}return{ok:!0,message:"Tweet posted successfully.",...X}}h({site:"twitter",name:"post",access:"write",description:"Post a new tweet/thread",domain:"x.com",strategy:C.UI,browser:!0,args:[{name:"text",type:"string",required:!0,positional:!0,help:"The text content of the tweet"},{name:"images",type:"string",required:!1,help:"Image paths, comma-separated, max 4 (jpg/png/gif/webp)"}],columns:["status","message","text","id","url"],func:async(z,Q)=>{if(!z)throw new q("Browser session required for twitter post");const H=Q.images?c(String(Q.images)):[],Y=String(Q.text??"");P(Y,{label:"推文内容"});try{await z.goto(S,{waitUntil:"load",settleMs:2500});await z.wait({selector:'[data-testid="tweetTextarea_0"]',timeout:15})}catch(V){if(H.length>0)throw V;const K=await u(z,Y);if(!K?.ok)throw new q(K?.message??"Tweet failed to post.");return[{status:"success",message:K?.message??"Tweet posted.",text:Y,...K?.id?{id:K.id}:{},...K?.url?{url:K.url}:{}}]}if(H.length>0){await z.wait({selector:A,timeout:20});await b(z,A,H);const V=await n(z,H.length);if(!V?.ok)throw new q(V?.message??`Image upload timed out (${D/1000}s).`)}const $=await l(z,Y);if(!$?.ok)throw new q($?.message??"Could not type tweet text.");await z.wait(1);let Z=null,F=null;try{Z=await E(z,Y)}catch(V){F=V}if(!Z?.ok){const{confirmTweetByReadback:V}=await import("./tweets.js"),K=await V(z,Y);if(K.found)return[{status:"success",message:"提交回执未确认(可能超时),但发后回查已在时间线确认发布成功,请勿重发。",text:Y,...K.id?{id:K.id}:{},...K.url?{url:K.url}:{},confirmed_by:"readback"}];const W=F?String(F&&F.message||F):Z?.message??"Tweet was not confirmed sent.",X=K.error?`发后回查失败(${K.error})——无法确认,重试前请先人工核对时间线`:`发后回查已检查最近 ${K.checked} 条推文,未发现该内容,可安全重试`;throw new q(`${W};${X}`)}return[{status:"success",message:Z?.message??"Tweet posted.",text:Y,...Z?.id?{id:Z.id}:{},...Z?.url?{url:Z.url}:{}}]}});
131
+ }`));if(W?.httpStatus===401||W?.httpStatus===403)throw new j("x.com",`Twitter CreateTweet returned HTTP ${W.httpStatus}`);if(!W?.ok){const G=k(W?.bodyJson);return{ok:!1,message:U("CreateTweet",W?.httpStatus??0,G||void 0)}}const X=g(W.bodyJson);if(!X){const G=k(W.bodyJson);return{ok:!1,message:G?`CreateTweet failed: ${G}`:"CreateTweet returned no created tweet id."}}return{ok:!0,message:"Tweet posted successfully.",...X}}h({site:"twitter",name:"post",access:"write",description:"Post a new tweet/thread",domain:"x.com",strategy:C.UI,browser:!0,args:[{name:"text",fromFile:!0,type:"string",required:!0,positional:!0,help:"The text content of the tweet"},{name:"images",type:"string",required:!1,help:"Image paths, comma-separated, max 4 (jpg/png/gif/webp)"}],columns:["status","message","text","id","url"],func:async(z,Q)=>{if(!z)throw new q("Browser session required for twitter post");const H=Q.images?c(String(Q.images)):[],Y=String(Q.text??"");P(Y,{label:"推文内容"});try{await z.goto(S,{waitUntil:"load",settleMs:2500});await z.wait({selector:'[data-testid="tweetTextarea_0"]',timeout:15})}catch(V){if(H.length>0)throw V;const K=await u(z,Y);if(!K?.ok)throw new q(K?.message??"Tweet failed to post.");return[{status:"success",message:K?.message??"Tweet posted.",text:Y,...K?.id?{id:K.id}:{},...K?.url?{url:K.url}:{}}]}if(H.length>0){await z.wait({selector:A,timeout:20});await b(z,A,H);const V=await n(z,H.length);if(!V?.ok)throw new q(V?.message??`Image upload timed out (${D/1000}s).`)}const $=await l(z,Y);if(!$?.ok)throw new q($?.message??"Could not type tweet text.");await z.wait(1);let Z=null,F=null;try{Z=await E(z,Y)}catch(V){F=V}if(!Z?.ok){const{confirmTweetByReadback:V}=await import("./tweets.js"),K=await V(z,Y);if(K.found)return[{status:"success",message:"提交回执未确认(可能超时),但发后回查已在时间线确认发布成功,请勿重发。",text:Y,...K.id?{id:K.id}:{},...K.url?{url:K.url}:{},confirmed_by:"readback"}];const W=F?String(F&&F.message||F):Z?.message??"Tweet was not confirmed sent.",X=K.error?`发后回查失败(${K.error})——无法确认,重试前请先人工核对时间线`:`发后回查已检查最近 ${K.checked} 条推文,未发现该内容,可安全重试`;throw new q(`${W};${X}`)}return[{status:"success",message:Z?.message??"Tweet posted.",text:Y,...Z?.id?{id:Z.id}:{},...Z?.url?{url:Z.url}:{}}]}});
@@ -120,4 +120,4 @@ import*as Z from"node:fs";import{CommandExecutionError as J}from"@jackwener/open
120
120
  } catch (e) {
121
121
  return { ok: false, message: e.toString() };
122
122
  }
123
- })()`)}$({site:"twitter",name:"quote",access:"write",description:"Quote-tweet a specific tweet with your own text, optionally with a local or remote image",domain:"x.com",strategy:f.UI,browser:!0,args:[{name:"url",type:"string",required:!0,positional:!0,help:"The URL of the tweet to quote"},{name:"text",type:"string",required:!0,positional:!0,help:"The text content of your quote"},{name:"image",help:"Optional local image path to attach to the quote tweet"},{name:"image-url",help:"Optional remote image URL to download and attach to the quote tweet"}],columns:["status","message","text"],func:async(q,j)=>{if(!q)throw new J("Browser session required for twitter quote");if(j.image&&j["image-url"])throw new J("Use either --image or --image-url, not both.");const v=K(j.url);let z,H;try{if(j.image)z=A(j.image);else if(j["image-url"]){const G=await y(j["image-url"]);z=G.absPath;H=G.cleanupDir}await q.goto(`https://x.com/compose/post?url=${encodeURIComponent(v.url)}`,{waitUntil:"load",settleMs:2500});await q.wait({selector:'[data-testid="tweetTextarea_0"]',timeout:15});if(z){await q.wait({selector:W,timeout:20});await X(q,z)}let B=await Y(q,j.text,v);if(B.retryWithMenuFallback){const G=await D(q,v);if(!G?.ok)return[{status:"failed",message:G?.message??"Could not open quote composer from repost menu.",text:j.text}];if(z){await q.wait({selector:W,timeout:20});await X(q,z)}B=await Y(q,j.text,v,{allowMenuFallback:!1})}if(B.ok)await q.wait(3);return[{status:B.ok?"success":"failed",message:B.message,text:j.text,...j.image?{image:j.image}:{},...j["image-url"]?{"image-url":j["image-url"]}:{}}]}finally{if(H)Z.rmSync(H,{recursive:!0,force:!0})}}});export const __test__={buildQuoteComposerUrl:N};
123
+ })()`)}$({site:"twitter",name:"quote",access:"write",description:"Quote-tweet a specific tweet with your own text, optionally with a local or remote image",domain:"x.com",strategy:f.UI,browser:!0,args:[{name:"url",type:"string",required:!0,positional:!0,help:"The URL of the tweet to quote"},{name:"text",fromFile:!0,type:"string",required:!0,positional:!0,help:"The text content of your quote"},{name:"image",help:"Optional local image path to attach to the quote tweet"},{name:"image-url",help:"Optional remote image URL to download and attach to the quote tweet"}],columns:["status","message","text"],func:async(q,j)=>{if(!q)throw new J("Browser session required for twitter quote");if(j.image&&j["image-url"])throw new J("Use either --image or --image-url, not both.");const v=K(j.url);let z,H;try{if(j.image)z=A(j.image);else if(j["image-url"]){const G=await y(j["image-url"]);z=G.absPath;H=G.cleanupDir}await q.goto(`https://x.com/compose/post?url=${encodeURIComponent(v.url)}`,{waitUntil:"load",settleMs:2500});await q.wait({selector:'[data-testid="tweetTextarea_0"]',timeout:15});if(z){await q.wait({selector:W,timeout:20});await X(q,z)}let B=await Y(q,j.text,v);if(B.retryWithMenuFallback){const G=await D(q,v);if(!G?.ok)return[{status:"failed",message:G?.message??"Could not open quote composer from repost menu.",text:j.text}];if(z){await q.wait({selector:W,timeout:20});await X(q,z)}B=await Y(q,j.text,v,{allowMenuFallback:!1})}if(B.ok)await q.wait(3);return[{status:B.ok?"success":"failed",message:B.message,text:j.text,...j.image?{image:j.image}:{},...j["image-url"]?{"image-url":j["image-url"]}:{}}]}finally{if(H)Z.rmSync(H,{recursive:!0,force:!0})}}});export const __test__={buildQuoteComposerUrl:N};
@@ -1,4 +1,4 @@
1
- import{CommandExecutionError as N}from"@jackwener/opencli/errors";import{cli as O,Strategy as P}from"@jackwener/opencli/registry";import{assertLiteralContent as Q}from"../_shared/content-guard.js";O({site:"twitter",name:"reply-dm",access:"write",description:"Send a message to recent DM conversations",domain:"x.com",strategy:P.UI,browser:!0,args:[{name:"text",type:"string",required:!0,positional:!0,help:'Message text to send (e.g. "我的微信 wxkabi")'},{name:"max",type:"int",required:!1,default:20,help:"Maximum number of conversations to reply to (default: 20)"},{name:"skip-replied",type:"boolean",required:!1,default:!0,help:"Skip conversations where you already sent the same text (default: true)"},{name:"timeout",type:"int",required:!1,default:600,help:"Max seconds for the overall command (default: 600 — batch op)"}],columns:["index","status","user","message"],func:async(b,A)=>{if(!b)throw new N("Browser session required for twitter reply-dm");const F=A.text;Q(F,{label:"私信内容"});const G=A.max??20,J=A["skip-replied"]!==!1,f=[];let B=0;await b.goto("https://x.com/messages");await b.wait({selector:'[data-testid="primaryColumn"]'});const K=G+10,D=await b.evaluate(`(async () => {
1
+ import{CommandExecutionError as N}from"@jackwener/opencli/errors";import{cli as O,Strategy as P}from"@jackwener/opencli/registry";import{assertLiteralContent as Q}from"../_shared/content-guard.js";O({site:"twitter",name:"reply-dm",access:"write",description:"Send a message to recent DM conversations",domain:"x.com",strategy:P.UI,browser:!0,args:[{name:"text",fromFile:!0,type:"string",required:!0,positional:!0,help:'Message text to send (e.g. "我的微信 wxkabi")'},{name:"max",type:"int",required:!1,default:20,help:"Maximum number of conversations to reply to (default: 20)"},{name:"skip-replied",type:"boolean",required:!1,default:!0,help:"Skip conversations where you already sent the same text (default: true)"},{name:"timeout",type:"int",required:!1,default:600,help:"Max seconds for the overall command (default: 600 — batch op)"}],columns:["index","status","user","message"],func:async(b,A)=>{if(!b)throw new N("Browser session required for twitter reply-dm");const F=A.text;Q(F,{label:"私信内容"});const G=A.max??20,J=A["skip-replied"]!==!1,f=[];let B=0;await b.goto("https://x.com/messages");await b.wait({selector:'[data-testid="primaryColumn"]'});const K=G+10,D=await b.evaluate(`(async () => {
2
2
  try {
3
3
  // Wait for initial items
4
4
  let attempts = 0;
@@ -93,4 +93,4 @@ import{ArgumentError as i,CommandExecutionError as d}from"@jackwener/opencli/err
93
93
  if (alert) return { ok: false, message: (alert.textContent || 'Scheduled post failed.').trim() };
94
94
  }
95
95
  return { ok: false, message: 'Schedule submission did not complete before timeout.' };
96
- })()`)}m({site:"twitter",name:"schedule",access:"write",description:"Schedule a new X post through the web composer",domain:"x.com",strategy:f.UI,browser:!0,args:[{name:"text",type:"string",required:!0,positional:!0,help:"The text content of the scheduled post"},{name:"at",type:"string",required:!1,help:'Local scheduled time, e.g. "2026-05-24 21:30"'},{name:"delay-minutes",type:"int",default:a,help:"Schedule this many minutes from now"}],columns:["status","message","text","scheduledFor"],func:async(e,s)=>{if(!e)throw new d("Browser session required for twitter schedule");const t=String(s.text??"").trim();if(!t)throw new i("Scheduled post text is required");const r=parseScheduleTarget(s);await e.goto(b,{waitUntil:"load",settleMs:2500});await e.wait({selector:'[data-testid="tweetTextarea_0"]',timeout:15});const n=await c(e,r);if(!n?.ok)return[{status:"failed",message:n?.message??"Could not configure schedule.",text:t,scheduledFor:r.iso}];await e.wait({selector:'[data-testid="tweetTextarea_0"]',timeout:15});const o=await w(e,t);if(!o?.ok)return[{status:"failed",message:o?.message??"Could not type scheduled post text.",text:t,scheduledFor:r.iso}];await e.wait(1);const l=await p(e);return[{status:l?.ok?"success":"failed",message:l?.message??"Scheduled post failed.",text:t,scheduledFor:r.iso}]}});export const __test__={parseScheduleTarget,setSchedule:c};
96
+ })()`)}m({site:"twitter",name:"schedule",access:"write",description:"Schedule a new X post through the web composer",domain:"x.com",strategy:f.UI,browser:!0,args:[{name:"text",fromFile:!0,type:"string",required:!0,positional:!0,help:"The text content of the scheduled post"},{name:"at",type:"string",required:!1,help:'Local scheduled time, e.g. "2026-05-24 21:30"'},{name:"delay-minutes",type:"int",default:a,help:"Schedule this many minutes from now"}],columns:["status","message","text","scheduledFor"],func:async(e,s)=>{if(!e)throw new d("Browser session required for twitter schedule");const t=String(s.text??"").trim();if(!t)throw new i("Scheduled post text is required");const r=parseScheduleTarget(s);await e.goto(b,{waitUntil:"load",settleMs:2500});await e.wait({selector:'[data-testid="tweetTextarea_0"]',timeout:15});const n=await c(e,r);if(!n?.ok)return[{status:"failed",message:n?.message??"Could not configure schedule.",text:t,scheduledFor:r.iso}];await e.wait({selector:'[data-testid="tweetTextarea_0"]',timeout:15});const o=await w(e,t);if(!o?.ok)return[{status:"failed",message:o?.message??"Could not type scheduled post text.",text:t,scheduledFor:r.iso}];await e.wait(1);const l=await p(e);return[{status:l?.ok?"success":"failed",message:l?.message??"Scheduled post failed.",text:t,scheduledFor:r.iso}]}});export const __test__={parseScheduleTarget,setSchedule:c};
package/clis/vk/post.js CHANGED
@@ -1 +1 @@
1
- import{cli as W,Strategy as X}from"@jackwener/opencli/registry";import{ArgumentError as M,CommandExecutionError as Y,EmptyResultError as Z}from"@jackwener/opencli/errors";import{assertLiteralContent as $}from"../_shared/content-guard.js";import{mintVkToken as z,vkApiCall as G}from"./shared.js";function L(q){if(q===void 0||q===null||String(q).trim()==="")return null;const b=String(q).trim();if(!/^-?\d+$/.test(b))throw new M(`Invalid --owner-id: ${JSON.stringify(b)}`,"个人墙用当前用户 id(正整数),社区墙用社区 id 加负号(如 -123456)。");return b}W({site:"vk",name:"post",access:"write",description:"Publish a text post to a VK wall (wall.post). Posts to your own wall by default; pass --owner-id (negative for a community) to target another wall, and --from-group to post as the community. Text-only for now (attachments not yet supported).",domain:"vk.com",strategy:X.COOKIE,browser:!0,args:[{name:"text",type:"string",required:!0,positional:!0,help:"帖子正文(message)"},{name:"owner-id",type:"string",required:!1,help:"目标墙 id:默认你自己的墙;社区加负号(如 -123456)"},{name:"from-group",type:"bool",default:!1,help:"以社区身份发帖(仅对社区墙有效)"},{name:"friends-only",type:"bool",default:!1,help:"仅好友可见"}],columns:["status","message","post_id","url"],func:async(q,b)=>{if(!q)throw new Y("Browser session required for vk post");const F=String(b.text??"");$(F,{label:"帖子正文"});if(!F.trim())throw new M("帖子正文(text)不能为空。");const N=L(b["owner-id"]),P=b["from-group"]===!0,Q=b["friends-only"]===!0,{token:U,userId:J}=await z(q),B=N??(J?String(J):null),D={message:F};if(B)D.owner_id=B;if(P)D.from_group=1;if(Q)D.friends_only=1;const H=(await G("wall.post",D,U))?.post_id;if(!H)throw new Z("vk post","wall.post 未返回 post_id — 发布可能未成功,请回查后再重试。");const K=B?`https://vk.com/wall${B}_${H}`:null;return[{status:"success",message:"VK post published.",post_id:String(H),...K?{url:K}:{}}]}});
1
+ import{cli as W,Strategy as X}from"@jackwener/opencli/registry";import{ArgumentError as M,CommandExecutionError as Y,EmptyResultError as Z}from"@jackwener/opencli/errors";import{assertLiteralContent as $}from"../_shared/content-guard.js";import{mintVkToken as z,vkApiCall as G}from"./shared.js";function L(q){if(q===void 0||q===null||String(q).trim()==="")return null;const b=String(q).trim();if(!/^-?\d+$/.test(b))throw new M(`Invalid --owner-id: ${JSON.stringify(b)}`,"个人墙用当前用户 id(正整数),社区墙用社区 id 加负号(如 -123456)。");return b}W({site:"vk",name:"post",access:"write",description:"Publish a text post to a VK wall (wall.post). Posts to your own wall by default; pass --owner-id (negative for a community) to target another wall, and --from-group to post as the community. Text-only for now (attachments not yet supported).",domain:"vk.com",strategy:X.COOKIE,browser:!0,args:[{name:"text",fromFile:!0,type:"string",required:!0,positional:!0,help:"帖子正文(message)"},{name:"owner-id",type:"string",required:!1,help:"目标墙 id:默认你自己的墙;社区加负号(如 -123456)"},{name:"from-group",type:"bool",default:!1,help:"以社区身份发帖(仅对社区墙有效)"},{name:"friends-only",type:"bool",default:!1,help:"仅好友可见"}],columns:["status","message","post_id","url"],func:async(q,b)=>{if(!q)throw new Y("Browser session required for vk post");const F=String(b.text??"");$(F,{label:"帖子正文"});if(!F.trim())throw new M("帖子正文(text)不能为空。");const N=L(b["owner-id"]),P=b["from-group"]===!0,Q=b["friends-only"]===!0,{token:U,userId:J}=await z(q),B=N??(J?String(J):null),D={message:F};if(B)D.owner_id=B;if(P)D.from_group=1;if(Q)D.friends_only=1;const H=(await G("wall.post",D,U))?.post_id;if(!H)throw new Z("vk post","wall.post 未返回 post_id — 发布可能未成功,请回查后再重试。");const K=B?`https://vk.com/wall${B}_${H}`:null;return[{status:"success",message:"VK post published.",post_id:String(H),...K?{url:K}:{}}]}});
@@ -240,7 +240,7 @@ import*as f from"node:fs";import*as T from"node:path";import{cli as x,Strategy a
240
240
  }
241
241
  return { ok: false };
242
242
  })(${JSON.stringify(Z)})
243
- `);if($?.ok)break;if(V<K-1)await z.wait({time:0.5})}if(!$?.ok){await z.screenshot({path:"/tmp/wechat-channels_publish_submit_debug.png"});throw new q(`找不到"${Z[0]}"按钮(按钮可能被禁用或表单未完成),`+"截图已保存到 /tmp/wechat-channels_publish_submit_debug.png")}return $}x({site:"wechat-channels",name:"publish",access:"write",description:"发布视频到视频号",domain:"channels.weixin.qq.com",strategy:P.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"video",required:!0,positional:!0,help:"视频文件路径 (.mp4/.mov/.avi/.webm)"},{name:"title",required:!1,help:"短标题(建议 6-16 字)"},{name:"caption",required:!1,help:"描述内容,支持直接写 #话题(如:日常生活 #搞笑 #生活)"},{name:"schedule",required:!1,help:'定时发布时间(ISO8601 或 Unix 秒,如 "2026-05-20 10:00")'},{name:"draft",type:"bool",default:!1,help:"保存为草稿"},{name:"manual",type:"bool",default:!1,help:"填完所有字段后不自动发布,由用户手动点击发表(务必同时传 --site-session persistent,否则表单页约 30 秒后会被重置为空白页)"},{name:"timeout",type:"int",required:!1,default:600,help:"命令整体超时秒数(含登录等待 + 上传转码,默认 600)"}],columns:["status","title","detail"],func:async(z,J)=>{if(!z)throw new q("需要浏览器页面");const Z=v(J.timeout),K=Date.now()+Z*1000,$=N(J.video,"视频",d),V=String(J.title??"").trim(),W=String(J.caption??"").trim(),X=k(J.schedule||null),G=R(J.draft),B=R(J.manual);await z.goto(A);await z.wait({time:4});{const Y=(I)=>typeof I==="string"&&I.includes("/platform/post/create"),b=await j(z,"() => location.href");if(!Y(b)){process.stderr.write(`
243
+ `);if($?.ok)break;if(V<K-1)await z.wait({time:0.5})}if(!$?.ok){await z.screenshot({path:"/tmp/wechat-channels_publish_submit_debug.png"});throw new q(`找不到"${Z[0]}"按钮(按钮可能被禁用或表单未完成),`+"截图已保存到 /tmp/wechat-channels_publish_submit_debug.png")}return $}x({site:"wechat-channels",name:"publish",access:"write",description:"发布视频到视频号",domain:"channels.weixin.qq.com",strategy:P.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"video",required:!0,positional:!0,help:"视频文件路径 (.mp4/.mov/.avi/.webm)"},{name:"title",required:!1,help:"短标题(建议 6-16 字)"},{name:"caption",fromFile:!0,required:!1,help:"描述内容,支持直接写 #话题(如:日常生活 #搞笑 #生活)"},{name:"schedule",required:!1,help:'定时发布时间(ISO8601 或 Unix 秒,如 "2026-05-20 10:00")'},{name:"draft",type:"bool",default:!1,help:"保存为草稿"},{name:"manual",type:"bool",default:!1,help:"填完所有字段后不自动发布,由用户手动点击发表(务必同时传 --site-session persistent,否则表单页约 30 秒后会被重置为空白页)"},{name:"timeout",type:"int",required:!1,default:600,help:"命令整体超时秒数(含登录等待 + 上传转码,默认 600)"}],columns:["status","title","detail"],func:async(z,J)=>{if(!z)throw new q("需要浏览器页面");const Z=v(J.timeout),K=Date.now()+Z*1000,$=N(J.video,"视频",d),V=String(J.title??"").trim(),W=String(J.caption??"").trim(),X=k(J.schedule||null),G=R(J.draft),B=R(J.manual);await z.goto(A);await z.wait({time:4});{const Y=(I)=>typeof I==="string"&&I.includes("/platform/post/create"),b=await j(z,"() => location.href");if(!Y(b)){process.stderr.write(`
244
244
  ⚠️ 未登录视频号(已被重定向离开发布页)。请在 Chrome 中扫码登录...
245
245
  `+" 当前地址: "+b+`
246
246
  `+` 登录完成后将自动继续发布。
@@ -124,7 +124,7 @@ import{cli as Y,Strategy as Z}from"@jackwener/opencli/registry";import{CommandEx
124
124
  var el = document.querySelector('#js_save_success');
125
125
  if (el && window.getComputedStyle(el).display !== 'none') return true;
126
126
  return document.body.innerText.includes('已保存');
127
- })()`))return!0}return!1}export const createDraftCommand=Y({site:"weixin",name:"create-draft",access:"write",description:"创建微信公众号图文草稿",domain:A,strategy:Z.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"title",required:!0,help:"文章标题 (最长64字)"},{name:"content",required:!0,positional:!0,help:"文章正文"},{name:"author",help:"作者名 (最长8字)"},{name:"cover-image",help:"封面图片路径 (会先上传到正文再设为封面)"},{name:"summary",help:"文章摘要"},{name:"timeout",type:"int",required:!1,default:180,help:"Max seconds for the overall command (default: 180)"}],columns:["status","detail"],func:async(q,z)=>{await F(q);if(!(await V(q,".title-editor__input .ProseMirror",z.title))?.ok){if(!(await L(q,"textarea#title",z.title))?.ok)throw new J("Failed to fill title")}if(z.author){if(!(await L(q,"input#author",z.author))?.ok)throw new J("Failed to fill author")}if(!(await V(q,".rich_media_content.ProseMirror, .rich_media_content .ProseMirror",z.content))?.ok)throw new J("Failed to fill content");if(z["cover-image"]){await O(q,z["cover-image"]);const R=await W(q)}if(z.summary)await L(q,"textarea#js_description",z.summary);const G=await q.evaluate(`(() => {
127
+ })()`))return!0}return!1}export const createDraftCommand=Y({site:"weixin",name:"create-draft",access:"write",description:"创建微信公众号图文草稿",domain:A,strategy:Z.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"title",required:!0,help:"文章标题 (最长64字)"},{name:"content",fromFile:!0,required:!0,positional:!0,help:"文章正文"},{name:"author",help:"作者名 (最长8字)"},{name:"cover-image",help:"封面图片路径 (会先上传到正文再设为封面)"},{name:"summary",help:"文章摘要"},{name:"timeout",type:"int",required:!1,default:180,help:"Max seconds for the overall command (default: 180)"}],columns:["status","detail"],func:async(q,z)=>{await F(q);if(!(await V(q,".title-editor__input .ProseMirror",z.title))?.ok){if(!(await L(q,"textarea#title",z.title))?.ok)throw new J("Failed to fill title")}if(z.author){if(!(await L(q,"input#author",z.author))?.ok)throw new J("Failed to fill author")}if(!(await V(q,".rich_media_content.ProseMirror, .rich_media_content .ProseMirror",z.content))?.ok)throw new J("Failed to fill content");if(z["cover-image"]){await O(q,z["cover-image"]);const R=await W(q)}if(z.summary)await L(q,"textarea#js_description",z.summary);const G=await q.evaluate(`(() => {
128
128
  var t = document.querySelector('.title-editor__input .ProseMirror');
129
129
  var c = document.querySelector('.rich_media_content.ProseMirror, .rich_media_content .ProseMirror');
130
130
  var tt = document.querySelector('textarea#title');
@@ -1 +1 @@
1
- import{AuthRequiredError as d,CommandExecutionError as p,selectorError as h}from"@jackwener/opencli/errors";import{cli as w,Strategy as f}from"@jackwener/opencli/registry";import{buildChatUrl as u,buildExtractChatStateEvaluate as c,buildSendMessageEvaluate as l,requireEvaluateObject as _}from"./im.js";import{normalizeNumericId as n}from"./utils.js";w({site:"xianyu",name:"chat",access:"write",description:"打开闲鱼聊一聊会话,并可选发送消息",domain:"www.goofish.com",strategy:f.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"item_id",required:!0,positional:!0,help:"闲鱼商品 item_id"},{name:"user_id",required:!0,positional:!0,help:"聊一聊对方的 user_id / peerUserId"},{name:"text",help:"Message to send after opening the chat"}],columns:["status","peer_name","item_title","price","location","message"],func:async(t,i)=>{const r=n(i.item_id,"item_id","1038951278192"),a=n(i.user_id,"user_id","3650092411"),s=u(r,a),o=String(i.text||"").trim();await t.goto(s);await t.wait(2);const e=_(await t.evaluate(c()),"chat");if(e?.requiresAuth)throw new d("www.goofish.com","Xianyu chat requires a logged-in browser session");if(!e?.can_input)throw h("闲鱼聊天输入框","未找到可用的聊天输入框,请确认该会话页已正确加载");if(!o)return[{status:"ready",peer_name:e.peer_name||"",item_title:e.item_title||"",price:e.price||"",location:e.location||"",message:(e.visible_messages||[]).slice(-1)[0]||"",peer_user_id:a,item_id:r,url:s,item_url:e.item_url||""}];const m=_(await t.evaluate(l(o)),"chat send");if(!m?.ok)throw new p(`Xianyu chat did not observe the sent message: ${m?.reason||"unknown-reason"}`);await t.wait(1);return[{status:"sent",peer_name:e.peer_name||"",item_title:e.item_title||"",price:e.price||"",location:e.location||"",message:o,peer_user_id:a,item_id:r,url:s,item_url:e.item_url||""}]}});export const __test__={normalizeNumericId:n,buildChatUrl:u,buildExtractChatStateEvaluate:c,buildSendMessageEvaluate:l};
1
+ import{AuthRequiredError as d,CommandExecutionError as p,selectorError as h}from"@jackwener/opencli/errors";import{cli as w,Strategy as f}from"@jackwener/opencli/registry";import{buildChatUrl as u,buildExtractChatStateEvaluate as c,buildSendMessageEvaluate as l,requireEvaluateObject as _}from"./im.js";import{normalizeNumericId as n}from"./utils.js";w({site:"xianyu",name:"chat",access:"write",description:"打开闲鱼聊一聊会话,并可选发送消息",domain:"www.goofish.com",strategy:f.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"item_id",required:!0,positional:!0,help:"闲鱼商品 item_id"},{name:"user_id",required:!0,positional:!0,help:"聊一聊对方的 user_id / peerUserId"},{name:"text",fromFile:!0,help:"Message to send after opening the chat"}],columns:["status","peer_name","item_title","price","location","message"],func:async(t,i)=>{const r=n(i.item_id,"item_id","1038951278192"),a=n(i.user_id,"user_id","3650092411"),s=u(r,a),o=String(i.text||"").trim();await t.goto(s);await t.wait(2);const e=_(await t.evaluate(c()),"chat");if(e?.requiresAuth)throw new d("www.goofish.com","Xianyu chat requires a logged-in browser session");if(!e?.can_input)throw h("闲鱼聊天输入框","未找到可用的聊天输入框,请确认该会话页已正确加载");if(!o)return[{status:"ready",peer_name:e.peer_name||"",item_title:e.item_title||"",price:e.price||"",location:e.location||"",message:(e.visible_messages||[]).slice(-1)[0]||"",peer_user_id:a,item_id:r,url:s,item_url:e.item_url||""}];const m=_(await t.evaluate(l(o)),"chat send");if(!m?.ok)throw new p(`Xianyu chat did not observe the sent message: ${m?.reason||"unknown-reason"}`);await t.wait(1);return[{status:"sent",peer_name:e.peer_name||"",item_title:e.item_title||"",price:e.price||"",location:e.location||"",message:o,peer_user_id:a,item_id:r,url:s,item_url:e.item_url||""}]}});export const __test__={normalizeNumericId:n,buildChatUrl:u,buildExtractChatStateEvaluate:c,buildSendMessageEvaluate:l};
@@ -1 +1 @@
1
- import{ArgumentError as P,CommandExecutionError as j}from"@jackwener/opencli/errors";import{cli as Z,Strategy as $}from"@jackwener/opencli/registry";import{HOME_URL as f,mtopRequest as W}from"./mtop.js";import{normalizeNumericId as A}from"./utils.js";function C(F,B){const G=String(F??"").trim();if(!G)return 1;if(!/^\d+$/.test(G)||Number(G)<1)throw new P(`xianyu ${B} must be a positive integer`);return Number(G)}function H(F){const B=F.cardData||{},G=B.priceInfo||{},J=B.picInfo||{},K=B.detailParams||{};return{item_id:String(B.id||K.itemId||""),title:B.title||K.title||"",price:G.price||K.soldPrice||"",status:B.itemStatus,main_pic:J.picUrl||K.picUrl||"",post_info:K.postInfo||""}}Z({site:"xianyu",name:"mine",access:"read",description:"列出当前账号在售/已发布商品(分页)",domain:"www.goofish.com",strategy:$.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"page",type:"int",default:1,help:"页码,从 1 开始"},{name:"page_size",type:"int",default:20,help:"每页条数"}],columns:["item_id","title","price","status","main_pic"],func:async(F,B)=>{const G=C(B.page,"page"),J=C(B.page_size,"page_size");await F.goto(f);await F.wait(1);const K=await W(F,"item mine",{api:"mtop.idle.web.user.page.head",data:{self:!0},version:"1.0"}),Q=String((K.base||K.baseInfo||{}).kcUserId||"").trim();if(!Q)throw new j("无法解析当前账号 userId,请确认已登录闲鱼");const V=await W(F,"item mine",{api:"mtop.idle.web.xyh.item.list",data:{userId:Q,pageNumber:G,pageSize:J},version:"1.0"}),X=(Array.isArray(V.cardList)?V.cardList:Array.isArray(V.itemList)?V.itemList:[]).filter((Y)=>Y&&typeof Y==="object").map(H);return X.length?X:[{item_id:"",title:"(无在售商品)",price:"",status:"",main_pic:""}]}});Z({site:"xianyu",name:"polish",access:"write",description:"擦亮商品(刷新曝光排序,不改内容)",domain:"www.goofish.com",strategy:$.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"item_id",required:!0,positional:!0,help:"商品 itemId"}],columns:["item_id","ok","already_polished"],func:async(F,B)=>{const G=A(B.item_id,"item_id","1017530128619");await F.goto(f);await F.wait(1);let J;try{J=await W(F,"item polish",{api:"mtop.taobao.idle.item.polish",data:{itemId:G},version:"1.0"})}catch(K){const Q=String(K?.message||K);if(/POLISH_AGAIN|已经擦亮/.test(Q))return[{item_id:G,ok:!0,already_polished:!0,note:"今天已擦亮过,明天再来"}];throw K}return[{item_id:G,ok:!0,already_polished:!1,data:J}]}});Z({site:"xianyu",name:"delete",access:"write",description:"下架/删除商品",domain:"www.goofish.com",strategy:$.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"item_id",required:!0,positional:!0,help:"商品 itemId"}],columns:["item_id","ok"],func:async(F,B)=>{const G=A(B.item_id,"item_id","1017530128619");await F.goto(f);await F.wait(1);const J=await W(F,"item delete",{api:"com.taobao.idle.item.delete",data:{itemId:G},version:"1.1"});return[{item_id:G,ok:!0,data:J}]}});Z({site:"xianyu",name:"edit",access:"write",description:"编辑商品(标题/详情/价格/划线价/库存,按需传)",domain:"www.goofish.com",strategy:$.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"item_id",required:!0,positional:!0,help:"商品 itemId"},{name:"title",help:"新标题"},{name:"desc",help:"新详情描述"},{name:"price",type:"float",help:"新价格(元)"},{name:"original_price",type:"float",help:"新划线价(元),传 0 清除"},{name:"quantity",type:"int",help:"新库存数量"}],columns:["item_id","ok"],func:async(F,B)=>{const G=A(B.item_id,"item_id","1017530128619"),J=(T)=>T!=null&&String(T).trim()!=="";if(![B.title,B.desc,B.price,B.original_price,B.quantity].some(J))throw new P("至少传一个待改字段:--title / --desc / --price / --original-price / --quantity");await F.goto(f);await F.wait(1);const K=await W(F,"item edit",{api:"mtop.idle.pc.idleitem.editDetail",data:{itemId:G},version:"1.0"});if(!K||!Object.keys(K).length)throw new j(`editDetail 返回为空:itemId=${G}(可能已下架 / 不属于当前账号 / 风控)`);const Q={...K,itemId:G};if(J(B.title)||J(B.desc)){const T={...K.itemTextDTO||{}};if(J(B.title))T.title=String(B.title);if(J(B.desc))T.desc=String(B.desc);if(T.titleDescSeparate==null)T.titleDescSeparate=!0;Q.itemTextDTO=T}if(J(B.price)||J(B.original_price)){const T={...K.itemPriceDTO||{}},X=Number(B.price),Y=Number(B.original_price);if(J(B.price)&&X>0)T.priceInCent=String(Math.round(X*100));if(J(B.original_price))if(Y>0)T.origPriceInCent=String(Math.round(Y*100));else delete T.origPriceInCent;Q.itemPriceDTO=T}if(J(B.quantity))Q.quantity=String(Number(B.quantity));if(Q.uniqueCode==null)Q.uniqueCode=String(Date.now()*1000);if(Q.sourceId==null)Q.sourceId="pcMainPublish";if(Q.bizcode==null)Q.bizcode="pcMainPublish";if(Q.publishScene==null)Q.publishScene="pcMainPublish";const V=await W(F,"item edit",{api:"mtop.idle.pc.idleitem.edit",data:Q,version:"1.0"});return[{item_id:G,ok:!0,data:V}]}});export const __test__={normalizeMyItem:H,normalizePage:C};
1
+ import{ArgumentError as P,CommandExecutionError as j}from"@jackwener/opencli/errors";import{cli as Z,Strategy as $}from"@jackwener/opencli/registry";import{HOME_URL as f,mtopRequest as W}from"./mtop.js";import{normalizeNumericId as A}from"./utils.js";function C(F,B){const G=String(F??"").trim();if(!G)return 1;if(!/^\d+$/.test(G)||Number(G)<1)throw new P(`xianyu ${B} must be a positive integer`);return Number(G)}function H(F){const B=F.cardData||{},G=B.priceInfo||{},J=B.picInfo||{},K=B.detailParams||{};return{item_id:String(B.id||K.itemId||""),title:B.title||K.title||"",price:G.price||K.soldPrice||"",status:B.itemStatus,main_pic:J.picUrl||K.picUrl||"",post_info:K.postInfo||""}}Z({site:"xianyu",name:"mine",access:"read",description:"列出当前账号在售/已发布商品(分页)",domain:"www.goofish.com",strategy:$.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"page",type:"int",default:1,help:"页码,从 1 开始"},{name:"page_size",type:"int",default:20,help:"每页条数"}],columns:["item_id","title","price","status","main_pic"],func:async(F,B)=>{const G=C(B.page,"page"),J=C(B.page_size,"page_size");await F.goto(f);await F.wait(1);const K=await W(F,"item mine",{api:"mtop.idle.web.user.page.head",data:{self:!0},version:"1.0"}),Q=String((K.base||K.baseInfo||{}).kcUserId||"").trim();if(!Q)throw new j("无法解析当前账号 userId,请确认已登录闲鱼");const V=await W(F,"item mine",{api:"mtop.idle.web.xyh.item.list",data:{userId:Q,pageNumber:G,pageSize:J},version:"1.0"}),X=(Array.isArray(V.cardList)?V.cardList:Array.isArray(V.itemList)?V.itemList:[]).filter((Y)=>Y&&typeof Y==="object").map(H);return X.length?X:[{item_id:"",title:"(无在售商品)",price:"",status:"",main_pic:""}]}});Z({site:"xianyu",name:"polish",access:"write",description:"擦亮商品(刷新曝光排序,不改内容)",domain:"www.goofish.com",strategy:$.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"item_id",required:!0,positional:!0,help:"商品 itemId"}],columns:["item_id","ok","already_polished"],func:async(F,B)=>{const G=A(B.item_id,"item_id","1017530128619");await F.goto(f);await F.wait(1);let J;try{J=await W(F,"item polish",{api:"mtop.taobao.idle.item.polish",data:{itemId:G},version:"1.0"})}catch(K){const Q=String(K?.message||K);if(/POLISH_AGAIN|已经擦亮/.test(Q))return[{item_id:G,ok:!0,already_polished:!0,note:"今天已擦亮过,明天再来"}];throw K}return[{item_id:G,ok:!0,already_polished:!1,data:J}]}});Z({site:"xianyu",name:"delete",access:"write",description:"下架/删除商品",domain:"www.goofish.com",strategy:$.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"item_id",required:!0,positional:!0,help:"商品 itemId"}],columns:["item_id","ok"],func:async(F,B)=>{const G=A(B.item_id,"item_id","1017530128619");await F.goto(f);await F.wait(1);const J=await W(F,"item delete",{api:"com.taobao.idle.item.delete",data:{itemId:G},version:"1.1"});return[{item_id:G,ok:!0,data:J}]}});Z({site:"xianyu",name:"edit",access:"write",description:"编辑商品(标题/详情/价格/划线价/库存,按需传)",domain:"www.goofish.com",strategy:$.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"item_id",required:!0,positional:!0,help:"商品 itemId"},{name:"title",help:"新标题"},{name:"desc",fromFile:!0,help:"新详情描述"},{name:"price",type:"float",help:"新价格(元)"},{name:"original_price",type:"float",help:"新划线价(元),传 0 清除"},{name:"quantity",type:"int",help:"新库存数量"}],columns:["item_id","ok"],func:async(F,B)=>{const G=A(B.item_id,"item_id","1017530128619"),J=(T)=>T!=null&&String(T).trim()!=="";if(![B.title,B.desc,B.price,B.original_price,B.quantity].some(J))throw new P("至少传一个待改字段:--title / --desc / --price / --original-price / --quantity");await F.goto(f);await F.wait(1);const K=await W(F,"item edit",{api:"mtop.idle.pc.idleitem.editDetail",data:{itemId:G},version:"1.0"});if(!K||!Object.keys(K).length)throw new j(`editDetail 返回为空:itemId=${G}(可能已下架 / 不属于当前账号 / 风控)`);const Q={...K,itemId:G};if(J(B.title)||J(B.desc)){const T={...K.itemTextDTO||{}};if(J(B.title))T.title=String(B.title);if(J(B.desc))T.desc=String(B.desc);if(T.titleDescSeparate==null)T.titleDescSeparate=!0;Q.itemTextDTO=T}if(J(B.price)||J(B.original_price)){const T={...K.itemPriceDTO||{}},X=Number(B.price),Y=Number(B.original_price);if(J(B.price)&&X>0)T.priceInCent=String(Math.round(X*100));if(J(B.original_price))if(Y>0)T.origPriceInCent=String(Math.round(Y*100));else delete T.origPriceInCent;Q.itemPriceDTO=T}if(J(B.quantity))Q.quantity=String(Number(B.quantity));if(Q.uniqueCode==null)Q.uniqueCode=String(Date.now()*1000);if(Q.sourceId==null)Q.sourceId="pcMainPublish";if(Q.bizcode==null)Q.bizcode="pcMainPublish";if(Q.publishScene==null)Q.publishScene="pcMainPublish";const V=await W(F,"item edit",{api:"mtop.idle.pc.idleitem.edit",data:Q,version:"1.0"});return[{item_id:G,ok:!0,data:V}]}});export const __test__={normalizeMyItem:H,normalizePage:C};
@@ -1 +1 @@
1
- import{ArgumentError as M}from"@jackwener/opencli/errors";import{cli as $,Strategy as C}from"@jackwener/opencli/registry";import{HOME_URL as h,mtopRequest as f}from"./mtop.js";import{normalizeNumericId as Y}from"./utils.js";const j=["ALL","WAITING_PAY","WAITING_SEND","WAITING_RECEIVE","WAITING_RATE","SUCCESS","REFUND"];function q(x){const B=String(x||"ALL").trim().toUpperCase();if(!j.includes(B))throw new M(`xianyu order --status must be one of: ${j.join(", ")}`);return B}function L(x,B){const F=String(x??"").trim();if(!F)return 1;if(!/^\d+$/.test(F)||Number(F)<1)throw new M(`xianyu order ${B} must be a positive integer`);return Number(F)}$({site:"xianyu",name:"order",access:"read",description:"列出卖家已售订单(可按状态筛选)",domain:"www.goofish.com",strategy:C.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"page",type:"int",default:1,help:"页码,从 1 开始"},{name:"page_size",type:"int",default:20,help:"每页条数"},{name:"status",default:"ALL",help:`订单状态筛选:${j.join(" / ")}`}],columns:["order_id","item_id","buyer_nick","status_str","total_price","create_time"],func:async(x,B)=>{const F=L(B.page,"page"),Q=L(B.page_size,"page_size"),J=q(B.status);await x.goto(h);await x.wait(1);const X=(await f(x,"order list",{api:"mtop.taobao.idle.trade.merchant.sold.get",data:{pageNumber:F,rowsPerPage:Q,orderIds:"",queryCode:J,orderSearchParam:"{}"},version:"1.0"})).module||{},W=(Array.isArray(X.items)?X.items:[]).map((K)=>{const G=K.order||K.orderInfo||K,Z=K.buyer||K.buyerInfo||{},H=K.item||K.itemInfo||{};return{order_id:String(G.orderId||G.bizOrderId||G.id||""),item_id:String(H.itemId||H.id||""),item_title:H.title||"",buyer_id:String(Z.userId||Z.buyerId||""),buyer_nick:Z.nick||Z.userNick||"",status:G.status??G.orderStatus??"",status_str:G.statusStr||G.orderStatusStr||"",total_price:String(G.totalPrice||G.actualPrice||""),create_time:String(G.createTime||G.gmtCreate||"")}});return W.length?W:[{order_id:"",status_str:"(无已售订单)",total_price:"",buyer_nick:"",item_id:"",create_time:""}]}});$({site:"xianyu",name:"order-get",access:"read",description:"查询单个闲鱼订单详情",domain:"www.goofish.com",strategy:C.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"order_id",required:!0,positional:!0,help:"订单号 orderId"},{name:"role",default:"seller",help:"视角:seller / buyer"}],columns:["order_id","item_id","status_str","total_price","buyer_nick"],func:async(x,B)=>{const F=Y(B.order_id,"order_id","2800000000000000000"),Q=String(B.role||"seller").trim()==="buyer"?"buyer":"seller";await x.goto(h);await x.wait(1);const J=await f(x,"order get",{api:"mtop.idle.web.trade.order.detail",data:{orderId:F,role:Q},version:"1.0"}),V=J.itemInfoDTO||{},X=J.buyerInfoDTO||{},D=J.sellerInfoDTO||{},W=J.priceInfoDTO||{},K=J.logisticsInfoDTO||{},G=J.statusInfoDTO||{};return[{order_id:F,role:Q,item_id:String(V.itemId||""),title:V.title||"",main_pic:V.picUrl||"",quantity:Number(V.quantity||1),buyer_id:String(X.buyerId||""),buyer_nick:X.userNick||"",seller_id:String(D.sellerId||""),seller_nick:D.userNick||"",total_price:String(W.totalPrice||""),auction_price:String(W.auctionPrice||""),post_fee:String(W.postFee||""),status:G.orderStatus,status_str:G.orderStatusStr||"",logistic_no:K.logisticsNo||"",logistic_company:K.logisticsCompany||""}]}});$({site:"xianyu",name:"order-ship-dummy",access:"write",description:"虚拟发货(仅虚拟商品,无需物流单号)",domain:"www.goofish.com",strategy:C.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"order_id",required:!0,positional:!0,help:"订单号 orderId"},{name:"text",default:"",help:"发货附言(可选)"}],columns:["order_id","ok"],func:async(x,B)=>{const F=Y(B.order_id,"order_id","2800000000000000000");await x.goto(h);await x.wait(1);const Q=await f(x,"order ship-dummy",{api:"mtop.taobao.idle.logistic.consign.dummy",data:{orderId:F,tradeText:String(B.text||""),picList:[],newUnconsign:!0},version:"1.0"});return[{order_id:F,ok:!0,data:Q}]}});$({site:"xianyu",name:"order-ship-freeshipping",access:"write",description:"包邮发货(拼团活动专用)",domain:"www.goofish.com",strategy:C.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"order_id",required:!0,positional:!0,help:"订单号 bizOrderId"},{name:"item_id",required:!0,help:"商品 itemId"},{name:"buyer_id",required:!0,help:"买家 userId"}],columns:["order_id","ok"],func:async(x,B)=>{const F=Y(B.order_id,"order_id","2800000000000000000"),Q=Y(B.item_id,"item_id","1017530128619"),J=Y(B.buyer_id,"buyer_id","2208502750464");await x.goto(h);await x.wait(1);const V=await f(x,"order ship-freeshipping",{api:"mtop.idle.groupon.activity.seller.freeshipping",data:{bizOrderId:F,itemId:Number(Q),buyerId:Number(J)},version:"1.0"});return[{order_id:F,ok:!0,data:V}]}});export const __test__={normalizeStatus:q,normalizePage:L,ORDER_STATUS:j};
1
+ import{ArgumentError as M}from"@jackwener/opencli/errors";import{cli as $,Strategy as C}from"@jackwener/opencli/registry";import{HOME_URL as h,mtopRequest as f}from"./mtop.js";import{normalizeNumericId as Y}from"./utils.js";const j=["ALL","WAITING_PAY","WAITING_SEND","WAITING_RECEIVE","WAITING_RATE","SUCCESS","REFUND"];function q(x){const B=String(x||"ALL").trim().toUpperCase();if(!j.includes(B))throw new M(`xianyu order --status must be one of: ${j.join(", ")}`);return B}function L(x,B){const F=String(x??"").trim();if(!F)return 1;if(!/^\d+$/.test(F)||Number(F)<1)throw new M(`xianyu order ${B} must be a positive integer`);return Number(F)}$({site:"xianyu",name:"order",access:"read",description:"列出卖家已售订单(可按状态筛选)",domain:"www.goofish.com",strategy:C.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"page",type:"int",default:1,help:"页码,从 1 开始"},{name:"page_size",type:"int",default:20,help:"每页条数"},{name:"status",default:"ALL",help:`订单状态筛选:${j.join(" / ")}`}],columns:["order_id","item_id","buyer_nick","status_str","total_price","create_time"],func:async(x,B)=>{const F=L(B.page,"page"),Q=L(B.page_size,"page_size"),J=q(B.status);await x.goto(h);await x.wait(1);const X=(await f(x,"order list",{api:"mtop.taobao.idle.trade.merchant.sold.get",data:{pageNumber:F,rowsPerPage:Q,orderIds:"",queryCode:J,orderSearchParam:"{}"},version:"1.0"})).module||{},W=(Array.isArray(X.items)?X.items:[]).map((K)=>{const G=K.order||K.orderInfo||K,Z=K.buyer||K.buyerInfo||{},H=K.item||K.itemInfo||{};return{order_id:String(G.orderId||G.bizOrderId||G.id||""),item_id:String(H.itemId||H.id||""),item_title:H.title||"",buyer_id:String(Z.userId||Z.buyerId||""),buyer_nick:Z.nick||Z.userNick||"",status:G.status??G.orderStatus??"",status_str:G.statusStr||G.orderStatusStr||"",total_price:String(G.totalPrice||G.actualPrice||""),create_time:String(G.createTime||G.gmtCreate||"")}});return W.length?W:[{order_id:"",status_str:"(无已售订单)",total_price:"",buyer_nick:"",item_id:"",create_time:""}]}});$({site:"xianyu",name:"order-get",access:"read",description:"查询单个闲鱼订单详情",domain:"www.goofish.com",strategy:C.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"order_id",required:!0,positional:!0,help:"订单号 orderId"},{name:"role",default:"seller",help:"视角:seller / buyer"}],columns:["order_id","item_id","status_str","total_price","buyer_nick"],func:async(x,B)=>{const F=Y(B.order_id,"order_id","2800000000000000000"),Q=String(B.role||"seller").trim()==="buyer"?"buyer":"seller";await x.goto(h);await x.wait(1);const J=await f(x,"order get",{api:"mtop.idle.web.trade.order.detail",data:{orderId:F,role:Q},version:"1.0"}),V=J.itemInfoDTO||{},X=J.buyerInfoDTO||{},D=J.sellerInfoDTO||{},W=J.priceInfoDTO||{},K=J.logisticsInfoDTO||{},G=J.statusInfoDTO||{};return[{order_id:F,role:Q,item_id:String(V.itemId||""),title:V.title||"",main_pic:V.picUrl||"",quantity:Number(V.quantity||1),buyer_id:String(X.buyerId||""),buyer_nick:X.userNick||"",seller_id:String(D.sellerId||""),seller_nick:D.userNick||"",total_price:String(W.totalPrice||""),auction_price:String(W.auctionPrice||""),post_fee:String(W.postFee||""),status:G.orderStatus,status_str:G.orderStatusStr||"",logistic_no:K.logisticsNo||"",logistic_company:K.logisticsCompany||""}]}});$({site:"xianyu",name:"order-ship-dummy",access:"write",description:"虚拟发货(仅虚拟商品,无需物流单号)",domain:"www.goofish.com",strategy:C.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"order_id",required:!0,positional:!0,help:"订单号 orderId"},{name:"text",fromFile:!0,default:"",help:"发货附言(可选)"}],columns:["order_id","ok"],func:async(x,B)=>{const F=Y(B.order_id,"order_id","2800000000000000000");await x.goto(h);await x.wait(1);const Q=await f(x,"order ship-dummy",{api:"mtop.taobao.idle.logistic.consign.dummy",data:{orderId:F,tradeText:String(B.text||""),picList:[],newUnconsign:!0},version:"1.0"});return[{order_id:F,ok:!0,data:Q}]}});$({site:"xianyu",name:"order-ship-freeshipping",access:"write",description:"包邮发货(拼团活动专用)",domain:"www.goofish.com",strategy:C.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"order_id",required:!0,positional:!0,help:"订单号 bizOrderId"},{name:"item_id",required:!0,help:"商品 itemId"},{name:"buyer_id",required:!0,help:"买家 userId"}],columns:["order_id","ok"],func:async(x,B)=>{const F=Y(B.order_id,"order_id","2800000000000000000"),Q=Y(B.item_id,"item_id","1017530128619"),J=Y(B.buyer_id,"buyer_id","2208502750464");await x.goto(h);await x.wait(1);const V=await f(x,"order ship-freeshipping",{api:"mtop.idle.groupon.activity.seller.freeshipping",data:{bizOrderId:F,itemId:Number(Q),buyerId:Number(J)},version:"1.0"});return[{order_id:F,ok:!0,data:V}]}});export const __test__={normalizeStatus:q,normalizePage:L,ORDER_STATUS:j};
@@ -242,4 +242,4 @@ import*as z from"node:fs";import*as C from"node:path";import{ArgumentError as L,
242
242
  bodySnippet: bodyText.slice(0, 500),
243
243
  };
244
244
  })()
245
- `}export const publishCommand=S({site:"xianyu",name:"publish",access:"write",description:"发布闲鱼宝贝(需先在浏览器中登录闲鱼)",domain:"www.goofish.com",strategy:F.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"title",required:!0,positional:!0,help:"商品标题"},{name:"description",required:!0,positional:!0,help:"商品描述/详情"},{name:"price",required:!0,positional:!0,type:"float",help:"出售价格(元)"},{name:"condition",required:!0,positional:!0,help:"成色:全新 / 几乎全新 / 轻微使用 / 明显使用 / 老旧"},{name:"category",required:!0,positional:!0,help:"商品分类关键词(如:手机、衣服、图书)"},{name:"original_price",type:"float",help:"原价(选填,用于显示折扣)"},{name:"location",help:"所在地区(选填,如:杭州)"},{name:"images",help:"本地图片路径,多张用逗号分隔(选填,如:/tmp/a.jpg,/tmp/b.jpg)"}],columns:["status","item_id","title","price","condition","url","message"],func:async(j,B)=>{const q=P(B);await j.goto(X());await j.wait(3);let G=await j.evaluate(h());for(let A=0;A<30;A++){if(G?.requiresAuth||G?.hasPublishForm)break;await j.wait(0.5);G=await j.evaluate(h())}if(G?.requiresAuth)throw new I("www.goofish.com","发布闲鱼需要先登录,请在 Chrome 中打开 goofish.com 并完成登录");if(!G?.hasPublishForm)throw new M("Xianyu publish form was not detected","Confirm goofish.com is logged in and the publish page finished loading.");let J=await j.evaluate(_(q.category));for(let A=0;A<30;A++){if(J?.ok||J?.reason!=="category-trigger-not-found")break;await j.wait(0.5);J=await j.evaluate(_(q.category))}if(!J?.ok)throw new M(`Xianyu category selection failed: ${J?.reason||"unknown-reason"}`);await j.wait(1.5);let K=await j.evaluate(Z(q));for(let A=0;A<30;A++){if(K?.ok)break;await j.wait(0.5);K=await j.evaluate(Z(q))}if(!K?.ok){const A=Array.isArray(K?.missing)?K.missing.join(", "):"unknown";throw new M(`Xianyu publish form fill failed; missing fields: ${A}`)}await j.wait(1);if(q.images.length>0){let A=await j.evaluate($());for(let H=0;H<30;H++){if(A?.ok)break;await j.wait(0.5);A=await j.evaluate($())}if(!A?.ok)throw new M(`Xianyu image upload input was not found: ${A?.reason||"unknown-reason"}`);try{await f(j,A.selector||'input[type="file"]',q.images);await j.wait(3)}catch(H){throw new M(`Xianyu image upload failed: ${H?.message||H}`)}}let Q=await j.evaluate(E());for(let A=0;A<30;A++){if(Q?.ok)break;await j.wait(0.5);Q=await j.evaluate(E())}if(!Q?.ok)throw new M(`Xianyu publish submit failed: ${Q?.reason||"unknown-reason"}`);await j.wait(2);let D="",T=await Y(j),N="";for(let A=0;A<10;A++){await j.wait(1.5);const H=await j.evaluate(U());T=await Y(j);if(H?.status==="published"){D=String(H.item_id||"").replace(/\D/g,"");return[{status:"published",item_id:D,title:q.title.slice(0,50),price:`¥${q.price}`,condition:q.condition,url:H.url||T,message:"发布成功"}]}if(H?.status==="failed"){N=H.message||"发布失败";break}}throw new M(N||"Xianyu publish result was not confirmed before timeout",`Open ${T} and verify whether the listing was published.`)}}),__test__={CONDITION_CHOICES:W,MAX_IMAGES:V,validateImagePaths:y,normalizePublishArgs:P,buildPublishUrl:X,getCurrentPageUrl:Y,buildFillFormEvaluate:Z,buildSelectCategoryEvaluate:_,buildFindFileInputSelectorEvaluate:$,buildDetectSuccessEvaluate:U};
245
+ `}export const publishCommand=S({site:"xianyu",name:"publish",access:"write",description:"发布闲鱼宝贝(需先在浏览器中登录闲鱼)",domain:"www.goofish.com",strategy:F.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"title",required:!0,positional:!0,help:"商品标题"},{name:"description",fromFile:!0,required:!0,positional:!0,help:"商品描述/详情"},{name:"price",required:!0,positional:!0,type:"float",help:"出售价格(元)"},{name:"condition",required:!0,positional:!0,help:"成色:全新 / 几乎全新 / 轻微使用 / 明显使用 / 老旧"},{name:"category",required:!0,positional:!0,help:"商品分类关键词(如:手机、衣服、图书)"},{name:"original_price",type:"float",help:"原价(选填,用于显示折扣)"},{name:"location",help:"所在地区(选填,如:杭州)"},{name:"images",help:"本地图片路径,多张用逗号分隔(选填,如:/tmp/a.jpg,/tmp/b.jpg)"}],columns:["status","item_id","title","price","condition","url","message"],func:async(j,B)=>{const q=P(B);await j.goto(X());await j.wait(3);let G=await j.evaluate(h());for(let A=0;A<30;A++){if(G?.requiresAuth||G?.hasPublishForm)break;await j.wait(0.5);G=await j.evaluate(h())}if(G?.requiresAuth)throw new I("www.goofish.com","发布闲鱼需要先登录,请在 Chrome 中打开 goofish.com 并完成登录");if(!G?.hasPublishForm)throw new M("Xianyu publish form was not detected","Confirm goofish.com is logged in and the publish page finished loading.");let J=await j.evaluate(_(q.category));for(let A=0;A<30;A++){if(J?.ok||J?.reason!=="category-trigger-not-found")break;await j.wait(0.5);J=await j.evaluate(_(q.category))}if(!J?.ok)throw new M(`Xianyu category selection failed: ${J?.reason||"unknown-reason"}`);await j.wait(1.5);let K=await j.evaluate(Z(q));for(let A=0;A<30;A++){if(K?.ok)break;await j.wait(0.5);K=await j.evaluate(Z(q))}if(!K?.ok){const A=Array.isArray(K?.missing)?K.missing.join(", "):"unknown";throw new M(`Xianyu publish form fill failed; missing fields: ${A}`)}await j.wait(1);if(q.images.length>0){let A=await j.evaluate($());for(let H=0;H<30;H++){if(A?.ok)break;await j.wait(0.5);A=await j.evaluate($())}if(!A?.ok)throw new M(`Xianyu image upload input was not found: ${A?.reason||"unknown-reason"}`);try{await f(j,A.selector||'input[type="file"]',q.images);await j.wait(3)}catch(H){throw new M(`Xianyu image upload failed: ${H?.message||H}`)}}let Q=await j.evaluate(E());for(let A=0;A<30;A++){if(Q?.ok)break;await j.wait(0.5);Q=await j.evaluate(E())}if(!Q?.ok)throw new M(`Xianyu publish submit failed: ${Q?.reason||"unknown-reason"}`);await j.wait(2);let D="",T=await Y(j),N="";for(let A=0;A<10;A++){await j.wait(1.5);const H=await j.evaluate(U());T=await Y(j);if(H?.status==="published"){D=String(H.item_id||"").replace(/\D/g,"");return[{status:"published",item_id:D,title:q.title.slice(0,50),price:`¥${q.price}`,condition:q.condition,url:H.url||T,message:"发布成功"}]}if(H?.status==="failed"){N=H.message||"发布失败";break}}throw new M(N||"Xianyu publish result was not confirmed before timeout",`Open ${T} and verify whether the listing was published.`)}}),__test__={CONDITION_CHOICES:W,MAX_IMAGES:V,validateImagePaths:y,normalizePublishArgs:P,buildPublishUrl:X,getCurrentPageUrl:Y,buildFillFormEvaluate:Z,buildSelectCategoryEvaluate:_,buildFindFileInputSelectorEvaluate:$,buildDetectSuccessEvaluate:U};
@@ -1 +1 @@
1
- import{ArgumentError as J,AuthRequiredError as X,CommandExecutionError as Y,selectorError as Z}from"@jackwener/opencli/errors";import{cli as _,Strategy as $}from"@jackwener/opencli/registry";import{buildChatUrl as Q,buildClickInboxConversationEvaluate as q,buildExtractChatStateEvaluate as y,buildSendMessageEvaluate as T,normalizeRank as z,requireClickResult as M,requireEvaluateObject as V,requireText as R}from"./im.js";import{normalizeNumericId as W}from"./utils.js";_({site:"xianyu",name:"reply",access:"write",description:"回复指定闲鱼私信会话",domain:"www.goofish.com",strategy:$.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"item_id",positional:!0,help:"闲鱼商品 item_id"},{name:"user_id",positional:!0,help:"聊一聊对方的 user_id / peerUserId"},{name:"text",required:!0,help:"Message text to send"},{name:"rank",type:"int",default:0,help:"Conversation rank from xianyu inbox; clicks the visible row instead of requiring IDs"}],columns:["status","peer_name","item_title","price","location","message"],func:async(f,j)=>{const F=j.item_id!=null&&j.item_id!=="",G=j.user_id!=null&&j.user_id!=="",D=z(j.rank);if(D>0&&(F||G))throw new J("xianyu reply accepts either item_id/user_id or --rank, not both");if(D===0&&F!==G)throw new J("xianyu reply requires both item_id and user_id, or --rank from xianyu inbox");if(D===0&&!F&&!G)throw new J("xianyu reply requires item_id/user_id or --rank from xianyu inbox");const H=F&&G,K=H?W(j.item_id,"item_id","1038951278192"):"",L=H?W(j.user_id,"user_id","3650092411"):"",N=R(j.text,"xianyu reply --text"),O=H?Q(K,L):"";if(H)await f.goto(O);else if(!f.getCurrentUrl||!/https:\/\/www\.goofish\.com\/im\b/.test(await f.getCurrentUrl()))await f.goto("https://www.goofish.com/im");await f.wait(2);if(D>0){M(await f.evaluate(q(D-1)),"reply rank click");await f.wait(2)}const B=V(await f.evaluate(y()),"reply");if(B?.requiresAuth)throw new X("www.goofish.com","Xianyu reply requires a logged-in browser session");if(!B?.can_input)throw Z("闲鱼聊天输入框","未找到可用的聊天输入框,请确认该会话页已正确加载");const P=V(await f.evaluate(T(N)),"reply send");if(!P?.ok)throw new Y(`Xianyu reply did not observe the sent message: ${P?.reason||"unknown-reason"}`);await f.wait(1);return[{status:"sent",peer_name:B.peer_name||"",item_title:B.item_title||"",price:B.price||"",location:B.location||"",message:N,peer_user_id:L,item_id:K,url:O||"",item_url:B.item_url||""}]}});export const __test__={buildChatUrl:Q,buildSendMessageEvaluate:T};
1
+ import{ArgumentError as J,AuthRequiredError as X,CommandExecutionError as Y,selectorError as Z}from"@jackwener/opencli/errors";import{cli as _,Strategy as $}from"@jackwener/opencli/registry";import{buildChatUrl as Q,buildClickInboxConversationEvaluate as q,buildExtractChatStateEvaluate as y,buildSendMessageEvaluate as T,normalizeRank as z,requireClickResult as M,requireEvaluateObject as V,requireText as R}from"./im.js";import{normalizeNumericId as W}from"./utils.js";_({site:"xianyu",name:"reply",access:"write",description:"回复指定闲鱼私信会话",domain:"www.goofish.com",strategy:$.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"item_id",positional:!0,help:"闲鱼商品 item_id"},{name:"user_id",positional:!0,help:"聊一聊对方的 user_id / peerUserId"},{name:"text",fromFile:!0,required:!0,help:"Message text to send"},{name:"rank",type:"int",default:0,help:"Conversation rank from xianyu inbox; clicks the visible row instead of requiring IDs"}],columns:["status","peer_name","item_title","price","location","message"],func:async(f,j)=>{const F=j.item_id!=null&&j.item_id!=="",G=j.user_id!=null&&j.user_id!=="",D=z(j.rank);if(D>0&&(F||G))throw new J("xianyu reply accepts either item_id/user_id or --rank, not both");if(D===0&&F!==G)throw new J("xianyu reply requires both item_id and user_id, or --rank from xianyu inbox");if(D===0&&!F&&!G)throw new J("xianyu reply requires item_id/user_id or --rank from xianyu inbox");const H=F&&G,K=H?W(j.item_id,"item_id","1038951278192"):"",L=H?W(j.user_id,"user_id","3650092411"):"",N=R(j.text,"xianyu reply --text"),O=H?Q(K,L):"";if(H)await f.goto(O);else if(!f.getCurrentUrl||!/https:\/\/www\.goofish\.com\/im\b/.test(await f.getCurrentUrl()))await f.goto("https://www.goofish.com/im");await f.wait(2);if(D>0){M(await f.evaluate(q(D-1)),"reply rank click");await f.wait(2)}const B=V(await f.evaluate(y()),"reply");if(B?.requiresAuth)throw new X("www.goofish.com","Xianyu reply requires a logged-in browser session");if(!B?.can_input)throw Z("闲鱼聊天输入框","未找到可用的聊天输入框,请确认该会话页已正确加载");const P=V(await f.evaluate(T(N)),"reply send");if(!P?.ok)throw new Y(`Xianyu reply did not observe the sent message: ${P?.reason||"unknown-reason"}`);await f.wait(1);return[{status:"sent",peer_name:B.peer_name||"",item_title:B.item_title||"",price:B.price||"",location:B.location||"",message:N,peer_user_id:L,item_id:K,url:O||"",item_url:B.item_url||""}]}});export const __test__={buildChatUrl:Q,buildSendMessageEvaluate:T};
@@ -1 +1 @@
1
- import*as u from"node:path";import{cli as p,Strategy as m}from"@jackwener/opencli/registry";import{CliError as d}from"@jackwener/opencli/errors";import{log as c}from"@jackwener/opencli/logger";import{YOLLOMI_DOMAIN as f,yollomiPost as g,downloadOutput as y,fmtBytes as O}from"./utils.js";p({site:"yollomi",name:"background",access:"write",description:"Generate AI background for a product/object image (5 credits)",domain:f,strategy:m.COOKIE,args:[{name:"image",positional:!0,required:!0,help:'Image URL (upload via "ppcli yollomi upload" first)'},{name:"prompt",default:"",help:"Background description (optional)"},{name:"output",default:"./yollomi-output",help:"Output directory"},{name:"no-download",type:"boolean",default:!1,help:"Only show URL"}],columns:["status","file","size","url"],func:async(a,e)=>{const{image:i,prompt:r}=e;c.status("Generating background...");const o=await g(a,"/api/ai/ai-background-generator",{images:[i],prompt:r||void 0,aspect_ratio:"1:1"}),t=o.image||o.images?.[0];if(!t)throw new d("EMPTY_RESPONSE","No result","Try a different image");if(e["no-download"])return[{status:"generated",file:"-",size:"-",url:t}];try{const n=`yollomi_bg_${Date.now()}.png`,{path:l,size:s}=await y(t,e.output,n);return[{status:"saved",file:u.relative(".",l),size:O(s),url:t}]}catch{return[{status:"download-failed",file:"-",size:"-",url:t}]}}});
1
+ import*as s from"node:path";import{cli as m,Strategy as p}from"@jackwener/opencli/registry";import{CliError as d}from"@jackwener/opencli/errors";import{log as c}from"@jackwener/opencli/logger";import{YOLLOMI_DOMAIN as f,yollomiPost as g,downloadOutput as y,fmtBytes as O}from"./utils.js";m({site:"yollomi",name:"background",access:"write",description:"Generate AI background for a product/object image (5 credits)",domain:f,strategy:p.COOKIE,args:[{name:"image",positional:!0,required:!0,help:'Image URL (upload via "ppcli yollomi upload" first)'},{name:"prompt",fromFile:!0,default:"",help:"Background description (optional)"},{name:"output",default:"./yollomi-output",help:"Output directory"},{name:"no-download",type:"boolean",default:!1,help:"Only show URL"}],columns:["status","file","size","url"],func:async(a,e)=>{const{image:i,prompt:r}=e;c.status("Generating background...");const o=await g(a,"/api/ai/ai-background-generator",{images:[i],prompt:r||void 0,aspect_ratio:"1:1"}),t=o.image||o.images?.[0];if(!t)throw new d("EMPTY_RESPONSE","No result","Try a different image");if(e["no-download"])return[{status:"generated",file:"-",size:"-",url:t}];try{const n=`yollomi_bg_${Date.now()}.png`,{path:l,size:u}=await y(t,e.output,n);return[{status:"saved",file:s.relative(".",l),size:O(u),url:t}]}catch{return[{status:"download-failed",file:"-",size:"-",url:t}]}}});
@@ -1 +1 @@
1
- import*as f from"node:path";import{cli as w,Strategy as h}from"@jackwener/opencli/registry";import{CliError as y}from"@jackwener/opencli/errors";import{log as l}from"@jackwener/opencli/logger";import{YOLLOMI_DOMAIN as q,yollomiPost as O,downloadOutput as E,fmtBytes as I}from"./utils.js";w({site:"yollomi",name:"edit",access:"write",description:"Edit images with AI text prompts (Qwen image edit)",domain:q,strategy:h.COOKIE,args:[{name:"image",positional:!0,required:!0,help:'Input image URL (upload via "ppcli yollomi upload" first)'},{name:"prompt",positional:!0,required:!0,help:'Editing instruction (e.g. "Make it look vintage")'},{name:"model",default:"qwen-image-edit",choices:["qwen-image-edit","qwen-image-edit-plus"],help:"Edit model"},{name:"output",default:"./yollomi-output",help:"Output directory"},{name:"no-download",type:"boolean",default:!1,help:"Only show URL"}],columns:["status","file","size","credits","url"],func:async(d,e)=>{const{image:s,prompt:m,model:a}=e;let n;if(a==="qwen-image-edit-plus")n={prompt:m,images:[s]};else n={image:s,prompt:m,go_fast:!0,output_format:"png"};const p=a==="qwen-image-edit-plus"?"/api/ai/qwen-image-edit-plus":"/api/ai/qwen-image-edit";l.status(`Editing with ${a}...`);const i=await O(d,p,n),r=i.images||(i.image?[i.image]:[]);if(!r.length)throw new y("EMPTY_RESPONSE","No result","Try a different prompt");const t=i.remainingCredits,o=r[0];if(e["no-download"])return[{status:"edited",file:"-",size:"-",credits:t??"-",url:o}];try{const u=`yollomi_edit_${Date.now()}.png`,{path:g,size:c}=await E(o,e.output,u);if(t!==void 0)l.status(`Credits remaining: ${t}`);return[{status:"saved",file:f.relative(".",g),size:I(c),credits:t??"-",url:o}]}catch{return[{status:"download-failed",file:"-",size:"-",credits:t??"-",url:o}]}}});
1
+ import*as f from"node:path";import{cli as w,Strategy as h}from"@jackwener/opencli/registry";import{CliError as y}from"@jackwener/opencli/errors";import{log as l}from"@jackwener/opencli/logger";import{YOLLOMI_DOMAIN as q,yollomiPost as O,downloadOutput as E,fmtBytes as I}from"./utils.js";w({site:"yollomi",name:"edit",access:"write",description:"Edit images with AI text prompts (Qwen image edit)",domain:q,strategy:h.COOKIE,args:[{name:"image",positional:!0,required:!0,help:'Input image URL (upload via "ppcli yollomi upload" first)'},{name:"prompt",fromFile:!0,positional:!0,required:!0,help:'Editing instruction (e.g. "Make it look vintage")'},{name:"model",default:"qwen-image-edit",choices:["qwen-image-edit","qwen-image-edit-plus"],help:"Edit model"},{name:"output",default:"./yollomi-output",help:"Output directory"},{name:"no-download",type:"boolean",default:!1,help:"Only show URL"}],columns:["status","file","size","credits","url"],func:async(d,e)=>{const{image:s,prompt:r,model:a}=e;let n;if(a==="qwen-image-edit-plus")n={prompt:r,images:[s]};else n={image:s,prompt:r,go_fast:!0,output_format:"png"};const p=a==="qwen-image-edit-plus"?"/api/ai/qwen-image-edit-plus":"/api/ai/qwen-image-edit";l.status(`Editing with ${a}...`);const i=await O(d,p,n),m=i.images||(i.image?[i.image]:[]);if(!m.length)throw new y("EMPTY_RESPONSE","No result","Try a different prompt");const t=i.remainingCredits,o=m[0];if(e["no-download"])return[{status:"edited",file:"-",size:"-",credits:t??"-",url:o}];try{const u=`yollomi_edit_${Date.now()}.png`,{path:g,size:c}=await E(o,e.output,u);if(t!==void 0)l.status(`Credits remaining: ${t}`);return[{status:"saved",file:f.relative(".",g),size:I(c),credits:t??"-",url:o}]}catch{return[{status:"download-failed",file:"-",size:"-",credits:t??"-",url:o}]}}});
@@ -1 +1 @@
1
- import*as w from"node:path";import{cli as b,Strategy as I}from"@jackwener/opencli/registry";import{CliError as c}from"@jackwener/opencli/errors";import{log as f}from"@jackwener/opencli/logger";import{YOLLOMI_DOMAIN as U,yollomiPost as z,downloadOutput as D,fmtBytes as E,MODEL_ROUTES as L}from"./utils.js";function P(p){const e={"1:1":[1024,1024],"16:9":[1344,768],"9:16":[768,1344],"4:3":[1152,896],"3:4":[896,1152]},[n,t]=e[p]||[1024,1024];return{width:n,height:t}}b({site:"yollomi",name:"generate",access:"write",description:"Generate images with AI (text-to-image or image-to-image)",domain:U,strategy:I.COOKIE,args:[{name:"prompt",positional:!0,required:!0,help:"Text prompt describing the image"},{name:"model",default:"z-image-turbo",help:"Model ID (z-image-turbo, flux-schnell, nano-banana, flux-2-pro, ...)"},{name:"ratio",default:"1:1",choices:["1:1","16:9","9:16","4:3","3:4"],help:"Aspect ratio"},{name:"image",help:'Input image URL for image-to-image (upload via "ppcli yollomi upload" first)'},{name:"output",default:"./yollomi-output",help:"Output directory"},{name:"no-download",type:"boolean",default:!1,help:"Only show URLs, skip download"}],columns:["index","status","file","size","url"],func:async(p,e)=>{const{prompt:n,model:t,ratio:l}=e,d=L[t];if(!d)throw new c("INVALID_MODEL",`Unknown model: ${t}`,'Run "ppcli yollomi models --type image" to see available models');let i;if(t==="z-image-turbo"){const{width:o,height:a}=P(l);i={prompt:n,width:o,height:a,output_format:"jpg",output_quality:85,guidance_scale:0,num_inference_steps:8}}else if(t==="flux-2-pro"){i={prompt:n,aspectRatio:l,outputNumber:1};if(e.image)i.imageUrl=e.image}else if(t==="flux-kontext-pro"){i={prompt:n,output_format:"jpg"};if(e.image)i.imageUrl=e.image;if(l!=="1:1")i.aspect_ratio=l}else{i={prompt:n,aspect_ratio:l};if(e.image)i.imageUrl=e.image}f.status(`Generating with ${t}...`);const r=await z(p,d,i),u=r.images||(r.image?[r.image]:[]);if(!u.length)throw new c("EMPTY_RESPONSE","No images returned","Try a different prompt or model");const g=e["no-download"],h=e.output,m=[];for(let o=0;o<u.length;o++){const a=u[o];if(g){m.push({index:o+1,status:"generated",file:"-",size:"-",url:a});continue}try{const s=(()=>{try{return new URL(a).pathname}catch{return a}})(),y=s.endsWith(".png")||s.endsWith(".webp")?s.slice(s.lastIndexOf(".")):".jpg",x=`yollomi_${t}_${Date.now()}_${o+1}${y}`,{path:O,size:_}=await D(a,h,x);m.push({index:o+1,status:"saved",file:w.relative(".",O),size:E(_),url:a})}catch{m.push({index:o+1,status:"download-failed",file:"-",size:"-",url:a})}}if(r.remainingCredits!==void 0)f.status(`Credits remaining: ${r.remainingCredits}`);return m}});
1
+ import*as w from"node:path";import{cli as b,Strategy as I}from"@jackwener/opencli/registry";import{CliError as c}from"@jackwener/opencli/errors";import{log as f}from"@jackwener/opencli/logger";import{YOLLOMI_DOMAIN as U,yollomiPost as z,downloadOutput as D,fmtBytes as E,MODEL_ROUTES as L}from"./utils.js";function P(p){const e={"1:1":[1024,1024],"16:9":[1344,768],"9:16":[768,1344],"4:3":[1152,896],"3:4":[896,1152]},[n,t]=e[p]||[1024,1024];return{width:n,height:t}}b({site:"yollomi",name:"generate",access:"write",description:"Generate images with AI (text-to-image or image-to-image)",domain:U,strategy:I.COOKIE,args:[{name:"prompt",fromFile:!0,positional:!0,required:!0,help:"Text prompt describing the image"},{name:"model",default:"z-image-turbo",help:"Model ID (z-image-turbo, flux-schnell, nano-banana, flux-2-pro, ...)"},{name:"ratio",default:"1:1",choices:["1:1","16:9","9:16","4:3","3:4"],help:"Aspect ratio"},{name:"image",help:'Input image URL for image-to-image (upload via "ppcli yollomi upload" first)'},{name:"output",default:"./yollomi-output",help:"Output directory"},{name:"no-download",type:"boolean",default:!1,help:"Only show URLs, skip download"}],columns:["index","status","file","size","url"],func:async(p,e)=>{const{prompt:n,model:t,ratio:l}=e,d=L[t];if(!d)throw new c("INVALID_MODEL",`Unknown model: ${t}`,'Run "ppcli yollomi models --type image" to see available models');let i;if(t==="z-image-turbo"){const{width:o,height:a}=P(l);i={prompt:n,width:o,height:a,output_format:"jpg",output_quality:85,guidance_scale:0,num_inference_steps:8}}else if(t==="flux-2-pro"){i={prompt:n,aspectRatio:l,outputNumber:1};if(e.image)i.imageUrl=e.image}else if(t==="flux-kontext-pro"){i={prompt:n,output_format:"jpg"};if(e.image)i.imageUrl=e.image;if(l!=="1:1")i.aspect_ratio=l}else{i={prompt:n,aspect_ratio:l};if(e.image)i.imageUrl=e.image}f.status(`Generating with ${t}...`);const r=await z(p,d,i),u=r.images||(r.image?[r.image]:[]);if(!u.length)throw new c("EMPTY_RESPONSE","No images returned","Try a different prompt or model");const g=e["no-download"],h=e.output,m=[];for(let o=0;o<u.length;o++){const a=u[o];if(g){m.push({index:o+1,status:"generated",file:"-",size:"-",url:a});continue}try{const s=(()=>{try{return new URL(a).pathname}catch{return a}})(),y=s.endsWith(".png")||s.endsWith(".webp")?s.slice(s.lastIndexOf(".")):".jpg",x=`yollomi_${t}_${Date.now()}_${o+1}${y}`,{path:O,size:_}=await D(a,h,x);m.push({index:o+1,status:"saved",file:w.relative(".",O),size:E(_),url:a})}catch{m.push({index:o+1,status:"download-failed",file:"-",size:"-",url:a})}}if(r.remainingCredits!==void 0)f.status(`Credits remaining: ${r.remainingCredits}`);return m}});
@@ -1 +1 @@
1
- import*as h from"node:path";import{cli as v,Strategy as g}from"@jackwener/opencli/registry";import{CliError as y}from"@jackwener/opencli/errors";import{log as n}from"@jackwener/opencli/logger";import{YOLLOMI_DOMAIN as w,yollomiPost as O,downloadOutput as I,fmtBytes as z}from"./utils.js";v({site:"yollomi",name:"video",access:"write",description:"Generate videos with AI (text-to-video or image-to-video)",domain:w,strategy:g.COOKIE,args:[{name:"prompt",positional:!0,required:!0,help:"Text prompt describing the video"},{name:"model",default:"kling-2-1",help:"Model (kling-2-1, openai-sora-2, google-veo-3-1, wan-2-5-t2v, ...)"},{name:"image",help:"Input image URL for image-to-video"},{name:"ratio",default:"16:9",choices:["1:1","16:9","9:16","4:3","3:4"],help:"Aspect ratio"},{name:"output",default:"./yollomi-output",help:"Output directory"},{name:"no-download",type:"boolean",default:!1,help:"Only show URL, skip download"}],columns:["status","file","size","credits","url"],func:async(d,e)=>{const{prompt:l,model:i}=e,a={aspect_ratio:e.ratio};if(e.image)a.image=e.image;const s={modelId:i,prompt:l,inputs:a};n.status(`Generating video with ${i} (may take a while)...`);const r=await O(d,"/api/ai/video",s),t=r.video||"";if(!t)throw new y("EMPTY_RESPONSE","No video returned","Try a different prompt or model");const o=r.remainingCredits,m=e["no-download"],p=e.output;if(m)return[{status:"generated",file:"-",size:"-",credits:o??"-",url:t}];try{const u=`yollomi_${i}_${Date.now()}.mp4`,{path:c,size:f}=await I(t,p,u);if(o!==void 0)n.status(`Credits remaining: ${o}`);return[{status:"saved",file:h.relative(".",c),size:z(f),credits:o??"-",url:t}]}catch{return[{status:"download-failed",file:"-",size:"-",credits:o??"-",url:t}]}}});
1
+ import*as h from"node:path";import{cli as v,Strategy as g}from"@jackwener/opencli/registry";import{CliError as y}from"@jackwener/opencli/errors";import{log as n}from"@jackwener/opencli/logger";import{YOLLOMI_DOMAIN as w,yollomiPost as O,downloadOutput as I,fmtBytes as z}from"./utils.js";v({site:"yollomi",name:"video",access:"write",description:"Generate videos with AI (text-to-video or image-to-video)",domain:w,strategy:g.COOKIE,args:[{name:"prompt",fromFile:!0,positional:!0,required:!0,help:"Text prompt describing the video"},{name:"model",default:"kling-2-1",help:"Model (kling-2-1, openai-sora-2, google-veo-3-1, wan-2-5-t2v, ...)"},{name:"image",help:"Input image URL for image-to-video"},{name:"ratio",default:"16:9",choices:["1:1","16:9","9:16","4:3","3:4"],help:"Aspect ratio"},{name:"output",default:"./yollomi-output",help:"Output directory"},{name:"no-download",type:"boolean",default:!1,help:"Only show URL, skip download"}],columns:["status","file","size","credits","url"],func:async(d,e)=>{const{prompt:l,model:i}=e,a={aspect_ratio:e.ratio};if(e.image)a.image=e.image;const s={modelId:i,prompt:l,inputs:a};n.status(`Generating video with ${i} (may take a while)...`);const r=await O(d,"/api/ai/video",s),t=r.video||"";if(!t)throw new y("EMPTY_RESPONSE","No video returned","Try a different prompt or model");const o=r.remainingCredits,m=e["no-download"],p=e.output;if(m)return[{status:"generated",file:"-",size:"-",credits:o??"-",url:t}];try{const u=`yollomi_${i}_${Date.now()}.mp4`,{path:c,size:f}=await I(t,p,u);if(o!==void 0)n.status(`Credits remaining: ${o}`);return[{status:"saved",file:h.relative(".",c),size:z(f),credits:o??"-",url:t}]}catch{return[{status:"download-failed",file:"-",size:"-",credits:o??"-",url:t}]}}});