Skip to content

Commit d653a13

Browse files
committed
Tokens update command added
1 parent b6f18a9 commit d653a13

6 files changed

Lines changed: 261 additions & 1 deletion

File tree

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ linkedin account list
6060
# Switch active account
6161
linkedin account switch "Vlad"
6262

63+
# Update tokens for active account (e.g. after regenerating tokens on the dashboard)
64+
linkedin account update
65+
66+
# Update tokens for a specific account
67+
linkedin account update "Vlad"
68+
69+
# Non-interactive token update
70+
linkedin account update --linked-api-token=xxx --identification-token=yyy
71+
6372
# Rename a saved account
6473
linkedin account rename "Vlad" --name "My Work Account"
6574

@@ -772,6 +781,34 @@ linkedin account list
772781

773782
The active account is marked with `*`.
774783

784+
#### `account update`
785+
786+
Update tokens for a saved account. Useful when your tokens were regenerated on the dashboard and you need to refresh them in the CLI.
787+
788+
```bash
789+
linkedin account update [name] [flags]
790+
```
791+
792+
| Arg | Required | Description |
793+
|-----|----------|-------------|
794+
| `name` | no | Account name (case-insensitive substring match). Defaults to active account. |
795+
796+
| Flag | Type | Description |
797+
|------|------|-------------|
798+
| `--linked-api-token` | string | New Linked API Token (skips prompt) |
799+
| `--identification-token` | string | New Identification Token (skips prompt) |
800+
801+
```bash
802+
# Update active account interactively
803+
linkedin account update
804+
805+
# Update a specific account
806+
linkedin account update "Vlad"
807+
808+
# Non-interactive (for scripts/CI)
809+
linkedin account update --linked-api-token=xxx --identification-token=yyy
810+
```
811+
775812
#### `account switch`
776813

777814
Switch the active LinkedIn account.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@linkedapi/linkedin-cli",
3-
"version": "1.0.6",
3+
"version": "1.0.7",
44
"description": "AI-agent-friendly CLI for controlling LinkedIn accounts and retrieving real-time data.",
55
"author": "Linked API",
66
"license": "MIT",

src/commands/account/update.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { findAccountByName, listAccounts, updateAccountTokens } from '@core/auth/config-store';
2+
import { buildClient } from '@core/client/build-client';
3+
import { EXIT_CODE } from '@core/errors/exit-codes';
4+
import { Args, Command, Flags } from '@oclif/core';
5+
import { readMaskedInput } from '@utils/masked-input';
6+
import { isStdinTty } from '@utils/tty';
7+
8+
export default class AccountUpdate extends Command {
9+
static override description = 'Update tokens for a saved LinkedIn account';
10+
11+
static override args = {
12+
name: Args.string({
13+
description:
14+
'Account name to update (case-insensitive substring match). Defaults to active account.',
15+
}),
16+
};
17+
18+
static override flags = {
19+
'linked-api-token': Flags.string({
20+
description: 'New Linked API Token (for non-interactive use)',
21+
}),
22+
'identification-token': Flags.string({
23+
description: 'New Identification Token (for non-interactive use)',
24+
}),
25+
};
26+
27+
static override examples = [
28+
'<%= config.bin %> account update',
29+
'<%= config.bin %> account update "John"',
30+
'<%= config.bin %> account update --linked-api-token=xxx --identification-token=yyy',
31+
];
32+
33+
public async run(): Promise<void> {
34+
const { args, flags } = await this.parse(AccountUpdate);
35+
36+
const account = this.resolveAccount(args.name);
37+
38+
if (!account) {
39+
return;
40+
}
41+
42+
let linkedApiToken: string;
43+
let identificationToken: string;
44+
45+
if (flags['linked-api-token'] && flags['identification-token']) {
46+
linkedApiToken = flags['linked-api-token'];
47+
identificationToken = flags['identification-token'];
48+
} else if (!isStdinTty()) {
49+
process.stderr.write(`Cannot run interactive update in non-interactive mode.
50+
Use flags instead:
51+
52+
linkedin account update --linked-api-token=xxx --identification-token=yyy
53+
54+
Get tokens at https://app.linkedapi.io\n`);
55+
this.exit(EXIT_CODE.AUTH);
56+
return;
57+
} else {
58+
process.stdout.write(
59+
`Updating tokens for "${account.name}".\n` +
60+
'Get new tokens at https://app.linkedapi.io\n\n',
61+
);
62+
63+
linkedApiToken = await readMaskedInput('New Linked API Token: ', 'account_');
64+
identificationToken = await readMaskedInput('New Identification Token: ', 'id_');
65+
}
66+
67+
if (!linkedApiToken || !identificationToken) {
68+
process.stderr.write('Both tokens are required.\n');
69+
this.exit(EXIT_CODE.AUTH);
70+
return;
71+
}
72+
73+
process.stdout.write('Verifying tokens... ');
74+
75+
let accountName = '';
76+
77+
try {
78+
const tempClient = buildClient({
79+
linkedApiToken,
80+
identificationToken,
81+
});
82+
const accountInfo = await tempClient.getAccountInfo();
83+
accountName = accountInfo.data?.name ?? '';
84+
process.stdout.write('OK\n');
85+
} catch {
86+
process.stdout.write('FAILED\n');
87+
process.stderr.write(
88+
'\nInvalid tokens. Make sure you copied the correct tokens from https://app.linkedapi.io\n',
89+
);
90+
this.exit(EXIT_CODE.AUTH);
91+
return;
92+
}
93+
94+
const displayName = accountName || account.name;
95+
96+
const updated = updateAccountTokens(account.identificationToken, {
97+
name: displayName,
98+
linkedApiToken,
99+
identificationToken,
100+
});
101+
102+
if (!updated) {
103+
process.stderr.write('Failed to update account.\n');
104+
this.exit(EXIT_CODE.GENERAL);
105+
return;
106+
}
107+
108+
process.stdout.write(`Account "${displayName}" updated successfully.\n`);
109+
}
110+
111+
private resolveAccount(
112+
nameQuery?: string,
113+
): { name: string; identificationToken: string } | undefined {
114+
const accounts = listAccounts();
115+
116+
if (accounts.length === 0) {
117+
process.stderr.write('No accounts configured. Run "linkedin setup" to add one.\n');
118+
this.exit(EXIT_CODE.AUTH);
119+
return undefined;
120+
}
121+
122+
if (nameQuery) {
123+
const account = findAccountByName(nameQuery);
124+
125+
if (!account) {
126+
process.stderr.write(
127+
`Account "${nameQuery}" not found. Run "linkedin account list" to see available accounts.\n`,
128+
);
129+
this.exit(EXIT_CODE.GENERAL);
130+
return undefined;
131+
}
132+
133+
return account;
134+
}
135+
136+
const current = accounts.find((account) => account.isCurrent);
137+
138+
if (!current) {
139+
process.stderr.write(
140+
'No active account. Run "linkedin account list" to see available accounts.\n',
141+
);
142+
this.exit(EXIT_CODE.AUTH);
143+
return undefined;
144+
}
145+
146+
return current;
147+
}
148+
}

