Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,18 @@ linq tokens list
linq tokens list --json
```

#### `linq tokens show`

Print the API token saved in your local config. Useful if you lost the token printed at signup and need to copy it elsewhere (password manager, CI env var, etc.).

```bash
linq tokens show
linq tokens show --copy # copy to clipboard instead of printing
linq tokens show --json
```

> Reads from your local `~/.linq/config.json` — never from the server. For security, raw tokens are never stored server-side. If you don't have the token locally either, regenerate it with `linq tokens regenerate <id>` or create a new one.

#### `linq tokens create`

Create a new API token. Run bare for an interactive wizard (name + expiration preset/custom/never). With flags, runs non-interactively and defaults to no expiry.
Expand Down
4 changes: 2 additions & 2 deletions src/commands/chats/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,10 @@ export default class ChatsCreate extends BaseCommand {
if (e instanceof Linq.PermissionDeniedError) {
this.log(chalk.yellow(`\n Can't message this contact yet.\n`));
if (isSharedLine(config)) {
this.log(chalk.dim(` On a shared line, you need to add the contact (${chalk.cyan('linq contacts add +1234567890')})`));
this.log(chalk.dim(` On a Shared line, you need to add the contact (${chalk.cyan('linq contacts add +1234567890')})`));
this.log(chalk.dim(` and they must text you (${chalk.bold(fromPhone)}) first before you can message them.\n`));
} else {
this.log(chalk.dim(` On a sandbox line, the contact must text you (${chalk.bold(fromPhone)}) first before you can message them.\n`));
this.log(chalk.dim(` On a Free line, the contact must text you (${chalk.bold(fromPhone)}) first before you can message them.\n`));
}
this.exit(1);
}
Expand Down
4 changes: 2 additions & 2 deletions src/commands/chats/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { createApiClient } from '../../lib/api-client.js';
import { formatChatsList } from '../../lib/format.js';

