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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,7 @@ linq webhooks listen --json
Events are displayed in a structured log format:

```
2024-01-15T10:30:45.123Z [message.received] message.id=msg_123 message.body="Hello world" message.chat_id=chat_456
2024-01-15T10:30:45.123Z [message.received] data.id=msg_123 data.chat.id=chat_456 data.body="Hello world"
```

Press `Ctrl+C` to stop. The CLI automatically cleans up the webhook subscription.
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@
},
"attachments": {
"description": "Manage file attachments"
},
"tokens": {
"description": "Manage API tokens"
},
"contacts": {
"description": "Manage shared-line contacts"
}
}
},
Expand Down
3 changes: 2 additions & 1 deletion src/commands/attachments/get.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Args, Flags } from '@oclif/core';
import { bail } from '../../lib/errors.js';
import { BaseCommand } from '../../lib/base-command.js';
import { loadConfig, requireToken } from '../../lib/config.js';
import { createApiClient } from '../../lib/api-client.js';
Expand Down Expand Up @@ -49,7 +50,7 @@ export default class AttachmentsGet extends BaseCommand {
this.log(formatAttachmentMeta(data));
}
} catch (e) {
this.error(`Failed to get attachment: ${e instanceof Error ? e.message : String(e)}`);
bail(this, flags.json, e);
}
}
}
3 changes: 2 additions & 1 deletion src/commands/attachments/upload.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Flags } from '@oclif/core';
import { bail } from '../../lib/errors.js';
import { BaseCommand } from '../../lib/base-command.js';
import { loadConfig, requireToken } from '../../lib/config.js';
import { createApiClient } from '../../lib/api-client.js';
Expand Down Expand Up @@ -59,7 +60,7 @@ export default class AttachmentsUpload extends BaseCommand {
this.log(formatUploadUrl(data));
}
} catch (e) {
this.error(`Failed to request upload: ${e instanceof Error ? e.message : String(e)}`);
bail(this, flags.json, e);
}
}
}
10 changes: 5 additions & 5 deletions src/commands/chats/create.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Flags } from '@oclif/core';
import { bail } from '../../lib/errors.js';
import chalk from 'chalk';
import { BaseCommand } from '../../lib/base-command.js';
import { loadConfig, requireToken, requireFromPhone } from '../../lib/config.js';
import { loadConfig, requireToken, requireFromPhone, isSharedLine } from '../../lib/config.js';
import { createApiClient } from '../../lib/api-client.js';
import { formatChatCreated } from '../../lib/format.js';
import { addBreadcrumb } from '../../lib/telemetry.js';
Expand Down Expand Up @@ -97,7 +98,7 @@ export default class ChatsCreate extends BaseCommand {
} else if (BUBBLE_EFFECTS.includes(flags.effect)) {
effect = { type: 'bubble', name: flags.effect };
} else {
this.error(`Invalid effect: ${flags.effect}. Valid effects: ${ALL_EFFECTS.join(', ')}`);
bail(this, flags.json, `Invalid effect: ${flags.effect}. Valid effects: ${ALL_EFFECTS.join(', ')}`);
}
}

