publishport-opencli 1.8.5-pp.34 → 1.8.5-pp.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli-manifest.json CHANGED
@@ -11864,6 +11864,92 @@
11864
11864
  "sourceFile": "discord-app/threads.js",
11865
11865
  "navigateBefore": true
11866
11866
  },
11867
+ {
11868
+ "site": "discord-web",
11869
+ "name": "login",
11870
+ "description": "Open discord-web login and wait until the browser session is authenticated",
11871
+ "access": "write",
11872
+ "domain": "discord.com",
11873
+ "strategy": "cookie",
11874
+ "browser": true,
11875
+ "args": [
11876
+ {
11877
+ "name": "timeout",
11878
+ "type": "int",
11879
+ "default": 300,
11880
+ "required": false,
11881
+ "help": "Maximum seconds to wait for the user to finish login"
11882
+ }
11883
+ ],
11884
+ "columns": [
11885
+ "status",
11886
+ "logged_in",
11887
+ "site",
11888
+ "username",
11889
+ "display_name"
11890
+ ],
11891
+ "type": "js",
11892
+ "modulePath": "discord-web/auth.js",
11893
+ "sourceFile": "discord-web/auth.js",
11894
+ "navigateBefore": false,
11895
+ "siteSession": "persistent",
11896
+ "defaultWindowMode": "foreground"
11897
+ },
11898
+ {
11899
+ "site": "discord-web",
11900
+ "name": "send",
11901
+ "description": "Send a message to a Discord channel (discord.com). Pass the channel URL or \"<guild>/<channel>\". Multi-line text is sent as one message.",
11902
+ "access": "write",
11903
+ "domain": "discord.com",
11904
+ "strategy": "cookie",
11905
+ "browser": true,
11906
+ "args": [
11907
+ {
11908
+ "name": "channel",
11909
+ "type": "string",
11910
+ "required": true,
11911
+ "positional": true,
11912
+ "help": "Channel URL (https://discord.com/channels/<guild>/<channel>) or \"<guild>/<channel>\""
11913
+ },
11914
+ {
11915
+ "name": "text",
11916
+ "type": "string",
11917
+ "required": true,
11918
+ "positional": true,
11919
+ "help": "Message text (multi-line allowed)"
11920
+ }
11921
+ ],
11922
+ "columns": [
11923
+ "status",
11924
+ "channel_id",
11925
+ "message"
11926
+ ],
11927
+ "type": "js",
11928
+ "modulePath": "discord-web/send.js",
11929
+ "sourceFile": "discord-web/send.js",
11930
+ "navigateBefore": "https://discord.com"
11931
+ },
11932
+ {
11933
+ "site": "discord-web",
11934
+ "name": "whoami",
11935
+ "description": "Show the current logged-in discord-web account",
11936
+ "access": "read",
11937
+ "domain": "discord.com",
11938
+ "strategy": "cookie",
11939
+ "browser": true,
11940
+ "args": [],
11941
+ "columns": [
11942
+ "logged_in",
11943
+ "site",
11944
+ "username",
11945
+ "display_name"
11946
+ ],
11947
+ "type": "js",
11948
+ "modulePath": "discord-web/auth.js",
11949
+ "sourceFile": "discord-web/auth.js",
11950
+ "navigateBefore": false,
11951
+ "siteSession": "persistent"
11952
+ },
11867
11953
  {
11868
11954
  "site": "dockerhub",
11869
11955
  "name": "image",
@@ -17272,6 +17358,37 @@
17272
17358
  "modulePath": "hackernews/jobs.js",
17273
17359
  "sourceFile": "hackernews/jobs.js"
17274
17360
  },
17361
+ {
17362
+ "site": "hackernews",
17363
+ "name": "login",
17364
+ "description": "Open hackernews login and wait until the browser session is authenticated",
17365
+ "access": "write",
17366
+ "domain": "news.ycombinator.com",
17367
+ "strategy": "cookie",
17368
+ "browser": true,
17369
+ "args": [
17370
+ {
17371
+ "name": "timeout",
17372
+ "type": "int",
17373
+ "default": 300,
17374
+ "required": false,
17375
+ "help": "Maximum seconds to wait for the user to finish login"
17376
+ }
17377
+ ],
17378
+ "columns": [
17379
+ "status",
17380
+ "logged_in",
17381
+ "site",
17382
+ "username",
17383
+ "karma"
17384
+ ],
17385
+ "type": "js",
17386
+ "modulePath": "hackernews/auth.js",
17387
+ "sourceFile": "hackernews/auth.js",
17388
+ "navigateBefore": false,
17389
+ "siteSession": "persistent",
17390
+ "defaultWindowMode": "foreground"
17391
+ },
17275
17392
  {
17276
17393
  "site": "hackernews",
17277
17394
  "name": "new",
@@ -17492,6 +17609,27 @@
17492
17609
  "modulePath": "hackernews/user.js",
17493
17610
  "sourceFile": "hackernews/user.js"
17494
17611
  },