export default class ChatsList extends BaseCommand {
static override description = 'List all chats for a Blue Number';
static override description = 'List all chats for a Linq Number';

static override examples = [
'<%= config.bin %> <%= command.id %>',
Expand All @@ -17,7 +17,7 @@ export default class ChatsList extends BaseCommand {

static override flags = {
from: Flags.string({
description: 'Blue Number to list chats for (E.164 format). Uses config fromPhone if not specified.',
description: 'Linq Number to list chats for (E.164 format). Uses config fromPhone if not specified.',
}),
limit: Flags.integer({
description: 'Maximum number of chats to return (default: 20, max: 100)',
Expand Down
12 changes: 6 additions & 6 deletions src/commands/contacts/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { addBreadcrumb } from '../../lib/telemetry.js';
import { bail, throwHttpError } from '../../lib/errors.js';

export default class ContactsAdd extends BaseCommand {
static override description = 'Add a contact to your shared line';
static override description = 'Add a contact to your Shared line';

static override examples = [
'<%= config.bin %> <%= command.id %> +12025551234',
Expand Down Expand Up @@ -47,20 +47,20 @@ export default class ContactsAdd extends BaseCommand {
const data = await res.json() as { contactPhone: string };
addBreadcrumb('Contact added');

const blueNumber = config.fromPhone;
const linqNumber = config.fromPhone;

if (flags.json) {
this.log(JSON.stringify({
contactPhone: data.contactPhone,
blueNumber: blueNumber ?? null,
linqNumber: linqNumber ?? null,
}, null, 2));
return;
}

this.log(chalk.green(`\n ✓ Contact ${data.contactPhone} added.\n`));
this.log(chalk.yellow(' Inbound-first: this contact must text your Blue Number before you can text them.\n'));
if (blueNumber) {
this.log(` Text ${chalk.cyan(blueNumber)} from ${chalk.cyan(data.contactPhone)} to start the conversation.\n`);
this.log(chalk.yellow(' Inbound-first: this contact must text your Linq Number before you can text them.\n'));
if (linqNumber) {
this.log(` Text ${chalk.cyan(linqNumber)} from ${chalk.cyan(data.contactPhone)} to start the conversation.\n`);
}
} catch (e) {
bail(this, flags.json, e);
Expand Down
2 changes: 1 addition & 1 deletion src/commands/contacts/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { BACKEND_URL } from '../../lib/api-client.js';
import { bail, throwHttpError } from '../../lib/errors.js';

export default class ContactsList extends BaseCommand {
static override description = 'List contacts on your shared line';
static override description = 'List contacts on your Shared line';

static override examples = [
'<%= config.bin %> <%= command.id %>',
Expand Down
2 changes: 1 addition & 1 deletion src/commands/contacts/remove.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { addBreadcrumb } from '../../lib/telemetry.js';
import { bail, throwHttpError } from '../../lib/errors.js';

export default class ContactsRemove extends BaseCommand {
static override description = 'Remove a contact from your shared line';
static override description = 'Remove a contact from your Shared line';

static override examples = [
'<%= config.bin %> <%= command.id %> +12025551234',
Expand Down
4 changes: 2 additions & 2 deletions src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ export default class Doctor extends BaseCommand {
}

if (config.fromPhone) {
ok('blue_number', `Blue Number set (${config.fromPhone})`);
ok('linq_number', `Linq Number set (${config.fromPhone})`);
} else {
fail('blue_number', 'Blue Number not set — run `linq phonenumbers set` to pick a default');
fail('linq_number', 'Linq Number not set — run `linq phonenumbers set` to pick a default');
}

const sessionExpiry = config.sessionExpiresAt || config.expiresAt;
Expand Down
46 changes: 33 additions & 13 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { Flags } from '@oclif/core';
import { password, select, input } from '@inquirer/prompts';
import chalk from 'chalk';
import { BaseCommand } from '../lib/base-command.js';
import {
saveProfile,
setCurrentProfile,
getCurrentProfile,
listProfiles,
SANDBOX_PROFILE,
getDisplayTier,
getLineType,
type AccountLabel,
} from '../lib/config.js';
import { BACKEND_URL } from '../lib/api-client.js';
Expand Down Expand Up @@ -36,6 +39,11 @@ export default class Init extends BaseCommand {
this.error(`The "${SANDBOX_PROFILE}" profile is reserved for \`linq signup\`. Use --profile <name> to init a different profile.`);
}

if (process.stdout.isTTY) {
await renderBanner();
console.log('\n Welcome to Linq CLI Setup\n');
}

if (!profileName) {
const current = await getCurrentProfile() || 'default';
const profiles = (await listProfiles()).filter(p => p !== SANDBOX_PROFILE);
Expand Down Expand Up @@ -64,9 +72,6 @@ export default class Init extends BaseCommand {
}
}

await renderBanner();
console.log('\n Welcome to Linq CLI Setup\n');

// Prompt for API token
const token = await password({
message: 'Enter your API token:',
Expand All @@ -82,6 +87,7 @@ export default class Init extends BaseCommand {
this.log('\nValidating token...');
let orgId: string | undefined;
let name: string | undefined;
let email: string | undefined;
let partnerId: string | undefined;
let accountPhones: { phoneNumber: string }[] = [];
let accountLabel: AccountLabel | undefined;
Expand All @@ -100,6 +106,7 @@ export default class Init extends BaseCommand {
partnerId?: string;
orgId?: string;
name?: string | null;
email?: string | null;
accountInfo?: {
phones: { phoneNumber: string }[];
accountLabel?: AccountLabel;
Expand All @@ -108,6 +115,7 @@ export default class Init extends BaseCommand {
partnerId = acc.partnerId;
orgId = acc.orgId;
name = acc.name ?? undefined;
email = acc.email ?? undefined;
accountPhones = acc.accountInfo?.phones ?? [];
accountLabel = acc.accountInfo?.accountLabel;
} catch (e) {
Expand All @@ -122,11 +130,11 @@ export default class Init extends BaseCommand {

if (phones.length === 1) {
fromPhone = phones[0].phoneNumber;
this.log(`Default Blue Number set to ${fromPhone} (only number on account)\n`);
this.log(`Default Linq Number set to ${fromPhone} (only number on account)\n`);
} else if (phones.length > 1) {
if (accountLabel === 'Paid') {
fromPhone = await select({
message: 'Select a default Blue Number:',
message: 'Select a default Linq Number:',
choices: phones.map((p) => ({
name: p.phoneNumber,
value: p.phoneNumber,
Expand All @@ -144,17 +152,29 @@ export default class Init extends BaseCommand {
...(partnerId && { partnerId }),
...(orgId && { orgId }),
...(name && { name }),
...(email && { email }),
accountLabel,
});
await setCurrentProfile(profileName);

this.log(`\n\u2713 Configuration saved to profile "${profileName}"\n`);
this.log('Next steps:');
this.log(' linq phonenumbers List your Blue Numbers');
this.log(
' linq chats create --to +1XXXXXXXXXX -m "Hello!" Create a chat and send a message'
);
this.log(' linq webhooks listen Listen for webhook events');
this.log(' linq doctor Check your setup');
const tier = getDisplayTier(accountLabel);
const line = getLineType(accountLabel);

this.log(chalk.green('\n\u2713 You\'re set up!\n'));
if (tier) this.log(` ${chalk.dim('Tier:')} ${tier}`);
if (line) this.log(` ${chalk.dim('Line:')} ${line}`);
if (fromPhone) this.log(` ${chalk.dim('Linq Number:')} ${chalk.bold(fromPhone)}`);
if (name) this.log(` ${chalk.dim('Name:')} ${name}`);
if (line === 'Shared') {
this.log('');
this.log(` Shared line: add contacts with ${chalk.cyan('linq contacts add +1...')}, have them text you first, then reply.`);
} else if (tier === 'Free') {
this.log('');
this.log(' Free line is inbound-first: have someone text your Linq Number first, then reply.');
}
this.log('\nNext steps:');
this.log(` ${chalk.cyan('linq chats create --to +1XXXXXXXXXX -m "Hello!"')} ${chalk.dim('# Send a message')}`);
this.log(` ${chalk.cyan('linq webhooks listen')}${' '.repeat(33)}${chalk.dim('# Listen for events')}`);
this.log(` ${chalk.cyan('linq doctor')}${' '.repeat(42)}${chalk.dim('# Health check')}`);
}
}
34 changes: 28 additions & 6 deletions src/commands/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
getCurrentProfile,
listProfiles,
SANDBOX_PROFILE,
getDisplayTier,
getLineType,
type AccountLabel,
} from '../lib/config.js';
import { BACKEND_URL } from '../lib/api-client.js';
Expand Down Expand Up @@ -42,6 +44,14 @@ export default class Login extends BaseCommand {
this.error(`The "${SANDBOX_PROFILE}" profile is reserved for \`linq signup\`. Use --profile <name> to log in to a different profile.`);
}

// Show banner up front for interactive runs (skip when --token is
// passed — that path is for scripts / AI agents and shouldn't get
// animated decoration cluttering their logs).
if (!flags.token && process.stdout.isTTY) {
await renderBanner();
console.log('\n Welcome back to Linq CLI\n');
}

if (!profileName) {
// If --token was supplied, assume non-interactive (script / AI
// agent) — pick the current/default profile silently. Only the
Expand Down Expand Up @@ -82,8 +92,6 @@ export default class Login extends BaseCommand {
if (!process.stdin.isTTY) {
this.error('linq login needs a terminal for the interactive token prompt. To run non-interactively, pass --token: linq login --token <your-token>');
}
await renderBanner();
console.log('\n Welcome back to Linq CLI\n');
try {
token = await password({
message: 'Enter your API token:',
Expand All @@ -103,6 +111,7 @@ export default class Login extends BaseCommand {
this.log('\nValidating token...');
let orgId: string | undefined;
let name: string | undefined;
let email: string | undefined;
let partnerId: string | undefined;
let accountPhones: { phoneNumber: string }[] = [];
let accountLabel: AccountLabel | undefined;
Expand All @@ -121,6 +130,7 @@ export default class Login extends BaseCommand {
partnerId?: string;
orgId?: string;
name?: string | null;
email?: string | null;
accountInfo?: {
phones: { phoneNumber: string }[];
accountLabel?: AccountLabel;
Expand All @@ -129,6 +139,7 @@ export default class Login extends BaseCommand {
partnerId = acc.partnerId;
orgId = acc.orgId;
name = acc.name ?? undefined;
email = acc.email ?? undefined;
accountPhones = acc.accountInfo?.phones ?? [];
accountLabel = acc.accountInfo?.accountLabel;
} catch (e) {
Expand All @@ -147,7 +158,7 @@ export default class Login extends BaseCommand {
if (accountLabel === 'Paid') {
try {
fromPhone = await select({
message: 'Select a default Blue Number:',
message: 'Select a default Linq Number:',
choices: phones.map(p => ({ name: p.phoneNumber, value: p.phoneNumber })),
});
} catch (error) {
Expand All @@ -167,15 +178,26 @@ export default class Login extends BaseCommand {
...(partnerId && { partnerId }),
...(orgId && { orgId }),
...(name && { name }),
...(email && { email }),
accountLabel,
});
await setCurrentProfile(profileName);

const tier = getDisplayTier(accountLabel);
const line = getLineType(accountLabel);

this.log(chalk.green('✓ Welcome back!\n'));
if (accountLabel) this.log(` ${chalk.dim('Account:')} ${accountLabel}`);
if (fromPhone) this.log(` ${chalk.dim('Blue Number:')} ${chalk.bold(fromPhone)}`);
if (tier) this.log(` ${chalk.dim('Tier:')} ${tier}`);
if (line) this.log(` ${chalk.dim('Line:')} ${line}`);
if (fromPhone) this.log(` ${chalk.dim('Linq Number:')} ${chalk.bold(fromPhone)}`);
if (name) this.log(` ${chalk.dim('Name:')} ${name}`);
this.log(` ${chalk.dim('Profile:')} ${profileName}`);
if (line === 'Shared') {
this.log('');
this.log(` Shared line: add contacts with ${chalk.cyan('linq contacts add +1...')}, have them text you first, then reply.`);
} else if (tier === 'Free') {
this.log('');
this.log(' Free line is inbound-first: have someone text your Linq Number first, then reply.');
}
this.log('');
}
}
10 changes: 5 additions & 5 deletions src/commands/phonenumbers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { createApiClient } from '../lib/api-client.js';

export default class PhoneNumbers extends BaseCommand {
static override description = 'List your Blue Numbers or set a default';
static override description = 'List your Linq Numbers or set a default';

static override examples = [
'<%= config.bin %> <%= command.id %>',
Expand All @@ -16,7 +16,7 @@

static override args = {
action: Args.string({
description: 'Action: "set" to pick a default Blue Number',
description: 'Action: "set" to pick a default Linq Number',
required: false,
}),
};
Expand Down Expand Up @@ -48,13 +48,13 @@
let phones: { id: string; phone_number: string }[];
try {
const data = await client.phoneNumbers.list();
phones = (data as any).phone_numbers || [];

Check warning on line 51 in src/commands/phonenumbers.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
} catch (e) {
bail(this, flags.json, e);
}

if (phones.length === 0) {
this.log('\n No Blue Numbers found.\n');
this.log('\n No Linq Numbers found.\n');
return;
}

Expand All @@ -68,7 +68,7 @@
return;
}

this.log(`\n ${chalk.bold('Your Blue Numbers')}\n`);
this.log(`\n ${chalk.bold('Your Linq Numbers')}\n`);
for (const p of phones) {
const isDefault = p.phone_number === config.fromPhone;
this.log(` ${chalk.cyan(p.id)} ${this.formatPhone(p.phone_number)}${isDefault ? chalk.green(' ← default') : ''}`);
Expand All @@ -89,7 +89,7 @@

try {
const chosen = await select({
message: 'Select your default Blue Number:',
message: 'Select your default Linq Number:',
choices: phones.map(p => ({
name: p.phone_number === currentDefault
? `${this.formatPhone(p.phone_number)} (current default)`
Expand Down
2 changes: 1 addition & 1 deletion src/commands/profile/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default class ProfileCreate extends BaseCommand {
}),
'from-phone': Flags.string({
char: 'f',
description: 'Default sender Blue Number',
description: 'Default sender Linq Number',
}),
};

Expand Down
2 changes: 1 addition & 1 deletion src/commands/signup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { runAuthFlow, checkExistingSession } from '../lib/auth-flow.js';
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

export default class Signup extends BaseCommand {
static override description = 'Create a Linq developer account and get a shared Blue Number';
static override description = 'Create a Linq developer account and get a Shared line';

static override examples = [
'<%= config.bin %> <%= command.id %>',
Expand Down
Loading
Loading