Expand All @@ -120,17 +121,16 @@ export default class ChatsCreate extends BaseCommand {
}
} catch (e) {
if (e instanceof Linq.PermissionDeniedError) {
const lineType = config.tier === 0 && config.tenantType === 'SINGLE' ? 'sandbox' : 'shared';
this.log(chalk.yellow(`\n Can't message this contact yet.\n`));
if (lineType === 'shared') {
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(` 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.exit(1);
}
this.error(`Failed to create chat: ${e instanceof Error ? e.message : String(e)}`);
bail(this, flags.json, e);
}
}
}
3 changes: 2 additions & 1 deletion src/commands/chats/get.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Args, Flags } from '@oclif/core';
import { bail } from '../../lib/errors.js';
import { BaseCommand } from '../../lib/base-command.js';
import { loadConfig, requireToken } from '../../lib/config.js';
import { createApiClient } from '../../lib/api-client.js';
Expand Down Expand Up @@ -49,7 +50,7 @@ export default class ChatsGet extends BaseCommand {
this.log(formatChatDetail(data));
}
} catch (e) {
this.error(`Failed to get chat: ${e instanceof Error ? e.message : String(e)}`);
bail(this, flags.json, e);
}
}
}
7 changes: 4 additions & 3 deletions src/commands/chats/list.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Flags } from '@oclif/core';
import { bail } from '../../lib/errors.js';
import { BaseCommand } from '../../lib/base-command.js';
import { loadConfig, requireToken, requireFromPhone } from '../../lib/config.js';
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 phone number';
static override description = 'List all chats for a Blue Number';

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

static override flags = {
from: Flags.string({
description: 'Phone number to list chats for (E.164 format). Uses config fromPhone if not specified.',
description: 'Blue 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 Expand Up @@ -60,7 +61,7 @@ export default class ChatsList extends BaseCommand {
this.log(formatChatsList(data));
}
} catch (e) {
this.error(`Failed to list chats: ${e instanceof Error ? e.message : String(e)}`);
bail(this, flags.json, e);
}
}
}
3 changes: 2 additions & 1 deletion src/commands/chats/participants/add.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Args, Flags } from '@oclif/core';
import { bail } from '../../../lib/errors.js';
import chalk from 'chalk';
import { BaseCommand } from '../../../lib/base-command.js';
import { loadConfig, requireToken } from '../../../lib/config.js';
Expand Down Expand Up @@ -55,7 +56,7 @@ export default class ParticipantsAdd extends BaseCommand {
this.log(chalk.green(`\n \u2713 Added ${flags.handle} to chat.\n`));
}
} catch (e) {
this.error(`Failed to add participant: ${e instanceof Error ? e.message : String(e)}`);
bail(this, flags.json, e);
}
}
}
3 changes: 2 additions & 1 deletion src/commands/chats/participants/remove.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Args, Flags } from '@oclif/core';
import { bail } from '../../../lib/errors.js';
import chalk from 'chalk';
import { BaseCommand } from '../../../lib/base-command.js';
import { loadConfig, requireToken } from '../../../lib/config.js';
Expand Down Expand Up @@ -55,7 +56,7 @@ export default class ParticipantsRemove extends BaseCommand {
this.log(chalk.green(`\n \u2713 Removed ${flags.handle} from chat.\n`));
}
} catch (e) {
this.error(`Failed to remove participant: ${e instanceof Error ? e.message : String(e)}`);
bail(this, flags.json, e);
}
}
}
8 changes: 7 additions & 1 deletion src/commands/chats/read.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Args, Flags } from '@oclif/core';
import { bail } from '../../lib/errors.js';
import { BaseCommand } from '../../lib/base-command.js';
import { loadConfig, requireToken } from '../../lib/config.js';
import { createApiClient } from '../../lib/api-client.js';
Expand Down Expand Up @@ -26,6 +27,7 @@ export default class ChatsRead extends BaseCommand {
char: 't',
description: 'API token (overrides stored token)',
}),
json: Flags.boolean({ description: 'Output as JSON', default: false }),
};

async run(): Promise<void> {
Expand All @@ -37,9 +39,13 @@ export default class ChatsRead extends BaseCommand {

try {
await client.chats.markAsRead(args.chatId);
if (flags.json) {
this.log(JSON.stringify({ success: true, chatId: args.chatId, action: 'read' }, null, 2));
return;
}
this.log(`Chat ${args.chatId} marked as read.`);
} catch (e) {
this.error(`Failed to mark chat as read: ${e instanceof Error ? e.message : String(e)}`);
bail(this, flags.json, e);
}
}
}
8 changes: 7 additions & 1 deletion src/commands/chats/share-contact.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Args, Flags } from '@oclif/core';
import { bail } from '../../lib/errors.js';
import { BaseCommand } from '../../lib/base-command.js';
import { loadConfig, requireToken } from '../../lib/config.js';
import { createApiClient } from '../../lib/api-client.js';
Expand Down Expand Up @@ -26,6 +27,7 @@ export default class ChatsShareContact extends BaseCommand {
char: 't',
description: 'API token (overrides stored token)',
}),
json: Flags.boolean({ description: 'Output as JSON', default: false }),
};

async run(): Promise<void> {
Expand All @@ -37,9 +39,13 @@ export default class ChatsShareContact extends BaseCommand {

try {
await client.chats.shareContactCard(args.chatId);
if (flags.json) {
this.log(JSON.stringify({ success: true, chatId: args.chatId, action: 'share_contact' }, null, 2));
return;
}
this.log('Contact card shared successfully.');
} catch (e) {
this.error(`Failed to share contact: ${e instanceof Error ? e.message : String(e)}`);
bail(this, flags.json, e);
}
}
}
12 changes: 9 additions & 3 deletions src/commands/chats/typing.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Args, Flags } from '@oclif/core';
import { bail } from '../../lib/errors.js';
import { BaseCommand } from '../../lib/base-command.js';
import { loadConfig, requireToken } from '../../lib/config.js';
import { createApiClient } from '../../lib/api-client.js';
Expand Down Expand Up @@ -31,6 +32,7 @@ export default class ChatsTyping extends BaseCommand {
char: 't',
description: 'API token (overrides stored token)',
}),
json: Flags.boolean({ description: 'Output as JSON', default: false }),
};

async run(): Promise<void> {
Expand All @@ -41,15 +43,19 @@ export default class ChatsTyping extends BaseCommand {
const client = createApiClient(token);

try {
const action = flags.stop ? 'stop' : 'start';
if (flags.stop) {
await client.chats.typing.stop(args.chatId);
this.log('Typing indicator stopped.');
} else {
await client.chats.typing.start(args.chatId);
this.log('Typing indicator started.');
}
if (flags.json) {
this.log(JSON.stringify({ success: true, chatId: args.chatId, action: `typing.${action}` }, null, 2));
return;
}
this.log(flags.stop ? 'Typing indicator stopped.' : 'Typing indicator started.');
} catch (e) {
this.error(`Failed to ${flags.stop ? 'stop' : 'start'} typing: ${e instanceof Error ? e.message : String(e)}`);
bail(this, flags.json, e);
}
}
}
5 changes: 3 additions & 2 deletions src/commands/chats/update.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Args, Flags } from '@oclif/core';
import { bail } from '../../lib/errors.js';
import { BaseCommand } from '../../lib/base-command.js';
import { loadConfig, requireToken } from '../../lib/config.js';
import { createApiClient } from '../../lib/api-client.js';
Expand Down Expand Up @@ -44,7 +45,7 @@ export default class ChatsUpdate extends BaseCommand {
const { args, flags } = await this.parse(ChatsUpdate);

if (!flags.name && !flags.icon) {
this.error('At least one of --name or --icon must be specified');
bail(this, flags.json, 'At least one of --name or --icon must be specified');
}

const config = await loadConfig(flags.profile);
Expand All @@ -65,7 +66,7 @@ export default class ChatsUpdate extends BaseCommand {
this.log(formatChatDetail(chat));
}
} catch (e) {
this.error(`Failed to update chat: ${e instanceof Error ? e.message : String(e)}`);
bail(this, flags.json, e);
}
}
}
12 changes: 10 additions & 2 deletions src/commands/chats/voicememo.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Args, Flags } from '@oclif/core';
import { bail } from '../../lib/errors.js';
import { BaseCommand } from '../../lib/base-command.js';
import { loadConfig, requireToken } from '../../lib/config.js';
import { createApiClient } from '../../lib/api-client.js';
Expand Down Expand Up @@ -30,6 +31,7 @@ export default class ChatsVoicememo extends BaseCommand {
char: 't',
description: 'API token (overrides stored token)',
}),
json: Flags.boolean({ description: 'Output as JSON', default: false }),
};

async run(): Promise<void> {
Expand All @@ -44,9 +46,15 @@ export default class ChatsVoicememo extends BaseCommand {
voice_memo_url: flags.url,
});

this.log(JSON.stringify(data, null, 2));
if (flags.json) {
this.log(JSON.stringify(data, null, 2));
return;
}

const messageId = (data as { message?: { id?: string } })?.message?.id;
this.log(`Voice memo sent to chat ${args.chatId}${messageId ? ` (message ${messageId})` : ''}.`);
} catch (e) {
this.error(`Failed to send voice memo: ${e instanceof Error ? e.message : String(e)}`);
bail(this, flags.json, e);
}
}
}
40 changes: 22 additions & 18 deletions src/commands/contacts/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { BaseCommand } from '../../lib/base-command.js';
import { loadConfig, requireToken, requireSharedLine } from '../../lib/config.js';
import { BACKEND_URL } from '../../lib/api-client.js';
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';
Expand All @@ -19,6 +20,7 @@ export default class ContactsAdd extends BaseCommand {
static override flags = {
profile: Flags.string({ char: 'p', description: 'Config profile to use', hidden: true }),
token: Flags.string({ char: 't', description: 'API token', hidden: true }),
json: Flags.boolean({ description: 'Output as JSON', default: false }),
};

async run(): Promise<void> {
Expand All @@ -30,8 +32,7 @@ export default class ContactsAdd extends BaseCommand {

const orgId = config.orgId;
if (!orgId) {
this.log(chalk.yellow(`\n Not logged in. Run ${chalk.cyan('linq signup')} or ${chalk.cyan('linq login')}.\n`));
this.exit(1);
bail(this, flags.json, 'Not logged in. Run `linq signup` or `linq login`.');
}

try {
Expand All @@ -41,25 +42,28 @@ export default class ContactsAdd extends BaseCommand {
body: JSON.stringify({ orgId, contactPhone: args.phone }),
});

if (!res.ok) {
const err = await res.json() as { message?: string };
const reason = err.message || 'Unknown error';
this.log(chalk.red(`\n Failed to add contact: ${reason}\n`));
if (/another partner/i.test(reason)) {
this.log(chalk.dim(' Upgrade to a dedicated line to message without limits —'));
this.log(chalk.dim(' email support@linqapp.com to get started.\n'));
}
this.exit(1);
}
if (!res.ok) await throwHttpError(res);

const data = await res.json() as { contactPhone: string };
addBreadcrumb('Contact added');
this.log(chalk.green(`\n \u2713 Contact ${data.contactPhone} added.\n`));
this.log(chalk.dim(' Note: this contact must send a message to your Linq number first before you can reply.\n'));
} catch (error) {
if (error instanceof Error && 'oclif' in error) throw error;
this.log(chalk.red('\n Could not connect to Linq. Please try again later.\n'));
this.exit(1);

const blueNumber = config.fromPhone;

if (flags.json) {
this.log(JSON.stringify({
contactPhone: data.contactPhone,
blueNumber: blueNumber ?? 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`);
}
} catch (e) {
bail(this, flags.json, e);
}
}
}
Loading
Loading