From 23c20c6ada23c25fd0f845407aacd062c42c3cf8 Mon Sep 17 00:00:00 2001 From: Kiril Date: Fri, 16 Oct 2026 07:41:24 +0200 Subject: [PATCH 1/2] Add person endorse command --- package-lock.json | 12 ++-- package.json | 6 +- src/commands/admin/limits/delete.ts | 7 +- src/commands/admin/limits/set.ts | 7 +- src/commands/person/endorse.ts | 102 ++++++++++++++++++++++++++++ 5 files changed, 117 insertions(+), 17 deletions(-) create mode 100644 src/commands/person/endorse.ts diff --git a/package-lock.json b/package-lock.json index ba9e104..ea365ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@linkedapi/linkedin-cli", - "version": "1.2.21", + "version": "1.2.22", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@linkedapi/linkedin-cli", - "version": "1.2.21", + "version": "1.2.22", "license": "MIT", "dependencies": { - "@linkedapi/node": "^2.3.17", + "@linkedapi/node": "^2.3.19", "@oclif/core": "^4.2.10" }, "bin": { @@ -2530,9 +2530,9 @@ } }, "node_modules/@linkedapi/node": { - "version": "2.3.17", - "resolved": "https://registry.npmjs.org/@linkedapi/node/-/node-2.3.17.tgz", - "integrity": "sha512-2eudeWgD00FTeLjiBzN1Lq/Dwlu68YT4ZmwM3rfb9NlErL/2jeT8dPYvhVApRUGl8ifrmP2pz8rZE/ELHgXgWQ==", + "version": "2.3.19", + "resolved": "https://registry.npmjs.org/@linkedapi/node/-/node-2.3.19.tgz", + "integrity": "sha512-PKLzAK78JxLBGbekmNhvtwVophnkkVP5CVJDo/bPDNwZVMWReAh2rpv76f/B5FcWNmOww+0ZlOHy5PXIEp+rjg==", "license": "MIT" }, "node_modules/@nodable/entities": { diff --git a/package.json b/package.json index 3d3b647..ee10d6f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@linkedapi/linkedin-cli", - "version": "1.2.21", + "version": "1.2.22", "description": "AI-agent-friendly CLI for controlling LinkedIn accounts and retrieving real-time data.", "author": "Linked API", "license": "MIT", @@ -43,7 +43,7 @@ "ai-agent" ], "dependencies": { - "@linkedapi/node": "^2.3.17", + "@linkedapi/node": "^2.3.19", "@oclif/core": "^4.2.10" }, "devDependencies": { @@ -74,7 +74,7 @@ "description": "Manage connected LinkedIn accounts" }, "person": { - "description": "Fetch and search LinkedIn profiles" + "description": "Fetch and search LinkedIn profiles and endorse skills" }, "company": { "description": "Fetch and search LinkedIn companies" diff --git a/src/commands/admin/limits/delete.ts b/src/commands/admin/limits/delete.ts index 25199cd..9625c3d 100644 --- a/src/commands/admin/limits/delete.ts +++ b/src/commands/admin/limits/delete.ts @@ -1,8 +1,7 @@ -import { Args, Flags } from '@oclif/core'; -import type { TLimitCategory, TLimitPeriod } from '@linkedapi/node'; - import { AdminBaseCommand } from '@admin-base-command'; import { formatAdminVoidOutput } from '@core/output/admin-formatter'; +import type { TLimitCategory, TLimitPeriod } from '@linkedapi/node'; +import { Args, Flags } from '@oclif/core'; export default class AdminLimitsDelete extends AdminBaseCommand { static override description = 'Delete specific limits (falls back to defaults)'; @@ -17,7 +16,7 @@ export default class AdminLimitsDelete extends AdminBaseCommand { static override flags = { ...AdminBaseCommand.baseFlags, category: Flags.string({ - description: 'Limit category', + description: 'Limit category (e.g. stMessages, stEndorsements)', required: true, }), period: Flags.string({ diff --git a/src/commands/admin/limits/set.ts b/src/commands/admin/limits/set.ts index 30aeabe..c430816 100644 --- a/src/commands/admin/limits/set.ts +++ b/src/commands/admin/limits/set.ts @@ -1,8 +1,7 @@ -import { Args, Flags } from '@oclif/core'; -import type { TLimitCategory, TLimitPeriod } from '@linkedapi/node'; - import { AdminBaseCommand } from '@admin-base-command'; import { formatAdminVoidOutput } from '@core/output/admin-formatter'; +import type { TLimitCategory, TLimitPeriod } from '@linkedapi/node'; +import { Args, Flags } from '@oclif/core'; export default class AdminLimitsSet extends AdminBaseCommand { static override description = 'Set limits for an account'; @@ -17,7 +16,7 @@ export default class AdminLimitsSet extends AdminBaseCommand { static override flags = { ...AdminBaseCommand.baseFlags, category: Flags.string({ - description: 'Limit category', + description: 'Limit category (e.g. stMessages, stEndorsements)', required: true, }), period: Flags.string({ diff --git a/src/commands/person/endorse.ts b/src/commands/person/endorse.ts new file mode 100644 index 0000000..8ce24ef --- /dev/null +++ b/src/commands/person/endorse.ts @@ -0,0 +1,102 @@ +import { BaseCommand } from '@base-command'; +import { EXIT_CODE } from '@core/errors/exit-codes'; +import { formatOutput, formatVoidOutput } from '@core/output/formatter'; +import { runVoidWorkflow, runWorkflow } from '@core/workflow/workflow-runner'; +import { Args, Flags } from '@oclif/core'; +import { isStdoutTty } from '@utils/tty'; + +export default class PersonEndorse extends BaseCommand { + static override description = + "Endorse skills of a 1st-degree connection, using names in the profile owner's language. Already-endorsed skills count as success. Each success counts toward the stEndorsements limit."; + + static override args = { + url: Args.string({ + description: 'LinkedIn profile URL of your 1st-degree connection', + required: true, + }), + }; + + static override flags = { + ...BaseCommand.baseFlags, + skill: Flags.string({ + description: 'Exact skill name (case and extra spaces are ignored)', + }), + pattern: Flags.string({ + description: + 'RE2 pattern, case-insensitive, matching anywhere; use ^…$ for a whole name (repeatable, 1-20 patterns of 1-200 characters)', + multiple: true, + multipleNonGreedy: true, + }), + all: Flags.boolean({ + description: 'Endorse all skills', + default: false, + }), + }; + + static override examples = [ + '<%= config.bin %> person endorse https://www.linkedin.com/in/john-doe --skill "Project Management"', + '<%= config.bin %> person endorse https://www.linkedin.com/in/john-doe --pattern manage --pattern "^scrum$"', + '<%= config.bin %> person endorse https://www.linkedin.com/in/john-doe --all', + ]; + + public async run(): Promise { + const { args, flags } = await this.parse(PersonEndorse); + const selectionCount = [ + flags.skill !== undefined, + flags.pattern !== undefined, + flags.all, + ].filter(Boolean).length; + + if (selectionCount !== 1) { + this.error('Provide exactly one of --skill, --pattern or --all.', { + exit: EXIT_CODE.VALIDATION, + }); + } + + const client = await this.buildAuthenticatedClient(); + + try { + if (flags.skill !== undefined) { + const result = await runVoidWorkflow( + client.endorsePersonSkill, + { personUrl: args.url, skillName: flags.skill }, + { isQuiet: flags.quiet }, + ); + + formatVoidOutput({ + errors: result.errors, + isJson: flags.json, + isQuiet: flags.quiet, + successMessage: `Endorsed: ${flags.skill}`, + }); + return; + } + + const result = await runWorkflow( + client.endorsePersonSkills, + { + personUrl: args.url, + ...(flags.pattern !== undefined ? { skillNamePatterns: flags.pattern } : {}), + }, + { isQuiet: flags.quiet }, + ); + + const data = + flags.json || !isStdoutTty() || flags.fields !== undefined + ? result.data + : result.data?.map(({ skillName, success, error }) => + success ? `Endorsed: ${skillName}` : `Not endorsed: ${skillName} - ${error?.type}`, + ); + + formatOutput({ + data, + errors: result.errors, + isJson: flags.json, + fields: flags.fields, + isQuiet: flags.quiet, + }); + } catch (error) { + this.handleError(error); + } + } +} From 293d121b7c4c3db07db236d68c0c3853ee1c335c Mon Sep 17 00:00:00 2001 From: Kiril Date: Fri, 16 Oct 2026 08:35:52 +0200 Subject: [PATCH 2/2] Say that deleted limits leave the category unlimited until set again --- src/commands/admin/limits/delete.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/admin/limits/delete.ts b/src/commands/admin/limits/delete.ts index 9625c3d..13c2503 100644 --- a/src/commands/admin/limits/delete.ts +++ b/src/commands/admin/limits/delete.ts @@ -4,7 +4,8 @@ import type { TLimitCategory, TLimitPeriod } from '@linkedapi/node'; import { Args, Flags } from '@oclif/core'; export default class AdminLimitsDelete extends AdminBaseCommand { - static override description = 'Delete specific limits (falls back to defaults)'; + static override description = + 'Delete a specific limit. The removed category/period has no limit until set again. Use linkedin admin limits reset to restore all default limits for the account.'; static override args = { accountId: Args.string({