From 10ff0ec5b012225a620bde23f06b49b8725b2b30 Mon Sep 17 00:00:00 2001 From: Abhiram Date: Tue, 26 May 2026 15:27:10 -0500 Subject: [PATCH] feat: linq number rename, tier+line model, upgrade command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "Blue Number" → "Linq Number" across all user-facing strings - Replace single "Account" label with two-axis "Tier" (Free/Paid) + "Line" (Shared/Dedicated). Server-side accountLabel unchanged; CLI maps for display. - New `linq upgrade` command — Free opens the upgrade URL, Paid shows Account-Manager contact info. Full --json support. - Banner moved to top of `linq init` and `linq login` (was sandwiched between prompts). - Drop the noisy "Profile:" line from login/init output. - Scoped post-signup / post-login messages per line type: Shared shows the contacts workflow, Free shows inbound-first reminder. - Signup "Get started" gains a `linq chats create` example so users discover the right command instead of guessing "linq send". - Capitalize "shared line" → "Shared line" in command descriptions and error messages. Drop internal "sandbox" wording from user-facing copy. - Preemptively read `email` from /cli/account-info; auto-populates in whoami once the backend ships email. --- README.md | 12 +++++++ src/commands/chats/create.ts | 4 +-- src/commands/chats/list.ts | 4 +-- src/commands/contacts/add.ts | 12 +++---- src/commands/contacts/list.ts | 2 +- src/commands/contacts/remove.ts | 2 +- src/commands/doctor.ts | 4 +-- src/commands/init.ts | 46 +++++++++++++++++------- src/commands/login.ts | 34 ++++++++++++++---- src/commands/phonenumbers.ts | 10 +++--- src/commands/profile/create.ts | 2 +- src/commands/signup.ts | 2 +- src/commands/upgrade.ts | 64 +++++++++++++++++++++++++++++++++ src/commands/whoami.ts | 12 +++++-- src/lib/auth-flow.ts | 41 +++++++++++++++------ src/lib/config.ts | 23 ++++++++++-- src/lib/format.ts | 4 +-- test/commands/doctor.test.ts | 4 +-- 18 files changed, 222 insertions(+), 60 deletions(-) create mode 100644 src/commands/upgrade.ts diff --git a/README.md b/README.md index c0b21aa..1d4103b 100644 --- a/README.md +++ b/README.md @@ -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 ` 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. diff --git a/src/commands/chats/create.ts b/src/commands/chats/create.ts index 92b6a62..2a5bcd4 100644 --- a/src/commands/chats/create.ts +++ b/src/commands/chats/create.ts @@ -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); } diff --git a/src/commands/chats/list.ts b/src/commands/chats/list.ts index f8bd0bc..b0b10c2 100644 --- a/src/commands/chats/list.ts +++ b/src/commands/chats/list.ts @@ -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 %>', @@ -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)', diff --git a/src/commands/contacts/add.ts b/src/commands/contacts/add.ts index 7237c96..419b6b9 100644 --- a/src/commands/contacts/add.ts +++ b/src/commands/contacts/add.ts @@ -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', @@ -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); diff --git a/src/commands/contacts/list.ts b/src/commands/contacts/list.ts index 9d0e728..9169bf9 100644 --- a/src/commands/contacts/list.ts +++ b/src/commands/contacts/list.ts @@ -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 %>', diff --git a/src/commands/contacts/remove.ts b/src/commands/contacts/remove.ts index ab28439..275887d 100644 --- a/src/commands/contacts/remove.ts +++ b/src/commands/contacts/remove.ts @@ -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', diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index a0e1200..751b833 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -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; diff --git a/src/commands/init.ts b/src/commands/init.ts index 5418ff4..0191c7d 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -1,5 +1,6 @@ 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, @@ -7,6 +8,8 @@ import { getCurrentProfile, listProfiles, SANDBOX_PROFILE, + getDisplayTier, + getLineType, type AccountLabel, } from '../lib/config.js'; import { BACKEND_URL } from '../lib/api-client.js'; @@ -36,6 +39,11 @@ export default class Init extends BaseCommand { this.error(`The "${SANDBOX_PROFILE}" profile is reserved for \`linq signup\`. Use --profile 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); @@ -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:', @@ -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; @@ -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; @@ -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) { @@ -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, @@ -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')}`); } } diff --git a/src/commands/login.ts b/src/commands/login.ts index 4c9a7ff..29af2e5 100644 --- a/src/commands/login.ts +++ b/src/commands/login.ts @@ -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'; @@ -42,6 +44,14 @@ export default class Login extends BaseCommand { this.error(`The "${SANDBOX_PROFILE}" profile is reserved for \`linq signup\`. Use --profile 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 @@ -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 '); } - await renderBanner(); - console.log('\n Welcome back to Linq CLI\n'); try { token = await password({ message: 'Enter your API token:', @@ -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; @@ -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; @@ -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) { @@ -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) { @@ -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(''); } } diff --git a/src/commands/phonenumbers.ts b/src/commands/phonenumbers.ts index f4ef0a1..af0901d 100644 --- a/src/commands/phonenumbers.ts +++ b/src/commands/phonenumbers.ts @@ -7,7 +7,7 @@ import { loadConfig, requireToken, saveProfile, saveSandboxProfile, getCurrentPr 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 %>', @@ -16,7 +16,7 @@ export default class PhoneNumbers extends BaseCommand { 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, }), }; @@ -54,7 +54,7 @@ export default class PhoneNumbers extends BaseCommand { } if (phones.length === 0) { - this.log('\n No Blue Numbers found.\n'); + this.log('\n No Linq Numbers found.\n'); return; } @@ -68,7 +68,7 @@ export default class PhoneNumbers extends BaseCommand { 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') : ''}`); @@ -89,7 +89,7 @@ export default class PhoneNumbers extends BaseCommand { 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)` diff --git a/src/commands/profile/create.ts b/src/commands/profile/create.ts index f8f3d22..5263332 100644 --- a/src/commands/profile/create.ts +++ b/src/commands/profile/create.ts @@ -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', }), }; diff --git a/src/commands/signup.ts b/src/commands/signup.ts index 9d42576..746437d 100644 --- a/src/commands/signup.ts +++ b/src/commands/signup.ts @@ -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 %>', diff --git a/src/commands/upgrade.ts b/src/commands/upgrade.ts new file mode 100644 index 0000000..73befcc --- /dev/null +++ b/src/commands/upgrade.ts @@ -0,0 +1,64 @@ +import { Flags } from '@oclif/core'; +import open from 'open'; +import chalk from 'chalk'; +import { BaseCommand } from '../lib/base-command.js'; +import { loadConfig, getAccountLabel, getDisplayTier } from '../lib/config.js'; + +const UPGRADE_URL = 'https://linqapp.com/s/talk-to-an-expert'; +const SUPPORT_EMAIL = 'support@linqapp.com'; +const PAID_MESSAGE = + 'Please contact your Linq Account Manager to add more lines or email support@linqapp.com.'; + +export default class Upgrade extends BaseCommand { + static override description = 'Upgrade your Linq plan'; + + static override examples = [ + '<%= config.bin %> <%= command.id %>', + '<%= config.bin %> <%= command.id %> --json', + ]; + + static override flags = { + profile: Flags.string({ char: 'p', description: 'Config profile to use' }), + json: Flags.boolean({ description: 'Output as JSON', default: false }), + }; + + async run(): Promise { + const { flags } = await this.parse(Upgrade); + const config = await loadConfig(flags.profile); + const tier = getDisplayTier(getAccountLabel(config)); + + if (tier === 'Paid') { + if (flags.json) { + this.log(JSON.stringify({ + tier: 'Paid', + action: 'contact', + message: PAID_MESSAGE, + supportEmail: SUPPORT_EMAIL, + }, null, 2)); + return; + } + this.log(chalk.green('\n ✓ You\'re already on Paid.\n')); + this.log(` ${PAID_MESSAGE}\n`); + return; + } + + // Free (or unknown — treat as Free since this is the upgrade path) + if (flags.json) { + this.log(JSON.stringify({ + tier: tier ?? 'Free', + action: 'open', + url: UPGRADE_URL, + }, null, 2)); + return; + } + + this.log(`\n Opening ${chalk.cyan(UPGRADE_URL)}\n`); + if (process.stdout.isTTY) { + try { + await open(UPGRADE_URL); + } catch { + // Silent — URL is already printed so the user can copy it. + } + } + } +} diff --git a/src/commands/whoami.ts b/src/commands/whoami.ts index 322cb8a..a7de5f0 100644 --- a/src/commands/whoami.ts +++ b/src/commands/whoami.ts @@ -6,6 +6,8 @@ import { requireToken, isSessionExpired, getAccountLabel, + getDisplayTier, + getLineType, } from '../lib/config.js'; export default class Whoami extends BaseCommand { @@ -39,6 +41,8 @@ export default class Whoami extends BaseCommand { const maskedToken = token.length > 12 ? `${token.slice(0, 12)}...` : token; const accountLabel = getAccountLabel(config); + const tier = getDisplayTier(accountLabel); + const line = getLineType(accountLabel); if (flags.json) { this.log(JSON.stringify({ @@ -48,16 +52,18 @@ export default class Whoami extends BaseCommand { apiKey: maskedToken, orgId: config.orgId, partnerId: config.partnerId, - accountLabel, + tier, + line, }, null, 2)); return; } this.log(''); - if (accountLabel) this.log(` ${chalk.dim('Account:')} ${accountLabel}`); + if (tier) this.log(` ${chalk.dim('Tier:')} ${tier}`); + if (line) this.log(` ${chalk.dim('Line:')} ${line}`); if (config.name) this.log(` ${chalk.dim('Name:')} ${config.name}`); if (config.email) this.log(` ${chalk.dim('Email:')} ${config.email} ${chalk.dim('(login)')}`); - if (config.fromPhone) this.log(` ${chalk.dim('Blue Number:')} ${config.fromPhone} ${chalk.dim('(your Linq line)')}`); + if (config.fromPhone) this.log(` ${chalk.dim('Linq Number:')} ${config.fromPhone}`); this.log(` ${chalk.dim('API Key:')} ${maskedToken}`); this.log(''); } diff --git a/src/lib/auth-flow.ts b/src/lib/auth-flow.ts index 77ad98f..40ca9a3 100644 --- a/src/lib/auth-flow.ts +++ b/src/lib/auth-flow.ts @@ -6,6 +6,8 @@ import { setCurrentProfile, loadConfig, isSessionExpired, + getDisplayTier, + getLineType, } from './config.js'; import { BACKEND_URL } from './api-client.js'; import { addBreadcrumb } from './telemetry.js'; @@ -259,8 +261,11 @@ export async function runAuthFlow(opts: AuthFlowOptions): Promise { addBreadcrumb('Account created', { phone: phoneNumber }); log(''); log(chalk.green(' ✓ Account created!\n')); - if (accountLabel) log(` ${chalk.dim('Account:')} ${accountLabel}`); - log(` ${chalk.dim('Blue Number:')} ${chalk.bold(phoneNumber || 'pending')}`); + const tier = getDisplayTier(accountLabel); + const line = getLineType(accountLabel); + if (tier) log(` ${chalk.dim('Tier:')} ${tier}`); + if (line) log(` ${chalk.dim('Line:')} ${line}`); + log(` ${chalk.dim('Linq Number:')} ${chalk.bold(phoneNumber || 'pending')}`); log(` ${chalk.dim('Email:')} ${verifyResult.email}`); log(` ${chalk.dim('API Key:')} ${chalk.bold(verifyResult.token)}`); log(''); @@ -277,14 +282,18 @@ export async function runAuthFlow(opts: AuthFlowOptions): Promise { log(chalk.yellow(' ⚠ Save this token securely — it will not be shown again.')); } log(''); - if (phoneNumber && accountLabel === 'Shared') { - log(' Your Blue Number is shared and allows a max of 20 contacts.'); - log(' Start by adding a contact. Your Blue Number is inbound-first:'); - log(' others text you first and then you can start the conversation.\n'); + if (line === 'Shared') { + log(' Your Shared line allows up to 20 contacts.'); + log(` Start by adding one with ${chalk.cyan('linq contacts add +1...')}.`); + log(' They must text your Linq Number first, then you can reply.\n'); + } else if (tier === 'Free') { + log(' Your Free line is inbound-first.'); + log(' Anyone can text your Linq Number first, then you can reply.\n'); } log(' Get started:\n'); - log(` ${chalk.cyan('linq contacts add +1234567890')} ${chalk.dim('# Add a contact')}`); - log(` ${chalk.cyan('linq webhooks listen')} ${chalk.dim('# Watch for incoming events')}`); + log(` ${chalk.cyan('linq contacts add +1234567890')} ${chalk.dim('# Add a contact')}`); + log(` ${chalk.cyan('linq webhooks listen')} ${chalk.dim('# Watch for incoming events')}`); + log(` ${chalk.cyan('linq chats create --to +1234567890 -m "Hi from CLI"')} ${chalk.dim('# Send a message')}`); log(''); log(` ${chalk.dim('Full API docs:')} https://apidocs.linqapp.com`); log(''); @@ -292,15 +301,25 @@ export async function runAuthFlow(opts: AuthFlowOptions): Promise { addBreadcrumb('Login successful', { accountType: accountLabel || 'unknown' }); log(''); log(chalk.green(' ✓ Welcome back!\n')); - if (accountLabel) log(` ${chalk.dim('Account:')} ${accountLabel}`); + const tier = getDisplayTier(accountLabel); + const line = getLineType(accountLabel); + if (tier) log(` ${chalk.dim('Tier:')} ${tier}`); + if (line) log(` ${chalk.dim('Line:')} ${line}`); if (phones.length > 1) { - log(` ${chalk.dim('Blue Number:')} ${chalk.yellow(`${phones.length} Blue Numbers available`)}`); + log(` ${chalk.dim('Linq Number:')} ${chalk.yellow(`${phones.length} Linq Numbers available`)}`); log(` Run ${chalk.cyan('linq phonenumbers set')} to pick a default.`); } else { - log(` ${chalk.dim('Blue Number:')} ${chalk.bold(phoneNumber || 'none')}`); + log(` ${chalk.dim('Linq Number:')} ${chalk.bold(phoneNumber || 'none')}`); } log(` ${chalk.dim('Email:')} ${verifyResult.email}`); log(` ${chalk.dim('API Key:')} ${verifyResult.token}`); + if (line === 'Shared') { + log(''); + log(` Shared line: add contacts with ${chalk.cyan('linq contacts add +1...')}, have them text you first, then reply.`); + } else if (tier === 'Free') { + log(''); + log(' Free line is inbound-first: have someone text your Linq Number first, then reply.'); + } log(''); } } diff --git a/src/lib/config.ts b/src/lib/config.ts index 959fc4a..9c949c0 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -278,10 +278,29 @@ export function requireFromPhone( // ── Account type helpers ───────────────────────────────────────── +export type DisplayTier = 'Free' | 'Paid'; +export type LineType = 'Shared' | 'Dedicated'; + export function getAccountLabel(config: Profile): AccountLabel | undefined { return config.accountLabel; } +// User-facing tier (Free or Paid). Collapses Shared/Sandbox into Free +// so the CLI surfaces only Free vs Paid externally. +export function getDisplayTier(label: AccountLabel | undefined): DisplayTier | undefined { + if (label === 'Paid') return 'Paid'; + if (label === 'Shared' || label === 'Sandbox') return 'Free'; + return undefined; +} + +// User-facing line type (Shared or Dedicated). Shared lines route +// many partners through the same number; Dedicated is one-owner. +export function getLineType(label: AccountLabel | undefined): LineType | undefined { + if (label === 'Shared') return 'Shared'; + if (label === 'Sandbox' || label === 'Paid') return 'Dedicated'; + return undefined; +} + export const isSandbox = (config: Profile): boolean => getAccountLabel(config) === 'Sandbox'; @@ -294,12 +313,12 @@ export const isPaid = (config: Profile): boolean => export function requireSharedLine(config: Profile): void { if (isSandbox(config)) { throw new Errors.CLIError( - 'This command is for shared line accounts only.\nYour sandbox account can text any number directly (an inbound message is needed first).' + 'This command is for Shared line accounts only.\nYour Free line can text any number directly once they text you first.' ); } if (isPaid(config)) { throw new Errors.CLIError( - 'This command is for shared line accounts only.\nYour account can text any number directly — no contacts needed.' + 'This command is for Shared line accounts only.\nYour account can text any number directly — no contacts needed.' ); } } diff --git a/src/lib/format.ts b/src/lib/format.ts index ee8c602..aff6e89 100644 --- a/src/lib/format.ts +++ b/src/lib/format.ts @@ -87,9 +87,9 @@ interface PhoneNumberInfo { export function formatPhoneNumbers(data: { phone_numbers: PhoneNumberInfo[] | null }): string { const phones = data.phone_numbers || []; - if (phones.length === 0) return '\n No Blue Numbers found.\n'; + if (phones.length === 0) return '\n No Linq Numbers found.\n'; - const lines = ['\n ' + chalk.bold('Your Blue Numbers') + '\n']; + const lines = ['\n ' + chalk.bold('Your Linq Numbers') + '\n']; for (const p of phones) { lines.push(` ${chalk.cyan(p.id)} ${fmtPhone(p.phone_number)}`); } diff --git a/test/commands/doctor.test.ts b/test/commands/doctor.test.ts index b8ea383..0f9d275 100644 --- a/test/commands/doctor.test.ts +++ b/test/commands/doctor.test.ts @@ -86,7 +86,7 @@ describe('doctor', () => { const output = logs.join('\n'); expect(output).toContain('\u2713 Config file found'); expect(output).toContain('\u2713 API token configured'); - expect(output).toContain('\u2713 Blue Number set'); + expect(output).toContain('\u2713 Linq Number set'); expect(output).toContain('\u2713 API connected'); expect(output).toContain('passed'); }); @@ -99,7 +99,7 @@ describe('doctor', () => { const output = logs.join('\n'); expect(output).toContain('\u2717 API token not configured'); - expect(output).toContain('\u2717 Blue Number not set'); + expect(output).toContain('\u2717 Linq Number not set'); expect(output).not.toContain('0 issues found'); });