17612
+ {
17613
+ "site": "hackernews",
17614
+ "name": "whoami",
17615
+ "description": "Show the current logged-in hackernews account",
17616
+ "access": "read",
17617
+ "domain": "news.ycombinator.com",
17618
+ "strategy": "cookie",
17619
+ "browser": true,
17620
+ "args": [],
17621
+ "columns": [
17622
+ "logged_in",
17623
+ "site",
17624
+ "username",
17625
+ "karma"
17626
+ ],
17627
+ "type": "js",
17628
+ "modulePath": "hackernews/auth.js",
17629
+ "sourceFile": "hackernews/auth.js",
17630
+ "navigateBefore": false,
17631
+ "siteSession": "persistent"
17632
+ },
17495
17633
  {
17496
17634
  "site": "hashnode",
17497
17635
  "name": "login",
@@ -32186,6 +32324,86 @@
32186
32324
  "sourceFile": "reddit/search.js",
32187
32325
  "navigateBefore": "https://reddit.com"
32188
32326
  },
32327
+ {
32328
+ "site": "reddit",
32329
+ "name": "submit",
32330
+ "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.",
32331
+ "access": "write",
32332
+ "domain": "reddit.com",
32333
+ "strategy": "cookie",
32334
+ "browser": true,
32335
+ "args": [
32336
+ {
32337
+ "name": "subreddit",
32338
+ "type": "string",
32339
+ "required": true,
32340
+ "positional": true,
32341
+ "help": "Target subreddit, e.g. \"test\" or \"r/test\""
32342
+ },
32343
+ {
32344
+ "name": "title",
32345
+ "type": "string",
32346
+ "required": true,
32347
+ "positional": true,
32348
+ "help": "Post title (max 300 chars)"
32349
+ },
32350
+ {
32351
+ "name": "text",
32352
+ "type": "string",
32353
+ "required": false,
32354
+ "help": "Body text (markdown) for a self/text post"
32355
+ },
32356
+ {
32357
+ "name": "url",
32358
+ "type": "string",
32359
+ "required": false,
32360
+ "help": "External URL for a link post (mutually exclusive with --text)"
32361
+ },
32362
+ {
32363
+ "name": "flair-id",
32364
+ "type": "string",
32365
+ "required": false,
32366
+ "help": "Flair template id, if the subreddit requires one"
32367
+ },
32368
+ {
32369
+ "name": "flair-text",
32370
+ "type": "string",
32371
+ "required": false,
32372
+ "help": "Flair text to accompany --flair-id"
32373
+ },
32374
+ {
32375
+ "name": "nsfw",
32376
+ "type": "bool",
32377
+ "default": false,
32378
+ "required": false,
32379
+ "help": "Mark the post as NSFW"
32380
+ },
32381
+ {
32382
+ "name": "spoiler",
32383
+ "type": "bool",
32384
+ "default": false,
32385
+ "required": false,
32386
+ "help": "Mark the post as a spoiler"
32387
+ },
32388
+ {
32389
+ "name": "sendreplies",
32390
+ "type": "bool",
32391
+ "default": true,
32392
+ "required": false,
32393
+ "help": "Receive inbox replies for this post (default: true)"
32394
+ }
32395
+ ],
32396
+ "columns": [
32397
+ "status",
32398
+ "url",
32399
+ "id",
32400
+ "message"
32401
+ ],
32402
+ "type": "js",
32403
+ "modulePath": "reddit/submit.js",
32404
+ "sourceFile": "reddit/submit.js",
32405
+ "navigateBefore": "https://reddit.com"
32406
+ },
32189
32407
  {
32190
32408
  "site": "reddit",
32191
32409
  "name": "subreddit",
@@ -0,0 +1,10 @@
1
+ import{AuthRequiredError as z}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as H}from"../_shared/site-auth.js";const J="https://discord.com/channels/@me";async function K(b){return b.evaluate(`(() => {
2
+ if (/\\/login(\\b|\\/|\\?|$)/.test(location.pathname)) return null;
3
+ const panel = document.querySelector('[class*=panelTitleContainer]');
4
+ if (!panel) return null;
5
+ const display = (panel.textContent || '').trim();
6
+ const handleEl = document.querySelector('[class*=panelSubtextContainer] [class*=hovered]');
7
+ const handle = handleEl ? (handleEl.textContent || '').trim() : '';
8
+ if (!display && !handle) return null;
9
+ return { display_name: display, username: handle || display };
10
+ })()`)}async function B(b){await b.goto(J);for(let F=0;F<8;F++){const G=await K(b);if(G)return G;await b.wait(1)}const x=await b.evaluate("location.href");if(/\/login/.test(String(x||"")))throw new z("discord.com","Discord redirected to /login — not signed in");throw new z("discord.com","Discord account panel not found — not signed in or app failed to load")}H({site:"discord-web",domain:"discord.com",loginUrl:"https://discord.com/login",columns:["username","display_name"],quickCheck:async(b)=>{try{return{logged_in:!0,...await B(b)}}catch(x){if(x instanceof z)return{logged_in:!1};throw x}},verify:B,poll:async(b)=>B(b)});
@@ -0,0 +1,13 @@
1
+ import{ArgumentError as K,AuthRequiredError as $,CommandExecutionError as P}from"@jackwener/opencli/errors";import{cli as q,Strategy as z}from"@jackwener/opencli/registry";const G=/^(?:@me|\d{3,})\/\d{3,}$/;export function normalizeChannelUrl(b){const f=String(b||"").trim();if(!f)throw new K("Channel is required.",'Pass a channel URL like https://discord.com/channels/<guild>/<channel>, or "<guild>/<channel>".');if(/^https?:\/\//i.test(f)){let J;try{J=new URL(f)}catch{throw new K(`Invalid channel URL: ${f}`)}const M=J.hostname.toLowerCase();if(M!=="discord.com"&&!M.endsWith(".discord.com"))throw new K("Channel URL must be a discord.com URL.");const F=J.pathname.split("/").filter(Boolean);if(F[0]!=="channels"||F.length<3)throw new K("Channel URL must look like /channels/<guild>/<channel>.");return`https://discord.com/channels/${F[1]}/${F[2]}`}if(!G.test(f))throw new K(`Invalid channel reference: ${JSON.stringify(f)}`,'Use "<guild>/<channel>" (numeric ids) or "@me/<dm-channel>", or a full channel URL.');return`https://discord.com/channels/${f}`}q({site:"discord-web",name:"send",access:"write",description:'Send a message to a Discord channel (discord.com). Pass the channel URL or "<guild>/<channel>". Multi-line text is sent as one message.',domain:"discord.com",strategy:z.COOKIE,browser:!0,args:[{name:"channel",type:"string",required:!0,positional:!0,help:'Channel URL (https://discord.com/channels/<guild>/<channel>) or "<guild>/<channel>"'},{name:"text",type:"string",required:!0,positional:!0,help:"Message text (multi-line allowed)"}],columns:["status","channel_id","message"],func:async(b,f)=>{if(!b)throw new P("Browser session required");const J=normalizeChannelUrl(f.channel),M=J.split("/").pop(),F=String(f.text??"");if(!F.trim())throw new K("Message text cannot be empty.");await b.goto(J);let W=!1;for(let B=0;B<12;B++){const Z=await b.evaluate(`(() => {
2
+ if (/\\/login(\\b|\\/|\\?|$)/.test(location.pathname)) return 'login';
3
+ const ed = document.querySelector('[role="textbox"][data-slate-editor="true"]');
4
+ if (!ed) return 'no-editor';
5
+ return 'ready';
6
+ })()`);if(Z==="login")throw new $("discord.com","Discord redirected to /login — not signed in");if(Z==="ready"){W=!0;break}await b.wait(1)}if(!W)throw new P("Discord message box not found — the channel may not exist, or you lack access to it.");const X='[role="textbox"][data-slate-editor="true"]';await b.click(X);await b.pressKey("Control+A");await b.pressKey("Delete");const Q=F.split(`
7
+ `);for(let B=0;B<Q.length;B++){if(B>0)await b.pressKey("Shift+Enter");if(Q[B].length)await b.typeText(X,Q[B])}if(await b.evaluate(`(() => !!document.querySelector('[class*="autocomplete"]'))()`))await b.pressKey("Escape");const V=await b.evaluate(`(() => {
8
+ const ed = document.querySelector('[role="textbox"][data-slate-editor="true"]');
9
+ return ed ? (ed.textContent || '').replace(/\\uFEFF/g, '') : null;
10
+ })()`),Y=(B)=>String(B||"").replace(/\s+/g," ").trim();if(V===null||Y(V)!==Y(F))throw new P(`Composed text did not match before send (got ${JSON.stringify((V||"").slice(0,60))}).`);await b.pressKey("Enter");await b.wait(1);if((await b.evaluate(`(() => {
11
+ const ed = document.querySelector('[role="textbox"][data-slate-editor="true"]');
12
+ return ed ? (ed.textContent || '').replace(/\\uFEFF/g, '') : '';
13
+ })()`)).trim().length>0)throw new P("Message still in the box after Enter — send blocked (slow-mode, rate limit, or no permission to post here).");return[{status:"success",channel_id:M,message:"Message sent"}]}});
@@ -0,0 +1,9 @@
1
+ import{AuthRequiredError as n,CommandExecutionError as c}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as i}from"../_shared/site-auth.js";const t="https://news.ycombinator.com";async function r(e){return(await e.getCookies({url:t})).some((a)=>a.name==="user"&&a.value)}async function s(e){if(!await r(e))throw new n("news.ycombinator.com","Hacker News `user` cookie missing — not signed in");await e.goto(`${t}/news`);await e.wait(1);const o=await e.evaluate(`(() => {
2
+ // The top-right nav shows "<username> (karma) | logout" only when signed in.
3
+ const userLink = document.querySelector('span.pagetop a[href^="user?id="]');
4
+ const logout = document.querySelector('a[href^="logout"]');
5
+ if (!userLink || !logout) return { anon: true };
6
+ const username = (userLink.textContent || '').trim();
7
+ const karmaMatch = (document.querySelector('span.pagetop')?.textContent || '').match(/\\((\\d+)\\)/);
8
+ return { username, karma: karmaMatch ? karmaMatch[1] : '' };
9
+ })()`);if(o?.anon||!o?.username)throw new n("news.ycombinator.com","Hacker News shows no signed-in user — cookie stale");return{username:o.username,karma:o.karma||""}}i({site:"hackernews",domain:"news.ycombinator.com",loginUrl:`${t}/login`,columns:["username","karma"],quickCheck:r,verify:s,poll:async(e)=>{if(!await r(e))throw new n("news.ycombinator.com","Waiting for Hacker News `user` cookie");return s(e)}});
@@ -0,0 +1,42 @@
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 () => {
2
+ try {
3
+ const meRes = await fetch('/api/me.json', { credentials: 'include' });
4
+ if (meRes.status === 401 || meRes.status === 403) {
5
+ return { kind: 'auth', detail: 'Reddit /api/me.json HTTP ' + meRes.status };
6
+ }
7
+ const me = await meRes.json();
8
+ const modhash = me && me.data && me.data.modhash;
9
+ if (!modhash) return { kind: 'auth', detail: 'No modhash — anonymous or expired session' };
10
+
11
+ const p = new URLSearchParams();
12
+ p.set('sr', ${JSON.stringify(N)});
13
+ p.set('title', ${JSON.stringify(J)});
14
+ p.set('kind', ${JSON.stringify(Q)});
15
+ ${Q==="link"?`p.set('url', ${JSON.stringify(O)}); p.set('resubmit', 'true');`:`p.set('text', ${JSON.stringify(K)});`}
16
+ p.set('api_type', 'json');
17
+ p.set('validate_on_submit', 'true');
18
+ p.set('sendreplies', ${F.sendreplies===!1?"'false'":"'true'"});
19
+ ${F.nsfw?"p.set('nsfw', 'true');":""}
20
+ ${F.spoiler?"p.set('spoiler', 'true');":""}
21
+ ${F["flair-id"]?`p.set('flair_id', ${JSON.stringify(String(F["flair-id"]))});`:""}
22
+ ${F["flair-text"]?`p.set('flair_text', ${JSON.stringify(String(F["flair-text"]))});`:""}
23
+ p.set('uh', modhash);
24
+
25
+ const res = await fetch('/api/submit', {
26
+ method: 'POST',
27
+ credentials: 'include',
28
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
29
+ body: p.toString(),
30
+ });
31
+ if (!res.ok) return { kind: 'http', httpStatus: res.status };
32
+ const data = await res.json();
33
+ const errors = data && data.json && data.json.errors;
34
+ if (errors && errors.length > 0) {
35
+ return { kind: 'reject', detail: errors.map(e => e.join(': ')).join('; ') };
36
+ }
37
+ const d = (data && data.json && data.json.data) || {};
38
+ return { ok: true, url: d.url || '', id: d.id || (d.name || '') };
39
+ } catch (e) {
40
+ return { kind: 'exception', detail: String((e && e.message) || e) };
41
+ }
42
+ })()`);if(G?.kind==="auth")throw new V("reddit.com",G.detail);if(G?.kind==="http")throw new P(`HTTP ${G.httpStatus} from /api/submit`);if(G?.kind==="exception")throw new P(`Reddit submit failed: ${G.detail}`);if(G?.kind==="reject")return[{status:"failed",url:"",id:"",message:G.detail}];if(!G?.ok)throw new P(`Unexpected Reddit submit result: ${JSON.stringify(G)}`);return[{status:"success",url:G.url,id:G.id,message:"Post submitted to r/"+N}]}});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "publishport-opencli",
3
- "version": "1.8.5-pp.34",
3
+ "version": "1.8.5-pp.35",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "provenance": false