src/core/auth/config-store.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,34 @@ export function findAccountByName(nameQuery: string): TAccountEntry | undefined
225225
return config.accounts.find((account) => account.name.toLowerCase().includes(lowerQuery));
226226
}
227227

228+
export function updateAccountTokens(
229+
oldIdentificationToken: string,
230+
newTokens: { name: string; linkedApiToken: string; identificationToken: string },
231+
): boolean {
232+
const config = readRawConfig();
233+
234+
if (!config) {
235+
return false;
236+
}
237+
238+
const index = config.accounts.findIndex(
239+
(account) => account.identificationToken === oldIdentificationToken,
240+
);
241+
242+
if (index < 0) {
243+
return false;
244+
}
245+
246+
config.accounts[index] = newTokens;
247+
248+
if (config.currentAccount === oldIdentificationToken) {
249+
config.currentAccount = newTokens.identificationToken;
250+
}
251+
252+
saveRawConfig(config);
253+
return true;
254+
}
255+
228256
export function renameAccount(identificationToken: string, newName: string): boolean {
229257
const config = readRawConfig();
230258

src/core/errors/error-handler.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ interface TCliError {
77
error: string;
88
message: string;
99
workflowId?: string;
10+
hint?: string;
1011
}
1112

1213
export function mapLinkedApiErrorToCliError(error: LinkedApiError): TCliError {
@@ -28,6 +29,7 @@ export function mapLinkedApiErrorToCliError(error: LinkedApiError): TCliError {
2829
exitCode: EXIT_CODE.AUTH,
2930
error: error.type,
3031
message: error.message,
32+
hint: 'Run "linkedin account update" to refresh your tokens.',
3133
};
3234

3335
case 'subscriptionRequired':
@@ -81,4 +83,8 @@ export function writeErrorToStderr(cliError: TCliError): void {
8183
}
8284

8385
process.stderr.write(JSON.stringify(output) + '\n');
86+
87+
if (cliError.hint) {
88+
process.stderr.write(`\n${cliError.hint}\n`);
89+
}
8490
}

src/utils/masked-input.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
export function readMaskedInput(prompt: string, visiblePrefix: string): Promise<string> {
2+
return new Promise((resolve) => {
3+
process.stdout.write(prompt);
4+
5+
const stdin = process.stdin;
6+
stdin.setRawMode(true);
7+
stdin.resume();
8+
stdin.setEncoding('utf-8');
9+
10+
let input = '';
11+
12+
const onData = (str: string): void => {
13+
for (const char of str) {
14+
if (char === '\n' || char === '\r') {
15+
stdin.setRawMode(false);
16+
stdin.removeListener('data', onData);
17+
stdin.pause();
18+
process.stdout.write('\n');
19+
resolve(input);
20+
return;
21+
} else if (char === '\u007F' || char === '\b') {
22+
if (input.length > 0) {
23+
input = input.slice(0, -1);
24+
process.stdout.write('\b \b');
25+
}
26+
} else if (char === '\u0003') {
27+
stdin.setRawMode(false);
28+
process.stdout.write('\n');
29+
process.exit(0);
30+
} else if (char >= ' ') {
31+
input += char;
32+
const isInPrefix =
33+
input.length <= visiblePrefix.length && visiblePrefix.startsWith(input);
34+
process.stdout.write(isInPrefix ? char : '*');
35+
}
36+
}
37+
};
38+
39+
stdin.on('data', onData);
40+
});
41+
}

0 commit comments

Comments
 (0)