From 82324f5192c4918260f57b237fc1395252ebe190 Mon Sep 17 00:00:00 2001 From: Or Dvir Date: Tue, 11 Aug 2026 17:40:27 +0300 Subject: [PATCH 1/2] =?UTF-8?q?AIT-385:=20dedupe=20alert=20phone=20command?= =?UTF-8?q?s=20=E2=80=94=20alerts=20phone=20is=20the=20one=20surface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete the top-level `hookmyapp phone` command (never published): its set still carried per-category consent flags from before the consent model changed, and it duplicated `alerts phone` wholesale. The consents subcommand survives as `alerts phone consents` (the opt-out path), and the stale consent fields in the alerts set action type are gone. --- src/commands/__tests__/alerts.test.ts | 36 ++++- src/commands/__tests__/org-profile.test.ts | 76 +++++++++ src/commands/__tests__/phone.test.ts | 141 ----------------- src/commands/alerts.ts | 77 ++++++++- src/commands/phone.ts | 176 --------------------- src/index.ts | 2 - 6 files changed, 187 insertions(+), 321 deletions(-) create mode 100644 src/commands/__tests__/org-profile.test.ts delete mode 100644 src/commands/__tests__/phone.test.ts delete mode 100644 src/commands/phone.ts diff --git a/src/commands/__tests__/alerts.test.ts b/src/commands/__tests__/alerts.test.ts index 4f4e735..6de3108 100644 --- a/src/commands/__tests__/alerts.test.ts +++ b/src/commands/__tests__/alerts.test.ts @@ -6,7 +6,7 @@ vi.mock('../../api/client.js', () => ({ })); import { apiClient } from '../../api/client.js'; -import { alertPhoneRemove, alertPhoneSet, alertPhoneStatus, alertPhoneVerify } from '../alerts.js'; +import { alertPhoneConsents, alertPhoneRemove, alertPhoneSet, alertPhoneStatus, alertPhoneVerify } from '../alerts.js'; const VERIFIED = { phone: '+141•••2671', @@ -115,3 +115,37 @@ describe('alerts phone remove', () => { expect(apiClient).toHaveBeenCalledWith('/auth/phone', { method: 'DELETE' }); }); }); + +describe('alerts phone consents', () => { + beforeEach(() => { + vi.mocked(apiClient).mockReset(); + vi.spyOn(console, 'log').mockImplementation(() => {}); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + test('When flags are given, then only those fields are PATCHed', async () => { + // Arrange + vi.mocked(apiClient).mockResolvedValueOnce(VERIFIED); + // Act + await alertPhoneConsents({ marketing: 'off', prefer: 'sms', json: true }); + // Assert + const [path, init] = vi.mocked(apiClient).mock.calls[0]; + expect(path).toBe('/auth/phone/consents'); + expect(JSON.parse((init as { body: string }).body)).toEqual({ marketing: false, channelPreference: 'sms' }); + }); + + test('When no flags are given, then it fails locally without calling the API', async () => { + // Act + Assert + await expect(alertPhoneConsents({})).rejects.toThrow(/Nothing to update/); + expect(apiClient).not.toHaveBeenCalled(); + }); + + test('When a consent value is not on or off, then it is rejected before any call', async () => { + // Act + Assert + await expect(alertPhoneConsents({ product: 'yes' })).rejects.toThrow(/"on" or "off"/); + expect(apiClient).not.toHaveBeenCalled(); + }); +}); diff --git a/src/commands/__tests__/org-profile.test.ts b/src/commands/__tests__/org-profile.test.ts new file mode 100644 index 0000000..43d8e42 --- /dev/null +++ b/src/commands/__tests__/org-profile.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { Command } from 'commander'; + +vi.mock('../../api/client.js', () => ({ + apiClient: vi.fn(), +})); +vi.mock('../_helpers.js', () => ({ + getDefaultWorkspaceId: vi.fn().mockResolvedValue('ws_TEST0001'), + resolveOrgPublicIdForWorkspace: vi.fn().mockResolvedValue('org_abc12345'), +})); + +const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}); + +import { apiClient } from '../../api/client.js'; +import { registerOrgProfileCommand } from '../org-profile.js'; + +const mockedApi = vi.mocked(apiClient); + +function makeProgram(): Command { + const program = new Command(); + program.exitOverride(); + program.option('--json'); + registerOrgProfileCommand(program); + return program; +} + +beforeEach(() => { + mockedApi.mockReset(); + mockConsoleLog.mockClear(); +}); + +describe('hookmyapp org profile', () => { + const PROFILE = { + publicId: 'org_abc12345', + name: 'Acme', + email: null, + phone: null, + website: 'https://acme.com', + businessCategory: null, + businessNiche: null, + primaryUseCase: null, + }; + + it('org profile show GETs the org summary', async () => { + mockedApi.mockResolvedValue(PROFILE); + + await makeProgram().parseAsync(['node', 'hookmyapp', 'org', 'profile', 'show', '--json']); + + expect(mockedApi).toHaveBeenCalledWith('/organizations/org_abc12345'); + expect(mockConsoleLog.mock.calls[0][0]).toContain('acme.com'); + }); + + it('org profile set PATCHes only the provided fields', async () => { + mockedApi.mockResolvedValue(PROFILE); + + await makeProgram().parseAsync([ + 'node', 'hookmyapp', 'org', 'profile', 'set', + '--website', 'https://acme.com', '--business-category', 'E-commerce', + ]); + + const [path, init] = mockedApi.mock.calls[0]; + expect(path).toBe('/organizations/org_abc12345/profile'); + expect((init as { method: string }).method).toBe('PATCH'); + expect(JSON.parse((init as { body: string }).body)).toEqual({ + website: 'https://acme.com', + businessCategory: 'E-commerce', + }); + }); + + it('org profile set with no flags fails locally without calling the API', async () => { + await expect( + makeProgram().parseAsync(['node', 'hookmyapp', 'org', 'profile', 'set']), + ).rejects.toThrow(/Nothing to update/); + expect(mockedApi).not.toHaveBeenCalled(); + }); +}); diff --git a/src/commands/__tests__/phone.test.ts b/src/commands/__tests__/phone.test.ts deleted file mode 100644 index 7381035..0000000 --- a/src/commands/__tests__/phone.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { Command } from 'commander'; - -vi.mock('../../api/client.js', () => ({ - apiClient: vi.fn(), -})); -vi.mock('../_helpers.js', () => ({ - getDefaultWorkspaceId: vi.fn().mockResolvedValue('ws_TEST0001'), - resolveOrgPublicIdForWorkspace: vi.fn().mockResolvedValue('org_abc12345'), -})); - -const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}); - -import { apiClient } from '../../api/client.js'; -import { registerPhoneCommand } from '../phone.js'; -import { registerOrgProfileCommand } from '../org-profile.js'; - -const mockedApi = vi.mocked(apiClient); - -const STATUS = { - phone: '+155•••4567', - verified: true, - consents: { operational: true, product: false, marketing: false }, - channelPreference: 'whatsapp', -}; - -function makeProgram(): Command { - const program = new Command(); - program.exitOverride(); - program.option('--json'); - registerPhoneCommand(program); - registerOrgProfileCommand(program); - return program; -} - -beforeEach(() => { - mockedApi.mockReset(); - mockConsoleLog.mockClear(); -}); - -describe('hookmyapp phone', () => { - it('phone status GETs /auth/phone and prints JSON with --json', async () => { - mockedApi.mockResolvedValue(STATUS); - - await makeProgram().parseAsync(['node', 'hookmyapp', 'phone', 'status', '--json']); - - expect(mockedApi).toHaveBeenCalledWith('/auth/phone'); - expect(mockConsoleLog.mock.calls[0][0]).toContain('+155•••4567'); - }); - - it('phone set normalizes the number, defaults operational on, POSTs /auth/phone', async () => { - mockedApi.mockResolvedValue({ delivery: 'sent' }); - - await makeProgram().parseAsync(['node', 'hookmyapp', 'phone', 'set', '+1 (555) 123-4567', '--product']); - - const [path, init] = mockedApi.mock.calls[0]; - expect(path).toBe('/auth/phone'); - expect(JSON.parse((init as { body: string }).body)).toEqual({ - phone: '+15551234567', - consentOperational: true, - consentProduct: true, - consentMarketing: false, - channelPreference: 'whatsapp', - }); - }); - - it('phone set rejects a non-E.164 number without calling the API', async () => { - await expect( - makeProgram().parseAsync(['node', 'hookmyapp', 'phone', 'set', 'not-a-number']), - ).rejects.toThrow(/international format/); - expect(mockedApi).not.toHaveBeenCalled(); - }); - - it('phone verify POSTs the code and rejects non-6-digit codes locally', async () => { - mockedApi.mockResolvedValue(STATUS); - await makeProgram().parseAsync(['node', 'hookmyapp', 'phone', 'verify', '123456']); - expect(mockedApi).toHaveBeenCalledWith('/auth/phone/verify', expect.objectContaining({ method: 'POST' })); - - mockedApi.mockClear(); - await expect( - makeProgram().parseAsync(['node', 'hookmyapp', 'phone', 'verify', '12']), - ).rejects.toThrow(/6 digits/); - expect(mockedApi).not.toHaveBeenCalled(); - }); - - it('phone consents PATCHes only the provided flags', async () => { - mockedApi.mockResolvedValue(STATUS); - - await makeProgram().parseAsync(['node', 'hookmyapp', 'phone', 'consents', '--marketing', 'on', '--prefer', 'both']); - - const [path, init] = mockedApi.mock.calls[0]; - expect(path).toBe('/auth/phone/consents'); - expect(JSON.parse((init as { body: string }).body)).toEqual({ marketing: true, channelPreference: 'both' }); - }); -}); - -describe('hookmyapp org profile', () => { - const PROFILE = { - publicId: 'org_abc12345', - name: 'Acme', - email: null, - phone: null, - website: 'https://acme.com', - businessCategory: null, - businessNiche: null, - primaryUseCase: null, - }; - - it('org profile show GETs the org summary', async () => { - mockedApi.mockResolvedValue(PROFILE); - - await makeProgram().parseAsync(['node', 'hookmyapp', 'org', 'profile', 'show', '--json']); - - expect(mockedApi).toHaveBeenCalledWith('/organizations/org_abc12345'); - expect(mockConsoleLog.mock.calls[0][0]).toContain('acme.com'); - }); - - it('org profile set PATCHes only the provided fields', async () => { - mockedApi.mockResolvedValue(PROFILE); - - await makeProgram().parseAsync([ - 'node', 'hookmyapp', 'org', 'profile', 'set', - '--website', 'https://acme.com', '--business-category', 'E-commerce', - ]); - - const [path, init] = mockedApi.mock.calls[0]; - expect(path).toBe('/organizations/org_abc12345/profile'); - expect((init as { method: string }).method).toBe('PATCH'); - expect(JSON.parse((init as { body: string }).body)).toEqual({ - website: 'https://acme.com', - businessCategory: 'E-commerce', - }); - }); - - it('org profile set with no flags fails locally without calling the API', async () => { - await expect( - makeProgram().parseAsync(['node', 'hookmyapp', 'org', 'profile', 'set']), - ).rejects.toThrow(/Nothing to update/); - expect(mockedApi).not.toHaveBeenCalled(); - }); -}); diff --git a/src/commands/alerts.ts b/src/commands/alerts.ts index 0fb28c3..f9cd1ad 100644 --- a/src/commands/alerts.ts +++ b/src/commands/alerts.ts @@ -181,6 +181,60 @@ export async function alertPhoneRemove(opts: { json?: boolean; yes?: boolean } = console.log(c.success('Alert phone removed.')); } +/** Opt-out / delivery-preference updates without re-verifying (PATCH /auth/phone/consents). */ +export async function alertPhoneConsents( + opts: { + json?: boolean; + operational?: string; + product?: string; + marketing?: string; + prefer?: string; + } = {}, +): Promise { + const toBool = (v: string | undefined, flag: string): boolean | undefined => { + if (v === undefined) return undefined; + if (v !== 'on' && v !== 'off') throw new ValidationError(`${flag} must be "on" or "off"`, 'ALERT_PHONE_CONSENT_FLAG'); + return v === 'on'; + }; + const body: Record = {}; + const operational = toBool(opts.operational, '--operational'); + const product = toBool(opts.product, '--product'); + const marketing = toBool(opts.marketing, '--marketing'); + if (operational !== undefined) body.operational = operational; + if (product !== undefined) body.product = product; + if (marketing !== undefined) body.marketing = marketing; + if (opts.prefer !== undefined) { + if (!['whatsapp', 'sms', 'both'].includes(opts.prefer)) { + throw new ValidationError('--prefer must be whatsapp, sms, or both', 'ALERT_PHONE_PREFER_FLAG'); + } + body.channelPreference = opts.prefer; + } + if (Object.keys(body).length === 0) { + throw new ValidationError( + 'Nothing to update — pass at least one of --operational/--product/--marketing/--prefer', + 'ALERT_PHONE_CONSENTS_EMPTY', + ); + } + const status = (await apiClient('/auth/phone/consents', { + method: 'PATCH', + body: JSON.stringify(body), + })) as AlertPhoneStatus; + if (opts.json) { + output(status, { json: true }); + return; + } + output( + { + phone: status.phone, + delivery: status.channelPreference, + 'problem alerts': status.consents.operational ? 'on' : 'off', + 'product news': status.consents.product ? 'on' : 'off', + offers: status.consents.marketing ? 'on' : 'off', + }, + { json: false, kind: 'read' }, + ); +} + export function registerAlertsCommand(_program: Command): void { const alerts = _program.command('alerts').description('Where we reach you when something breaks'); const phone = alerts.command('phone').description('Your alert phone number'); @@ -198,11 +252,23 @@ export function registerAlertsCommand(_program: Command): void { .description('Add or change your alert phone (international format, e.g. +14155552671)') .option('--sms', 'Deliver by SMS instead of WhatsApp') .option('--code ', 'Skip the prompt and verify with this code') - .action(async (phoneArg: string, cmdOpts: { sms?: boolean; product?: boolean; marketing?: boolean; code?: string }) => { + .action(async (phoneArg: string, cmdOpts: { sms?: boolean; code?: string }) => { const { program: rootProgram } = await import('../index.js'); await alertPhoneSet(phoneArg, { ...cmdOpts, json: !!rootProgram.opts().json }); }); + const consentsCmd = phone + .command('consents') + .description('Update what the number receives / delivery preference') + .option('--operational ', 'Problem alerts') + .option('--product ', 'Product news') + .option('--marketing ', 'Offers') + .option('--prefer ', 'Delivery channel: whatsapp | sms | both') + .action(async (cmdOpts: { operational?: string; product?: string; marketing?: string; prefer?: string }) => { + const { program: rootProgram } = await import('../index.js'); + await alertPhoneConsents({ ...cmdOpts, json: !!rootProgram.opts().json }); + }); + const removeCmd = phone .command('remove') .description('Remove your alert phone') @@ -271,6 +337,15 @@ EXAMPLES: EXAMPLES: $ hookmyapp alerts phone verify 123456 $ hookmyapp alerts phone verify 123456 --json +`, + ); + + addExamples( + consentsCmd, + ` +EXAMPLES: + $ hookmyapp alerts phone consents --marketing off + $ hookmyapp alerts phone consents --prefer sms `, ); } diff --git a/src/commands/phone.ts b/src/commands/phone.ts deleted file mode 100644 index a14ae1f..0000000 --- a/src/commands/phone.ts +++ /dev/null @@ -1,176 +0,0 @@ -import type { Command } from 'commander'; -import { apiClient } from '../api/client.js'; -import { output } from '../output/format.js'; -import { ValidationError } from '../output/error.js'; -import { addExamples } from '../output/help.js'; - -/** - * AIT-370 — `hookmyapp phone`: the calling user's PRIVATE alert phone - * (breakdown/product/marketing notifications over WhatsApp/SMS). Thin wrapper - * over POST/GET /auth/phone + /auth/phone/verify + PATCH /auth/phone/consents. - * - * Agent etiquette: the number and the code come from the HUMAN. Never invent, - * guess, or reuse either; a decline is a normal outcome — skip and continue. - */ - -interface PhoneStatus { - phone: string | null; - verified: boolean; - consents: { operational: boolean; product: boolean; marketing: boolean }; - channelPreference: string; -} - -const E164 = /^\+[1-9]\d{6,14}$/; - -function printStatus(status: PhoneStatus, json: boolean): void { - if (json) { - console.log(JSON.stringify(status, null, 2)); - return; - } - output( - [ - { - PHONE: status.phone ?? '(none)', - VERIFIED: status.verified ? 'yes' : 'no', - OPERATIONAL: status.consents.operational ? 'on' : 'off', - PRODUCT: status.consents.product ? 'on' : 'off', - MARKETING: status.consents.marketing ? 'on' : 'off', - PREFERENCE: status.channelPreference, - }, - ], - { human: true }, - ); -} - -export function registerPhoneCommand(program: Command): void { - const phone = program - .command('phone') - .description( - 'Your personal alert phone — HookMyApp texts it (WhatsApp/SMS) when your integration ' + - 'breaks. Ask the human for THEIR number and the code they receive; never invent either. ' + - 'Declining is fine — nothing else depends on it.', - ); - addExamples( - phone, - ` -EXAMPLES: - $ hookmyapp phone status - $ hookmyapp phone set +14155552671 --product - $ hookmyapp phone verify 123456 -`, - ); - - const status = phone - .command('status', { isDefault: true }) - .description('Show the verified alert phone (masked) and notification consents') - .option('--json', 'Output machine-readable JSON') - .action(async (opts: { json?: boolean }) => { - const res = (await apiClient('/auth/phone')) as PhoneStatus; - printStatus(res, Boolean(opts.json || program.opts().json)); - }); - addExamples(status, `\nEXAMPLES:\n $ hookmyapp phone status\n $ hookmyapp phone status --json\n`); - - const set = phone - .command('set') - .description('Start verification: sends a 6-digit code to the number (international format)') - .argument('', 'The phone number in international format, e.g. +14155552671') - .option('--no-operational', 'Opt out of breakdown alerts (on by default)') - .option('--product', 'Opt in to feature announcements') - .option('--marketing', 'Opt in to offers/promotions') - .option('--prefer ', 'Delivery channel: whatsapp | sms | both', 'whatsapp') - .option('--json', 'Output machine-readable JSON') - .action( - async ( - number: string, - opts: { operational?: boolean; product?: boolean; marketing?: boolean; prefer?: string; json?: boolean }, - ) => { - const cleaned = number.replace(/[\s\-().]/g, ''); - if (!E164.test(cleaned)) { - // Static message: ValidationError reaches Sentry via captureException - // when telemetry is on — never interpolate the raw number. - throw new ValidationError( - 'Invalid phone number — use international format, e.g. +14155552671.', - ); - } - if (!['whatsapp', 'sms', 'both'].includes(opts.prefer ?? 'whatsapp')) { - throw new ValidationError('--prefer must be whatsapp, sms, or both'); - } - const res = (await apiClient('/auth/phone', { - method: 'POST', - body: JSON.stringify({ - phone: cleaned, - consentOperational: opts.operational ?? true, - consentProduct: opts.product ?? false, - consentMarketing: opts.marketing ?? false, - channelPreference: opts.prefer ?? 'whatsapp', - }), - })) as { delivery: 'sent' | 'unavailable' }; - if (opts.json || program.opts().json) { - console.log(JSON.stringify(res, null, 2)); - return; - } - if (res.delivery === 'sent') { - console.log('Code sent. Ask the human for the 6-digit code, then run: hookmyapp phone verify '); - } else { - console.log('Code delivery is unavailable in this environment. Verify later from the web app.'); - } - }, - ); - addExamples(set, `\nEXAMPLES:\n $ hookmyapp phone set +14155552671\n $ hookmyapp phone set +14155552671 --product --prefer both\n`); - - const verify = phone - .command('verify') - .description('Complete verification with the 6-digit code the human received') - .argument('', 'The 6-digit code from the phone') - .option('--json', 'Output machine-readable JSON') - .action(async (code: string, opts: { json?: boolean }) => { - if (!/^\d{6}$/.test(code)) { - // Static message: a mistyped-but-real code must never reach Sentry - // through the telemetry error path. - throw new ValidationError('Invalid code — expected exactly 6 digits.'); - } - const res = (await apiClient('/auth/phone/verify', { - method: 'POST', - body: JSON.stringify({ code }), - })) as PhoneStatus; - printStatus(res, Boolean(opts.json || program.opts().json)); - }); - addExamples(verify, `\nEXAMPLES:\n $ hookmyapp phone verify 123456\n $ hookmyapp phone verify 654321 --json\n`); - - const consents = phone - .command('consents') - .description('Update notification consents / delivery preference without re-verifying') - .option('--operational ', 'Breakdown alerts') - .option('--product ', 'Feature announcements') - .option('--marketing ', 'Offers/promotions') - .option('--prefer ', 'Delivery channel: whatsapp | sms | both') - .option('--json', 'Output machine-readable JSON') - .action( - async (opts: { operational?: string; product?: string; marketing?: string; prefer?: string; json?: boolean }) => { - const toBool = (v: string | undefined, flag: string): boolean | undefined => { - if (v === undefined) return undefined; - if (v !== 'on' && v !== 'off') throw new ValidationError(`${flag} must be "on" or "off"`); - return v === 'on'; - }; - const body: Record = {}; - const operational = toBool(opts.operational, '--operational'); - const product = toBool(opts.product, '--product'); - const marketing = toBool(opts.marketing, '--marketing'); - if (operational !== undefined) body.operational = operational; - if (product !== undefined) body.product = product; - if (marketing !== undefined) body.marketing = marketing; - if (opts.prefer !== undefined) { - if (!['whatsapp', 'sms', 'both'].includes(opts.prefer)) { - throw new ValidationError('--prefer must be whatsapp, sms, or both'); - } - body.channelPreference = opts.prefer; - } - if (Object.keys(body).length === 0) { - throw new ValidationError('Nothing to update — pass at least one of --operational/--product/--marketing/--prefer'); - } - const res = (await apiClient('/auth/phone/consents', { method: 'PATCH', body: JSON.stringify(body) })) as PhoneStatus; - printStatus(res, Boolean(opts.json || program.opts().json)); - }, - ); - addExamples(consents, `\nEXAMPLES:\n $ hookmyapp phone consents --marketing on\n $ hookmyapp phone consents --operational off --prefer sms\n`); -} diff --git a/src/index.ts b/src/index.ts index a572582..ef5ce08 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,7 +17,6 @@ import { registerWorkspaceCommand } from './commands/workspace.js'; import { registerCustomersCommand } from './commands/customers.js'; import { registerSupportCommand } from './commands/support.js'; import { registerNotificationsCommand } from './commands/notifications.js'; -import { registerPhoneCommand } from './commands/phone.js'; import { registerOrgProfileCommand } from './commands/org-profile.js'; import { registerSandboxCommand } from './commands/sandbox/index.js'; import { registerListenCommand } from './commands/sandbox-listen/index.js'; @@ -198,7 +197,6 @@ registerWorkspaceCommand(program); registerCustomersCommand(program); registerSupportCommand(program); registerNotificationsCommand(program); -registerPhoneCommand(program); registerOrgProfileCommand(program); // Persistent CLI config (env profile: local | staging | production) From b3c6c182118670d060ab0fe83a39afea0e06459b Mon Sep 17 00:00:00 2001 From: Or Dvir Date: Tue, 11 Aug 2026 17:43:23 +0300 Subject: [PATCH 2/2] AIT-385: org profile help points at alerts phone --- src/commands/org-profile.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/org-profile.ts b/src/commands/org-profile.ts index dd710a4..d4e4d81 100644 --- a/src/commands/org-profile.ts +++ b/src/commands/org-profile.ts @@ -8,7 +8,7 @@ import { getDefaultWorkspaceId, resolveOrgPublicIdForWorkspace } from './_helper /** * AIT-370 — `hookmyapp org profile`: read/write the organization profile * (company info). This is COMPANY data — the `--phone` here is the company - * phone, NOT anyone's personal alert phone (that is `hookmyapp phone`). + * phone, NOT anyone's personal alert phone (that is `hookmyapp alerts phone`). * Ask the human for company details; never infer or invent them. Org admins only. */ @@ -64,7 +64,7 @@ EXAMPLES: .command('profile') .description( 'Company profile (email, phone, website, business category/niche, use case). ' + - 'Company data — for your personal alert number use: hookmyapp phone', + 'Company data — for your personal alert number use: hookmyapp alerts phone', ); addExamples( profile,