From 879f92acec9eda1d6c44ec295ba866e47b36d961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Thu, 21 May 2026 02:48:07 +0200 Subject: [PATCH 01/26] wip --- packages/cli/README.md | 4 +- packages/cli/src/commands/setup.ts | 100 ++++++++++++++++++----------- packages/cli/src/commands/watch.ts | 4 +- packages/cli/test/cli-smoke.ts | 27 +++++++- packages/npm/scripts/build.ts | 31 ++++++++- 5 files changed, 122 insertions(+), 44 deletions(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index f1167857..6c70613b 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -2415,8 +2415,8 @@ Flags: | Flag | Type | Description | | --- | --- | --- | | `-c, --chat=...` | option | Chat ID to subscribe to. Defaults to all chats. | -| `--exclude-type=...` | option | Drop events of these types. Repeat for multiple. | -| `--include-type=...` | option | Only forward events of these types. Repeat for multiple. | +| `--exclude-type=...` | option | Drop events of these types. Repeat for multiple. | +| `--include-type=...` | option | Only forward events of these types. Repeat for multiple. | | `--webhook=` | option | Forward each event to this URL as a POST request (best-effort, fire-and-forget) | | `--webhook-queue=` | option | Maximum pending webhook deliveries before dropping events Default: 64 | | `--webhook-secret=` | option | HMAC-SHA256 secret. Signs payloads with X-Beeper-Signature: sha256= | diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index 5b45031b..96c06787 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -16,6 +16,7 @@ import { readConfig, readTarget, listTargets, + pathExists, saveTargetAuth, updateConfig, writeTarget, @@ -102,7 +103,7 @@ export default class Setup extends BeeperCommand { return } if (flags.json || !process.stdin.isTTY) { - await printData(setupSessionFoundOutput(local, setupCmd), flags.json ? 'json' : 'human') + await printData(setupSessionFoundOutput(local, setupCmd, detected.serverInstalled), flags.json ? 'json' : 'human') return } printLocalDesktopPreview(local) @@ -250,12 +251,14 @@ export default class Setup extends BeeperCommand { } private async setupFromChoice(flags: SetupFlags): Promise { + const serverInstalled = await isServerInstalled() process.stdout.write('No usable Beeper Desktop session was found on this device.\n\n') process.stdout.write('How do you want to connect Beeper CLI?\n\n') process.stdout.write(' 1. Install Beeper Desktop\n') - process.stdout.write(' 2. Install local Beeper Server\n') + process.stdout.write(` 2. ${serverInstalled ? 'Use installed local Beeper Server' : 'Install local Beeper Server'}\n`) process.stdout.write(' 3. Connect with Desktop API on another device\n\n') - const choice = await promptChoice('Choose [1]: ', ['1', '2', '3'], '1') + const defaultChoice = serverInstalled ? '2' : '1' + const choice = await promptChoice(`Choose [${defaultChoice}]: `, ['1', '2', '3'], defaultChoice) if (choice === '1') { if (!await promptYesNoDefaultYes('Install Beeper Desktop stable from beeper.com?')) return await installWithCopy('desktop', { ...flags, channel: 'stable' }) @@ -264,8 +267,10 @@ export default class Setup extends BeeperCommand { return } if (choice === '2') { - if (!await promptYesNoDefaultYes('Install local Beeper Server stable from beeper.com?')) return - await installWithCopy('server', { ...flags, channel: 'stable', 'server-env': 'production' }) + if (!serverInstalled) { + if (!await promptYesNoDefaultYes('Install local Beeper Server stable from beeper.com?')) return + await installWithCopy('server', { ...flags, channel: 'stable', 'server-env': 'production' }) + } await this.setupManaged('server', { ...flags, install: false, server: true, channel: 'stable' }) return } @@ -275,12 +280,13 @@ export default class Setup extends BeeperCommand { } private async handleBrokenCurrentTarget(target: Target, readiness: Readiness, flags: SetupFlags): Promise { + const serverInstalled = await isServerInstalled() process.stdout.write(`Beeper CLI is set up for ${target.name ?? target.id}, but it is not reachable.\n\n`) if (readiness.message) process.stdout.write(`${readiness.message}\n\n`) process.stdout.write('What do you want to do?\n\n') process.stdout.write(` 1. Retry ${target.name ?? target.id}\n`) process.stdout.write(' 2. Use Beeper Desktop on this device\n') - process.stdout.write(' 3. Install local Beeper Server\n') + process.stdout.write(` 3. ${serverInstalled ? 'Use installed local Beeper Server' : 'Install local Beeper Server'}\n`) process.stdout.write(' 4. Connect with Desktop API on another device\n\n') const choice = await promptChoice('Choose [1]: ', ['1', '2', '3', '4'], '1') if (choice === '1') return false @@ -290,8 +296,10 @@ export default class Setup extends BeeperCommand { return true } if (choice === '3') { - if (!await promptYesNoDefaultYes('Install local Beeper Server stable from beeper.com?')) return true - await installWithCopy('server', { ...flags, channel: 'stable', 'server-env': 'production' }) + if (!serverInstalled) { + if (!await promptYesNoDefaultYes('Install local Beeper Server stable from beeper.com?')) return true + await installWithCopy('server', { ...flags, channel: 'stable', 'server-env': 'production' }) + } await this.setupManaged('server', { ...flags, install: false, server: true, channel: 'stable' }) return true } @@ -336,11 +344,11 @@ type PreparedLocalDesktopSetup = { } type DesktopSetupDetection = - | { kind: 'session-found'; local: PreparedLocalDesktopSetup } - | { kind: 'installed-not-running' } - | { kind: 'running-signed-out'; readiness?: Readiness } - | { kind: 'session-unreadable'; reason: string; readiness?: Readiness } - | { kind: 'not-installed' } + | { kind: 'session-found'; local: PreparedLocalDesktopSetup; serverInstalled: boolean } + | { kind: 'installed-not-running'; serverInstalled: boolean } + | { kind: 'running-signed-out'; readiness?: Readiness; serverInstalled: boolean } + | { kind: 'session-unreadable'; reason: string; readiness?: Readiness; serverInstalled: boolean } + | { kind: 'not-installed'; serverInstalled: boolean } async function setupTarget(flags: SetupFlags): Promise { if (flags['base-url']) return { id: customTargetID, type: 'desktop', baseURL: flags['base-url'] } @@ -397,29 +405,33 @@ async function prepareLocalDesktopSetup(target: Target, flags: SetupFlags): Prom async function detectDesktopSetup(target: Target, flags: SetupFlags): Promise { printProgress(flags, 'Checking Beeper Desktop') - const appInstalled = await isDesktopAppInstalled() + const installations = await readInstallations().catch((): Awaited> => ({})) + const serverInstalled = await isServerInstalled(installations) + const appInstalled = Boolean(installations.desktop?.path || await findDesktopAppPath()) printProgress(flags, 'Reading local Desktop session') const local = await prepareLocalDesktopSetup(target, flags).catch(error => ({ error })) - if (!('error' in local)) return { kind: 'session-found', local } + if (!('error' in local)) return { kind: 'session-found', local, serverInstalled } printProgress(flags, 'Checking Desktop readiness') const desktop = await findLocalDesktop({ baseURL: target.baseURL, scan: target.id === builtInDesktopTargetID, timeoutMs: 500 }).catch(() => undefined) if (desktop) { const readiness = await evaluateReadiness({ baseURL: desktop.baseURL, target: target.id, token: false }) - if (readiness.state === 'needs-login') return { kind: 'running-signed-out', readiness } + if (readiness.state === 'needs-login') return { kind: 'running-signed-out', readiness, serverInstalled } return { kind: 'session-unreadable', reason: local.error instanceof Error ? local.error.message : String(local.error), readiness, + serverInstalled, } } - return appInstalled ? { kind: 'installed-not-running' } : { kind: 'not-installed' } + return appInstalled ? { kind: 'installed-not-running', serverInstalled } : { kind: 'not-installed', serverInstalled } } -async function isDesktopAppInstalled(): Promise { - const installations = await readInstallations().catch((): Awaited> => ({})) - return Boolean(installations.desktop?.path || await findDesktopAppPath()) +async function isServerInstalled(installations?: Awaited>): Promise { + if (process.env.BEEPER_SERVER_BIN) return true + const installation = installations ?? await readInstallations().catch((): Awaited> => ({})) + return Boolean(installation.server?.path && await pathExists(installation.server.path)) } async function commitLocalDesktopSetup(prepared: PreparedLocalDesktopSetup): Promise { @@ -498,7 +510,13 @@ function printLocalDesktopPreview(prepared: PreparedLocalDesktopSetup): void { process.stdout.write('\n') } -function setupSessionFoundOutput(local: PreparedLocalDesktopSetup, setupCmd: string): Record { +function setupSessionFoundOutput(local: PreparedLocalDesktopSetup, setupCmd: string, serverInstalled: boolean): Record { + const availableActions = [ + action('use-desktop-session', `${setupCmd} --local`), + action('desktop-oauth', `${setupCmd} --oauth`), + action('connect-remote', 'beeper setup --remote '), + ] + if (serverInstalled) availableActions.push(installedServerAction(true)) return { state: local.readiness.state === 'ready' ? 'desktop-ready' : 'desktop-session-found', message: local.readiness.state === 'ready' @@ -508,11 +526,7 @@ function setupSessionFoundOutput(local: PreparedLocalDesktopSetup, setupCmd: str readiness: local.readiness, localDesktop: localDesktopPreview(local), recommendedAction: action('use-desktop-session', `${setupCmd} --local`), - availableActions: [ - action('use-desktop-session', `${setupCmd} --local`), - action('desktop-oauth', `${setupCmd} --oauth`), - action('connect-remote', 'beeper setup --remote '), - ], + availableActions, } } @@ -611,6 +625,7 @@ function printNextSteps(): void { function setupStateOutput(detected: Exclude, target: Target): Record { if (detected.kind === 'installed-not-running') { + const serverAction = installedServerAction(detected.serverInstalled) return setupActionEnvelope({ state: 'desktop-installed-not-running', message: 'Beeper Desktop is installed but not running.', @@ -619,24 +634,31 @@ function setupStateOutput(detected: Exclude'), - action('install-server', 'beeper setup --server --install --yes'), + serverAction, ], }) } if (detected.kind === 'running-signed-out') { + const availableActions = [ + action('open-desktop', 'beeper setup --desktop --yes'), + action('connect-remote', 'beeper setup --remote '), + ] + if (detected.serverInstalled) availableActions.push(installedServerAction(true)) return setupActionEnvelope({ state: 'desktop-running-signed-out', message: 'Beeper Desktop is running but not signed in.', target, readiness: detected.readiness, recommendedAction: action('open-desktop', 'beeper setup --desktop --yes'), - availableActions: [ - action('open-desktop', 'beeper setup --desktop --yes'), - action('connect-remote', 'beeper setup --remote '), - ], + availableActions, }) } if (detected.kind === 'session-unreadable') { + const availableActions = [ + action('desktop-oauth', 'beeper setup --oauth --yes'), + action('connect-remote', 'beeper setup --remote '), + ] + if (detected.serverInstalled) availableActions.push(installedServerAction(true)) return setupActionEnvelope({ state: 'desktop-running-session-unreadable', message: 'Beeper Desktop is running, but CLI could not read the local session.', @@ -644,25 +666,29 @@ function setupStateOutput(detected: Exclude'), - ], + availableActions, }) } + const serverAction = installedServerAction(detected.serverInstalled) return setupActionEnvelope({ state: 'desktop-not-installed', message: 'No Beeper Desktop installation was found on this device.', target, - recommendedAction: action('install-desktop', 'beeper setup --desktop --install --yes'), + recommendedAction: detected.serverInstalled ? serverAction : action('install-desktop', 'beeper setup --desktop --install --yes'), availableActions: [ action('install-desktop', 'beeper setup --desktop --install --yes'), - action('install-server', 'beeper setup --server --install --yes'), + serverAction, action('connect-remote', 'beeper setup --remote '), ], }) } +function installedServerAction(installed: boolean): { id: string; command: string } { + return installed + ? action('use-installed-server', 'beeper setup --server --yes') + : action('install-server', 'beeper setup --server --install --yes') +} + function currentTargetBrokenOutput(target: Target, readiness: Readiness): Record { return { state: 'current-target-unreachable', diff --git a/packages/cli/src/commands/watch.ts b/packages/cli/src/commands/watch.ts index 4a4e7e5f..38e28d74 100644 --- a/packages/cli/src/commands/watch.ts +++ b/packages/cli/src/commands/watch.ts @@ -14,8 +14,8 @@ export default class Watch extends BeeperCommand { static override flags = { chat: Flags.string({ char: 'c', multiple: true, description: 'Chat ID to subscribe to. Defaults to all chats.' }), json: Flags.boolean({ default: false, description: 'Print raw JSON, one event per line' }), - 'include-type': Flags.string({ multiple: true, options: ['chat.upserted', 'chat.deleted', 'message.upserted', 'message.deleted'], description: 'Only forward events of these types. Repeat for multiple.' }), - 'exclude-type': Flags.string({ multiple: true, options: ['chat.upserted', 'chat.deleted', 'message.upserted', 'message.deleted'], description: 'Drop events of these types. Repeat for multiple.' }), + 'include-type': Flags.string({ multiple: true, options: ['chat.upserted', 'chat.deleted', 'message.upserted', 'message.deleted', 'message.stream'], description: 'Only forward events of these types. Repeat for multiple.' }), + 'exclude-type': Flags.string({ multiple: true, options: ['chat.upserted', 'chat.deleted', 'message.upserted', 'message.deleted', 'message.stream'], description: 'Drop events of these types. Repeat for multiple.' }), webhook: Flags.string({ description: 'Forward each event to this URL as a POST request (best-effort, fire-and-forget)' }), 'webhook-secret': Flags.string({ description: 'HMAC-SHA256 secret. Signs payloads with X-Beeper-Signature: sha256=' }), 'webhook-queue': Flags.integer({ default: 64, description: 'Maximum pending webhook deliveries before dropping events' }), diff --git a/packages/cli/test/cli-smoke.ts b/packages/cli/test/cli-smoke.ts index f10fb246..1dd76d51 100644 --- a/packages/cli/test/cli-smoke.ts +++ b/packages/cli/test/cli-smoke.ts @@ -1,6 +1,6 @@ import assert from 'node:assert/strict' import { spawnSync } from 'node:child_process' -import { existsSync, readdirSync, rmSync } from 'node:fs' +import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { commandManifest } from '../dist/lib/manifest.js' @@ -235,6 +235,31 @@ envelope = JSON.parse(result.stderr) assert.equal(envelope.success, false) assert.match(envelope.error, /Unknown Beeper target/) +rmSync(configDir, { recursive: true, force: true }) +const fakeServerPath = join(configDir, 'bin', 'beeper-server') +mkdirSync(join(configDir, 'bin'), { recursive: true }) +writeFileSync(fakeServerPath, '#!/bin/sh\n', { mode: 0o755 }) +writeFileSync(join(configDir, 'installations.json'), `${JSON.stringify({ + server: { + kind: 'server', + channel: 'stable', + serverEnv: 'production', + bundleID: 'com.automattic.beeper.server', + version: 'test', + path: fakeServerPath, + feedURL: 'https://example.invalid/feed', + downloadURL: 'https://example.invalid/download', + installedAt: '2026-05-18T00:00:00.000Z', + updatedAt: '2026-05-18T00:00:00.000Z', + }, +}, null, 2)}\n`) +result = run('setup', '--json') +assert.equal(result.status, 0, result.stderr) +envelope = JSON.parse(result.stdout) +assert.equal(envelope.success, true) +assert(envelope.data.availableActions.some(action => action.id === 'use-installed-server' && action.command === 'beeper setup --server --yes')) +assert(!envelope.data.availableActions.some(action => action.id === 'install-server'), 'setup must not offer to reinstall an already installed Server') + const rpcResult = spawnSync(process.execPath, ['./bin/dev.js', 'rpc'], { cwd: root, encoding: 'utf8', diff --git a/packages/npm/scripts/build.ts b/packages/npm/scripts/build.ts index 95b406b9..b8dd6405 100644 --- a/packages/npm/scripts/build.ts +++ b/packages/npm/scripts/build.ts @@ -57,30 +57,41 @@ const expectedBinarySha256 = artifact.binarySha256 || artifact.sha256 if (!existsSync(binPath) || await sha256(binPath).catch(() => '') !== expectedBinarySha256) { const tempDir = join(tmpdir(), \`beeper-cli-\${manifest.version}-\${process.pid}\`) const archivePath = join(tempDir, artifact.file) + const downloadURL = \`https://github.com/beeper/cli/releases/download/v\${manifest.version}/\${artifact.file}\` + logStep(\`installing beeper-cli \${manifest.version} for \${platform}\`) await rm(tempDir, { recursive: true, force: true }) await mkdir(tempDir, { recursive: true }) - await download(\`https://github.com/beeper/cli/releases/download/v\${manifest.version}/\${artifact.file}\`, archivePath) + await download(downloadURL, archivePath) + logStep('verifying download') const actual = await sha256(archivePath) if (actual !== artifact.sha256) { await rm(tempDir, { recursive: true, force: true }) console.error(\`beeper-cli binary checksum mismatch for \${artifact.file}.\`) process.exit(1) } + logStep('extracting binary') await extract(archivePath, tempDir) const extractedBin = join(tempDir, 'bin', manifest.command || 'beeper') await chmod(extractedBin, 0o755) + logStep(\`caching binary in \${cacheDir}\`) await rm(cacheDir, { recursive: true, force: true }) await mkdir(dirname(binPath), { recursive: true }) await rename(extractedBin, binPath) await rm(tempDir, { recursive: true, force: true }) + logStep('ready') } +if (process.env.BEEPER_CLI_LAUNCHER_DEBUG === '1') logStep(\`starting \${binPath}\`) const child = spawn(binPath, process.argv.slice(2), { stdio: 'inherit', env: process.env }) child.on('exit', (code, signal) => { if (signal) process.kill(process.pid, signal) process.exit(code ?? 1) }) +function logStep(message) { + console.error(\`beeper-cli: \${message}\`) +} + function targetPlatform() { const os = osPlatform() const cpu = osArch() @@ -96,11 +107,14 @@ async function sha256(path) { } async function download(url, destination) { + logStep(\`downloading \${artifact.file}\`) await new Promise((resolve, reject) => { get(url, response => { if ([301, 302, 303, 307, 308].includes(response.statusCode ?? 0) && response.headers.location) { response.resume() - download(response.headers.location, destination).then(resolve, reject) + const nextURL = new URL(response.headers.location, url).toString() + logStep(\`redirecting to \${new URL(nextURL).host}\`) + download(nextURL, destination).then(resolve, reject) return } if (response.statusCode !== 200) { @@ -108,7 +122,20 @@ async function download(url, destination) { reject(new Error(\`Download failed with HTTP \${response.statusCode}: \${url}\`)) return } + const total = Number(response.headers['content-length'] ?? 0) + let downloaded = 0 + let nextLoggedPercent = 25 const file = createWriteStream(destination, { mode: 0o755 }) + response.on('data', chunk => { + downloaded += chunk.length + if (!total) return + const percent = Math.floor(downloaded / total * 100) + if (percent >= nextLoggedPercent || percent === 100) { + const milestone = percent === 100 ? 100 : nextLoggedPercent + logStep(\`downloaded \${milestone}%\`) + nextLoggedPercent = milestone + 25 + } + }) response.pipe(file) file.on('finish', () => file.close(resolve)) file.on('error', reject) From 6d302f301215ce5530a92d05cfab1dea72f91bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 17:59:48 +0200 Subject: [PATCH 02/26] wip --- README.md | 36 +- docs/.gitignore | 16 + docs/astro.config.mjs | 100 ++ docs/bun.lock | 871 ++++++++++++++++++ docs/package.json | 19 + docs/public/favicon.svg | 4 + docs/src/assets/logo.svg | 4 + docs/src/content.config.ts | 7 + .../src/content/docs/accounts.mdx | 45 +- docs/src/content/docs/api.mdx | 47 + .../auth.md => docs/src/content/docs/auth.mdx | 38 +- .../src/content/docs/chats.mdx | 35 +- docs/src/content/docs/config.mdx | 55 ++ docs/src/content/docs/connect.mdx | 146 +++ .../src/content/docs/contacts.mdx | 20 +- docs/src/content/docs/exit-codes.mdx | 25 + docs/src/content/docs/export.mdx | 53 ++ docs/src/content/docs/index.mdx | 88 ++ docs/src/content/docs/install.mdx | 76 ++ .../src/content/docs/media.mdx | 22 +- .../src/content/docs/messages.mdx | 42 +- docs/src/content/docs/plugins.mdx | 45 + .../src/content/docs/presence.mdx | 21 +- docs/src/content/docs/quickstart.mdx | 89 ++ .../rpc.md => docs/src/content/docs/rpc.mdx | 26 +- docs/src/content/docs/scripting.mdx | 85 ++ .../send.md => docs/src/content/docs/send.mdx | 46 +- .../src/content/docs/targets.mdx | 34 +- docs/src/content/docs/update.mdx | 39 + docs/src/content/docs/watch.mdx | 66 ++ docs/src/styles/theme.css | 37 + docs/tsconfig.json | 5 + packages/cli/README.md | 412 ++++++--- packages/cli/docs/api.md | 29 - packages/cli/docs/config.md | 32 - packages/cli/docs/export.md | 39 - packages/cli/docs/setup.md | 38 - packages/cli/docs/update.md | 27 - packages/cli/docs/watch.md | 35 - packages/cli/package.json | 3 + packages/cli/scripts/generate-command-map.ts | 4 +- packages/cli/scripts/generate-readme.ts | 44 +- packages/cli/src/commands.generated.ts | 173 ++-- packages/cli/src/commands/accounts/add.ts | 18 +- packages/cli/src/commands/accounts/remove.ts | 6 +- packages/cli/src/commands/accounts/use.ts | 10 +- packages/cli/src/commands/api/post.ts | 6 +- packages/cli/src/commands/api/request.ts | 6 +- .../cli/src/commands/auth/email/response.ts | 6 +- packages/cli/src/commands/auth/logout.ts | 6 +- packages/cli/src/commands/chats/archive.ts | 6 +- packages/cli/src/commands/chats/avatar.ts | 6 +- .../cli/src/commands/chats/description.ts | 6 +- packages/cli/src/commands/chats/disappear.ts | 6 +- packages/cli/src/commands/chats/draft.ts | 10 +- packages/cli/src/commands/chats/focus.ts | 6 +- packages/cli/src/commands/chats/mark-read.ts | 6 +- .../cli/src/commands/chats/mark-unread.ts | 6 +- packages/cli/src/commands/chats/mute.ts | 6 +- .../cli/src/commands/chats/notify-anyway.ts | 6 +- packages/cli/src/commands/chats/pin.ts | 6 +- packages/cli/src/commands/chats/priority.ts | 6 +- packages/cli/src/commands/chats/remind.ts | 6 +- packages/cli/src/commands/chats/rename.ts | 6 +- packages/cli/src/commands/chats/start.ts | 6 +- packages/cli/src/commands/chats/unarchive.ts | 6 +- packages/cli/src/commands/chats/unmute.ts | 6 +- packages/cli/src/commands/chats/unpin.ts | 6 +- packages/cli/src/commands/chats/unremind.ts | 6 +- packages/cli/src/commands/config/reset.ts | 6 +- packages/cli/src/commands/config/set.ts | 6 +- packages/cli/src/commands/contacts/search.ts | 4 +- packages/cli/src/commands/export.ts | 17 +- packages/cli/src/commands/install/desktop.ts | 6 +- packages/cli/src/commands/install/server.ts | 6 +- packages/cli/src/commands/man.ts | 51 +- packages/cli/src/commands/media/download.ts | 7 +- packages/cli/src/commands/messages/delete.ts | 6 +- packages/cli/src/commands/messages/edit.ts | 6 +- packages/cli/src/commands/messages/export.ts | 17 +- packages/cli/src/commands/messages/search.ts | 4 +- packages/cli/src/commands/presence.ts | 6 +- packages/cli/src/commands/resolve/account.ts | 46 + packages/cli/src/commands/resolve/bridge.ts | 49 + packages/cli/src/commands/resolve/chat.ts | 68 ++ packages/cli/src/commands/resolve/contact.ts | 55 ++ packages/cli/src/commands/resolve/target.ts | 52 ++ packages/cli/src/commands/schema.ts | 125 +++ packages/cli/src/commands/send/file.ts | 9 +- packages/cli/src/commands/send/react.ts | 7 +- packages/cli/src/commands/send/sticker.ts | 29 +- packages/cli/src/commands/send/text.ts | 9 +- packages/cli/src/commands/send/unreact.ts | 7 +- packages/cli/src/commands/send/voice.ts | 31 +- packages/cli/src/commands/setup.ts | 18 +- .../cli/src/commands/targets/add/desktop.ts | 6 +- .../cli/src/commands/targets/add/remote.ts | 6 +- .../cli/src/commands/targets/add/server.ts | 6 +- packages/cli/src/commands/targets/disable.ts | 6 +- packages/cli/src/commands/targets/enable.ts | 6 +- packages/cli/src/commands/targets/remove.ts | 6 +- packages/cli/src/commands/targets/restart.ts | 6 +- packages/cli/src/commands/targets/start.ts | 10 +- packages/cli/src/commands/targets/stop.ts | 6 +- packages/cli/src/commands/targets/use.ts | 6 +- packages/cli/src/commands/update.ts | 6 +- packages/cli/src/commands/verify.ts | 6 +- packages/cli/src/commands/verify/approve.ts | 6 +- packages/cli/src/commands/verify/cancel.ts | 6 +- .../cli/src/commands/verify/qr-confirm.ts | 6 +- packages/cli/src/commands/verify/qr-scan.ts | 6 +- .../cli/src/commands/verify/recovery-key.ts | 6 +- .../src/commands/verify/reset-recovery-key.ts | 6 +- .../cli/src/commands/verify/sas-confirm.ts | 6 +- packages/cli/src/commands/verify/sas.ts | 6 +- packages/cli/src/commands/verify/start.ts | 6 +- packages/cli/src/commands/watch.ts | 4 +- packages/cli/src/lib/command-metadata.ts | 54 ++ packages/cli/src/lib/command.ts | 92 +- packages/cli/src/lib/errors.ts | 16 +- packages/cli/src/lib/manifest.ts | 41 +- packages/cli/src/lib/output.ts | 178 +++- packages/cli/src/lib/resolve.ts | 29 +- packages/cli/test/cli-smoke.ts | 35 +- .../test/messages-search-validation.test.ts | 6 +- 125 files changed, 3780 insertions(+), 706 deletions(-) create mode 100644 docs/.gitignore create mode 100644 docs/astro.config.mjs create mode 100644 docs/bun.lock create mode 100644 docs/package.json create mode 100644 docs/public/favicon.svg create mode 100644 docs/src/assets/logo.svg create mode 100644 docs/src/content.config.ts rename packages/cli/docs/accounts.md => docs/src/content/docs/accounts.mdx (55%) create mode 100644 docs/src/content/docs/api.mdx rename packages/cli/docs/auth.md => docs/src/content/docs/auth.mdx (61%) rename packages/cli/docs/chats.md => docs/src/content/docs/chats.mdx (72%) create mode 100644 docs/src/content/docs/config.mdx create mode 100644 docs/src/content/docs/connect.mdx rename packages/cli/docs/contacts.md => docs/src/content/docs/contacts.mdx (60%) create mode 100644 docs/src/content/docs/exit-codes.mdx create mode 100644 docs/src/content/docs/export.mdx create mode 100644 docs/src/content/docs/index.mdx create mode 100644 docs/src/content/docs/install.mdx rename packages/cli/docs/media.md => docs/src/content/docs/media.mdx (50%) rename packages/cli/docs/messages.md => docs/src/content/docs/messages.mdx (70%) create mode 100644 docs/src/content/docs/plugins.mdx rename packages/cli/docs/presence.md => docs/src/content/docs/presence.mdx (56%) create mode 100644 docs/src/content/docs/quickstart.mdx rename packages/cli/docs/rpc.md => docs/src/content/docs/rpc.mdx (58%) create mode 100644 docs/src/content/docs/scripting.mdx rename packages/cli/docs/send.md => docs/src/content/docs/send.mdx (60%) rename packages/cli/docs/targets.md => docs/src/content/docs/targets.mdx (51%) create mode 100644 docs/src/content/docs/update.mdx create mode 100644 docs/src/content/docs/watch.mdx create mode 100644 docs/src/styles/theme.css create mode 100644 docs/tsconfig.json delete mode 100644 packages/cli/docs/api.md delete mode 100644 packages/cli/docs/config.md delete mode 100644 packages/cli/docs/export.md delete mode 100644 packages/cli/docs/setup.md delete mode 100644 packages/cli/docs/update.md delete mode 100644 packages/cli/docs/watch.md create mode 100644 packages/cli/src/commands/resolve/account.ts create mode 100644 packages/cli/src/commands/resolve/bridge.ts create mode 100644 packages/cli/src/commands/resolve/chat.ts create mode 100644 packages/cli/src/commands/resolve/contact.ts create mode 100644 packages/cli/src/commands/resolve/target.ts create mode 100644 packages/cli/src/commands/schema.ts create mode 100644 packages/cli/src/lib/command-metadata.ts diff --git a/README.md b/README.md index 6e4e5cc5..30abd8d7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,15 @@ -# beeper — One CLI for all your chats +
-> Built for you and your agent. Batteries included. +# beeper + +**One CLI for all your chats.** Built for you and your agent — batteries included. + +[![npm](https://img.shields.io/npm/v/beeper-cli.svg?label=npm&color=6E56F8)](https://www.npmjs.com/package/beeper-cli) +[![license](https://img.shields.io/badge/license-MIT-6E56F8.svg)](https://github.com/beeper/desktop-api-cli/blob/main/packages/cli/LICENSE) +[![docs](https://img.shields.io/badge/docs-online-6E56F8.svg)](https://example.com) +[![built with Bun](https://img.shields.io/badge/built%20with-Bun-6E56F8.svg)](https://bun.sh) + +
Talks to Beeper Desktop on this machine, to a Beeper Server you self-host, or to either one running somewhere else. Send and receive across the chat @@ -13,7 +22,7 @@ Facebook Messenger · X (Twitter) DMs · LinkedIn · Slack · Google Messages (RCS/SMS) · Google Chat · Matrix · IRC · Bluesky. Run `beeper bridges list` for the live list on your target. -Command manual: `beeper man` · CLI docs: `beeper docs` +📖 **[Read the docs](https://example.com)** · command manual: `beeper man` · open docs: `beeper docs` ## Features @@ -193,19 +202,22 @@ WhatsApp, Telegram, Discord, iMessage, and the rest show up under `accounts list ## Documentation +Full documentation lives at **[example.com](https://example.com)** +(built from [`docs/`](docs/) with Astro Starlight — a fully static site). + | Topic | Page | Commands | | --- | --- | --- | -| **Setup + install** | [setup](docs/setup.md) · [auth](docs/auth.md) | `setup` · `install desktop` · `install server` · `verify` · `status` · `doctor` · `auth status` | -| **Targets** | [targets](docs/targets.md) | `targets list` · `targets add desktop` · `targets add server` · `targets add remote` · `targets use` · `targets status` · `targets logs` | -| **Bridges + accounts** | [accounts](docs/accounts.md) | `bridges list` · `bridges show` · `accounts list` · `accounts add` · `accounts show` · `accounts use` · `accounts remove` | -| **Chats** | [chats](docs/chats.md) | `chats list` · `chats search` · `chats show` · `chats start` · `chats archive` · `chats pin` · `chats mute` · `chats priority` · `chats remind` · `chats rename` · `chats draft` · `chats focus` | -| **Messages** | [messages](docs/messages.md) · [send](docs/send.md) · [presence](docs/presence.md) | `messages list` · `messages search` · `messages export` · `send text` · `send file` · `send sticker` · `send voice` · `send react` · `presence` | -| **Contacts + media** | [contacts](docs/contacts.md) · [media](docs/media.md) · [export](docs/export.md) | `contacts list` · `contacts search` · `media download` · `export` | -| **Automation** | [watch](docs/watch.md) · [rpc](docs/rpc.md) · [api](docs/api.md) | `watch` · `watch --webhook` · `rpc` · `man` · `api get` · `api post` · `api request` | -| **Maintenance** | [config](docs/config.md) · [update](docs/update.md) | `update` · `config` · `completion` · `docs` · `version` | +| **Setup + install** | [connect](https://example.com/connect/) · [install](https://example.com/install/) · [auth](https://example.com/auth/) | `setup` · `install desktop` · `install server` · `verify` · `status` · `doctor` · `auth status` | +| **Targets** | [targets](https://example.com/targets/) | `targets list` · `targets add desktop` · `targets add server` · `targets add remote` · `targets use` · `targets status` · `targets logs` | +| **Bridges + accounts** | [accounts](https://example.com/accounts/) | `bridges list` · `bridges show` · `accounts list` · `accounts add` · `accounts show` · `accounts use` · `accounts remove` | +| **Chats** | [chats](https://example.com/chats/) | `chats list` · `chats search` · `chats show` · `chats start` · `chats archive` · `chats pin` · `chats mute` · `chats priority` · `chats remind` · `chats rename` · `chats draft` · `chats focus` | +| **Messages** | [messages](https://example.com/messages/) · [send](https://example.com/send/) · [presence](https://example.com/presence/) | `messages list` · `messages search` · `messages export` · `send text` · `send file` · `send sticker` · `send voice` · `send react` · `presence` | +| **Contacts + media** | [contacts](https://example.com/contacts/) · [media](https://example.com/media/) · [export](https://example.com/export/) | `contacts list` · `contacts search` · `media download` · `export` | +| **Automation** | [scripting](https://example.com/scripting/) · [watch](https://example.com/watch/) · [rpc](https://example.com/rpc/) · [api](https://example.com/api/) | `watch` · `watch --webhook` · `rpc` · `man` · `api get` · `api post` · `api request` | +| **Maintenance** | [config](https://example.com/config/) · [update](https://example.com/update/) | `update` · `config` · `completion` · `docs` · `version` | Use `beeper docs` to open the CLI docs and `beeper man` to print the local -command manual. +command manual. To work on the docs site locally: `cd docs && bun install && bun run dev`. ## Configuration diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..294d3b6a --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,16 @@ +# build output +dist/ +# generated types +.astro/ +# dependencies +node_modules/ +# environment +.env +.env.production +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +# macOS +.DS_Store diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs new file mode 100644 index 00000000..2cf60acd --- /dev/null +++ b/docs/astro.config.mjs @@ -0,0 +1,100 @@ +// @ts-check +import { defineConfig } from 'astro/config'; +import starlight from '@astrojs/starlight'; + +// Fully static export. `output: 'static'` is Astro's default; it is set +// explicitly here so the intent is obvious and `astro build` always emits a +// self-contained `./dist` you can drop on any static host or CDN. +// +// When you pick a home for the docs, set `site` to the canonical origin (used +// for sitemap + canonical URLs) and, if serving from a sub-path, set `base`. +export default defineConfig({ + site: 'https://example.com', + base: '/', + output: 'static', + trailingSlash: 'always', + integrations: [ + starlight({ + title: 'Beeper CLI', + description: + 'One CLI for all your chats — WhatsApp, iMessage, Telegram, Signal, Discord and more, shaped for scripts, agents, and humans in a hurry.', + tagline: 'One CLI for all your chats. Built for you and your agent.', + logo: { + src: './src/assets/logo.svg', + replacesTitle: false, + }, + social: [ + { + icon: 'github', + label: 'GitHub', + href: 'https://github.com/beeper/desktop-api-cli', + }, + ], + editLink: { + baseUrl: + 'https://github.com/beeper/desktop-api-cli/edit/main/docs/', + }, + customCss: ['./src/styles/theme.css'], + // Starlight ships full-text search (Pagefind) and dark mode by default. + sidebar: [ + { + label: 'Start here', + items: [ + { label: 'Overview', link: '/' }, + { label: 'Install', link: '/install/' }, + { label: 'Connect a target', link: '/connect/' }, + { label: 'Quick start', link: '/quickstart/' }, + ], + }, + { + label: 'Targets & accounts', + items: [ + { label: 'Targets', link: '/targets/' }, + { label: 'Bridges & accounts', link: '/accounts/' }, + { label: 'Auth & verification', link: '/auth/' }, + ], + }, + { + label: 'Messaging', + items: [ + { label: 'Chats', link: '/chats/' }, + { label: 'Messages', link: '/messages/' }, + { label: 'Sending', link: '/send/' }, + { label: 'Contacts', link: '/contacts/' }, + { label: 'Media', link: '/media/' }, + { label: 'Export', link: '/export/' }, + { label: 'Presence', link: '/presence/' }, + ], + }, + { + label: 'Automation & agents', + items: [ + { label: 'Output & scripting', link: '/scripting/' }, + { label: 'Watch (live events)', link: '/watch/' }, + { label: 'RPC', link: '/rpc/' }, + { label: 'Raw API access', link: '/api/' }, + { label: 'Exit codes', link: '/exit-codes/' }, + ], + }, + { + label: 'Reference', + items: [ + { label: 'Configuration', link: '/config/' }, + { label: 'Plugins', link: '/plugins/' }, + { label: 'Updating', link: '/update/' }, + { + label: 'Full command reference', + link: 'https://github.com/beeper/desktop-api-cli/blob/main/packages/cli/README.md', + attrs: { target: '_blank' }, + }, + { + label: 'Desktop API reference', + link: 'https://developers.beeper.com/desktop-api-reference', + attrs: { target: '_blank' }, + }, + ], + }, + ], + }), + ], +}); diff --git a/docs/bun.lock b/docs/bun.lock new file mode 100644 index 00000000..5aa85d3f --- /dev/null +++ b/docs/bun.lock @@ -0,0 +1,871 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "@beeper/cli-docs", + "dependencies": { + "@astrojs/starlight": "^0.39.2", + "astro": "^6.4.2", + "sharp": "^0.34.5", + }, + }, + }, + "packages": { + "@astrojs/compiler": ["@astrojs/compiler@4.0.0", "", {}, "sha512-eouss7G8ygdZqHuke033VMcVw5HTZUu+PXd/h06DGDUg/jt5btPYPqh66ENWw/mU78rBrf/oeC4oqoBwMtDMNA=="], + + "@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.0", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.1.1", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-Ry2R3VPeIN4uPCSA4xQc+e+vsJXkalKpEbDc07hV+a/o5Bs2N/s/uDcPJH/05L19DKh9tAy7e6JM3YZ6Cxfezw=="], + + "@astrojs/markdown-remark": ["@astrojs/markdown-remark@7.2.0", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.0", "@astrojs/prism": "4.0.2", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-+YxmVQu1Bd+MFfSzjq1rOJvD9+nIOJzz5YIIhdIH01RrxRkKbyKoEgyIqP3yv51MhzMDgd79QaPv+kCVPT8vHw=="], + + "@astrojs/mdx": ["@astrojs/mdx@5.0.6", "", { "dependencies": { "@astrojs/markdown-remark": "7.1.2", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.16.0", "es-module-lexer": "^2.0.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^6.0.0" } }, "sha512-4dKe0ZMmqujofPNDHahzClkwinn9f8jHPcaXcgdGvPAlboD2mjzkUCofli2cBnxYAkdfhC6d50gBJ8i/cH8gHw=="], + + "@astrojs/prism": ["@astrojs/prism@4.0.2", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA=="], + + "@astrojs/sitemap": ["@astrojs/sitemap@3.7.3", "", { "dependencies": { "sitemap": "^9.0.0", "stream-replace-string": "^2.0.0", "zod": "^4.3.6" } }, "sha512-f8euLVsyeAmAkSm/1M2Kb8sL8byQmfgbvBNaHFItCheTj/IpiJYSEWVcqDHZ/yEHxiS7+w87mQkzwZaPHmk5GA=="], + + "@astrojs/starlight": ["@astrojs/starlight@0.39.2", "", { "dependencies": { "@astrojs/markdown-remark": "^7.1.1", "@astrojs/mdx": "^5.0.4", "@astrojs/sitemap": "^3.7.2", "@pagefind/default-ui": "^1.3.0", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", "@types/mdast": "^4.0.4", "astro-expressive-code": "^0.42.0", "bcp-47": "^2.1.0", "hast-util-from-html": "^2.0.3", "hast-util-select": "^6.0.4", "hast-util-to-string": "^3.0.1", "hastscript": "^9.0.1", "i18next": "^26.0.7", "js-yaml": "^4.1.1", "klona": "^2.0.6", "magic-string": "^0.30.21", "mdast-util-directive": "^3.1.0", "mdast-util-to-markdown": "^2.1.2", "mdast-util-to-string": "^4.0.0", "pagefind": "^1.3.0", "rehype": "^13.0.2", "rehype-format": "^5.0.1", "remark-directive": "^4.0.0", "ultrahtml": "^1.6.0", "unified": "^11.0.5", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^6.0.0" } }, "sha512-vlw+bwnjtf5buCTUtLU7JfV6D3knslxqnspr6LKs6hfRuFZiyr5hT44F7GyDqR9FKANUqFxnIzWM81F1k/kOUA=="], + + "@astrojs/telemetry": ["@astrojs/telemetry@3.3.2", "", { "dependencies": { "ci-info": "^4.4.0", "dset": "^3.1.4", "is-docker": "^4.0.0", "is-wsl": "^3.1.1", "which-pm-runs": "^1.1.0" } }, "sha512-j8DNruA8ors99Al39RYZPJK4DC1bKkoNm93mAMuBhY9TCNC4R8n1q7ovFnJ5qhGh5Lsh7pa1gpQVpYpsJPeTHQ=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "@capsizecss/unpack": ["@capsizecss/unpack@4.0.0", "", { "dependencies": { "fontkitten": "^1.0.0" } }, "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA=="], + + "@clack/core": ["@clack/core@1.4.0", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-7Wctjq6f7c1CPz8sPpkwUnz8yRgVANkpNupb81q432FjcJg4l+Sw7XANdNSdWfAKq0IHI0JTcUeK5dxs/HrGPw=="], + + "@clack/prompts": ["@clack/prompts@1.5.0", "", { "dependencies": { "@clack/core": "1.4.0", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-wKh+wTjmrUoUdkZg8KpJO5X+p9PWV+KE9mePseq9UYWkukgTKsGS47RRL2HstwVcvDQH+PenrPJWII8+MfiiyA=="], + + "@ctrl/tinycolor": ["@ctrl/tinycolor@4.2.0", "", {}, "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + + "@expressive-code/core": ["@expressive-code/core@0.42.0", "", { "dependencies": { "@ctrl/tinycolor": "^4.0.4", "hast-util-select": "^6.0.2", "hast-util-to-html": "^9.0.1", "hast-util-to-text": "^4.0.1", "hastscript": "^9.0.0", "postcss": "^8.4.38", "postcss-nested": "^6.0.1", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1" } }, "sha512-MN11+9nfmaC7sYu2BZJXAXqwkBRt8t1xTSqP+Ti1NfTEskgl6xUnzDxoaiQkg0BMzpglA0pys4dpDKquP/cyIw=="], + + "@expressive-code/plugin-frames": ["@expressive-code/plugin-frames@0.42.0", "", { "dependencies": { "@expressive-code/core": "^0.42.0" } }, "sha512-XtkPm+941Uta7Y+81Acv+OA/20F1NJmJhCX6UYGKpqEIGqplNh3PTOhcURp6tcruhlzJcWcvpWy6Oigz3SrjqA=="], + + "@expressive-code/plugin-shiki": ["@expressive-code/plugin-shiki@0.42.0", "", { "dependencies": { "@expressive-code/core": "^0.42.0", "shiki": "^4.0.2" } }, "sha512-PMKey/kLmewttAHQezL+Y5Fx3vVssfDi3+FJOYQQS2mXP3tQspFELtKKAfsXfmSXdToZYgwoO69HJndqfE+09g=="], + + "@expressive-code/plugin-text-markers": ["@expressive-code/plugin-text-markers@0.42.0", "", { "dependencies": { "@expressive-code/core": "^0.42.0" } }, "sha512-l59lUx8fq1v5g6SpmbDjiU0+7IdfbiWnAyRmtTVSpfhyq+nZMN4UcmYyu2b9Mynhzt7Gr+O+cXyEPDNb2AVWVQ=="], + + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], + + "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], + + "@pagefind/darwin-arm64": ["@pagefind/darwin-arm64@1.5.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ=="], + + "@pagefind/darwin-x64": ["@pagefind/darwin-x64@1.5.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw=="], + + "@pagefind/default-ui": ["@pagefind/default-ui@1.5.2", "", {}, "sha512-pm1LMnQg8N2B3n2TnjKlhaFihpz6zTiA4HiGQ6/slKO/+8K9CAU5kcjdSSPgpuk1PMuuN4hxLipUIifnrkl3Sg=="], + + "@pagefind/freebsd-x64": ["@pagefind/freebsd-x64@1.5.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA=="], + + "@pagefind/linux-arm64": ["@pagefind/linux-arm64@1.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw=="], + + "@pagefind/linux-x64": ["@pagefind/linux-x64@1.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA=="], + + "@pagefind/windows-arm64": ["@pagefind/windows-arm64@1.5.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g=="], + + "@pagefind/windows-x64": ["@pagefind/windows-x64@1.5.2", "", { "os": "win32", "cpu": "x64" }, "sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg=="], + + "@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.4", "", { "os": "android", "cpu": "arm" }, "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.4", "", { "os": "android", "cpu": "arm64" }, "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.4", "", { "os": "linux", "cpu": "arm" }, "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.4", "", { "os": "linux", "cpu": "arm" }, "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.4", "", { "os": "none", "cpu": "arm64" }, "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw=="], + + "@shikijs/core": ["@shikijs/core@4.1.0", "", { "dependencies": { "@shikijs/primitive": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ=="], + + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-YquhawCUgaBfhsS72e2Y/dI59gCBNPHu3fEO/tvLaXrTssxZrY5ddjtNLTwndrMgPo8b3IscE+xoICDzpTmlFQ=="], + + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-axLpjVs45YBvvINa+dJF+NPW+KtFkNXsFr4SDw2BMj9GdeMnGxVB9PQb2xXlJYovslt/nz6giedAyOANkfc7hg=="], + + "@shikijs/langs": ["@shikijs/langs@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0" } }, "sha512-nwOMruEkbgdZfQ/b8CgpNBVOpvG1k0N5tbmgiFeqsan401+x3ILqlzZJowSla4Agmq4hG2Uf2wh5jLTEhR8VSg=="], + + "@shikijs/primitive": ["@shikijs/primitive@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zx2/2Uwj2q9X3KSyYREEhXO23xBw5WUhP4orK2lE4r+t9JGITmEe0JH+wPmJhqHpOT2bRRs6lAL945+LDvOAGw=="], + + "@shikijs/themes": ["@shikijs/themes@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0" } }, "sha512-emCcTnUM7yO2wltYbaxm+yLvcCI4+h8XBKc4KmJ7EZUXoSGjcCHifkI//R4OFit9ewpg7H2/9tjOuXrT2v/Knw=="], + + "@shikijs/types": ["@shikijs/types@4.1.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA=="], + + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], + + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + + "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], + + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + + "@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="], + + "@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="], + + "@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="], + + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="], + + "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], + + "astro": ["astro@6.4.2", "", { "dependencies": { "@astrojs/compiler": "^4.0.0", "@astrojs/internal-helpers": "0.10.0", "@astrojs/markdown-remark": "7.2.0", "@astrojs/telemetry": "3.3.2", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^1.1.1", "devalue": "^5.6.3", "diff": "^8.0.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.27.3", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.4", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.1.1", "jsonc-parser": "^3.3.1", "magic-string": "^0.30.21", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", "rehype": "^13.0.2", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "svgo": "^4.0.1", "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "ultrahtml": "^1.6.0", "unifont": "~0.7.4", "unist-util-visit": "^5.1.0", "unstorage": "^1.17.5", "vfile": "^6.0.3", "vite": "^7.3.2", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.3.6" }, "optionalDependencies": { "sharp": "^0.34.0" }, "bin": { "astro": "./bin/astro.mjs" } }, "sha512-8H89CH2dKL5SCU99OCqdU9BGjmPkSJqaPurywj5XMo7eMFGUFD3vsNhdEKnEh4mK4LgGje3/QDTTSIIGst0G0Q=="], + + "astro-expressive-code": ["astro-expressive-code@0.42.0", "", { "dependencies": { "rehype-expressive-code": "^0.42.0" }, "peerDependencies": { "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta" } }, "sha512-aiTePi2Cn0mJPYWZSzP1GcxCinX9mNtJyCCshVVPSg1yRwM7ADvFJOx0FnS440M9t65hp8JH//dc2qr22Bm4ag=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + + "bcp-47": ["bcp-47@2.1.0", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w=="], + + "bcp-47-match": ["bcp-47-match@2.0.3", "", {}, "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ=="], + + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + + "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + + "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], + + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + + "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], + + "common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="], + + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="], + + "crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], + + "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], + + "css-selector-parser": ["css-selector-parser@3.3.0", "", {}, "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g=="], + + "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], + + "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + + "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "devalue": ["devalue@5.8.1", "", {}, "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw=="], + + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + + "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], + + "direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="], + + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + + "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], + + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="], + + "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], + + "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], + + "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + + "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], + + "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="], + + "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], + + "estree-util-scope": ["estree-util-scope@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="], + + "estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="], + + "estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="], + + "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + + "expressive-code": ["expressive-code@0.42.0", "", { "dependencies": { "@expressive-code/core": "^0.42.0", "@expressive-code/plugin-frames": "^0.42.0", "@expressive-code/plugin-shiki": "^0.42.0", "@expressive-code/plugin-text-markers": "^0.42.0" } }, "sha512-V5DtJLEKuj4wf9O6IRtPtRObkMVy2ggR+S0MdjrTw6m58krZnDioyhW1si3Y04c5YPeooP4nd85Yq9NwEVHS4g=="], + + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], + + "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], + + "fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="], + + "fontace": ["fontace@0.4.1", "", { "dependencies": { "fontkitten": "^1.0.2" } }, "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw=="], + + "fontkitten": ["fontkitten@1.0.3", "", { "dependencies": { "tiny-inflate": "^1.0.3" } }, "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "get-tsconfig": ["get-tsconfig@5.0.0-beta.4", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ=="], + + "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], + + "h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="], + + "hast-util-embedded": ["hast-util-embedded@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-is-element": "^3.0.0" } }, "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA=="], + + "hast-util-format": ["hast-util-format@1.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-minify-whitespace": "^1.0.0", "hast-util-phrasing": "^3.0.0", "hast-util-whitespace": "^3.0.0", "html-whitespace-sensitive-tag-names": "^3.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA=="], + + "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="], + + "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], + + "hast-util-has-property": ["hast-util-has-property@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA=="], + + "hast-util-is-body-ok-link": ["hast-util-is-body-ok-link@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ=="], + + "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="], + + "hast-util-minify-whitespace": ["hast-util-minify-whitespace@1.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-is-element": "^3.0.0", "hast-util-whitespace": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw=="], + + "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], + + "hast-util-phrasing": ["hast-util-phrasing@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-has-property": "^3.0.0", "hast-util-is-body-ok-link": "^3.0.0", "hast-util-is-element": "^3.0.0" } }, "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ=="], + + "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], + + "hast-util-select": ["hast-util-select@6.0.4", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "bcp-47-match": "^2.0.0", "comma-separated-tokens": "^2.0.0", "css-selector-parser": "^3.0.0", "devlop": "^1.0.0", "direction": "^2.0.0", "hast-util-has-property": "^3.0.0", "hast-util-to-string": "^3.0.0", "hast-util-whitespace": "^3.0.0", "nth-check": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw=="], + + "hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="], + + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], + + "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], + + "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], + + "hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="], + + "hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + + "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], + + "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], + + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], + + "html-whitespace-sensitive-tag-names": ["html-whitespace-sensitive-tag-names@3.0.1", "", {}, "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA=="], + + "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], + + "i18next": ["i18next@26.3.0", "", { "peerDependencies": { "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-gHSgGpUXVmuqE2El1W61DmxeyeTlFfZgdJRWMo9jScAn5pu7TuTuiccb1zh3E2J9hEBVGJ23+96x0ieBhfuIHA=="], + + "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + + "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], + + "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], + + "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], + + "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], + + "is-docker": ["is-docker@4.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA=="], + + "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], + + "klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="], + + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + + "lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="], + + "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], + + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + + "mdast-util-definitions": ["mdast-util-definitions@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="], + + "mdast-util-directive": ["mdast-util-directive@3.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q=="], + + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], + + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], + + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], + + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], + + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], + + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + + "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="], + + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], + + "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], + + "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + + "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], + + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-extension-directive": ["micromark-extension-directive@4.0.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "parse-entities": "^4.0.0" } }, "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg=="], + + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], + + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + + "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="], + + "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="], + + "micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="], + + "micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="], + + "micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + + "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="], + + "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], + + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], + + "node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + + "ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], + + "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], + + "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="], + + "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], + + "p-limit": ["p-limit@7.3.0", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw=="], + + "p-queue": ["p-queue@9.3.0", "", { "dependencies": { "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" } }, "sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang=="], + + "p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="], + + "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], + + "pagefind": ["pagefind@1.5.2", "", { "optionalDependencies": { "@pagefind/darwin-arm64": "1.5.2", "@pagefind/darwin-x64": "1.5.2", "@pagefind/freebsd-x64": "1.5.2", "@pagefind/linux-arm64": "1.5.2", "@pagefind/linux-x64": "1.5.2", "@pagefind/windows-arm64": "1.5.2", "@pagefind/windows-x64": "1.5.2" }, "bin": { "pagefind": "lib/runner/bin.cjs" } }, "sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q=="], + + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + + "parse-latin": ["parse-latin@7.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="], + + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], + + "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], + + "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + + "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], + + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + + "radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="], + + "readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + + "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], + + "recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], + + "recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="], + + "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], + + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], + + "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + + "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + + "rehype": ["rehype@13.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", "unified": "^11.0.0" } }, "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A=="], + + "rehype-expressive-code": ["rehype-expressive-code@0.42.0", "", { "dependencies": { "expressive-code": "^0.42.0" } }, "sha512-8rp/1YMEVVSYbtz+bFBx+uSx3vA4i4T8RwRm5Q/IWbucQnnQqQ0hDqtmKOr8tv+59Cik6cu5aH3WPo0I7csuTA=="], + + "rehype-format": ["rehype-format@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-format": "^1.0.0" } }, "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ=="], + + "rehype-parse": ["rehype-parse@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-html": "^2.0.0", "unified": "^11.0.0" } }, "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag=="], + + "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], + + "rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="], + + "rehype-stringify": ["rehype-stringify@10.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-html": "^9.0.0", "unified": "^11.0.0" } }, "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA=="], + + "remark-directive": ["remark-directive@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-directive": "^3.0.0", "micromark-extension-directive": "^4.0.0", "unified": "^11.0.0" } }, "sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA=="], + + "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], + + "remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="], + + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], + + "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], + + "remark-smartypants": ["remark-smartypants@3.0.2", "", { "dependencies": { "retext": "^9.0.0", "retext-smartypants": "^6.0.0", "unified": "^11.0.4", "unist-util-visit": "^5.0.0" } }, "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA=="], + + "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="], + + "retext-latin": ["retext-latin@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="], + + "retext-smartypants": ["retext-smartypants@6.2.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ=="], + + "retext-stringify": ["retext-stringify@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" } }, "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA=="], + + "rollup": ["rollup@4.60.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.4", "@rollup/rollup-android-arm64": "4.60.4", "@rollup/rollup-darwin-arm64": "4.60.4", "@rollup/rollup-darwin-x64": "4.60.4", "@rollup/rollup-freebsd-arm64": "4.60.4", "@rollup/rollup-freebsd-x64": "4.60.4", "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", "@rollup/rollup-linux-arm-musleabihf": "4.60.4", "@rollup/rollup-linux-arm64-gnu": "4.60.4", "@rollup/rollup-linux-arm64-musl": "4.60.4", "@rollup/rollup-linux-loong64-gnu": "4.60.4", "@rollup/rollup-linux-loong64-musl": "4.60.4", "@rollup/rollup-linux-ppc64-gnu": "4.60.4", "@rollup/rollup-linux-ppc64-musl": "4.60.4", "@rollup/rollup-linux-riscv64-gnu": "4.60.4", "@rollup/rollup-linux-riscv64-musl": "4.60.4", "@rollup/rollup-linux-s390x-gnu": "4.60.4", "@rollup/rollup-linux-x64-gnu": "4.60.4", "@rollup/rollup-linux-x64-musl": "4.60.4", "@rollup/rollup-openbsd-x64": "4.60.4", "@rollup/rollup-openharmony-arm64": "4.60.4", "@rollup/rollup-win32-arm64-msvc": "4.60.4", "@rollup/rollup-win32-ia32-msvc": "4.60.4", "@rollup/rollup-win32-x64-gnu": "4.60.4", "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g=="], + + "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], + + "semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], + + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + + "shiki": ["shiki@4.1.0", "", { "dependencies": { "@shikijs/core": "4.1.0", "@shikijs/engine-javascript": "4.1.0", "@shikijs/engine-oniguruma": "4.1.0", "@shikijs/langs": "4.1.0", "@shikijs/themes": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-l/ABZPUR5v70jI10EzqfMS/I96vjSGv2y0ihUV+WYFzv0EfvW4s54m0Lg8wCrrL+2IkwBzFTuxkZjPf8b2NX9Q=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "sitemap": ["sitemap@9.0.1", "", { "dependencies": { "@types/node": "^24.9.2", "@types/sax": "^1.2.1", "arg": "^5.0.0", "sax": "^1.4.1" }, "bin": { "sitemap": "dist/esm/cli.js" } }, "sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ=="], + + "smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="], + + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + + "stream-replace-string": ["stream-replace-string@2.0.0", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="], + + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], + + "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + + "svgo": ["svgo@4.0.1", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w=="], + + "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], + + "tinyclip": ["tinyclip@0.1.13", "", {}, "sha512-8OqlXQ35euK9+e7L68u8UwcODxkHoIkjbGsgXuARKNyQ5G6xt8nw1YPeMbxMLgCPFkToU+UEK5j05t2t8edKpQ=="], + + "tinyexec": ["tinyexec@1.2.3", "", {}, "sha512-g62dB+w1/OEFnPvmX0yd/HnetYITOL+1nJW7kitOycOeAvmbWC/nu0fwmmQ/kupNojqExzyC/T++pST/jRJ2mQ=="], + + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + + "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], + + "ultrahtml": ["ultrahtml@1.6.0", "", {}, "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw=="], + + "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + + "unifont": ["unifont@0.7.4", "", { "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg=="], + + "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], + + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-modify-children": ["unist-util-modify-children@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "array-iterate": "^2.0.0" } }, "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw=="], + + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="], + + "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-children": ["unist-util-visit-children@3.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + + "unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], + + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + + "vite": ["vite@7.3.3", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA=="], + + "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], + + "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], + + "which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="], + + "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], + + "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], + + "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], + + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + + "@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@7.1.2", "", { "dependencies": { "@astrojs/internal-helpers": "0.9.1", "@astrojs/prism": "4.0.2", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "js-yaml": "^4.1.1", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "retext-smartypants": "^6.2.0", "shiki": "^4.0.0", "smol-toml": "^1.6.0", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-caXZ4Dc2St2dW8luEg22GlP0gupLdztCTQE4EzZOxW1pqWXz9mbeJEuHUkgDYcKWW8tjIHkydYDhWLVoxJ327Q=="], + + "@mdx-js/mdx/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], + + "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "estree-util-build-jsx/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + + "@astrojs/mdx/@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.9.1", "", { "dependencies": { "picomatch": "^4.0.4" } }, "sha512-1pWuARqYom/TzuU3+0ZugsTrKlUydWKuULmDqSMTuonY+9IRDUEGKX/8PXQ1nBxRq3w85uGtd9q9SXfqEldMIQ=="], + + "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000..6a69220a --- /dev/null +++ b/docs/package.json @@ -0,0 +1,19 @@ +{ + "name": "@beeper/cli-docs", + "private": true, + "type": "module", + "version": "0.0.0", + "description": "Documentation site for the Beeper CLI (Astro Starlight, fully static export).", + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "build": "astro build", + "preview": "astro preview", + "check": "astro check" + }, + "dependencies": { + "@astrojs/starlight": "^0.39.2", + "astro": "^6.4.2", + "sharp": "^0.34.5" + } +} diff --git a/docs/public/favicon.svg b/docs/public/favicon.svg new file mode 100644 index 00000000..1648cdfc --- /dev/null +++ b/docs/public/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/docs/src/assets/logo.svg b/docs/src/assets/logo.svg new file mode 100644 index 00000000..baf84afc --- /dev/null +++ b/docs/src/assets/logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/docs/src/content.config.ts b/docs/src/content.config.ts new file mode 100644 index 00000000..6a7b7a02 --- /dev/null +++ b/docs/src/content.config.ts @@ -0,0 +1,7 @@ +import { defineCollection } from 'astro:content'; +import { docsLoader } from '@astrojs/starlight/loaders'; +import { docsSchema } from '@astrojs/starlight/schema'; + +export const collections = { + docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), +}; diff --git a/packages/cli/docs/accounts.md b/docs/src/content/docs/accounts.mdx similarity index 55% rename from packages/cli/docs/accounts.md rename to docs/src/content/docs/accounts.mdx index 11610a42..7dae136a 100644 --- a/packages/cli/docs/accounts.md +++ b/docs/src/content/docs/accounts.mdx @@ -1,12 +1,26 @@ -# accounts +--- +title: Bridges & accounts +description: List or add chat-network accounts (WhatsApp, Discord, iMessage, …), choose a default account for --account-filtered commands, or remove one. +sidebar: + label: Bridges & accounts +--- -Read when: listing or adding chat-network accounts (WhatsApp, Discord, -iMessage, etc.), choosing a default account for `--account`-filtered -commands, or removing one. +import { Aside } from '@astrojs/starlight/components'; + +A **bridge** is the connector used to add or reconnect a chat account. An +**account** is a signed-in instance of a network on your target. + + ## Commands ```sh +beeper bridges list +beeper bridges show + beeper accounts list [--account SELECTOR]... [--ids] beeper accounts add [bridge] [--flow ID] [--login-id ID] [--cookie name=value]... [--field id=value]... [--webview] [--non-interactive] [--no-guided] beeper accounts show @@ -14,21 +28,22 @@ beeper accounts use # "" clears defaultAccount beeper accounts remove ``` +## Account selectors + +An **account selector** matches by account ID, network name, bridge type/id, or +user identity (display name, username, email, phone). A network name can expand +to multiple matching accounts. + ## Notes -- An *account selector* matches by account ID, network name, bridge type/id, - or user identity (display name, username, email, phone). -- A *bridge* is the connector used to add or reconnect a chat account. -- `accounts add` without a bridge opens the account-connection chooser. -- `bridges list` is the scriptable catalog; `accounts add` is the guided - account connection flow. +- `bridges list` is the scriptable catalog; `accounts add` is the guided account + connection flow. `accounts add` without a bridge opens the connection chooser. - `accounts use NAME` persists `defaultAccount` in CLI config. Subsequent - account-scoped commands fall back to that default when `--account` is - omitted. -- `accounts use ""` clears the default. + account-scoped commands fall back to that default when `--account` is omitted. + `accounts use ""` clears it. - `accounts list --json` annotates the default account with `default: true`. -- For non-interactive sign-in, pass `--flow`, `--field`, and `--cookie` and - add `--non-interactive` to fail instead of prompting. +- For non-interactive sign-in, pass `--flow`, `--field`, and `--cookie`, and add + `--non-interactive` to fail instead of prompting. - For cookie-based sign-in, `--webview` can use Bun.WebView with Chrome to collect cookie fields before falling back to prompts. Chrome remote debugging must be enabled for a visible interactive tab; otherwise Bun may spawn a diff --git a/docs/src/content/docs/api.mdx b/docs/src/content/docs/api.mdx new file mode 100644 index 00000000..5e85ede1 --- /dev/null +++ b/docs/src/content/docs/api.mdx @@ -0,0 +1,47 @@ +--- +title: Raw API access +description: Call raw Desktop API endpoints the CLI doesn't yet wrap with a workflow command. +sidebar: + label: Raw API access +--- + +import { Aside } from '@astrojs/starlight/components'; + + + +## Commands + +```sh +beeper api get [--no-auth] +beeper api post [--body JSON] [--no-auth] +beeper api request [--body JSON] [--no-auth] +``` + +## Notes + +- `` is a Desktop API path, e.g. `/v1/info` or `/v1/chats/{chatID}/read`. +- `--no-auth` calls a public path without the bearer token. +- `--body` is sent as `application/json`; default is `{}` for `post`. +- `api request` lets you hit `GET | POST | PUT | PATCH | DELETE`; the others are + convenience shortcuts. +- `--read-only` blocks `api post` / `api put` / `api patch` / `api delete` / + `api request `. + +## Examples + +```sh +beeper api get /v1/info +beeper api get /v1/chats --json +beeper api post /v1/chats/abc/read --body '{"messageID":"x"}' +beeper api request PATCH /v1/chats/abc --body '{"isPinned":true}' +beeper api request DELETE /v1/chats/abc/messages/def/reactions --body '{"reactionKey":"👍"}' +``` + +## See also + +The full Desktop API surface is documented at the +[Beeper Desktop API reference](https://developers.beeper.com/desktop-api-reference). diff --git a/packages/cli/docs/auth.md b/docs/src/content/docs/auth.mdx similarity index 61% rename from packages/cli/docs/auth.md rename to docs/src/content/docs/auth.mdx index dfd1bbff..b32a6583 100644 --- a/packages/cli/docs/auth.md +++ b/docs/src/content/docs/auth.mdx @@ -1,12 +1,21 @@ -# auth +--- +title: Auth & verification +description: Check sign-in status, clear stored tokens, or drive an end-to-end device-verification flow for encrypted messages. +sidebar: + label: Auth & verification +--- -Read when: checking sign-in status, clearing stored tokens, or driving an -end-to-end device-verification flow for encrypted messages. +import { Aside } from '@astrojs/starlight/components'; `auth` commands inspect and manage CLI-side authentication state and encryption-readiness. The selected target's stored OAuth token lives in the target file under `~/.beeper/targets/`; `BEEPER_ACCESS_TOKEN` overrides it. + + ## Commands ```sh @@ -28,12 +37,18 @@ beeper auth verify cancel ## Notes -- `auth status` reports the token source (env vs. target file) and metadata; it does not call the network. -- `auth logout` revokes the token at the Desktop OAuth endpoint and clears the local copy. -- `auth verify` (no subcommand) walks the most common SAS/emoji verification flow interactively. -- For agents, drive the explicit subcommands (`start` → `sas` → `sas-confirm`) and use `--json` to inspect state. -- `verify status` returns the encryption-readiness state (`ready`, `needs-verification`, `verification-in-progress`). -- `recovery-key` and `reset-recovery-key` apply to the encrypted-messages key, not to Beeper account login. +- `auth status` reports the token source (env vs. target file) and metadata; it + does not call the network. +- `auth logout` revokes the token at the Desktop OAuth endpoint and clears the + local copy. +- `auth verify` (no subcommand) walks the most common SAS/emoji verification + flow interactively. +- For agents, drive the explicit subcommands (`start` → `sas` → `sas-confirm`) + and use `--json` to inspect state. +- `verify status` returns the encryption-readiness state (`ready`, + `needs-verification`, `verification-in-progress`). +- `recovery-key` and `reset-recovery-key` apply to the encrypted-messages key, + not to Beeper account login. ## Examples @@ -44,3 +59,8 @@ beeper auth verify recovery-key --code ABCD-EFGH-IJKL-MNOP beeper auth verify reset-recovery-key beeper auth logout ``` + + diff --git a/packages/cli/docs/chats.md b/docs/src/content/docs/chats.mdx similarity index 72% rename from packages/cli/docs/chats.md rename to docs/src/content/docs/chats.mdx index 11f9f361..7e0fc70d 100644 --- a/packages/cli/docs/chats.md +++ b/docs/src/content/docs/chats.mdx @@ -1,8 +1,15 @@ -# chats +--- +title: Chats +description: List, search, inspect, and change chat state — archive, pin, mute, mark-read, priority, rename, draft, focus, disappearing timer, reminders. +--- -Read when: listing, searching, inspecting, or changing chat state — archive, -pin, mute, mark-read, priority (Inbox vs Low Priority), rename, draft, focus, -disappear timer, reminders. +import { Aside } from '@astrojs/starlight/components'; + + ## Commands @@ -27,15 +34,23 @@ beeper chats unremind --chat SEL [--pick N] beeper chats focus --chat SEL [--message MSG_ID] [--draft TEXT] [--attachment PATH] [--pick N] ``` +## Selecting a chat + +All `--chat` flags accept a **Beeper chat ID, a local chat ID, the exact title, +or search text**. Ambiguous matches return numbered choices; pass `--pick N` to +select one in scripts. + ## Notes -- All `--chat` flags accept a Beeper chat ID, a local chat ID, the exact title, or search text. -- Ambiguous matches return numbered choices; pass `--pick N` to select one. -- `chats list` filters compose: e.g. `--unread --no-muted --pinned` returns only pinned, unread, non-muted chats. -- `chats mute` is currently boolean — the Desktop API does not yet expose a mute duration. -- `chats focus` opens Beeper Desktop on the selected chat (and optionally scrolls to a message or prefills the composer). +- `chats list` filters compose: e.g. `--unread --no-muted --pinned` returns only + pinned, unread, non-muted chats. +- `chats mute` is currently boolean — the Desktop API does not yet expose a mute + duration. +- `chats focus` opens Beeper Desktop on the selected chat (and optionally scrolls + to a message or prefills the composer). - `chats disappear --seconds 0` turns disappearing messages off. -- Labels are not yet supported by the Desktop API; there is no `chats label` command in this CLI. +- Labels are not yet supported by the Desktop API; there is no `chats label` + command in this CLI. ## Examples diff --git a/docs/src/content/docs/config.mdx b/docs/src/content/docs/config.mdx new file mode 100644 index 00000000..b763a9a3 --- /dev/null +++ b/docs/src/content/docs/config.mdx @@ -0,0 +1,55 @@ +--- +title: Configuration +description: Inspect, change, or reset the CLI's local configuration, and the environment variables that override it. +--- + +import { Aside } from '@astrojs/starlight/components'; + +CLI configuration is stored under your user config dir — `~/.beeper/config.json`, +or wherever `BEEPER_CLI_CONFIG_DIR` points. Print the path with `beeper config +path`. The default Beeper Client API target is `http://127.0.0.1:23373`. + + + +## Commands + +```sh +beeper config path +beeper config get [defaultTarget | defaultAccount | baseURL | auth] +beeper config set +beeper config reset +``` + +## Notes + +- `config path` prints the JSON config path (suitable for `cat` or `cd + $(dirname …)`). +- `config get` without a key prints the full config; passing a key prints just + that field. `auth.accessToken` is always redacted. +- `config set ""` clears the field. Only `defaultTarget` and + `defaultAccount` are settable here; other fields are written by commands like + `targets use` and `auth verify`. +- `config reset` deletes the config file. + +## Environment overrides + +| Variable | Effect | +| --- | --- | +| `BEEPER_ACCESS_TOKEN` | Bearer token for the selected target. Overrides stored OAuth login. | +| `BEEPER_DESKTOP_BASE_URL` | Beeper Client API base URL (Desktop or Server). Defaults to `http://127.0.0.1:23373`. | +| `BEEPER_TARGET` | Selects a configured target by name for a single shell. | +| `BEEPER_READONLY` | `1`/`true`/`yes`/`on` enables read-only mode globally. | +| `BEEPER_CLI_CONFIG_DIR` | Override config directory for testing or isolated profiles. | + +## Examples + +```sh +beeper config path +beeper config get --json +beeper config get defaultTarget +beeper config set defaultTarget work +beeper config set defaultAccount "" +beeper config reset +``` diff --git a/docs/src/content/docs/connect.mdx b/docs/src/content/docs/connect.mdx new file mode 100644 index 00000000..b072a530 --- /dev/null +++ b/docs/src/content/docs/connect.mdx @@ -0,0 +1,146 @@ +--- +title: Connect a target +description: A target is the Beeper endpoint the CLI talks to. Connect local Desktop, a self-hosted Server, a remote target over OAuth, or a bearer token in CI. +sidebar: + label: Connect a target + order: 2 +--- + +import { Tabs, TabItem, Aside, Steps } from '@astrojs/starlight/components'; + +A **target** is the Beeper endpoint `beeper` talks to — local Beeper Desktop, +local Beeper Server, or a remote Beeper Desktop or Beeper Server. Pick one of +four paths. `beeper setup` orchestrates all of them. + +The selected target is persisted in `~/.beeper/config.json` (override the whole +config directory with `BEEPER_CLI_CONFIG_DIR`). See [Targets](/targets/) to +manage more than one. + + + + **Default, recommended.** If Beeper Desktop is installed and signed in here, + `beeper setup` discovers it on `http://127.0.0.1:23373` and adopts the + existing session. If it's installed but not running, `setup` offers to launch + it. If it isn't installed at all, `--install` does that in one step. + + ```text + $ beeper setup --desktop --install + ▎ Installed Beeper Desktop (stable) + ▎ Launched Beeper Desktop + next Sign in to Beeper Desktop, then re-run `beeper setup`. + + $ beeper setup + ▎ Connected desktop + accounts whatsapp, telegram + ``` + + Variants: `beeper setup --local` skips discovery and forces the local path; + `beeper install desktop --channel nightly` uses the nightly channel. + + + + For a headless, long-running setup on this machine, install and adopt a + local Beeper Server. The CLI manages the process — `targets + start/stop/restart/logs/enable`. + + ```text + $ beeper setup --server --install + ▎ Installed Beeper Server (stable) + ▎ Started server on http://127.0.0.1:23373 + auth Opening browser to authorize this server… + ▎ Connected server + accounts (none) + next Run `beeper accounts add` to connect a network. + ``` + + Then connect a network — `beeper accounts add` walks each bridge through its + own login (QR, code, OAuth, cookie): + + ```text + $ beeper accounts add + ? Which bridge? whatsapp + Scan this QR code with WhatsApp on your phone: + ▄▄▄▄▄▄▄ ▄ ▄ ▄▄▄▄▄▄▄ + █ ███ █ ▄█▄ █ ███ █ + █ ███ █ ▀█▀ █ ███ █ + ▀▀▀▀▀▀▀ ▀ ▀ ▀▀▀▀▀▀▀ + ▎ Connected whatsapp · +1•••4242 + ``` + + Variants: `beeper install server`, `beeper install server --server-env staging`. + + + + For a Beeper Desktop or Server running on another machine, authorize the CLI + through a browser-based OAuth/PKCE flow. + + ```text + $ beeper setup --remote https://desktop.example.com + ▎ Authorizing https://desktop.example.com + flow OAuth (PKCE) — opening browser… + ▎ Connected remote (desktop.example.com) + accounts whatsapp, telegram, signal + ``` + + Variants: `beeper setup --oauth` (PKCE against the default Beeper auth); + `beeper targets add remote work https://desktop.example.com --default` + registers additional remotes. + + + + For agents, CI, and scripts, hand the CLI a bearer token directly — no + browser, no interactive prompts. + + ```sh + BEEPER_ACCESS_TOKEN=... beeper chats list --json + BEEPER_ACCESS_TOKEN=... BEEPER_DESKTOP_BASE_URL=https://desktop.example.com \ + beeper messages list --chat 10313 --json + ``` + + `BEEPER_ACCESS_TOKEN` overrides any stored OAuth login for the selected + target. See [Configuration](/config/) for every environment override. + + + +## The happy path + +If Beeper Desktop is already on this machine, there's nothing to choose: + + + +1. Run `beeper setup`. It finds Desktop, offers to launch it if needed, and + adopts the session. + + ```text + $ beeper setup + Looking for Beeper Desktop… found, not running. + Launch it now? [Y/n] y + ▎ Launched Beeper Desktop + + $ beeper setup + Use this Desktop session for CLI access? [Y/n] y + ▎ Connected desktop + accounts whatsapp, telegram, imessage + endpoint http://127.0.0.1:23373 + ``` + +2. Confirm you're ready with `beeper status` (or `beeper doctor` for full + setup/auth/encryption diagnostics). + +3. You're connected. Head to the [Quick start](/quickstart/). + + + + + +## Encrypted messages + +Reaching some networks requires device verification for end-to-end encrypted +messages. `beeper status` / `beeper doctor` tell you whether the target is +encryption-ready; [Auth & verification](/auth/) covers the SAS/QR and +recovery-key flows. diff --git a/packages/cli/docs/contacts.md b/docs/src/content/docs/contacts.mdx similarity index 60% rename from packages/cli/docs/contacts.md rename to docs/src/content/docs/contacts.mdx index 06f6e0f1..1aba9d6b 100644 --- a/packages/cli/docs/contacts.md +++ b/docs/src/content/docs/contacts.mdx @@ -1,6 +1,13 @@ -# contacts +--- +title: Contacts +description: Look up contacts across one or more accounts. +--- -Read when: looking up contacts across one or more accounts. +import { Aside } from '@astrojs/starlight/components'; + + ## Commands @@ -12,9 +19,12 @@ beeper contacts show [--account SEL]... ## Notes -- `contacts list` reads merged account contacts; without `--account` it iterates all accounts. -- `contacts search` runs the network search where available and returns merged results across accounts; omitting `--account` searches every account. -- `contacts show` accepts a user ID, display name, or phone/handle and finds it on the first matching account. +- `contacts list` reads merged account contacts; without `--account` it iterates + all accounts. +- `contacts search` runs the network search where available and returns merged + results across accounts; omitting `--account` searches every account. +- `contacts show` accepts a user ID, display name, or phone/handle and finds it + on the first matching account. ## Examples diff --git a/docs/src/content/docs/exit-codes.mdx b/docs/src/content/docs/exit-codes.mdx new file mode 100644 index 00000000..f9a1cce3 --- /dev/null +++ b/docs/src/content/docs/exit-codes.mdx @@ -0,0 +1,25 @@ +--- +title: Exit codes +description: The deterministic exit codes the Beeper CLI returns, so scripts and agents can branch on failure reasons. +--- + +Every command returns a deterministic exit code so scripts and agents can branch +on the failure reason without parsing text. + +| Code | Meaning | +| --- | --- | +| `0` | Success. | +| `1` | Generic runtime error. | +| `2` | Usage error (parsing, validation, missing required flag/arg, read-only refusal). | +| `3` | Auth required (no stored token; sign in or set `BEEPER_ACCESS_TOKEN`). | +| `4` | Target/account not ready (`doctor` reports this when readiness is not `ready`). | +| `5` | Selector matched nothing (unknown target, account, chat, contact). | +| `6` | Ambiguous selector (multiple matches; pass an exact ID or `--pick N`). | + +JSON output preserves the same envelope on failure, written to stderr: + +```json +{"success":false,"data":null,"error":"…","exitCode":N} +``` + +See [Output & scripting](/scripting/) for the full envelope and global flags. diff --git a/docs/src/content/docs/export.mdx b/docs/src/content/docs/export.mdx new file mode 100644 index 00000000..1c603f10 --- /dev/null +++ b/docs/src/content/docs/export.mdx @@ -0,0 +1,53 @@ +--- +title: Export +description: Make a heavy, multi-chat, attachment-including export of Beeper data to disk. Resumable. +--- + +import { Aside } from '@astrojs/starlight/components'; + + + +## Command + +```sh +beeper export + [-o, --out DIR] + [--account SEL]... + [--chat SEL]... + [--limit-chats N] + [--limit-messages N] + [--max-participants N] + [--no-attachments] + [--force] + [--quiet] + [--pick N] +``` + +## On-disk layout + +The export directory contains `accounts.json`, `chats.json`, `manifest.json`, +plus one directory per chat with `chat.json`, `messages.json`, +`messages.markdown`, `messages.html`, attachments, and per-chat checkpoint state. + +## Notes + +- Default `--out` directory is `beeper-export`. +- Exports are **resumable**. Re-running picks up where the last run left off + unless `--force` is set. +- `--max-participants` (default 500) bounds the participant list stored in each + `chat.json`. +- `--no-attachments` skips downloading media; metadata is still recorded. +- `--limit-chats` / `--limit-messages` are intended for sanity-checking large + exports. + +## Examples + +```sh +beeper export --out ./beeper-export +beeper export --chat "Family" --out ./family +beeper export --account whatsapp --no-attachments --quiet +beeper export --force --out ./beeper-export +``` diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx new file mode 100644 index 00000000..07cc2d8b --- /dev/null +++ b/docs/src/content/docs/index.mdx @@ -0,0 +1,88 @@ +--- +title: Beeper CLI +description: One CLI for all your chats — WhatsApp, iMessage, Telegram, Signal, Discord and more. Built for scripts, agents, and humans in a hurry. +template: splash +hero: + tagline: One CLI for all your chats. Built for you and your agent — batteries included. + image: + file: ../../assets/logo.svg + actions: + - text: Quick start + link: /quickstart/ + icon: right-arrow + variant: primary + - text: Install + link: /install/ + icon: download + variant: secondary + - text: View on GitHub + link: https://github.com/beeper/desktop-api-cli + icon: external + variant: minimal +--- + +import { Card, CardGrid, LinkCard } from '@astrojs/starlight/components'; + +`beeper` talks to **Beeper Desktop** on this machine, to a **Beeper Server** you +self-host, or to either one running somewhere else. Send and receive across +every chat network Beeper bridges — from one CLI shaped for scripts, agents, and +humans in a hurry. + +```sh +brew install beeper/tap/cli +beeper setup +beeper send text --to Family --message "on my way" +``` + +## What it does + + + + Local Beeper Desktop (default), a self-hosted Beeper Server you manage from + the CLI, or a remote target over OAuth/PKCE — or a bearer token in CI. + + + List, search, start, archive, pin, mute, rename, focus. Read, edit, delete, + react. Send text, files, stickers, voice, and typing indicators. + + + `--json` everywhere, NDJSON `--events`, a `watch` stream with HMAC-signed + webhooks, `rpc` over stdin/stdout, and `man --json` tool manifests. + + + `--read-only` rejects every mutating command. Writes stay explicit. Plugins + extend the CLI without forking it. + + + +## Supported chat networks + +Everything Beeper's bridges reach — run `beeper bridges list` for the live list +on your target. + +
    +
  • WhatsApp
  • +
  • iMessage
  • +
  • Telegram
  • +
  • Discord
  • +
  • Signal
  • +
  • Instagram DMs
  • +
  • Facebook Messenger
  • +
  • X (Twitter) DMs
  • +
  • LinkedIn
  • +
  • Slack
  • +
  • Google Messages (RCS/SMS)
  • +
  • Google Chat
  • +
  • Matrix
  • +
  • IRC
  • +
  • Bluesky
  • +
+ +## Start here + + + + + + + diff --git a/docs/src/content/docs/install.mdx b/docs/src/content/docs/install.mdx new file mode 100644 index 00000000..4798574f --- /dev/null +++ b/docs/src/content/docs/install.mdx @@ -0,0 +1,76 @@ +--- +title: Install +description: Install the Beeper CLI via Homebrew, npm, or from source. The installed command is `beeper`. +sidebar: + order: 1 +--- + +import { Tabs, TabItem, Aside } from '@astrojs/starlight/components'; + +The package name is `beeper-cli`; the installed command is `beeper`. + + + + Recommended on macOS and Linux. Installs a signed standalone binary. + + ```sh + brew install beeper/tap/cli + ``` + + Upgrade later with `brew upgrade beeper/tap/cli`, or run `beeper update --cli` + to print the right command for your install method. + + + Run it once without installing: + + ```sh + npx beeper-cli --help + ``` + + Or install it globally: + + ```sh + npm install -g beeper-cli + ``` + + The npm package is a thin launcher that downloads, verifies, and runs the + matching standalone binary for your platform. + + + This repo is a [Bun](https://bun.sh) workspace. From the repo root: + + ```sh + bun install + bun --filter @beeper/cli run build + bun --filter @beeper/cli run dev -- --help + ``` + + For local CLI development inside `packages/cli`: + + ```sh + bun run dev -- --help + ``` + + + +## Verify the install + +```sh +beeper version +beeper --help +``` + +The CLI checks for updates in the background and prints a one-line notice when a +newer release is available. It never upgrades itself — see [Updating](/update/). + + + +## Next step + +You have the CLI, but it isn't talking to anything yet. Point it at a Beeper +target — see [Connect a target](/connect/). diff --git a/packages/cli/docs/media.md b/docs/src/content/docs/media.mdx similarity index 50% rename from packages/cli/docs/media.md rename to docs/src/content/docs/media.mdx index 9fc9dd55..79f77a3b 100644 --- a/packages/cli/docs/media.md +++ b/docs/src/content/docs/media.mdx @@ -1,6 +1,13 @@ -# media +--- +title: Media +description: Download a media file attached to a message. +--- -Read when: downloading a media file attached to a message. +import { Aside } from '@astrojs/starlight/components'; + + ## Commands @@ -10,8 +17,10 @@ beeper media download [-o, --out DIR | -] ## Notes -- `` accepts `mxc://` and `localmxc://` URLs (typically taken from a message payload). -- `--out` defaults to `.` (current directory); the file is named from the URL path. +- `` accepts `mxc://` and `localmxc://` URLs (typically taken from a message + payload). +- `--out` defaults to `.` (current directory); the file is named from the URL + path. - `--out -` streams the binary to stdout for piping. ## Examples @@ -20,3 +29,8 @@ beeper media download [-o, --out DIR | -] beeper media download mxc://beeper.com/abc --out ./downloads beeper media download mxc://beeper.com/abc -o - > photo.jpg ``` + +## See also + +For bulk media, [`export`](/export/) downloads every chat's attachments into a +resumable on-disk archive. diff --git a/packages/cli/docs/messages.md b/docs/src/content/docs/messages.mdx similarity index 70% rename from packages/cli/docs/messages.md rename to docs/src/content/docs/messages.mdx index 70ccb009..1d258268 100644 --- a/packages/cli/docs/messages.md +++ b/docs/src/content/docs/messages.mdx @@ -1,7 +1,14 @@ -# messages +--- +title: Messages +description: List, search, show, contextualize, edit, delete, react to, or export messages from chats. +--- -Read when: listing, searching, showing, contextualizing, editing, deleting, -reacting to, or exporting messages from chats. +import { Aside } from '@astrojs/starlight/components'; + + ## Commands @@ -17,19 +24,28 @@ beeper messages unreact --chat SEL --id MSG_ID --reaction KEY [--pick N] # hi beeper messages export --chat SEL [--before-cursor MSG_ID | --after-cursor MSG_ID] [--after ISO] [--before ISO] [--limit N] [--output PATH | -o -] [--asc] [--pick N] ``` -## Notes +## Pagination & filtering -- `--before-cursor` / `--after-cursor` paginate by message ID (the SDK's cursor model). -- `--before` / `--after` in `messages search` and `messages export` filter by ISO timestamp. -- `messages search` rejects an empty query *and* no filter flags with exit code 2 (`usageError`). -- `messages list --sender` filters client-side: `me` (your own messages), `others`, or an exact user ID. +- `--before-cursor` / `--after-cursor` paginate by message ID (the SDK's cursor + model). +- `--before` / `--after` in `messages search` and `messages export` filter by + ISO timestamp. +- `messages list --sender` filters client-side: `me` (your own messages), + `others`, or an exact user ID. - `messages list --asc` reverses the default newest-first order. -- `messages export` writes one chat to JSON. Use top-level `export` for a full - export with transcripts, attachments, and multiple chats. -- `messages export --output -` writes JSON to stdout for piping. -- `messages delete --for-everyone` requires the network supports it; otherwise it falls back to delete-for-you. + +## Notes + +- `messages search` rejects an empty query *and* no filter flags with exit code + `2` (usage error). +- `messages export` writes one chat to JSON. Use top-level [`export`](/export/) + for a full export with transcripts, attachments, and multiple chats. + `messages export --output -` writes JSON to stdout for piping. +- `messages delete --for-everyone` requires the network to support it; otherwise + it falls back to delete-for-you. - `messages edit` only succeeds on your own text messages with no attachments. -- `messages react`/`unreact` are hidden in `--help` in favor of `send react`/`send unreact`. +- `messages react` / `unreact` are hidden in `--help` in favor of + [`send react`](/send/) / `send unreact`. ## Examples diff --git a/docs/src/content/docs/plugins.mdx b/docs/src/content/docs/plugins.mdx new file mode 100644 index 00000000..4fb7916b --- /dev/null +++ b/docs/src/content/docs/plugins.mdx @@ -0,0 +1,45 @@ +--- +title: Plugins +description: Extend the Beeper CLI with optional oclif plugins without forking it, and build your own with the plugin SDK. +--- + +import { Aside } from '@astrojs/starlight/components'; + +Beeper CLI supports optional [oclif](https://oclif.io) plugins, so you can extend +the CLI without forking it. + +## Using plugins + +List recommended Beeper plugins: + +```sh +beeper plugins available +``` + +Install a published plugin: + +```sh +beeper plugins install @beeper/cli-plugin-cloudflare +``` + +## First-party plugins + +| Package | Adds | +| --- | --- | +| `@beeper/cli-plugin-cloudflare` | `targets tunnel` — expose a selected Beeper target through Cloudflare Tunnel. | + +## Building a plugin + +Import from `@beeper/cli/plugin-sdk` and expose oclif commands from your package. +Link a local plugin while working on it: + +```sh +beeper plugins link ./packages/cli-plugin-cloudflare +beeper targets tunnel --help +``` + + diff --git a/packages/cli/docs/presence.md b/docs/src/content/docs/presence.mdx similarity index 56% rename from packages/cli/docs/presence.md rename to docs/src/content/docs/presence.mdx index 85e16d77..5f199844 100644 --- a/packages/cli/docs/presence.md +++ b/docs/src/content/docs/presence.mdx @@ -1,7 +1,13 @@ -# presence +--- +title: Presence +description: Send typing/paused indicators into a chat from a script or agent. +--- -Read when: sending typing/paused indicators into a chat from a script or -agent. +import { Aside } from '@astrojs/starlight/components'; + + ## Commands @@ -11,10 +17,13 @@ beeper presence --chat SEL [--state typing|paused] [--duration SECONDS] [--pick ## Notes -- Requires server-side support; networks without typing notifications return an error. +- Requires server-side support; networks without typing notifications return an + error. - `--state` defaults to `typing`. -- `--duration N` (only valid with `--state typing`) sends `typing`, sleeps N seconds, then sends `paused`. -- The selected chat must be addressable via the usual selector rules (ID, local ID, title, or search text). +- `--duration N` (only valid with `--state typing`) sends `typing`, sleeps N + seconds, then sends `paused`. +- The selected chat must be addressable via the usual selector rules (ID, local + ID, title, or search text). ## Examples diff --git a/docs/src/content/docs/quickstart.mdx b/docs/src/content/docs/quickstart.mdx new file mode 100644 index 00000000..81790cd9 --- /dev/null +++ b/docs/src/content/docs/quickstart.mdx @@ -0,0 +1,89 @@ +--- +title: Quick start +description: From zero to your first sent message in about a minute, on the happy path where Beeper Desktop is already on this machine. +sidebar: + order: 3 +--- + +import { Steps, Aside, LinkCard } from '@astrojs/starlight/components'; + +The happy path: Beeper Desktop is already on this machine. `beeper setup` finds +it, offers to launch it if it isn't running, and adopts the session. + + + +1. **Install** the CLI: + + ```sh + brew install beeper/tap/cli + ``` + +2. **Connect** to your local Desktop: + + ```text + $ beeper setup + Use this Desktop session for CLI access? [Y/n] y + ▎ Connected desktop + accounts whatsapp, telegram, imessage + endpoint http://127.0.0.1:23373 + ``` + +3. **List** your chats: + + ```text + $ beeper chats list --limit 3 + 10313 Family 3 unread + 8951 Alice · + 7204 Eng standup 12 unread + ``` + +4. **Search** across every network at once: + + ```text + $ beeper messages search "flight" + 8951 Alice · "your flight is at 6:40, gate B23" 2d ago + 10313 Family · "what flight are you on?" 1w ago + ``` + +5. **Send** a message: + + ```text + $ beeper send text --to Family --message "on my way" + ▎ Sent Family + message "on my way" + at 2026-05-18T14:02:11Z + ``` + +6. **Export** everything to disk when you want a backup: + + ```text + $ beeper export --out ./beeper-export + ▎ Exported ./beeper-export + chats 214 messages 38,901 attachments 1,205 + ``` + + + +## Addressing chats + +Recipients (`--to`, `--chat`) accept a numeric local chat ID, a full +Beeper/Matrix chat ID, an iMessage chat ID, an exact title, or search text. +Ambiguous matches prompt in a TTY; pass `--pick N` in scripts. + +```sh +beeper send text --to 10313 --message "by local id" +beeper send text --to "Family" --message "by exact title" +beeper send text --to "@alice:beeper.com" --message "by full chat id" +``` + + + +## Where to next + + + + diff --git a/packages/cli/docs/rpc.md b/docs/src/content/docs/rpc.mdx similarity index 58% rename from packages/cli/docs/rpc.md rename to docs/src/content/docs/rpc.mdx index d58ccb64..518b95b8 100644 --- a/packages/cli/docs/rpc.md +++ b/docs/src/content/docs/rpc.mdx @@ -1,7 +1,14 @@ -# rpc +--- +title: RPC +description: Drive many CLI commands from a long-lived process over newline-delimited JSON on stdin/stdout — no new process per command. +--- -Read when: scripting many CLI commands from a long-lived process (an agent, a -web server) without spawning a new node process per command. +import { Aside } from '@astrojs/starlight/components'; + + ## Command @@ -9,8 +16,8 @@ web server) without spawning a new node process per command. beeper rpc ``` -Reads newline-delimited JSON requests on stdin and writes one response line -per request on stdout. +Reads newline-delimited JSON requests on stdin and writes one response line per +request on stdout. ## Request shape @@ -21,9 +28,9 @@ per request on stdout. Each request must include one of: -- `command` — a single string parsed with shell-like quoting -- `args` — an explicit `argv` array -- `argv` — alias for `args` +- `command` — a single string parsed with shell-like quoting; +- `args` — an explicit `argv` array; +- `argv` — alias for `args`. `id` is echoed back in the response (string, number, or null). @@ -40,7 +47,8 @@ Each request must include one of: - Nesting `rpc` or `shell` is rejected to avoid recursion. - `--json` on inner commands produces the standard envelope inside `stdout`. -- Exit codes use the same table as direct CLI invocation; see [exit codes](../README.md#exit-codes). +- Exit codes use the same table as direct CLI invocation; see [Exit + codes](/exit-codes/). ## Examples diff --git a/docs/src/content/docs/scripting.mdx b/docs/src/content/docs/scripting.mdx new file mode 100644 index 00000000..1ece1386 --- /dev/null +++ b/docs/src/content/docs/scripting.mdx @@ -0,0 +1,85 @@ +--- +title: Output & scripting +description: JSON output, NDJSON events, global flags, addressing rules, and the conventions that make the Beeper CLI safe to drive from scripts and agents. +sidebar: + label: Output & scripting + order: 1 +--- + +import { Aside } from '@astrojs/starlight/components'; + +The CLI is designed to be driven by humans *and* programs. Every command prints +human-friendly text by default and switches to a stable machine envelope on +`--json`. + +## Output modes + +Most commands support: + +- **app-like text by default**, optimized for scanning chats, messages, contacts, + accounts, and media; +- **`--json`** for a `{"success":true,"data":...,"error":null}` envelope on stdout; +- **`--events`** for NDJSON lifecycle events on stderr from long-running commands; +- **`--read-only`** to reject commands that modify Beeper or local CLI state; +- **`--full`** to disable truncation; +- **`--debug`** for SDK debug logging; +- **`--target`** or **`--base-url`** to point at a different target. + +The JSON envelope is stable across success and failure: + +```json +{"success":true,"data":{ /* … */ },"error":null} +{"success":false,"data":null,"error":"…","exitCode":3} +``` + +On failure the envelope is written to **stderr** and the process exits with the +matching [exit code](/exit-codes/). + +## Global flags + +`--base-url` · `--target` · `--json` · `--events` · `--full` · `--timeout` · +`--read-only` · `--debug` · `--yes` · `--quiet` + + + +## Addressing + +- Chat arguments accept numeric local chat IDs, full Beeper/Matrix chat IDs, + iMessage chat IDs, exact titles, or search text. +- For scripts on the same target/profile, prefer the **numeric local chat ID** + shown by `beeper chats list`; use the **full Beeper/Matrix chat ID** when the + selector must work across targets or profiles. +- Numeric local chat IDs come from the selected Desktop database — treat them as + local to that target/profile. +- Ambiguous chat matches return numbered choices; pass `--pick N` to select one. +- Account arguments accept account IDs, network names, bridge type/id, or + account user identity. A network name can expand to multiple matching accounts. +- `contacts search` and `chats start` search across all accounts when + `--account` is omitted; `contacts list` accepts the same account selectors as + other account-scoped commands. + +## For agents + +- `man --json` prints a compact command manifest for tools and agents. +- `rpc` runs newline-delimited JSON command RPC over stdin/stdout — see [RPC](/rpc/). +- `watch` streams live events and can forward them to an HMAC-signed webhook — + see [Watch](/watch/). +- Raw, un-wrapped endpoints are reachable under [`api`](/api/). + +## Example: a JSON pipeline + +```sh +# Unread chat titles, newest first, as a plain list: +beeper chats list --unread --json \ + | jq -r '.data[].title' + +# Send to every pinned chat: +beeper chats list --pinned --json \ + | jq -r '.data[].id' \ + | while read -r id; do + beeper send text --to "$id" --message "heads up" --json + done +``` diff --git a/packages/cli/docs/send.md b/docs/src/content/docs/send.mdx similarity index 60% rename from packages/cli/docs/send.md rename to docs/src/content/docs/send.mdx index d8d01f04..b57b8cf5 100644 --- a/packages/cli/docs/send.md +++ b/docs/src/content/docs/send.mdx @@ -1,7 +1,16 @@ -# send +--- +title: Sending +description: Send text, files, reactions, stickers, or voice notes from scripts or interactive use. +sidebar: + label: Sending +--- -Read when: sending text, files, reactions, stickers, or voice notes from -scripts or interactive use. +import { Aside } from '@astrojs/starlight/components'; + + ## Commands @@ -14,21 +23,30 @@ beeper send react --to SEL --id MSG_ID --reaction KEY [--transaction TX_ID] [- beeper send unreact --to SEL --id MSG_ID --reaction KEY [--pick N] ``` +## Addressing + +`--to` accepts a chat ID, local chat ID, exact title, or search text. Prefer +numeric local chat IDs from `beeper chats list` when scripting against the same +target/profile; use full Beeper/Matrix chat IDs for selectors that need to work +across targets or profiles. + +## Confirming delivery + +Send commands return when Desktop **accepts** the send request. Use `--wait` when +you need to know whether the message left the pending state or failed — it blocks +until the message leaves pending (or fails). Default poll cap: `--wait-timeout +30000` ms. + ## Notes -- `--to` accepts a chat ID, local chat ID, exact title, or search text. -- Prefer numeric local chat IDs from `beeper chats list` when scripting against - the same target/profile. Use full Beeper/Matrix chat IDs for selectors that - need to work across targets or profiles. -- Send commands return when Desktop accepts the send request. Use `--wait` when - you need to know whether the message left the pending state or failed. -- `--wait` blocks until the message leaves the pending state (or fails). Default poll cap: `--wait-timeout 30000` ms. - `--reply-to` quotes an existing message ID. -- `send text --mention ` adds a Matrix mention; repeat for multiple users. -- `send text --no-preview` disables automatic link previews. +- `send text --mention ` adds a Matrix mention; repeat for multiple + users. `send text --no-preview` disables automatic link previews. - `send sticker` defaults `--mime` to `image/webp`; stickers should be 512×512. -- `send voice` defaults `--mime` to `audio/ogg`; pass `--duration` to override the detected length. -- `send file` accepts any file up to 500 MB. MIME type is detected from the upload if `--mime` is omitted. +- `send voice` defaults `--mime` to `audio/ogg`; pass `--duration` to override + the detected length. +- `send file` accepts any file up to 500 MB. MIME type is detected from the + upload if `--mime` is omitted. ## Examples diff --git a/packages/cli/docs/targets.md b/docs/src/content/docs/targets.mdx similarity index 51% rename from packages/cli/docs/targets.md rename to docs/src/content/docs/targets.mdx index d77ee831..5349538d 100644 --- a/packages/cli/docs/targets.md +++ b/docs/src/content/docs/targets.mdx @@ -1,13 +1,18 @@ -# targets +--- +title: Targets +description: Manage local Desktop, managed Server, and remote Beeper targets — add, switch, start/stop a managed runtime, or remove a target. +--- -Read when: managing local Desktop, managed Server, or remote Beeper API -targets — adding, switching, starting/stopping a managed runtime, or removing -a target. +import { Aside } from '@astrojs/starlight/components'; -A *target* is a runnable or reachable Beeper endpoint profile: local Server, -local Desktop, Desktop API, or a profile that combines Desktop/Server runtime -state. The CLI tracks an optional default; commands use it unless -`--target ` overrides. +A **target** is a runnable or reachable Beeper endpoint profile: local Desktop, +local Server, or a remote Desktop/Server. The CLI tracks an optional default; +commands use it unless `--target ` overrides. + + ## Commands @@ -27,12 +32,13 @@ beeper targets remove ## Notes -- `list` prints all configured targets; the one used by default has `default: true`. +- `list` prints all configured targets; the default one has `default: true`. - `show` defaults to the currently-selected target if no name is given. - `status` checks endpoint and process reachability. For setup/auth/encryption diagnostics use `beeper doctor`. -- `start`/`stop`/`restart` only apply to managed targets (`type: desktop|server`); they error for `remote`. -- `enable`/`disable` registers/unregisters the launchd or systemd unit that +- `start` / `stop` / `restart` only apply to managed targets (`type: + desktop|server`); they error for `remote`. +- `enable` / `disable` registers or unregisters the launchd or systemd unit that starts the managed target at login. - Removing the active default clears the `defaultTarget` config field. - `BEEPER_TARGET=` overrides the default for a single shell. @@ -48,3 +54,9 @@ beeper targets use work beeper targets logs work | less beeper targets restart work ``` + +## See also + +- [Connect a target](/connect/) — first-time setup for each target type. +- [Auth & verification](/auth/) — sign-in state and encryption readiness. +- [Configuration](/config/) — where the selected target is stored. diff --git a/docs/src/content/docs/update.mdx b/docs/src/content/docs/update.mdx new file mode 100644 index 00000000..5a6a827d --- /dev/null +++ b/docs/src/content/docs/update.mdx @@ -0,0 +1,39 @@ +--- +title: Updating +description: Check for new versions of the CLI, the CLI-managed Desktop install, or the CLI-managed Server install — and choose whether to install. +sidebar: + label: Updating +--- + +import { Aside } from '@astrojs/starlight/components'; + + + +## Command + +```sh +beeper update [--check] [--cli] [--desktop] [--server] +``` + +## Notes + +- With no kind flag, checks all three (CLI, Desktop, Server) that apply. +- `--check` prints what's available without installing. +- The CLI itself is **never auto-upgraded**; `--cli` prints the right command for + your install method (Homebrew, npm-global, or in-repo git build). +- `--desktop` reports on the CLI-owned Desktop install; updating Desktop itself + happens inside the Desktop app. +- `--server` updates the CLI-managed Server install in place, then restarts any + running managed Server targets. + +## Examples + +```sh +beeper update --check +beeper update --cli +beeper update --desktop --json +beeper update --server +``` diff --git a/docs/src/content/docs/watch.mdx b/docs/src/content/docs/watch.mdx new file mode 100644 index 00000000..79e15916 --- /dev/null +++ b/docs/src/content/docs/watch.mdx @@ -0,0 +1,66 @@ +--- +title: Watch (live events) +description: Subscribe to live Desktop API events — new/updated/deleted chats and messages — and optionally forward them to a webhook. +sidebar: + label: Watch (live events) +--- + +import { Aside } from '@astrojs/starlight/components'; + + + +## Commands + +```sh +beeper watch + [-c, --chat CHAT_ID]... + [--include-type EVENT_TYPE]... + [--exclude-type EVENT_TYPE]... + [--webhook URL [--webhook-secret SECRET] [--webhook-queue N]] + [--json] +``` + +## Notes + +- Subscribes to the Desktop API WebSocket at the path returned by `/v1/info` + (defaults to `/v1/ws`). +- Without `--chat`, subscribes to all chats. +- Event types come from the Desktop API: `chat.upserted`, `chat.deleted`, + `message.upserted`, `message.deleted`. +- `--include-type` and `--exclude-type` are mutually exclusive. +- `--webhook URL` forwards every event as a POST body (best-effort, + fire-and-forget). +- `--webhook-secret SECRET` signs the body with HMAC-SHA256 and sets + `X-Beeper-Signature: sha256=`. +- `--webhook-queue` (default 64) caps pending deliveries; excess events are + dropped with a stderr warning. +- `--quiet` suppresses the human-mode status line; `--json` prints raw events + line-delimited. + +## Examples + +```sh +beeper watch +beeper watch --chat '!abc:beeper.com' --json +beeper watch --include-type message.upserted --include-type message.deleted +beeper watch --webhook https://example.com/hook --webhook-secret "$BEEPER_WEBHOOK_SECRET" +``` + +## Verifying webhook signatures + +Each delivery is signed with `X-Beeper-Signature: sha256=` over the raw +request body. Recompute the HMAC with your shared secret and compare: + +```js +import { createHmac, timingSafeEqual } from 'node:crypto'; + +function verify(rawBody, header, secret) { + const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex'); + const a = Buffer.from(header); + const b = Buffer.from(expected); + return a.length === b.length && timingSafeEqual(a, b); +} +``` diff --git a/docs/src/styles/theme.css b/docs/src/styles/theme.css new file mode 100644 index 00000000..83769cc8 --- /dev/null +++ b/docs/src/styles/theme.css @@ -0,0 +1,37 @@ +/* Beeper CLI docs — brand accent on top of Starlight defaults. */ +:root { + /* Purple accent ramp (light) */ + --sl-color-accent-low: #e0d9ff; + --sl-color-accent: #6e56f8; + --sl-color-accent-high: #3f2db8; +} + +:root[data-theme='dark'] { + /* Purple accent ramp (dark) */ + --sl-color-accent-low: #2a2160; + --sl-color-accent: #8b78ff; + --sl-color-accent-high: #d6cdff; +} + +/* Roomier hero on the landing page. */ +.hero > .stack { + gap: clamp(1rem, 5vw, 2rem); +} + +/* Tighten the supported-networks list rendered on the overview page. */ +.networks { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + margin-block: 1rem; + padding: 0; + list-style: none; +} + +.networks li { + border: 1px solid var(--sl-color-gray-5); + border-radius: 999px; + padding: 0.15rem 0.7rem; + font-size: var(--sl-text-sm); + white-space: nowrap; +} diff --git a/docs/tsconfig.json b/docs/tsconfig.json new file mode 100644 index 00000000..8bf91d3b --- /dev/null +++ b/docs/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "astro/tsconfigs/strict", + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist"] +} diff --git a/packages/cli/README.md b/packages/cli/README.md index 6c70613b..3173f930 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,6 +1,15 @@ -# beeper — One CLI for all your chats +
-> Built for you and your agent. Batteries included. +# beeper + +**One CLI for all your chats.** Built for you and your agent — batteries included. + +[![npm](https://img.shields.io/npm/v/beeper-cli.svg?label=npm&color=6E56F8)](https://www.npmjs.com/package/beeper-cli) +[![license](https://img.shields.io/badge/license-MIT-6E56F8.svg)](https://github.com/beeper/desktop-api-cli/blob/main/packages/cli/LICENSE) +[![docs](https://img.shields.io/badge/docs-online-6E56F8.svg)](https://example.com) +[![built with Bun](https://img.shields.io/badge/built%20with-Bun-6E56F8.svg)](https://bun.sh) + +
Talks to Beeper Desktop on this machine, to a Beeper Server you self-host, or to either one running somewhere else. Send and receive across the chat @@ -13,7 +22,7 @@ Facebook Messenger · X (Twitter) DMs · LinkedIn · Slack · Google Messages (RCS/SMS) · Google Chat · Matrix · IRC · Bluesky. Run `beeper bridges list` for the live list on your target. -Command manual: `beeper man` · CLI docs: `beeper docs` +📖 **[Read the docs](https://example.com)** · command manual: `beeper man` · open docs: `beeper docs` ## Features @@ -193,19 +202,22 @@ WhatsApp, Telegram, Discord, iMessage, and the rest show up under `accounts list ## Documentation +Full documentation lives at **[example.com](https://example.com)** +(built from [`docs/`](docs/) with Astro Starlight — a fully static site). + | Topic | Page | Commands | | --- | --- | --- | -| **Setup + install** | [setup](docs/setup.md) · [auth](docs/auth.md) | `setup` · `install desktop` · `install server` · `verify` · `status` · `doctor` · `auth status` | -| **Targets** | [targets](docs/targets.md) | `targets list` · `targets add desktop` · `targets add server` · `targets add remote` · `targets use` · `targets status` · `targets logs` | -| **Bridges + accounts** | [accounts](docs/accounts.md) | `bridges list` · `bridges show` · `accounts list` · `accounts add` · `accounts show` · `accounts use` · `accounts remove` | -| **Chats** | [chats](docs/chats.md) | `chats list` · `chats search` · `chats show` · `chats start` · `chats archive` · `chats pin` · `chats mute` · `chats priority` · `chats remind` · `chats rename` · `chats draft` · `chats focus` | -| **Messages** | [messages](docs/messages.md) · [send](docs/send.md) · [presence](docs/presence.md) | `messages list` · `messages search` · `messages export` · `send text` · `send file` · `send sticker` · `send voice` · `send react` · `presence` | -| **Contacts + media** | [contacts](docs/contacts.md) · [media](docs/media.md) · [export](docs/export.md) | `contacts list` · `contacts search` · `media download` · `export` | -| **Automation** | [watch](docs/watch.md) · [rpc](docs/rpc.md) · [api](docs/api.md) | `watch` · `watch --webhook` · `rpc` · `man` · `api get` · `api post` · `api request` | -| **Maintenance** | [config](docs/config.md) · [update](docs/update.md) | `update` · `config` · `completion` · `docs` · `version` | +| **Setup + install** | [connect](https://example.com/connect/) · [install](https://example.com/install/) · [auth](https://example.com/auth/) | `setup` · `install desktop` · `install server` · `verify` · `status` · `doctor` · `auth status` | +| **Targets** | [targets](https://example.com/targets/) | `targets list` · `targets add desktop` · `targets add server` · `targets add remote` · `targets use` · `targets status` · `targets logs` | +| **Bridges + accounts** | [accounts](https://example.com/accounts/) | `bridges list` · `bridges show` · `accounts list` · `accounts add` · `accounts show` · `accounts use` · `accounts remove` | +| **Chats** | [chats](https://example.com/chats/) | `chats list` · `chats search` · `chats show` · `chats start` · `chats archive` · `chats pin` · `chats mute` · `chats priority` · `chats remind` · `chats rename` · `chats draft` · `chats focus` | +| **Messages** | [messages](https://example.com/messages/) · [send](https://example.com/send/) · [presence](https://example.com/presence/) | `messages list` · `messages search` · `messages export` · `send text` · `send file` · `send sticker` · `send voice` · `send react` · `presence` | +| **Contacts + media** | [contacts](https://example.com/contacts/) · [media](https://example.com/media/) · [export](https://example.com/export/) | `contacts list` · `contacts search` · `media download` · `export` | +| **Automation** | [scripting](https://example.com/scripting/) · [watch](https://example.com/watch/) · [rpc](https://example.com/rpc/) · [api](https://example.com/api/) | `watch` · `watch --webhook` · `rpc` · `man` · `api get` · `api post` · `api request` | +| **Maintenance** | [config](https://example.com/config/) · [update](https://example.com/update/) | `update` · `config` · `completion` · `docs` · `version` | Use `beeper docs` to open the CLI docs and `beeper man` to print the local -command manual. +command manual. To work on the docs site locally: `cd docs && bun install && bun run dev`. ## Configuration @@ -389,11 +401,17 @@ First-party optional plugins: | `contacts list` | List contacts | | `contacts search` | Search contacts | | `contacts show` | Show contact details | +| `resolve chat` | Resolve a chat selector to concrete chat candidates | +| `resolve account` | Resolve an account selector | +| `resolve contact` | Resolve a contact selector | +| `resolve target` | Resolve a target selector | +| `resolve bridge` | Resolve a bridge selector | | `media download` | Download message media | | `export` | Export accounts, chats, messages, Markdown transcripts, and attachments | | `watch` | Stream Desktop API WebSocket events | | `rpc` | Run newline-delimited JSON command RPC over stdin/stdout | | `man` | Print the command manual | +| `schema` | Print machine-readable command/flag schema | | `doctor` | Probe the target live and report diagnostics | | `status` | Show selected target and setup readiness | | `docs` | Open Beeper CLI docs | @@ -444,7 +462,7 @@ beeper setup --remote https://desktop.example.com beeper setup --desktop --install ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper install desktop` Install Beeper Desktop locally @@ -466,7 +484,7 @@ beeper install desktop beeper install desktop --channel nightly ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper install server` Install Beeper Server locally @@ -489,7 +507,7 @@ beeper install server beeper install server --server-env staging ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets list` List configured Beeper targets @@ -505,7 +523,7 @@ beeper targets list beeper targets list --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper bridges list` List bridges that can connect chat accounts @@ -530,7 +548,7 @@ beeper bridges list beeper bridges list --provider local --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper bridges show` Show bridge details, login flows, and connected accounts @@ -552,7 +570,7 @@ beeper bridges show local-whatsapp beeper bridges show telegram ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets add desktop` Add a managed Beeper Desktop target @@ -581,7 +599,7 @@ Examples: beeper targets add desktop work --default ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets add server` Add a managed Beeper Server target @@ -610,7 +628,7 @@ Examples: beeper targets add server prod --server-env production --default ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets add remote` Add a remote Beeper Desktop or Server target @@ -638,7 +656,7 @@ Examples: beeper targets add remote work https://desktop.example.com --default ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets use` Set the default target @@ -659,7 +677,7 @@ Examples: beeper targets use work ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets show` Show target details @@ -681,7 +699,7 @@ beeper targets show beeper targets show work ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets status` Check endpoint and process reachability for a target @@ -703,7 +721,7 @@ beeper targets status beeper targets status work --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets start` Start a local Server target or open Beeper Desktop @@ -724,7 +742,7 @@ Examples: beeper targets start work ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets stop` Stop a local Beeper Server target @@ -745,7 +763,7 @@ Examples: beeper targets stop work ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets restart` Restart a local Beeper Server target @@ -766,7 +784,7 @@ Examples: beeper targets restart work ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets logs` Print logs for a local Beeper Desktop or Server install @@ -795,7 +813,7 @@ Examples: beeper targets logs work ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets enable` Enable a local Beeper Server target at login @@ -816,7 +834,7 @@ Examples: beeper targets enable work ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets disable` Disable a local Beeper Server target at login @@ -837,7 +855,7 @@ Examples: beeper targets disable work ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets remove` Remove a target @@ -858,7 +876,7 @@ Examples: beeper targets remove work ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper targets tunnel` Expose a local Desktop API over a public Cloudflare tunnel @@ -889,7 +907,7 @@ beeper auth status beeper auth status --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper auth logout` Clear stored authentication @@ -904,7 +922,7 @@ Examples: beeper auth logout ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper auth email start` Start email sign-in for a target @@ -925,7 +943,7 @@ Examples: beeper auth email start --email you@example.com --target work --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper auth email response` Finish email sign-in with a verification code @@ -948,7 +966,7 @@ Examples: beeper auth email response --setup-request-id --code --target work --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify` Finish setup verification or verify another device @@ -970,7 +988,7 @@ beeper verify beeper verify --user @alice:beeper.com ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify status` Show encryption and device-verification readiness @@ -985,7 +1003,7 @@ Examples: beeper verify status --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify approve` Approve a pending device verification request @@ -1006,7 +1024,7 @@ Examples: beeper verify approve --id active ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify recovery-key` Unlock encrypted messages with a recovery key @@ -1027,7 +1045,7 @@ Examples: beeper verify recovery-key --key ABCD-EFGH-IJKL ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify reset-recovery-key` Create a new encrypted-messages recovery key @@ -1042,7 +1060,7 @@ Examples: beeper verify reset-recovery-key ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify cancel` Cancel an in-progress device verification @@ -1063,7 +1081,7 @@ Examples: beeper verify cancel ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify list` List active verification work @@ -1078,7 +1096,7 @@ Examples: beeper verify list ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify start` Start a device verification request @@ -1099,7 +1117,7 @@ Examples: beeper verify start --user @alice:beeper.com ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify show` Show the current active verification request @@ -1114,7 +1132,7 @@ Examples: beeper verify show --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify sas` Start emoji verification @@ -1135,7 +1153,7 @@ Examples: beeper verify sas ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify sas-confirm` Confirm matching emoji verification @@ -1156,7 +1174,7 @@ Examples: beeper verify sas-confirm ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify qr-scan` Submit a scanned QR-code verification payload @@ -1178,7 +1196,7 @@ Examples: beeper verify qr-scan --payload "..." ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper verify qr-confirm` Confirm that the other device scanned your QR code @@ -1199,7 +1217,7 @@ Examples: beeper verify qr-confirm ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper accounts list` List connected accounts @@ -1222,7 +1240,7 @@ beeper accounts list beeper accounts list --account whatsapp --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper accounts add` Connect a chat account by bridge @@ -1262,7 +1280,7 @@ beeper accounts add discord --non-interactive --cookie sessiontoken=... beeper accounts add discord --webview --webview-backend chrome ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper accounts show` Show account details @@ -1283,7 +1301,7 @@ Examples: beeper accounts show whatsapp-main ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper accounts remove` Remove an account @@ -1304,7 +1322,7 @@ Examples: beeper accounts remove whatsapp-main ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper accounts use` Select a default account for account-scoped commands @@ -1327,7 +1345,7 @@ Examples: beeper accounts use whatsapp-main ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats list` List chats @@ -1354,10 +1372,11 @@ Examples: ```sh beeper chats list beeper chats list --pinned --limit 50 -beeper chats list --unread --no-muted --json +beeper chats list --unread --no-muted --format json +beeper ls --format ids ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats search` Search chats @@ -1386,7 +1405,7 @@ Examples: beeper chats search Family ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats show` Show chat details @@ -1410,7 +1429,7 @@ beeper chats show --chat 10313 beeper chats show --chat '!plUOsWkvMmJmJPVAjS:beeper.com' ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats start` Start a chat @@ -1439,7 +1458,7 @@ beeper chats start +15551234567 beeper chats start @alice:beeper.com --title "Alice" ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats archive` Archive a chat @@ -1461,7 +1480,7 @@ Examples: beeper chats archive --chat 10313 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats unarchive` Unarchive a chat @@ -1483,7 +1502,7 @@ Examples: beeper chats unarchive --chat 10313 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats pin` Pin a chat @@ -1505,7 +1524,7 @@ Examples: beeper chats pin --chat 10313 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats unpin` Unpin a chat @@ -1527,7 +1546,7 @@ Examples: beeper chats unpin --chat 10313 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats mute` Mute a chat @@ -1549,7 +1568,7 @@ Examples: beeper chats mute --chat 10313 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats unmute` Unmute a chat @@ -1571,7 +1590,7 @@ Examples: beeper chats unmute --chat 10313 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats mark-read` Mark a chat as read @@ -1594,7 +1613,7 @@ Examples: beeper chats mark-read --chat 10313 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats mark-unread` Mark a chat as unread @@ -1617,7 +1636,7 @@ Examples: beeper chats mark-unread --chat 10313 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats priority` Move a chat to the Inbox or Low Priority @@ -1641,7 +1660,7 @@ beeper chats priority --chat 10313 --level inbox beeper chats priority --chat '!plUOsWkvMmJmJPVAjS:beeper.com' --level low ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats notify-anyway` Send an iMessage Notify Anyway alert @@ -1663,7 +1682,7 @@ Examples: beeper chats notify-anyway --chat 10313 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats rename` Rename a chat @@ -1686,7 +1705,7 @@ Examples: beeper chats rename --chat 10313 --title "Family" ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats description` Set a chat description @@ -1711,7 +1730,7 @@ beeper chats description --chat 10313 --description "Engineering chat" beeper chats description --chat 10313 --clear ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats avatar` Set a chat avatar @@ -1735,7 +1754,7 @@ Examples: beeper chats avatar --chat 10313 --file ./team.png ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats draft` Set or clear a chat draft @@ -1763,7 +1782,7 @@ beeper chats draft --chat 10313 --text "on my way" beeper chats draft --chat 10313 --clear ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats disappear` Set disappearing-message expiry @@ -1786,7 +1805,7 @@ Examples: beeper chats disappear --chat 10313 --seconds 86400 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats remind` Set a chat reminder @@ -1811,7 +1830,7 @@ beeper chats remind --chat 10313 --when 2026-06-01T09:00:00Z beeper chats remind --chat 10313 --when 2026-06-01T09:00:00Z --dismiss-on-message ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats unremind` Clear a chat reminder @@ -1833,7 +1852,7 @@ Examples: beeper chats unremind --chat 10313 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper chats focus` Focus Beeper Desktop on a chat @@ -1858,7 +1877,7 @@ Examples: beeper chats focus --chat 10313 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper messages list` List chat messages @@ -1888,7 +1907,7 @@ beeper messages list --chat 10313 --before-cursor "" --limit 100 beeper messages list --chat 10313 --sender me --asc ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper messages search` Search messages across chats @@ -1923,11 +1942,12 @@ Examples: ```sh beeper messages search invoice +beeper search invoice --format jsonl --select id,chatID,text beeper messages search --chat 10313 --sender me --media image beeper messages search "flight" --after 2026-01-01 --before 2026-02-01 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper messages show` Show one message @@ -1950,7 +1970,7 @@ Examples: beeper messages show --chat 10313 --id ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper messages context` Show message context @@ -1975,7 +1995,7 @@ Examples: beeper messages context --chat 10313 --id --before 5 --after 5 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper messages edit` Edit a message @@ -1999,7 +2019,7 @@ Examples: beeper messages edit --chat 10313 --id --message "fixed" ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper messages delete` Delete a message @@ -2023,7 +2043,7 @@ Examples: beeper messages delete --chat 10313 --id --for-everyone ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper messages export` Export one chat to JSON @@ -2056,7 +2076,7 @@ beeper messages export --chat 8951 --after 2026-01-01T00:00:00Z --output - beeper messages export --chat '!plUOsWkvMmJmJPVAjS:beeper.com' --before-cursor "" --limit 500 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper send text` Send a text message @@ -2083,12 +2103,13 @@ Flags: Examples: ```sh +beeper send --to 10313 --message "on my way" --dry-run --format json beeper send text --to 10313 --message "on my way" beeper send text --to 8951 --message "hi" beeper send text --to "Family" --message "hi" --pick 1 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper send file` Send a file @@ -2119,7 +2140,7 @@ Examples: beeper send file --to 8951 --file ./photo.jpg --caption "from today" ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper send react` Send a reaction to a message @@ -2144,7 +2165,7 @@ Examples: beeper send react --to 10313 --id --reaction "+1" ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper send sticker` Send a sticker @@ -2174,7 +2195,7 @@ Examples: beeper send sticker --to 10313 --file ./hi.webp ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper send unreact` Remove a reaction from a message @@ -2199,7 +2220,7 @@ Examples: beeper send unreact --to 10313 --id --reaction "+1" ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper send voice` Send a voice note @@ -2231,7 +2252,7 @@ beeper send voice --to 10313 --file ./note.ogg beeper send voice --to 10313 --file ./note.ogg --duration 12 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper presence` Send a typing (or paused) indicator to a chat @@ -2259,7 +2280,7 @@ beeper presence --chat 10313 --state paused beeper presence --chat 10313 --duration 5 ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper contacts list` List contacts @@ -2285,7 +2306,7 @@ Examples: beeper contacts list --account whatsapp --query alice ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper contacts search` Search contacts @@ -2314,7 +2335,7 @@ Examples: beeper contacts search alice ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper contacts show` Show contact details @@ -2341,7 +2362,147 @@ Examples: beeper contacts show "Alice" --account whatsapp ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. + +### `beeper resolve chat` +Resolve a chat selector to concrete chat candidates + +```sh +beeper resolve chat +``` + +Arguments: + +| Name | Required | Description | +| --- | --- | --- | +| `selector` | yes | Chat ID, local ID, exact title, or search text | + +Flags: + +| Flag | Type | Description | +| --- | --- | --- | +| `--account=...` | option | Limit to account selector. Repeat for multiple. | +| `--limit=` | option | Maximum candidates to return Default: 10 | +| `--pick=` | option | Select the Nth candidate (1-indexed) | + +Examples: + +```sh +beeper resolve chat Family --format json +beeper resolve chat Family --pick 1 --results-only +``` + +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. + +### `beeper resolve account` +Resolve an account selector + +```sh +beeper resolve account +``` + +Arguments: + +| Name | Required | Description | +| --- | --- | --- | +| `selector` | yes | Account ID, network, bridge, or account user selector | + +Flags: + +| Flag | Type | Description | +| --- | --- | --- | +| `--pick=` | option | Select the Nth candidate (1-indexed) | + +Examples: + +```sh +beeper resolve account whatsapp --format json +``` + +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. + +### `beeper resolve contact` +Resolve a contact selector + +```sh +beeper resolve contact +``` + +Arguments: + +| Name | Required | Description | +| --- | --- | --- | +| `selector` | yes | Contact name, username, phone, email, or ID | + +Flags: + +| Flag | Type | Description | +| --- | --- | --- | +| `--account=...` | option | Limit to account selector. Repeat for multiple. | +| `--limit=` | option | Maximum candidates to return per account Default: 10 | +| `--pick=` | option | Select the Nth candidate (1-indexed) | + +Examples: + +```sh +beeper resolve contact Alice --account whatsapp --format json +``` + +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. + +### `beeper resolve target` +Resolve a target selector + +```sh +beeper resolve target +``` + +Arguments: + +| Name | Required | Description | +| --- | --- | --- | +| `selector` | yes | Target name, ID, type, or base URL | + +Flags: + +| Flag | Type | Description | +| --- | --- | --- | +| `--pick=` | option | Select the Nth candidate (1-indexed) | + +Examples: + +```sh +beeper resolve target desktop --format json +``` + +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. + +### `beeper resolve bridge` +Resolve a bridge selector + +```sh +beeper resolve bridge +``` + +Arguments: + +| Name | Required | Description | +| --- | --- | --- | +| `selector` | yes | Bridge ID, type, provider, or display name | + +Flags: + +| Flag | Type | Description | +| --- | --- | --- | +| `--pick=` | option | Select the Nth candidate (1-indexed) | + +Examples: + +```sh +beeper resolve bridge whatsapp --format json +``` + +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper media download` Download message media @@ -2369,7 +2530,7 @@ beeper media download mxc://beeper.com/abc --out ./downloads beeper media download mxc://beeper.com/abc -o - > photo.jpg ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper export` Export accounts, chats, messages, Markdown transcripts, and attachments @@ -2386,7 +2547,6 @@ Flags: | --- | --- | --- | | `--account=...` | option | Limit to an account selector. Repeat to include more accounts. | | `--chat=...` | option | Limit to a chat selector. Repeat to include more chats. | -| `--force` | boolean | Re-export chats even if checkpoint state says they are complete. | | `--limit-chats=` | option | Maximum chats to export. Intended for testing large exports. | | `--limit-messages=` | option | Maximum messages per chat. Intended for testing large exports. | | `--max-participants=` | option | Maximum participants to include in each chat.json. Default: 500 | @@ -2401,7 +2561,7 @@ beeper export --out ./beeper-export beeper export --chat 10313 --out ./chat ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper watch` Stream Desktop API WebSocket events @@ -2430,7 +2590,7 @@ beeper watch --include-type message.upserted --include-type message.deleted beeper watch --webhook https://example.com/hook --webhook-secret "$BEEPER_WEBHOOK_SECRET" ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper rpc` Run newline-delimited JSON command RPC over stdin/stdout @@ -2447,7 +2607,7 @@ Examples: printf '{"id":1,"command":"chats list --json"}\n' | beeper rpc ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper man` Print the command manual @@ -2460,10 +2620,36 @@ Examples: ```sh beeper man -beeper man --json +beeper man --format json +beeper man --format ids +``` + +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. + +### `beeper schema` +Print machine-readable command/flag schema + +```sh +beeper schema [command] +``` + +Agent-first schema for commands, flags, args, examples, mutation metadata, selectors, output shapes, and related commands. + +Arguments: + +| Name | Required | Description | +| --- | --- | --- | +| `command` | no | Optional command path, such as "messages search" | + +Examples: + +```sh +beeper schema +beeper schema send --results-only +beeper schema --select commands.path,commands.flags.name --results-only ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper doctor` Probe the target live and report diagnostics @@ -2481,7 +2667,7 @@ beeper doctor beeper doctor --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper status` Show selected target and setup readiness @@ -2499,7 +2685,7 @@ beeper status beeper status --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper docs` Open Beeper CLI docs @@ -2514,7 +2700,7 @@ Examples: beeper docs ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper version` Print CLI version @@ -2529,7 +2715,7 @@ Examples: beeper version ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper completion` Print shell completion setup @@ -2589,7 +2775,7 @@ beeper plugins available beeper plugins available --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper update` Check and install Beeper updates @@ -2615,7 +2801,7 @@ beeper update --cli beeper update --server ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper config get` Print CLI configuration @@ -2637,7 +2823,7 @@ beeper config get beeper config get defaultTarget ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper config set` Set a CLI configuration value @@ -2659,7 +2845,7 @@ Examples: beeper config set defaultTarget work ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper config path` Print the CLI config path @@ -2674,7 +2860,7 @@ Examples: beeper config path ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper config reset` Reset CLI configuration @@ -2689,7 +2875,7 @@ Examples: beeper config reset ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper api get` Call a raw Desktop API GET path @@ -2717,7 +2903,7 @@ beeper api get /v1/info beeper api get /v1/chats --json ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper api post` Call a raw Desktop API POST path with a JSON body @@ -2745,7 +2931,7 @@ Examples: beeper api post /v1/chats/abc/read --body '{"messageID":"x"}' ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ### `beeper api request` Call a raw Desktop API path with any supported HTTP method @@ -2774,7 +2960,7 @@ Examples: beeper api request DELETE /v1/chats/abc/messages/def/reactions --body '{"reactionKey":"👍"}' ``` -Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`. +Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. ## Publishing diff --git a/packages/cli/docs/api.md b/packages/cli/docs/api.md deleted file mode 100644 index b9cf1de1..00000000 --- a/packages/cli/docs/api.md +++ /dev/null @@ -1,29 +0,0 @@ -# api - -Read when: calling raw Desktop API endpoints that the CLI doesn't yet wrap -with a workflow command. - -## Commands - -```sh -beeper api get [--no-auth] -beeper api post [--body JSON] [--no-auth] -beeper api request [--body JSON] [--no-auth] -``` - -## Notes - -- `` is a Desktop API path, e.g. `/v1/info` or `/v1/chats/{chatID}/read`. -- `--no-auth` calls a public path without the bearer token. -- `--body` is sent as `application/json`; default is `{}` for `post`. -- `api request` lets you hit `GET | POST | PUT | PATCH | DELETE`; the others are convenience shortcuts. -- `--read-only` blocks `api post` / `api put` / `api patch` / `api delete` / `api request `. - -## Examples - -```sh -beeper api get /v1/info -beeper api get /v1/chats --json -beeper api post /v1/chats/abc/read --body '{"messageID":"x"}' -beeper api request PATCH /v1/chats/abc --body '{"isPinned":true}' -``` diff --git a/packages/cli/docs/config.md b/packages/cli/docs/config.md deleted file mode 100644 index 333135a7..00000000 --- a/packages/cli/docs/config.md +++ /dev/null @@ -1,32 +0,0 @@ -# config - -Read when: inspecting, changing, or resetting the CLI's local configuration -file (`~/.beeper/config.json`, or wherever `BEEPER_CLI_CONFIG_DIR` points). - -## Commands - -```sh -beeper config path -beeper config get [defaultTarget | defaultAccount | baseURL | auth] -beeper config set -beeper config reset -``` - -## Notes - -- `config path` prints the JSON config path (suitable for `cat` or `cd $(dirname …)`). -- `config get` without a key prints the full config; passing a key prints just that field. -- `auth.accessToken` is always redacted in `config get` output. -- `config set ""` clears the field. Only `defaultTarget` and `defaultAccount` are settable here; other fields are written by commands like `targets use` and `auth verify`. -- `config reset` deletes the config file. - -## Examples - -```sh -beeper config path -beeper config get --json -beeper config get defaultTarget -beeper config set defaultTarget work -beeper config set defaultAccount "" -beeper config reset -``` diff --git a/packages/cli/docs/export.md b/packages/cli/docs/export.md deleted file mode 100644 index f3830af0..00000000 --- a/packages/cli/docs/export.md +++ /dev/null @@ -1,39 +0,0 @@ -# export - -Read when: making a heavy, multi-chat, attachment-including export of Beeper -data to disk. For a lightweight per-chat JSON dump, see [messages -export](messages.md). - -## Command - -```sh -beeper export - [-o, --out DIR] - [--account SEL]... - [--chat SEL]... - [--limit-chats N] - [--limit-messages N] - [--max-participants N] - [--no-attachments] - [--force] - [--quiet] - [--pick N] -``` - -## Notes - -- Default `--out` directory is `beeper-export`. -- Layout: `accounts.json`, `chats.json`, `manifest.json`, plus one directory per chat with `chat.json`, `messages.json`, `messages.markdown`, `messages.html`, attachments, and per-chat checkpoint state. -- Exports are resumable. Re-running picks up where the last run left off unless `--force` is set. -- `--max-participants` (default 500) bounds the participant list stored in each `chat.json`. -- `--no-attachments` skips downloading media; metadata is still recorded. -- `--limit-chats` / `--limit-messages` are intended for sanity-checking large exports. - -## Examples - -```sh -beeper export --out ./beeper-export -beeper export --chat "Family" --out ./family -beeper export --account whatsapp --no-attachments --quiet -beeper export --force --out ./beeper-export -``` diff --git a/packages/cli/docs/setup.md b/packages/cli/docs/setup.md deleted file mode 100644 index 9bf22140..00000000 --- a/packages/cli/docs/setup.md +++ /dev/null @@ -1,38 +0,0 @@ -# setup - -Read when: making a Beeper target ready for the first time, switching to a -different target, or installing a managed runtime. - -`beeper setup` orchestrates the path from "I have nothing" to "the selected -target is ready". By default it detects a running local Beeper Desktop, offers -to reuse that session, and falls back to a guided choice between Desktop / -Server / remote targets. - -## Commands - -```sh -beeper setup [--local | --oauth | --remote URL | --desktop | --server] [--install] [--channel stable|nightly] -beeper install desktop [--channel stable|nightly] -beeper install server [--channel stable|nightly] [--server-env production|staging] -``` - -## Notes - -- `setup --local` reuses the local Beeper Desktop session (fastest trusted-device path). -- `setup --oauth` runs browser-based OAuth/PKCE against the resolved target. -- `setup --remote URL` configures a remote Beeper Desktop or Server target. -- `setup --desktop --install` or `setup --server --install` installs the runtime if missing, then sets up. -- `install desktop|server` installs without changing the selected target. -- The selected target is persisted in `~/.beeper/config.json` (override with `BEEPER_CLI_CONFIG_DIR`). -- For non-interactive use, pass a token in the environment: `BEEPER_ACCESS_TOKEN=… beeper …`. - -## Examples - -```sh -beeper setup -beeper setup --local -beeper setup --oauth -beeper setup --remote https://desktop.example.com -beeper setup --desktop --install --channel nightly -beeper install server --server-env staging -``` diff --git a/packages/cli/docs/update.md b/packages/cli/docs/update.md deleted file mode 100644 index 8403eff0..00000000 --- a/packages/cli/docs/update.md +++ /dev/null @@ -1,27 +0,0 @@ -# update - -Read when: checking for new versions of the CLI, the CLI-managed Desktop -install, or the CLI-managed Server install — and choosing whether to install. - -## Command - -```sh -beeper update [--check] [--cli] [--desktop] [--server] -``` - -## Notes - -- With no kind flag, checks all three (CLI, Desktop, Server) that apply. -- `--check` prints what's available without installing. -- The CLI itself is never auto-upgraded; `--cli` prints the right command for your install method (Homebrew, npm-global, or in-repo git build). -- `--desktop` reports on the CLI-owned Desktop install; updating Desktop itself happens inside the Desktop app. -- `--server` updates the CLI-managed Server install in place, then restarts any running managed Server targets. - -## Examples - -```sh -beeper update --check -beeper update --cli -beeper update --desktop --json -beeper update --server -``` diff --git a/packages/cli/docs/watch.md b/packages/cli/docs/watch.md deleted file mode 100644 index a8df6c74..00000000 --- a/packages/cli/docs/watch.md +++ /dev/null @@ -1,35 +0,0 @@ -# watch - -Read when: subscribing to live Desktop API events (new/updated/deleted chats -and messages), optionally forwarding them to a webhook. - -## Commands - -```sh -beeper watch - [-c, --chat CHAT_ID]... - [--include-type EVENT_TYPE]... - [--exclude-type EVENT_TYPE]... - [--webhook URL [--webhook-secret SECRET] [--webhook-queue N]] - [--json] -``` - -## Notes - -- Subscribes to the Desktop API WebSocket at the path returned by `/v1/info` (defaults to `/v1/ws`). -- Without `--chat`, subscribes to all chats. -- Event types come from the Desktop API: `chat.upserted`, `chat.deleted`, `message.upserted`, `message.deleted`. -- `--include-type` and `--exclude-type` are mutually exclusive. -- `--webhook URL` forwards every event as a POST body (best-effort, fire-and-forget). -- `--webhook-secret SECRET` signs the body with HMAC-SHA256 and sets `X-Beeper-Signature: sha256=`. -- `--webhook-queue` (default 64) caps pending deliveries; excess events are dropped with a stderr warning. -- `--quiet` suppresses the human-mode status line; `--json` prints raw events line-delimited. - -## Examples - -```sh -beeper watch -beeper watch --chat '!abc:beeper.com' --json -beeper watch --include-type message.upserted --include-type message.deleted -beeper watch --webhook https://example.com/hook --webhook-secret "$BEEPER_WEBHOOK_SECRET" -``` diff --git a/packages/cli/package.json b/packages/cli/package.json index f79ec98f..b35a43b9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -100,6 +100,9 @@ "messages": { "description": "List, search, show, edit, delete, and export messages" }, + "resolve": { + "description": "Resolve selectors into concrete candidates" + }, "send": { "description": "Send text, files, and reactions" }, diff --git a/packages/cli/scripts/generate-command-map.ts b/packages/cli/scripts/generate-command-map.ts index 3e68239d..e809e4e9 100644 --- a/packages/cli/scripts/generate-command-map.ts +++ b/packages/cli/scripts/generate-command-map.ts @@ -10,8 +10,10 @@ const outPath = join(root, 'src', 'commands.generated.ts') const listAliases: Record = { 'accounts:list': ['accounts'], 'bridges:list': ['bridges'], - 'chats:list': ['chats', 'accounts:chats'], + 'chats:list': ['chats', 'accounts:chats', 'ls'], 'contacts:list': ['contacts'], + 'messages:search': ['search'], + 'send:text': ['send'], 'targets:list': ['targets'], } diff --git a/packages/cli/scripts/generate-readme.ts b/packages/cli/scripts/generate-readme.ts index f9529adc..60b7a47a 100644 --- a/packages/cli/scripts/generate-readme.ts +++ b/packages/cli/scripts/generate-readme.ts @@ -24,7 +24,7 @@ const commands = commandManifest.map(item => { }; }); -const globalFlags = new Set(['base-url', 'debug', 'events', 'full', 'json', 'quiet', 'read-only', 'target', 'timeout', 'yes']); +const globalFlags = new Set(['base-url', 'debug', 'dry-run', 'events', 'force', 'format', 'full', 'json', 'no-input', 'quiet', 'read-only', 'results-only', 'select', 'target', 'timeout', 'yes']); const commandList = commands.map(command => { const id = displayID(command.id); return `| \`${id}\` | ${escapeTable(text(command.summary || command.description || ''))} |`; @@ -33,9 +33,24 @@ const commandList = commands.map(command => { const examplesByID = new Map(commandManifest.map(item => [item.command, item.examples ?? []])); const commandSections = commands.map(command => commandSection(command)).join('\n\n'); -const intro = `# beeper — One CLI for all your chats +// Origin where the Astro docs site (in `docs/`) is published. Keep this in sync +// with `site` in `docs/astro.config.mjs`. Until the docs have a permanent home +// this is a placeholder; flip both in one commit when you pick a host. +const docsUrl = 'https://example.com'; +const repoUrl = 'https://github.com/beeper/desktop-api-cli'; -> Built for you and your agent. Batteries included. +const intro = `
+ +# beeper + +**One CLI for all your chats.** Built for you and your agent — batteries included. + +[![npm](https://img.shields.io/npm/v/beeper-cli.svg?label=npm&color=6E56F8)](https://www.npmjs.com/package/beeper-cli) +[![license](https://img.shields.io/badge/license-MIT-6E56F8.svg)](${repoUrl}/blob/main/packages/cli/LICENSE) +[![docs](https://img.shields.io/badge/docs-online-6E56F8.svg)](${docsUrl}) +[![built with Bun](https://img.shields.io/badge/built%20with-Bun-6E56F8.svg)](https://bun.sh) + +
Talks to Beeper Desktop on this machine, to a Beeper Server you self-host, or to either one running somewhere else. Send and receive across the chat @@ -48,7 +63,7 @@ Facebook Messenger · X (Twitter) DMs · LinkedIn · Slack · Google Messages (RCS/SMS) · Google Chat · Matrix · IRC · Bluesky. Run \`beeper bridges list\` for the live list on your target. -Command manual: \`beeper man\` · CLI docs: \`beeper docs\` +📖 **[Read the docs](${docsUrl})** · command manual: \`beeper man\` · open docs: \`beeper docs\` ## Features @@ -228,19 +243,22 @@ WhatsApp, Telegram, Discord, iMessage, and the rest show up under \`accounts lis ## Documentation +Full documentation lives at **[${docsUrl.replace(/^https?:\/\//, '')}](${docsUrl})** +(built from [\`docs/\`](docs/) with Astro Starlight — a fully static site). + | Topic | Page | Commands | | --- | --- | --- | -| **Setup + install** | [setup](docs/setup.md) · [auth](docs/auth.md) | \`setup\` · \`install desktop\` · \`install server\` · \`verify\` · \`status\` · \`doctor\` · \`auth status\` | -| **Targets** | [targets](docs/targets.md) | \`targets list\` · \`targets add desktop\` · \`targets add server\` · \`targets add remote\` · \`targets use\` · \`targets status\` · \`targets logs\` | -| **Bridges + accounts** | [accounts](docs/accounts.md) | \`bridges list\` · \`bridges show\` · \`accounts list\` · \`accounts add\` · \`accounts show\` · \`accounts use\` · \`accounts remove\` | -| **Chats** | [chats](docs/chats.md) | \`chats list\` · \`chats search\` · \`chats show\` · \`chats start\` · \`chats archive\` · \`chats pin\` · \`chats mute\` · \`chats priority\` · \`chats remind\` · \`chats rename\` · \`chats draft\` · \`chats focus\` | -| **Messages** | [messages](docs/messages.md) · [send](docs/send.md) · [presence](docs/presence.md) | \`messages list\` · \`messages search\` · \`messages export\` · \`send text\` · \`send file\` · \`send sticker\` · \`send voice\` · \`send react\` · \`presence\` | -| **Contacts + media** | [contacts](docs/contacts.md) · [media](docs/media.md) · [export](docs/export.md) | \`contacts list\` · \`contacts search\` · \`media download\` · \`export\` | -| **Automation** | [watch](docs/watch.md) · [rpc](docs/rpc.md) · [api](docs/api.md) | \`watch\` · \`watch --webhook\` · \`rpc\` · \`man\` · \`api get\` · \`api post\` · \`api request\` | -| **Maintenance** | [config](docs/config.md) · [update](docs/update.md) | \`update\` · \`config\` · \`completion\` · \`docs\` · \`version\` | +| **Setup + install** | [connect](${docsUrl}/connect/) · [install](${docsUrl}/install/) · [auth](${docsUrl}/auth/) | \`setup\` · \`install desktop\` · \`install server\` · \`verify\` · \`status\` · \`doctor\` · \`auth status\` | +| **Targets** | [targets](${docsUrl}/targets/) | \`targets list\` · \`targets add desktop\` · \`targets add server\` · \`targets add remote\` · \`targets use\` · \`targets status\` · \`targets logs\` | +| **Bridges + accounts** | [accounts](${docsUrl}/accounts/) | \`bridges list\` · \`bridges show\` · \`accounts list\` · \`accounts add\` · \`accounts show\` · \`accounts use\` · \`accounts remove\` | +| **Chats** | [chats](${docsUrl}/chats/) | \`chats list\` · \`chats search\` · \`chats show\` · \`chats start\` · \`chats archive\` · \`chats pin\` · \`chats mute\` · \`chats priority\` · \`chats remind\` · \`chats rename\` · \`chats draft\` · \`chats focus\` | +| **Messages** | [messages](${docsUrl}/messages/) · [send](${docsUrl}/send/) · [presence](${docsUrl}/presence/) | \`messages list\` · \`messages search\` · \`messages export\` · \`send text\` · \`send file\` · \`send sticker\` · \`send voice\` · \`send react\` · \`presence\` | +| **Contacts + media** | [contacts](${docsUrl}/contacts/) · [media](${docsUrl}/media/) · [export](${docsUrl}/export/) | \`contacts list\` · \`contacts search\` · \`media download\` · \`export\` | +| **Automation** | [scripting](${docsUrl}/scripting/) · [watch](${docsUrl}/watch/) · [rpc](${docsUrl}/rpc/) · [api](${docsUrl}/api/) | \`watch\` · \`watch --webhook\` · \`rpc\` · \`man\` · \`api get\` · \`api post\` · \`api request\` | +| **Maintenance** | [config](${docsUrl}/config/) · [update](${docsUrl}/update/) | \`update\` · \`config\` · \`completion\` · \`docs\` · \`version\` | Use \`beeper docs\` to open the CLI docs and \`beeper man\` to print the local -command manual. +command manual. To work on the docs site locally: \`cd docs && bun install && bun run dev\`. ## Configuration diff --git a/packages/cli/src/commands.generated.ts b/packages/cli/src/commands.generated.ts index ed34e12a..f8170748 100644 --- a/packages/cli/src/commands.generated.ts +++ b/packages/cli/src/commands.generated.ts @@ -60,45 +60,51 @@ import Command58 from './commands/messages/show.js' import Command59 from './commands/plugins.js' import Command60 from './commands/plugins/available.js' import Command61 from './commands/presence.js' -import Command62 from './commands/rpc.js' -import Command63 from './commands/send/file.js' -import Command64 from './commands/send/react.js' -import Command65 from './commands/send/sticker.js' -import Command66 from './commands/send/text.js' -import Command67 from './commands/send/unreact.js' -import Command68 from './commands/send/voice.js' -import Command69 from './commands/setup.js' -import Command70 from './commands/status.js' -import Command71 from './commands/targets/add/desktop.js' -import Command72 from './commands/targets/add/remote.js' -import Command73 from './commands/targets/add/server.js' -import Command74 from './commands/targets/disable.js' -import Command75 from './commands/targets/enable.js' -import Command76 from './commands/targets/list.js' -import Command77 from './commands/targets/logs.js' -import Command78 from './commands/targets/remove.js' -import Command79 from './commands/targets/restart.js' -import Command80 from './commands/targets/show.js' -import Command81 from './commands/targets/start.js' -import Command82 from './commands/targets/status.js' -import Command83 from './commands/targets/stop.js' -import Command84 from './commands/targets/use.js' -import Command85 from './commands/update.js' -import Command86 from './commands/verify.js' -import Command87 from './commands/verify/approve.js' -import Command88 from './commands/verify/cancel.js' -import Command89 from './commands/verify/list.js' -import Command90 from './commands/verify/qr-confirm.js' -import Command91 from './commands/verify/qr-scan.js' -import Command92 from './commands/verify/recovery-key.js' -import Command93 from './commands/verify/reset-recovery-key.js' -import Command94 from './commands/verify/sas.js' -import Command95 from './commands/verify/sas-confirm.js' -import Command96 from './commands/verify/show.js' -import Command97 from './commands/verify/start.js' -import Command98 from './commands/verify/status.js' -import Command99 from './commands/version.js' -import Command100 from './commands/watch.js' +import Command62 from './commands/resolve/account.js' +import Command63 from './commands/resolve/bridge.js' +import Command64 from './commands/resolve/chat.js' +import Command65 from './commands/resolve/contact.js' +import Command66 from './commands/resolve/target.js' +import Command67 from './commands/rpc.js' +import Command68 from './commands/schema.js' +import Command69 from './commands/send/file.js' +import Command70 from './commands/send/react.js' +import Command71 from './commands/send/sticker.js' +import Command72 from './commands/send/text.js' +import Command73 from './commands/send/unreact.js' +import Command74 from './commands/send/voice.js' +import Command75 from './commands/setup.js' +import Command76 from './commands/status.js' +import Command77 from './commands/targets/add/desktop.js' +import Command78 from './commands/targets/add/remote.js' +import Command79 from './commands/targets/add/server.js' +import Command80 from './commands/targets/disable.js' +import Command81 from './commands/targets/enable.js' +import Command82 from './commands/targets/list.js' +import Command83 from './commands/targets/logs.js' +import Command84 from './commands/targets/remove.js' +import Command85 from './commands/targets/restart.js' +import Command86 from './commands/targets/show.js' +import Command87 from './commands/targets/start.js' +import Command88 from './commands/targets/status.js' +import Command89 from './commands/targets/stop.js' +import Command90 from './commands/targets/use.js' +import Command91 from './commands/update.js' +import Command92 from './commands/verify.js' +import Command93 from './commands/verify/approve.js' +import Command94 from './commands/verify/cancel.js' +import Command95 from './commands/verify/list.js' +import Command96 from './commands/verify/qr-confirm.js' +import Command97 from './commands/verify/qr-scan.js' +import Command98 from './commands/verify/recovery-key.js' +import Command99 from './commands/verify/reset-recovery-key.js' +import Command100 from './commands/verify/sas.js' +import Command101 from './commands/verify/sas-confirm.js' +import Command102 from './commands/verify/show.js' +import Command103 from './commands/verify/start.js' +import Command104 from './commands/verify/status.js' +import Command105 from './commands/version.js' +import Command106 from './commands/watch.js' export const commands = { 'accounts': Command1, @@ -156,6 +162,7 @@ export const commands = { 'export': Command47, 'install:desktop': Command48, 'install:server': Command49, + 'ls': Command21, 'man': Command50, 'media:download': Command51, 'messages:context': Command52, @@ -168,44 +175,52 @@ export const commands = { 'plugins': Command59, 'plugins:available': Command60, 'presence': Command61, - 'rpc': Command62, - 'send:file': Command63, - 'send:react': Command64, - 'send:sticker': Command65, - 'send:text': Command66, - 'send:unreact': Command67, - 'send:voice': Command68, - 'setup': Command69, - 'status': Command70, - 'targets': Command76, - 'targets:add:desktop': Command71, - 'targets:add:remote': Command72, - 'targets:add:server': Command73, - 'targets:disable': Command74, - 'targets:enable': Command75, - 'targets:list': Command76, - 'targets:logs': Command77, - 'targets:remove': Command78, - 'targets:restart': Command79, - 'targets:show': Command80, - 'targets:start': Command81, - 'targets:status': Command82, - 'targets:stop': Command83, - 'targets:use': Command84, - 'update': Command85, - 'verify': Command86, - 'verify:approve': Command87, - 'verify:cancel': Command88, - 'verify:list': Command89, - 'verify:qr-confirm': Command90, - 'verify:qr-scan': Command91, - 'verify:recovery-key': Command92, - 'verify:reset-recovery-key': Command93, - 'verify:sas': Command94, - 'verify:sas-confirm': Command95, - 'verify:show': Command96, - 'verify:start': Command97, - 'verify:status': Command98, - 'version': Command99, - 'watch': Command100, + 'resolve:account': Command62, + 'resolve:bridge': Command63, + 'resolve:chat': Command64, + 'resolve:contact': Command65, + 'resolve:target': Command66, + 'rpc': Command67, + 'schema': Command68, + 'search': Command57, + 'send': Command72, + 'send:file': Command69, + 'send:react': Command70, + 'send:sticker': Command71, + 'send:text': Command72, + 'send:unreact': Command73, + 'send:voice': Command74, + 'setup': Command75, + 'status': Command76, + 'targets': Command82, + 'targets:add:desktop': Command77, + 'targets:add:remote': Command78, + 'targets:add:server': Command79, + 'targets:disable': Command80, + 'targets:enable': Command81, + 'targets:list': Command82, + 'targets:logs': Command83, + 'targets:remove': Command84, + 'targets:restart': Command85, + 'targets:show': Command86, + 'targets:start': Command87, + 'targets:status': Command88, + 'targets:stop': Command89, + 'targets:use': Command90, + 'update': Command91, + 'verify': Command92, + 'verify:approve': Command93, + 'verify:cancel': Command94, + 'verify:list': Command95, + 'verify:qr-confirm': Command96, + 'verify:qr-scan': Command97, + 'verify:recovery-key': Command98, + 'verify:reset-recovery-key': Command99, + 'verify:sas': Command100, + 'verify:sas-confirm': Command101, + 'verify:show': Command102, + 'verify:start': Command103, + 'verify:status': Command104, + 'version': Command105, + 'watch': Command106, } diff --git a/packages/cli/src/commands/accounts/add.ts b/packages/cli/src/commands/accounts/add.ts index 1db3c81b..a4b1865e 100644 --- a/packages/cli/src/commands/accounts/add.ts +++ b/packages/cli/src/commands/accounts/add.ts @@ -5,7 +5,7 @@ import { BeeperCommand, ensureWritable } from '../../lib/command.js' import type { Bridge, LoginFlow } from '@beeper/desktop-api/resources/bridges.js' import { createClient } from '../../lib/client.js' import { printAccountLoginStep, runGuidedAccountLogin } from '../../lib/account-login.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' type AccountType = Bridge @@ -67,6 +67,22 @@ export default class AccountsAdd extends BeeperCommand { if (!flags.json && loginFlows.length > 1) this.log(`Using flow ${flowID}`) } + if (flags['dry-run']) { + await printDryRun('accounts.add', { + bridgeID: accountType.id, + bridgeName: accountType.displayName, + flowID, + loginID: flags['login-id'], + guided: flags.guided, + nonInteractive: flags['non-interactive'], + cookieKeys: Object.keys(parseKeyValueFlags(flags.cookie, '--cookie')), + fieldKeys: Object.keys(parseKeyValueFlags(flags.field, '--field')), + webview: flags.webview, + webviewBackend: flags['webview-backend'], + }, flags.json ? 'json' : 'human') + return + } + const step = await client.bridges.loginSessions.create(accountType.id, { flowID, loginID: flags['login-id'], diff --git a/packages/cli/src/commands/accounts/remove.ts b/packages/cli/src/commands/accounts/remove.ts index 6bf7180f..93e20aa5 100644 --- a/packages/cli/src/commands/accounts/remove.ts +++ b/packages/cli/src/commands/accounts/remove.ts @@ -1,7 +1,7 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' import { resolveAccountID } from '../../lib/resolve.js' export default class AccountsRemove extends BeeperCommand { @@ -14,6 +14,10 @@ export default class AccountsRemove extends BeeperCommand { ensureWritable(flags) const client = await createClient(flags) const accountID = await resolveAccountID(client, args.account) + if (flags['dry-run']) { + await printDryRun('accounts.remove', { accountID }, flags.json ? 'json' : 'human') + return + } const accounts = client.accounts as any if (accounts.delete) await accounts.delete(accountID) else if (accounts.remove) await accounts.remove(accountID) diff --git a/packages/cli/src/commands/accounts/use.ts b/packages/cli/src/commands/accounts/use.ts index cbb88f4f..441f560c 100644 --- a/packages/cli/src/commands/accounts/use.ts +++ b/packages/cli/src/commands/accounts/use.ts @@ -1,7 +1,7 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' import { resolveAccountID } from '../../lib/resolve.js' import { updateConfig } from '../../lib/targets.js' @@ -15,12 +15,20 @@ export default class AccountsUse extends BeeperCommand { const { args, flags } = await this.parse(AccountsUse) ensureWritable(flags) if (args.account === '') { + if (flags['dry-run']) { + await printDryRun('accounts.use', { defaultAccount: undefined }, flags.json ? 'json' : 'human') + return + } await updateConfig(config => ({ ...config, defaultAccount: undefined })) await printSuccess({ message: 'Cleared default account' }, flags.json ? 'json' : 'human') return } const client = await createClient(flags) const accountID = await resolveAccountID(client, args.account) + if (flags['dry-run']) { + await printDryRun('accounts.use', { defaultAccount: accountID }, flags.json ? 'json' : 'human') + return + } await updateConfig(config => ({ ...config, defaultAccount: accountID })) await printSuccess({ message: `Default account: ${accountID}`, data: { accountID } }, flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/api/post.ts b/packages/cli/src/commands/api/post.ts index 95c573ce..429f9e22 100644 --- a/packages/cli/src/commands/api/post.ts +++ b/packages/cli/src/commands/api/post.ts @@ -2,7 +2,7 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { appRequest } from '../../lib/app-api.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' export default class ApiPost extends BeeperCommand { static override summary = 'Call a raw Desktop API POST path with a JSON body' @@ -24,6 +24,10 @@ export default class ApiPost extends BeeperCommand { } catch { throw new Error(`--body is not valid JSON: ${flags.body}`) } + if (flags['dry-run']) { + await printDryRun('api.post', { method: 'POST', path: args.path, body, noAuth: flags['no-auth'], target: flags.target, baseURL: flags['base-url'] }, flags.json ? 'json' : 'human') + return + } if (flags['no-auth']) { await printData(await appRequest('POST', args.path, { baseURL: flags['base-url'], body, target: flags.target, token: false }), flags.json ? 'json' : 'human') return diff --git a/packages/cli/src/commands/api/request.ts b/packages/cli/src/commands/api/request.ts index db3c297b..f75cd3d5 100644 --- a/packages/cli/src/commands/api/request.ts +++ b/packages/cli/src/commands/api/request.ts @@ -1,7 +1,7 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { appRequest, type AppRequestMethod } from '../../lib/app-api.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' export default class ApiRequest extends BeeperCommand { static override summary = 'Call a raw Desktop API path with any supported HTTP method' @@ -20,6 +20,10 @@ export default class ApiRequest extends BeeperCommand { const method = args.method as AppRequestMethod if (method !== 'GET') ensureWritable(flags) const body = flags.body ? JSON.parse(flags.body) as Record : undefined + if (flags['dry-run'] && method !== 'GET') { + await printDryRun('api.request', { method, path: args.path, body, noAuth: flags['no-auth'], target: flags.target, baseURL: flags['base-url'] }, flags.json ? 'json' : 'human') + return + } if (flags['no-auth']) { await printData(await appRequest(method, args.path, { baseURL: flags['base-url'], body, target: flags.target, token: false }), flags.json ? 'json' : 'human') return diff --git a/packages/cli/src/commands/auth/email/response.ts b/packages/cli/src/commands/auth/email/response.ts index 1a517ecb..f4a34d9d 100644 --- a/packages/cli/src/commands/auth/email/response.ts +++ b/packages/cli/src/commands/auth/email/response.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../../lib/command.js' import { resolveTarget } from '../../../lib/targets.js' import { finishEmailSetup } from '../../../lib/setup-login.js' -import { printData } from '../../../lib/output.js' +import { printData, printDryRun } from '../../../lib/output.js' export default class AuthEmailResponse extends BeeperCommand { static override summary = 'Finish email sign-in with a verification code' @@ -17,6 +17,10 @@ export default class AuthEmailResponse extends BeeperCommand { const { flags } = await this.parse(AuthEmailResponse) ensureWritable(flags) const target = await resolveTarget({ target: flags.target, baseURL: flags['base-url'] }) + if (flags['dry-run']) { + await printDryRun('auth.email.response', { target: target.id, baseURL: target.baseURL, setupRequestID: flags['setup-request-id'], username: flags.username, yes: flags.yes }, flags.json ? 'json' : 'human') + return + } const data = await finishEmailSetup(target, { code: flags.code, json: flags.json, diff --git a/packages/cli/src/commands/auth/logout.ts b/packages/cli/src/commands/auth/logout.ts index a100569f..df2e90b1 100644 --- a/packages/cli/src/commands/auth/logout.ts +++ b/packages/cli/src/commands/auth/logout.ts @@ -1,6 +1,6 @@ import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { clearTargetAuth, resolveTarget } from '../../lib/targets.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class AuthLogout extends BeeperCommand { static override summary = 'Clear stored authentication' @@ -13,6 +13,10 @@ export default class AuthLogout extends BeeperCommand { throw new Error('auth logout cannot clear BEEPER_ACCESS_TOKEN from the environment; unset it in the calling process.') } const token = target.auth?.accessToken + if (flags['dry-run']) { + await printDryRun('auth.logout', { target: target.id, baseURL: target.baseURL, hadToken: Boolean(token), revokeToken: Boolean(token) }, flags.json ? 'json' : 'human') + return + } let revoked = false if (token) { const response = await fetch(new URL('/oauth/revoke', target.baseURL), { diff --git a/packages/cli/src/commands/chats/archive.ts b/packages/cli/src/commands/chats/archive.ts index 17b27e61..cf690869 100644 --- a/packages/cli/src/commands/chats/archive.ts +++ b/packages/cli/src/commands/chats/archive.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsArchive extends BeeperCommand { @@ -14,6 +14,10 @@ export default class ChatsArchive extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.archive', { chatID, isArchived: true }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.update(chatID, { isArchived: true }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/avatar.ts b/packages/cli/src/commands/chats/avatar.ts index cee1f6b1..c511dbfc 100644 --- a/packages/cli/src/commands/chats/avatar.ts +++ b/packages/cli/src/commands/chats/avatar.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsAvatar extends BeeperCommand { @@ -14,6 +14,10 @@ export default class ChatsAvatar extends BeeperCommand { if (!flags.clear && !flags.file) throw new Error('Provide --file or --clear') const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.avatar', { chatID, imgURL: flags.clear ? null : flags.file }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.update(chatID, { imgURL: flags.clear ? null : flags.file }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/description.ts b/packages/cli/src/commands/chats/description.ts index 429c7ac6..6c382ae8 100644 --- a/packages/cli/src/commands/chats/description.ts +++ b/packages/cli/src/commands/chats/description.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsDescription extends BeeperCommand { @@ -14,6 +14,10 @@ export default class ChatsDescription extends BeeperCommand { if (!flags.clear && !flags.description) throw new Error('Provide --description or --clear') const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.description', { chatID, description: flags.clear ? null : flags.description }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.update(chatID, { description: flags.clear ? null : flags.description }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/disappear.ts b/packages/cli/src/commands/chats/disappear.ts index 16eace97..07c858fe 100644 --- a/packages/cli/src/commands/chats/disappear.ts +++ b/packages/cli/src/commands/chats/disappear.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsDisappear extends BeeperCommand { @@ -22,6 +22,10 @@ export default class ChatsDisappear extends BeeperCommand { const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) const expiry = flags.seconds.toLowerCase() === 'off' ? null : Number(flags.seconds) if (expiry !== null && (!Number.isInteger(expiry) || expiry < 0)) throw new Error('--seconds must be a positive integer or "off"') + if (flags['dry-run']) { + await printDryRun('chats.disappear', { chatID, messageExpirySeconds: expiry }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.update(chatID, { messageExpirySeconds: expiry }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/draft.ts b/packages/cli/src/commands/chats/draft.ts index 15c8df79..2db6abb3 100644 --- a/packages/cli/src/commands/chats/draft.ts +++ b/packages/cli/src/commands/chats/draft.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsDraft extends BeeperCommand { @@ -24,9 +24,17 @@ export default class ChatsDraft extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) if (flags.clear) { + if (flags['dry-run']) { + await printDryRun('chats.draft', { chatID, draft: null }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.update(chatID, { draft: null }), flags.json ? 'json' : 'human') return } + if (flags['dry-run']) { + await printDryRun('chats.draft', { chatID, draft: { text: flags.text!, file: flags.file, fileName: flags.filename, mimeType: flags.mime } }, flags.json ? 'json' : 'human') + return + } const upload = flags.file ? await client.assets.upload({ file: createReadStream(flags.file), fileName: flags.filename, mimeType: flags.mime }) : undefined await printData(await client.chats.update(chatID, { draft: { text: flags.text!, attachments: upload?.uploadID ? { [upload.uploadID]: upload as any } : undefined } }), flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/chats/focus.ts b/packages/cli/src/commands/chats/focus.ts index ef30b469..e8b68036 100644 --- a/packages/cli/src/commands/chats/focus.ts +++ b/packages/cli/src/commands/chats/focus.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsFocus extends BeeperCommand { @@ -20,6 +20,10 @@ export default class ChatsFocus extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.focus', { chatID, messageID: flags.message, draftText: flags.draft, draftAttachmentPath: flags.attachment }, flags.json ? 'json' : 'human') + return + } await printData(await client.focus({ chatID, messageID: flags.message, draftText: flags.draft, draftAttachmentPath: flags.attachment }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/mark-read.ts b/packages/cli/src/commands/chats/mark-read.ts index 4f942867..176ff556 100644 --- a/packages/cli/src/commands/chats/mark-read.ts +++ b/packages/cli/src/commands/chats/mark-read.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsMarkRead extends BeeperCommand { @@ -14,6 +14,10 @@ export default class ChatsMarkRead extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.mark-read', { chatID, messageID: flags.message }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.markRead(chatID, { messageID: flags.message }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/mark-unread.ts b/packages/cli/src/commands/chats/mark-unread.ts index 26bd3df7..e70043ff 100644 --- a/packages/cli/src/commands/chats/mark-unread.ts +++ b/packages/cli/src/commands/chats/mark-unread.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsMarkUnread extends BeeperCommand { @@ -14,6 +14,10 @@ export default class ChatsMarkUnread extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.mark-unread', { chatID, messageID: flags.message }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.markUnread(chatID, { messageID: flags.message }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/mute.ts b/packages/cli/src/commands/chats/mute.ts index 082fbcbb..39d6e338 100644 --- a/packages/cli/src/commands/chats/mute.ts +++ b/packages/cli/src/commands/chats/mute.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsMute extends BeeperCommand { @@ -16,6 +16,10 @@ export default class ChatsMute extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.mute', { chatID, isMuted: true }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.update(chatID, { isMuted: true }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/notify-anyway.ts b/packages/cli/src/commands/chats/notify-anyway.ts index 27f20517..1ffdf72e 100644 --- a/packages/cli/src/commands/chats/notify-anyway.ts +++ b/packages/cli/src/commands/chats/notify-anyway.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsNotifyAnyway extends BeeperCommand { @@ -14,6 +14,10 @@ export default class ChatsNotifyAnyway extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.notify-anyway', { chatID }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.notifyAnyway(chatID), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/pin.ts b/packages/cli/src/commands/chats/pin.ts index a5e0b024..26543935 100644 --- a/packages/cli/src/commands/chats/pin.ts +++ b/packages/cli/src/commands/chats/pin.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsPin extends BeeperCommand { @@ -14,6 +14,10 @@ export default class ChatsPin extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.pin', { chatID, isPinned: true }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.update(chatID, { isPinned: true }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/priority.ts b/packages/cli/src/commands/chats/priority.ts index 68d75ec5..1ae932f9 100644 --- a/packages/cli/src/commands/chats/priority.ts +++ b/packages/cli/src/commands/chats/priority.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsPriority extends BeeperCommand { @@ -19,6 +19,10 @@ export default class ChatsPriority extends BeeperCommand { const update = flags.level === 'inbox' ? { isArchived: false, isLowPriority: false } : { isLowPriority: true } + if (flags['dry-run']) { + await printDryRun('chats.priority', { chatID, level: flags.level, update }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.update(chatID, update), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/remind.ts b/packages/cli/src/commands/chats/remind.ts index 7edd7d6e..fce00d4e 100644 --- a/packages/cli/src/commands/chats/remind.ts +++ b/packages/cli/src/commands/chats/remind.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsRemind extends BeeperCommand { @@ -18,6 +18,10 @@ export default class ChatsRemind extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.remind', { chatID, reminder: { remindAt: flags.when, dismissOnIncomingMessage: flags['dismiss-on-message'] || undefined } }, flags.json ? 'json' : 'human') + return + } await client.chats.reminders.create(chatID, { reminder: { remindAt: flags.when, dismissOnIncomingMessage: flags['dismiss-on-message'] || undefined } }) await printSuccess({ message: 'Reminder set', detail: flags.when, data: { chatID, remindAt: flags.when } }, flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/chats/rename.ts b/packages/cli/src/commands/chats/rename.ts index 07139fe1..d965bff8 100644 --- a/packages/cli/src/commands/chats/rename.ts +++ b/packages/cli/src/commands/chats/rename.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsRename extends BeeperCommand { @@ -16,6 +16,10 @@ export default class ChatsRename extends BeeperCommand { ensureWritable(flags) const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.rename', { chatID, title: flags.title }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.update(chatID, { title: flags.title }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/start.ts b/packages/cli/src/commands/chats/start.ts index 59f5c6f7..170bd82b 100644 --- a/packages/cli/src/commands/chats/start.ts +++ b/packages/cli/src/commands/chats/start.ts @@ -2,7 +2,7 @@ import { Args, Flags } from '@oclif/core' import type { ChatStartParams } from '@beeper/desktop-api/resources/chats' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { listAccountIDs, resolveAccountID, userQueryFromInput } from '../../lib/resolve.js' export default class ChatsStart extends BeeperCommand { @@ -20,6 +20,10 @@ export default class ChatsStart extends BeeperCommand { const accountID = flags.account ? await resolveAccountID(client, flags.account) : await defaultAccountID(client) const user = userQueryFromInput(args.user) const payload: ChatStartParams & { title?: string } = { accountID, user, title: flags.title } + if (flags['dry-run']) { + await printDryRun('chats.start', payload as unknown as Record, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.start(payload), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/unarchive.ts b/packages/cli/src/commands/chats/unarchive.ts index 0da9088a..8df63c14 100644 --- a/packages/cli/src/commands/chats/unarchive.ts +++ b/packages/cli/src/commands/chats/unarchive.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsUnarchive extends BeeperCommand { @@ -14,6 +14,10 @@ export default class ChatsUnarchive extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.unarchive', { chatID, isArchived: false }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.update(chatID, { isArchived: false }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/unmute.ts b/packages/cli/src/commands/chats/unmute.ts index 022ae568..3c7d1ac0 100644 --- a/packages/cli/src/commands/chats/unmute.ts +++ b/packages/cli/src/commands/chats/unmute.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsUnmute extends BeeperCommand { @@ -14,6 +14,10 @@ export default class ChatsUnmute extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.unmute', { chatID, isMuted: false }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.update(chatID, { isMuted: false }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/unpin.ts b/packages/cli/src/commands/chats/unpin.ts index cb4ada38..750f6348 100644 --- a/packages/cli/src/commands/chats/unpin.ts +++ b/packages/cli/src/commands/chats/unpin.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsUnpin extends BeeperCommand { @@ -14,6 +14,10 @@ export default class ChatsUnpin extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.unpin', { chatID, isPinned: false }, flags.json ? 'json' : 'human') + return + } await printData(await client.chats.update(chatID, { isPinned: false }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/chats/unremind.ts b/packages/cli/src/commands/chats/unremind.ts index 437ceca5..15b28aad 100644 --- a/packages/cli/src/commands/chats/unremind.ts +++ b/packages/cli/src/commands/chats/unremind.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsUnremind extends BeeperCommand { @@ -14,6 +14,10 @@ export default class ChatsUnremind extends BeeperCommand { const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('chats.unremind', { chatID }, flags.json ? 'json' : 'human') + return + } await client.chats.reminders.delete(chatID) await printSuccess({ message: 'Reminder cleared', data: { chatID } }, flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/config/reset.ts b/packages/cli/src/commands/config/reset.ts index e5ee7a1a..e3066dad 100644 --- a/packages/cli/src/commands/config/reset.ts +++ b/packages/cli/src/commands/config/reset.ts @@ -1,6 +1,6 @@ import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { resetConfig } from '../../lib/targets.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class ConfigReset extends BeeperCommand { static override summary = 'Reset CLI configuration' @@ -8,6 +8,10 @@ export default class ConfigReset extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(ConfigReset) ensureWritable(flags) + if (flags['dry-run']) { + await printDryRun('config.reset', {}, flags.json ? 'json' : 'human') + return + } await resetConfig() await printSuccess({ message: 'Config reset' }, flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/config/set.ts b/packages/cli/src/commands/config/set.ts index 2bdc2a05..1bc9e5b2 100644 --- a/packages/cli/src/commands/config/set.ts +++ b/packages/cli/src/commands/config/set.ts @@ -1,7 +1,7 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { updateConfig } from '../../lib/targets.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class ConfigSet extends BeeperCommand { static override summary = 'Set a CLI configuration value' @@ -14,6 +14,10 @@ export default class ConfigSet extends BeeperCommand { const { args, flags } = await this.parse(ConfigSet) ensureWritable(flags) const nextValue = args.value === '' ? undefined : args.value + if (flags['dry-run']) { + await printDryRun('config.set', { [args.key]: nextValue }, flags.json ? 'json' : 'human') + return + } await updateConfig(config => ({ ...config, [args.key]: nextValue })) await printSuccess({ message: nextValue === undefined ? `Cleared ${args.key}` : `Set ${args.key}`, diff --git a/packages/cli/src/commands/contacts/search.ts b/packages/cli/src/commands/contacts/search.ts index 7a809f6d..2c9d1d08 100644 --- a/packages/cli/src/commands/contacts/search.ts +++ b/packages/cli/src/commands/contacts/search.ts @@ -2,7 +2,7 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { apiCopy, cliCopy } from '../../lib/copy.js' -import { printList } from '../../lib/output.js' +import { isMachineReadableOutput, printList } from '../../lib/output.js' import { listAccountIDs, resolveAccountIDs } from '../../lib/resolve.js' import { withInkSpinner as withSpinner } from '../../lib/ink/spinner.js' @@ -32,7 +32,7 @@ export default class ContactsSearch extends BeeperCommand { } return collected } - const useSpinner = !flags.json + const useSpinner = !isMachineReadableOutput(flags.json ? 'json' : 'human') const results = useSpinner ? await withSpinner(`Searching contacts for "${args.query}"…`, load, { done: value => `${value.length} match${value.length === 1 ? '' : 'es'} across ${accountIDs.length} account${accountIDs.length === 1 ? '' : 's'}`, diff --git a/packages/cli/src/commands/export.ts b/packages/cli/src/commands/export.ts index 525bc66a..ef7af0ae 100644 --- a/packages/cli/src/commands/export.ts +++ b/packages/cli/src/commands/export.ts @@ -1,7 +1,8 @@ import { Flags } from '@oclif/core' -import { BeeperCommand } from '../lib/command.js' +import { BeeperCommand, ensureWritable } from '../lib/command.js' import { createClient } from '../lib/client.js' import { exportBeeperData } from '../lib/export/index.js' +import { printDryRun } from '../lib/output.js' import { resolveAccountIDs, resolveChatID } from '../lib/resolve.js' export default class Export extends BeeperCommand { @@ -26,11 +27,25 @@ export default class Export extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(Export) + ensureWritable(flags) const client = await createClient(flags) const accountIDs = await resolveAccountIDs(client, flags.account, { allowMultiplePerInput: true }) const chatIDs = flags.chat?.length ? await Promise.all(flags.chat.map(chat => resolveChatID(client, chat, { accountIDs, pick: flags.pick }))) : undefined + if (flags['dry-run']) { + await printDryRun('export', { + accountIDs, + chatIDs, + downloadAttachments: !flags['no-attachments'], + force: flags.force, + limitChats: flags['limit-chats'], + limitMessages: flags['limit-messages'], + maxParticipants: flags['max-participants'], + outDir: flags.out, + }, flags.json ? 'json' : 'human') + return + } const manifest = await exportBeeperData(client, { accountIDs, diff --git a/packages/cli/src/commands/install/desktop.ts b/packages/cli/src/commands/install/desktop.ts index f617e172..37901fb8 100644 --- a/packages/cli/src/commands/install/desktop.ts +++ b/packages/cli/src/commands/install/desktop.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { installDesktop, type InstallChannel } from '../../lib/installations.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class SetupInstallDesktop extends BeeperCommand { static override summary = 'Install Beeper Desktop locally' @@ -12,6 +12,10 @@ export default class SetupInstallDesktop extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(SetupInstallDesktop) ensureWritable(flags) + if (flags['dry-run']) { + await printDryRun('install.desktop', { channel: flags.channel }, flags.json ? 'json' : 'human') + return + } const installation = await installDesktop({ channel: flags.channel as InstallChannel }) await printSuccess({ message: `Installed Beeper Desktop ${installation.version ?? ''}`.trim(), diff --git a/packages/cli/src/commands/install/server.ts b/packages/cli/src/commands/install/server.ts index c612a131..a2493a83 100644 --- a/packages/cli/src/commands/install/server.ts +++ b/packages/cli/src/commands/install/server.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { installServer, type InstallChannel } from '../../lib/installations.js' import { pathSetupHint } from '../../lib/env.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class SetupInstallServer extends BeeperCommand { static override summary = 'Install Beeper Server locally' @@ -14,6 +14,10 @@ export default class SetupInstallServer extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(SetupInstallServer) ensureWritable(flags) + if (flags['dry-run']) { + await printDryRun('install.server', { channel: flags.channel, serverEnv: flags['server-env'] }, flags.json ? 'json' : 'human') + return + } const installation = await installServer({ channel: flags.channel as InstallChannel, serverEnv: flags['server-env'] }) await printSuccess({ message: `Installed Beeper Server ${installation.version ?? ''}`.trim(), diff --git a/packages/cli/src/commands/man.ts b/packages/cli/src/commands/man.ts index 275fa1ee..c8fd9508 100644 --- a/packages/cli/src/commands/man.ts +++ b/packages/cli/src/commands/man.ts @@ -1,4 +1,5 @@ import { BeeperCommand } from '../lib/command.js' +import { metadataForCommand } from '../lib/command-metadata.js' import { commandManifest } from '../lib/manifest.js' import { printCommands } from '../lib/output.js' export default class Man extends BeeperCommand { @@ -17,53 +18,3 @@ export default class Man extends BeeperCommand { await printCommands(commands, flags.json ? 'json' : 'human', { title: 'Beeper CLI' }) } } - -function metadataForCommand(command: string): { - mutates: boolean - requiresAuth: boolean - selectors: string[] - output: 'data' | 'list' | 'stream' | 'success' | 'send-result' | 'manual' - related: string[] -} { - const parts = command.split(' ') - const root = parts[0] ?? '' - const mutatingRoots = new Set(['setup', 'install', 'send', 'update']) - const mutatingVerbs = new Set([ - 'add', 'archive', 'unarchive', 'pin', 'unpin', 'mute', 'unmute', 'mark-read', 'mark-unread', - 'priority', 'notify-anyway', 'rename', 'description', 'avatar', 'draft', 'disappear', 'remind', - 'unremind', 'focus', 'edit', 'delete', 'remove', 'use', 'set', 'reset', 'logout', 'start', 'stop', - 'restart', 'enable', 'disable', 'approve', 'recovery-key', 'reset-recovery-key', 'cancel', 'sas', - 'sas-confirm', 'qr-scan', 'qr-confirm', - ]) - const mutates = mutatingRoots.has(root) || parts.some(part => mutatingVerbs.has(part ?? '')) - const localOnly = root === 'config' || root === 'completion' || root === 'docs' || root === 'version' || root === 'man' - const requiresAuth = !localOnly && command !== 'targets list' && !command.startsWith('targets add') && !command.startsWith('install ') - const selectors = [ - command.includes('chats ') || command.includes('messages ') || command.startsWith('send ') || command === 'presence' ? 'chat' : undefined, - command.includes('accounts ') || command.includes('contacts ') || command === 'chats start' ? 'account' : undefined, - command.includes('targets ') || command === 'status' || command === 'doctor' || command.startsWith('auth ') || command.startsWith('verify') ? 'target' : undefined, - command.startsWith('bridges ') || command === 'accounts add' ? 'bridge' : undefined, - command.includes('messages ') || command.startsWith('send react') || command.startsWith('send unreact') ? 'message' : undefined, - ].filter((value): value is string => Boolean(value)) - const output = command.startsWith('send ') ? 'send-result' - : command === 'watch' || command === 'rpc' ? 'stream' - : command === 'man' ? 'manual' - : command.endsWith('list') || command.includes('search') || command === 'bridges list' ? 'list' - : mutates ? 'success' - : 'data' - const related = relatedForCommand(command) - return { mutates, requiresAuth, selectors, output, related } -} - -function relatedForCommand(command: string): string[] { - if (command.startsWith('send ')) return ['messages list', 'watch'] - if (command.startsWith('messages ')) return ['chats list', 'send text'] - if (command.startsWith('chats ')) return ['messages list', 'send text'] - if (command.startsWith('bridges ')) return ['accounts add', 'accounts list'] - if (command.startsWith('accounts ')) return ['bridges list', 'chats list'] - if (command.startsWith('targets ')) return ['status', 'doctor'] - if (command === 'status') return ['doctor', 'setup'] - if (command === 'doctor') return ['status', 'setup'] - if (command.startsWith('verify')) return ['setup', 'status'] - return [] -} diff --git a/packages/cli/src/commands/media/download.ts b/packages/cli/src/commands/media/download.ts index 9636d4d7..a6c01001 100644 --- a/packages/cli/src/commands/media/download.ts +++ b/packages/cli/src/commands/media/download.ts @@ -3,7 +3,7 @@ import { mkdir, writeFile } from 'node:fs/promises' import { basename, join } from 'node:path' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class MediaDownload extends BeeperCommand { static override summary = 'Download message media' static override args = { url: Args.string({ required: true, description: 'mxc:// or localmxc:// URL' }) } @@ -12,6 +12,11 @@ export default class MediaDownload extends BeeperCommand { } async run(): Promise { const { args, flags } = await this.parse(MediaDownload) + if (flags['dry-run'] && flags.out !== '-') { + ensureWritable(flags) + await printDryRun('media.download', { url: args.url, out: flags.out }, flags.json ? 'json' : 'human') + return + } const client = await createClient(flags) const response = await client.assets.serve({ url: args.url }) const buffer = Buffer.from(await response.arrayBuffer()) diff --git a/packages/cli/src/commands/messages/delete.ts b/packages/cli/src/commands/messages/delete.ts index 4488cdac..ac011a6f 100644 --- a/packages/cli/src/commands/messages/delete.ts +++ b/packages/cli/src/commands/messages/delete.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class MessagesDelete extends BeeperCommand { @@ -17,6 +17,10 @@ export default class MessagesDelete extends BeeperCommand { ensureWritable(flags) const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('messages.delete', { chatID, messageID: flags.id, forEveryone: flags['for-everyone'] }, flags.json ? 'json' : 'human') + return + } await client.messages.delete(flags.id, { chatID, forEveryone: flags['for-everyone'] || undefined }) await printSuccess({ message: flags['for-everyone'] ? 'Deleted for everyone' : 'Deleted', data: { chatID, messageID: flags.id, forEveryone: flags['for-everyone'] } }, flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/messages/edit.ts b/packages/cli/src/commands/messages/edit.ts index bc5a2cc4..a473017b 100644 --- a/packages/cli/src/commands/messages/edit.ts +++ b/packages/cli/src/commands/messages/edit.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class MessagesEdit extends BeeperCommand { @@ -17,6 +17,10 @@ export default class MessagesEdit extends BeeperCommand { ensureWritable(flags) const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('messages.edit', { chatID, messageID: flags.id, text: flags.message }, flags.json ? 'json' : 'human') + return + } await printData(await client.messages.update(flags.id, { chatID, text: flags.message }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/messages/export.ts b/packages/cli/src/commands/messages/export.ts index 74080e8f..9615fd16 100644 --- a/packages/cli/src/commands/messages/export.ts +++ b/packages/cli/src/commands/messages/export.ts @@ -1,7 +1,8 @@ import { writeFile } from 'node:fs/promises' import { Flags } from '@oclif/core' -import { BeeperCommand } from '../../lib/command.js' +import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' +import { printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class MessagesExport extends BeeperCommand { @@ -22,8 +23,22 @@ export default class MessagesExport extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(MessagesExport) if (flags['before-cursor'] && flags['after-cursor']) throw new Error('Use only one of --before-cursor or --after-cursor') + if (flags.output !== '-') ensureWritable(flags) const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('messages.export', { + chatID, + output: flags.output, + beforeCursor: flags['before-cursor'], + afterCursor: flags['after-cursor'], + after: flags.after, + before: flags.before, + limit: flags.limit, + asc: flags.asc, + }, flags.json ? 'json' : 'human') + return + } const cursor = flags['before-cursor'] ?? flags['after-cursor'] const direction = flags['before-cursor'] ? 'before' : flags['after-cursor'] ? 'after' : undefined const afterTs = flags.after ? Date.parse(flags.after) : undefined diff --git a/packages/cli/src/commands/messages/search.ts b/packages/cli/src/commands/messages/search.ts index d88cd8f6..fe82ed54 100644 --- a/packages/cli/src/commands/messages/search.ts +++ b/packages/cli/src/commands/messages/search.ts @@ -2,7 +2,7 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { usageError } from '../../lib/errors.js' -import { collectPage, printIDs, printList } from '../../lib/output.js' +import { collectPage, isMachineReadableOutput, printIDs, printList } from '../../lib/output.js' import { resolveAccountIDs, resolveChatID } from '../../lib/resolve.js' import { withInkSpinner as withSpinner } from '../../lib/ink/spinner.js' @@ -58,7 +58,7 @@ export default class MessagesSearch extends BeeperCommand { query: args.query, sender: flags.sender as 'me' | 'others' | (string & {}) | undefined, } - const useSpinner = !flags.json && !flags.ids + const useSpinner = !isMachineReadableOutput(flags.ids ? 'ids' : flags.json ? 'json' : 'human') const label = args.query ? `Searching messages for "${args.query}"…` : 'Searching messages…' const items = useSpinner ? await withSpinner(label, () => collectPage(client.messages.search(params), flags.limit), { diff --git a/packages/cli/src/commands/presence.ts b/packages/cli/src/commands/presence.ts index ffcc3df5..e914148f 100644 --- a/packages/cli/src/commands/presence.ts +++ b/packages/cli/src/commands/presence.ts @@ -2,7 +2,7 @@ import { setTimeout as delay } from 'node:timers/promises' import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../lib/command.js' import { createClient } from '../lib/client.js' -import { printSuccess } from '../lib/output.js' +import { printDryRun, printSuccess } from '../lib/output.js' import { resolveChatID } from '../lib/resolve.js' export default class Presence extends BeeperCommand { @@ -22,6 +22,10 @@ export default class Presence extends BeeperCommand { if (flags.duration !== undefined && flags.state !== 'typing') throw new Error('--duration only applies when --state is typing') const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) + if (flags['dry-run']) { + await printDryRun('presence', { chatID, state: flags.state, durationSeconds: flags.duration }, flags.json ? 'json' : 'human') + return + } const post = (state: 'typing' | 'paused') => client.post(`/v1/chats/${encodeURIComponent(chatID)}/typing`, { body: { state } }) diff --git a/packages/cli/src/commands/resolve/account.ts b/packages/cli/src/commands/resolve/account.ts new file mode 100644 index 00000000..a62ce1b9 --- /dev/null +++ b/packages/cli/src/commands/resolve/account.ts @@ -0,0 +1,46 @@ +import { Args, Flags } from '@oclif/core' +import { BeeperCommand } from '../../lib/command.js' +import { createClient } from '../../lib/client.js' +import { notFound } from '../../lib/errors.js' +import { printData } from '../../lib/output.js' +import { resolveAccountIDs } from '../../lib/resolve.js' + +export default class ResolveAccount extends BeeperCommand { + static override summary = 'Resolve an account selector' + static override args = { + selector: Args.string({ required: true, description: 'Account ID, network, bridge, or account user selector' }), + } + static override flags = { + pick: Flags.integer({ description: 'Select the Nth candidate (1-indexed)' }), + } + + async run(): Promise { + const { args, flags } = await this.parse(ResolveAccount) + const client = await createClient(flags) + const response = await client.accounts.list() + const rows = Array.isArray(response) ? response : ((response as any).items ?? []) + const ids = await resolveAccountIDs(client, [args.selector], { allowMultiplePerInput: true, applyDefault: false }) + const candidates = rows.filter((row: any) => ids?.includes(String(row.accountID ?? row.id))) + if (!candidates.length) throw notFound(`No account matches "${args.selector}"`, { selector: args.selector, kind: 'account' }) + const selected = flags.pick ? candidates[flags.pick - 1] : candidates.length === 1 ? candidates[0] : undefined + if (flags.pick && !selected) throw notFound(`--pick ${flags.pick} is outside the ${candidates.length} matching accounts`, { selector: args.selector, pick: flags.pick, count: candidates.length }) + await printData({ + selector: args.selector, + kind: 'account', + selected: selected ? accountCandidate(selected, candidates.indexOf(selected) + 1) : null, + candidates: candidates.map((account: any, index: number) => accountCandidate(account, index + 1)), + }, flags.json ? 'json' : 'human') + } +} + +function accountCandidate(account: any, pick: number): Record { + return { + pick, + id: account.accountID ?? account.id, + accountID: account.accountID, + network: account.network, + bridge: account.bridge, + user: account.user, + raw: account, + } +} diff --git a/packages/cli/src/commands/resolve/bridge.ts b/packages/cli/src/commands/resolve/bridge.ts new file mode 100644 index 00000000..eb0a42f6 --- /dev/null +++ b/packages/cli/src/commands/resolve/bridge.ts @@ -0,0 +1,49 @@ +import { Args, Flags } from '@oclif/core' +import { BeeperCommand } from '../../lib/command.js' +import { createClient } from '../../lib/client.js' +import { notFound } from '../../lib/errors.js' +import { printData } from '../../lib/output.js' + +export default class ResolveBridge extends BeeperCommand { + static override summary = 'Resolve a bridge selector' + static override args = { + selector: Args.string({ required: true, description: 'Bridge ID, type, provider, or display name' }), + } + static override flags = { + pick: Flags.integer({ description: 'Select the Nth candidate (1-indexed)' }), + } + + async run(): Promise { + const { args, flags } = await this.parse(ResolveBridge) + const client = await createClient(flags) + const response = await client.bridges.list() + const rows = ((response as unknown as { items?: Array> }).items ?? []) + const normalized = normalize(args.selector) + const candidates = rows.filter(bridge => + normalize(bridge.id) === normalized || + normalize(bridge.type) === normalized || + normalize(bridge.provider) === normalized || + normalize(bridge.name) === normalized || + normalize(bridge.displayName) === normalized || + normalize(bridge.id).includes(normalized) || + normalize(bridge.displayName).includes(normalized) + ) + if (!candidates.length) throw notFound(`No bridge matches "${args.selector}"`, { selector: args.selector, kind: 'bridge' }) + const selected = flags.pick ? candidates[flags.pick - 1] : candidates.length === 1 ? candidates[0] : undefined + if (flags.pick && !selected) throw notFound(`--pick ${flags.pick} is outside the ${candidates.length} matching bridges`, { selector: args.selector, pick: flags.pick, count: candidates.length }) + await printData({ + selector: args.selector, + kind: 'bridge', + selected: selected ? bridgeCandidate(selected, candidates.indexOf(selected) + 1) : null, + candidates: candidates.map((bridge, index) => bridgeCandidate(bridge, index + 1)), + }, flags.json ? 'json' : 'human') + } +} + +function bridgeCandidate(bridge: Record, pick: number): Record { + return { pick, id: bridge.id, type: bridge.type, provider: bridge.provider, displayName: bridge.displayName ?? bridge.name, status: bridge.status, raw: bridge } +} + +function normalize(value: unknown): string { + return String(value ?? '').trim().toLowerCase().replace(/[\s._-]+/g, '') +} diff --git a/packages/cli/src/commands/resolve/chat.ts b/packages/cli/src/commands/resolve/chat.ts new file mode 100644 index 00000000..82240f61 --- /dev/null +++ b/packages/cli/src/commands/resolve/chat.ts @@ -0,0 +1,68 @@ +import { Args, Flags } from '@oclif/core' +import { BeeperCommand } from '../../lib/command.js' +import { createClient } from '../../lib/client.js' +import { notFound } from '../../lib/errors.js' +import { printData } from '../../lib/output.js' +import { resolveAccountIDs } from '../../lib/resolve.js' + +export default class ResolveChat extends BeeperCommand { + static override summary = 'Resolve a chat selector to concrete chat candidates' + static override args = { + selector: Args.string({ required: true, description: 'Chat ID, local ID, exact title, or search text' }), + } + static override flags = { + account: Flags.string({ multiple: true, description: 'Limit to account selector. Repeat for multiple.' }), + pick: Flags.integer({ description: 'Select the Nth candidate (1-indexed)' }), + limit: Flags.integer({ default: 10, description: 'Maximum candidates to return' }), + } + + async run(): Promise { + const { args, flags } = await this.parse(ResolveChat) + const client = await createClient(flags) + const accountIDs = await resolveAccountIDs(client, flags.account, { allowMultiplePerInput: true }) + const candidates = await collect(client.chats.search({ accountIDs, query: args.selector, scope: 'titles' }), flags.limit) + const normalized = normalize(args.selector) + const exact = candidates.filter(chat => + normalize(chat.id) === normalized || + normalize(chat.localChatID) === normalized || + normalize(chat.title) === normalized + ) + const matches = exact.length ? exact : candidates + if (!matches.length) throw notFound(`No chat matches "${args.selector}"`, { selector: args.selector, kind: 'chat' }) + const selected = flags.pick ? matches[flags.pick - 1] : matches.length === 1 ? matches[0] : undefined + if (flags.pick && !selected) throw notFound(`--pick ${flags.pick} is outside the ${matches.length} matching chats`, { selector: args.selector, pick: flags.pick, count: matches.length }) + await printData({ + selector: args.selector, + kind: 'chat', + selected: selected ? chatCandidate(selected, matches.indexOf(selected) + 1) : null, + candidates: matches.map((chat, index) => chatCandidate(chat, index + 1)), + }, flags.json ? 'json' : 'human') + } +} + +type Chat = Record + +async function collect(iterable: AsyncIterable, limit: number): Promise { + const items: Chat[] = [] + for await (const item of iterable) { + items.push(item as Chat) + if (items.length >= limit) break + } + return items +} + +function chatCandidate(chat: Chat, pick: number): Record { + return { + pick, + id: chat.id, + localChatID: chat.localChatID, + title: chat.title, + network: chat.network, + accountID: chat.accountID, + raw: chat, + } +} + +function normalize(value: unknown): string { + return String(value ?? '').trim().toLowerCase().replace(/[\s._-]+/g, '') +} diff --git a/packages/cli/src/commands/resolve/contact.ts b/packages/cli/src/commands/resolve/contact.ts new file mode 100644 index 00000000..f85f1bd4 --- /dev/null +++ b/packages/cli/src/commands/resolve/contact.ts @@ -0,0 +1,55 @@ +import { Args, Flags } from '@oclif/core' +import { BeeperCommand } from '../../lib/command.js' +import { createClient } from '../../lib/client.js' +import { notFound } from '../../lib/errors.js' +import { printData } from '../../lib/output.js' +import { listAccountIDs, resolveAccountIDs } from '../../lib/resolve.js' + +export default class ResolveContact extends BeeperCommand { + static override summary = 'Resolve a contact selector' + static override args = { + selector: Args.string({ required: true, description: 'Contact name, username, phone, email, or ID' }), + } + static override flags = { + account: Flags.string({ multiple: true, description: 'Limit to account selector. Repeat for multiple.' }), + pick: Flags.integer({ description: 'Select the Nth candidate (1-indexed)' }), + limit: Flags.integer({ default: 10, description: 'Maximum candidates to return per account' }), + } + + async run(): Promise { + const { args, flags } = await this.parse(ResolveContact) + const client = await createClient(flags) + const accountIDs = await resolveAccountIDs(client, flags.account, { allowMultiplePerInput: true }) ?? await listAccountIDs(client) + const candidates: Array> = [] + for (const accountID of accountIDs) { + try { + const result = await client.accounts.contacts.search(accountID, { query: args.selector }) + candidates.push(...result.items.slice(0, flags.limit).map((item: unknown) => ({ ...(item as Record), accountID }))) + } catch { + // Keep searching accounts that support lookup for this selector shape. + } + } + if (!candidates.length) throw notFound(`No contact matches "${args.selector}"`, { selector: args.selector, kind: 'contact' }) + const selected = flags.pick ? candidates[flags.pick - 1] : candidates.length === 1 ? candidates[0] : undefined + if (flags.pick && !selected) throw notFound(`--pick ${flags.pick} is outside the ${candidates.length} matching contacts`, { selector: args.selector, pick: flags.pick, count: candidates.length }) + await printData({ + selector: args.selector, + kind: 'contact', + selected: selected ? contactCandidate(selected, candidates.indexOf(selected) + 1) : null, + candidates: candidates.map((contact, index) => contactCandidate(contact, index + 1)), + }, flags.json ? 'json' : 'human') + } +} + +function contactCandidate(contact: Record, pick: number): Record { + return { + pick, + id: contact.id, + accountID: contact.accountID, + displayName: contact.displayName ?? contact.fullName ?? contact.name, + username: contact.username, + phoneNumber: contact.phoneNumber, + email: contact.email, + raw: contact, + } +} diff --git a/packages/cli/src/commands/resolve/target.ts b/packages/cli/src/commands/resolve/target.ts new file mode 100644 index 00000000..a6b8b325 --- /dev/null +++ b/packages/cli/src/commands/resolve/target.ts @@ -0,0 +1,52 @@ +import { Args, Flags } from '@oclif/core' +import { BeeperCommand } from '../../lib/command.js' +import { notFound } from '../../lib/errors.js' +import { printData } from '../../lib/output.js' +import { builtInDesktopTargetID, listTargets, readConfig, type Target } from '../../lib/targets.js' + +export default class ResolveTarget extends BeeperCommand { + static override summary = 'Resolve a target selector' + static override args = { + selector: Args.string({ required: true, description: 'Target name, ID, type, or base URL' }), + } + static override flags = { + pick: Flags.integer({ description: 'Select the Nth candidate (1-indexed)' }), + } + + async run(): Promise { + const { args, flags } = await this.parse(ResolveTarget) + const config = await readConfig() + const builtIn: Target = { + id: builtInDesktopTargetID, + type: 'desktop', + name: 'Beeper Desktop', + baseURL: process.env.BEEPER_DESKTOP_BASE_URL || config.baseURL || 'http://127.0.0.1:23373', + auth: config.auth, + } + const targets = [builtIn, ...await listTargets()] + const normalized = normalize(args.selector) + const candidates = targets.filter(target => + normalize(target.id) === normalized || + normalize(target.name) === normalized || + normalize(target.type) === normalized || + normalize(target.baseURL).includes(normalized) + ) + if (!candidates.length) throw notFound(`No target matches "${args.selector}"`, { selector: args.selector, kind: 'target' }) + const selected = flags.pick ? candidates[flags.pick - 1] : candidates.length === 1 ? candidates[0] : undefined + if (flags.pick && !selected) throw notFound(`--pick ${flags.pick} is outside the ${candidates.length} matching targets`, { selector: args.selector, pick: flags.pick, count: candidates.length }) + await printData({ + selector: args.selector, + kind: 'target', + selected: selected ? targetCandidate(selected, candidates.indexOf(selected) + 1) : null, + candidates: candidates.map((target, index) => targetCandidate(target, index + 1)), + }, flags.json ? 'json' : 'human') + } +} + +function targetCandidate(target: Target, pick: number): Record { + return { pick, id: target.id, name: target.name, type: target.type, baseURL: target.baseURL, managed: target.managed, raw: target } +} + +function normalize(value: unknown): string { + return String(value ?? '').trim().toLowerCase().replace(/[\s._-]+/g, '') +} diff --git a/packages/cli/src/commands/schema.ts b/packages/cli/src/commands/schema.ts new file mode 100644 index 00000000..94e872c0 --- /dev/null +++ b/packages/cli/src/commands/schema.ts @@ -0,0 +1,125 @@ +import { Args } from '@oclif/core' +import { BeeperCommand } from '../lib/command.js' +import { metadataForCommand } from '../lib/command-metadata.js' +import { commandManifest } from '../lib/manifest.js' +import { printData } from '../lib/output.js' + +type RawCommand = { + id: string + aliases?: string[] + args?: Record + description?: string + flags?: Record + hidden?: boolean + pluginName?: string + summary?: string +} + +export default class Schema extends BeeperCommand { + static override summary = 'Print machine-readable command/flag schema' + static override description = 'Agent-first schema for commands, flags, args, examples, mutation metadata, selectors, output shapes, and related commands.' + static override args = { + command: Args.string({ required: false, description: 'Optional command path, such as "messages search"', multiple: true }), + } + + async run(): Promise { + const { args } = await this.parse(Schema) + const requested = Array.isArray(args.command) ? args.command.join(' ') : undefined + const manifestByCommand = new Map(commandManifest.map(item => [item.command, item])) + const commands = (this.config.commands as RawCommand[]) + .filter(command => !command.hidden) + .map(command => { + const path = command.id.replaceAll(':', ' ') + const manifest = manifestByCommand.get(path) + const metadata = metadataForCommand(path) + return { + path, + id: command.id, + aliases: (command.aliases ?? []).map(alias => alias.replaceAll(':', ' ')), + summary: command.summary ?? manifest?.description ?? command.description ?? '', + description: command.description ?? manifest?.description ?? command.summary ?? '', + examples: manifest?.examples ?? [], + args: normalizeFields(command.args), + flags: normalizeFields(command.flags), + ...metadata, + supports: { + dryRun: metadata.mutates, + force: metadata.mutates, + format: true, + noInput: true, + readOnly: true, + select: true, + }, + outputShape: outputShape(metadata.output), + } + }) + .sort((a, b) => a.path.localeCompare(b.path)) + + const filtered = requested + ? commands.filter(command => command.path === requested || command.path.startsWith(`${requested} `)) + : commands + + await printData({ + schemaVersion: 1, + bin: this.config.bin, + version: this.config.version, + defaults: { + stdout: 'primary command output only', + stderr: 'diagnostics, progress, events, and structured errors', + nonTTYFormat: 'json', + ttyFormat: 'table', + }, + formats: ['json', 'jsonl', 'table', 'text', 'ids'], + exitCodes: { + 0: 'success', + 1: 'generic runtime error', + 2: 'usage error', + 3: 'auth required', + 4: 'target/account not ready', + 5: 'selector matched nothing', + 6: 'ambiguous selector', + 127: 'declined did-you-mean suggestion', + }, + commands: filtered, + }, 'json') + } +} + +function normalizeFields(fields: Record | undefined): Array> { + if (!fields) return [] + return Object.entries(fields).map(([name, raw]) => normalizeField(name, raw)) +} + +function normalizeField(name: string, raw: unknown): Record { + const record = raw && typeof raw === 'object' ? raw as Record : {} + return { + name, + description: record.description ?? record.summary ?? '', + required: Boolean(record.required), + multiple: Boolean(record.multiple), + default: record.default, + options: record.options, + char: record.char, + type: typeName(record), + } +} + +function typeName(record: Record): string { + if (Array.isArray(record.options)) return 'enum' + if (record.type === 'boolean' || record.type === 'option') return String(record.type) + if (typeof record.parse === 'function') return 'string' + if (typeof record.default === 'boolean') return 'boolean' + if (typeof record.default === 'number') return 'integer' + return 'string' +} + +function outputShape(kind: string): Record { + const envelope = { ok: true, data: '', error: null, meta: '' } + switch (kind) { + case 'list': return { kind, envelope, data: 'array' } + case 'stream': return { kind, data: 'jsonl events or RPC lines' } + case 'success': return { kind, envelope, data: { message: 'string', detail: 'string?', entity: 'object?' } } + case 'send-result': return { kind, envelope, data: { chatID: 'string', pendingMessageID: 'string?', state: 'string?' } } + default: return { kind, envelope, data: 'object' } + } +} diff --git a/packages/cli/src/commands/send/file.ts b/packages/cli/src/commands/send/file.ts index 67ed8ae6..d021ccb8 100644 --- a/packages/cli/src/commands/send/file.ts +++ b/packages/cli/src/commands/send/file.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' import { sendMessage } from '../../lib/send-message.js' @@ -29,6 +29,11 @@ export default class SendFile extends BeeperCommand { ensureWritable(flags) const client = await createClient(flags) const chatID = await resolveChatID(client, flags.to, { pick: flags.pick }) - await printData(await sendMessage(client, { chatID, file: flags.file, fileName: flags.filename, mimeType: flags.mime, replyTo: flags['reply-to'], text: flags.caption || '', wait: flags.wait, waitTimeoutMs: flags['wait-timeout'] }), flags.json ? 'json' : 'human') + const request = { chatID, file: flags.file, fileName: flags.filename, mimeType: flags.mime, replyTo: flags['reply-to'], text: flags.caption || '', wait: flags.wait, waitTimeoutMs: flags['wait-timeout'] } + if (flags['dry-run']) { + await printDryRun('send.file', request, flags.json ? 'json' : 'human') + return + } + await printData(await sendMessage(client, request), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/send/react.ts b/packages/cli/src/commands/send/react.ts index af3fde14..c4d5cea5 100644 --- a/packages/cli/src/commands/send/react.ts +++ b/packages/cli/src/commands/send/react.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class SendReact extends BeeperCommand { @@ -18,6 +18,11 @@ export default class SendReact extends BeeperCommand { ensureWritable(flags) const client = await createClient(flags) const chatID = await resolveChatID(client, flags.to, { pick: flags.pick }) + const request = { chatID, messageID: flags.id, reactionKey: flags.reaction, transactionID: flags.transaction } + if (flags['dry-run']) { + await printDryRun('send.react', request, flags.json ? 'json' : 'human') + return + } await printData( await client.chats.messages.reactions.add(flags.id, { chatID, reactionKey: flags.reaction, transactionID: flags.transaction }), flags.json ? 'json' : 'human', diff --git a/packages/cli/src/commands/send/sticker.ts b/packages/cli/src/commands/send/sticker.ts index 3fb53bb2..284c8de0 100644 --- a/packages/cli/src/commands/send/sticker.ts +++ b/packages/cli/src/commands/send/sticker.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' import { sendMessage } from '../../lib/send-message.js' @@ -23,18 +23,23 @@ export default class SendSticker extends BeeperCommand { ensureWritable(flags) const client = await createClient(flags) const chatID = await resolveChatID(client, flags.to, { pick: flags.pick }) + const request = { + chatID, + file: flags.file, + fileName: flags.filename, + mimeType: flags.mime, + replyTo: flags['reply-to'], + text: '', + attachmentType: 'sticker' as const, + wait: flags.wait, + waitTimeoutMs: flags['wait-timeout'], + } + if (flags['dry-run']) { + await printDryRun('send.sticker', request, flags.json ? 'json' : 'human') + return + } await printData( - await sendMessage(client, { - chatID, - file: flags.file, - fileName: flags.filename, - mimeType: flags.mime, - replyTo: flags['reply-to'], - text: '', - attachmentType: 'sticker', - wait: flags.wait, - waitTimeoutMs: flags['wait-timeout'], - }), + await sendMessage(client, request), flags.json ? 'json' : 'human', ) } diff --git a/packages/cli/src/commands/send/text.ts b/packages/cli/src/commands/send/text.ts index 42fdacfe..d5bffad8 100644 --- a/packages/cli/src/commands/send/text.ts +++ b/packages/cli/src/commands/send/text.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' import { sendMessage } from '../../lib/send-message.js' @@ -28,6 +28,11 @@ export default class SendText extends BeeperCommand { ensureWritable(flags) const client = await createClient(flags) const chatID = await resolveChatID(client, flags.to, { pick: flags.pick }) - await printData(await sendMessage(client, { chatID, text: flags.message, replyTo: flags['reply-to'], mentions: flags.mention, noPreview: flags['no-preview'], wait: flags.wait, waitTimeoutMs: flags['wait-timeout'] }), flags.json ? 'json' : 'human') + const request = { chatID, text: flags.message, replyTo: flags['reply-to'], mentions: flags.mention, noPreview: flags['no-preview'], wait: flags.wait, waitTimeoutMs: flags['wait-timeout'] } + if (flags['dry-run']) { + await printDryRun('send.text', request, flags.json ? 'json' : 'human') + return + } + await printData(await sendMessage(client, request), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/send/unreact.ts b/packages/cli/src/commands/send/unreact.ts index b49bf7c9..c1494ea6 100644 --- a/packages/cli/src/commands/send/unreact.ts +++ b/packages/cli/src/commands/send/unreact.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class SendUnreact extends BeeperCommand { @@ -19,6 +19,11 @@ export default class SendUnreact extends BeeperCommand { ensureWritable(flags) const client = await createClient(flags) const chatID = await resolveChatID(client, flags.to, { pick: flags.pick }) + const request = { chatID, messageID: flags.id, reactionKey: flags.reaction } + if (flags['dry-run']) { + await printDryRun('send.unreact', request, flags.json ? 'json' : 'human') + return + } await printData( await client.chats.messages.reactions.delete(flags.reaction, { chatID, messageID: flags.id }), flags.json ? 'json' : 'human', diff --git a/packages/cli/src/commands/send/voice.ts b/packages/cli/src/commands/send/voice.ts index 5c831b72..3682957a 100644 --- a/packages/cli/src/commands/send/voice.ts +++ b/packages/cli/src/commands/send/voice.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' import { sendMessage } from '../../lib/send-message.js' @@ -24,19 +24,24 @@ export default class SendVoice extends BeeperCommand { ensureWritable(flags) const client = await createClient(flags) const chatID = await resolveChatID(client, flags.to, { pick: flags.pick }) + const request = { + chatID, + file: flags.file, + fileName: flags.filename, + mimeType: flags.mime, + replyTo: flags['reply-to'], + text: '', + attachmentType: 'voice-note' as const, + duration: flags.duration, + wait: flags.wait, + waitTimeoutMs: flags['wait-timeout'], + } + if (flags['dry-run']) { + await printDryRun('send.voice', request, flags.json ? 'json' : 'human') + return + } await printData( - await sendMessage(client, { - chatID, - file: flags.file, - fileName: flags.filename, - mimeType: flags.mime, - replyTo: flags['reply-to'], - text: '', - attachmentType: 'voice-note', - duration: flags.duration, - wait: flags.wait, - waitTimeoutMs: flags['wait-timeout'], - }), + await sendMessage(client, request), flags.json ? 'json' : 'human', ) } diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index 96c06787..f9a8c864 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -23,7 +23,7 @@ import { type AuthSource, type Target, } from '../lib/targets.js' -import { printData, printSuccess } from '../lib/output.js' +import { printData, printDryRun, printSuccess } from '../lib/output.js' export default class Setup extends BeeperCommand { static override summary = 'Make the selected target ready for messaging' @@ -58,6 +58,22 @@ export default class Setup extends BeeperCommand { if ((flags.local || flags.oauth) && (flags.remote || flags.server || flags.desktop)) { throw new Error('Use --local or --oauth with an existing target, not with --remote, --server, or --desktop.') } + if (flags['dry-run']) { + await printDryRun('setup', { + target: flags.target, + baseURL: flags['base-url'], + targetMode: flags.remote ? 'remote' : flags.server ? 'server' : flags.desktop ? 'desktop' : 'selected', + authMode: flags.local ? 'local' : flags.oauth ? 'oauth' : flags.email ? 'email' : 'auto', + remote: flags.remote, + install: flags.install, + channel: flags.channel, + serverEnv: flags['server-env'], + email: flags.email, + username: flags.username, + yes: flags.yes, + }, flags.json ? 'json' : 'human') + return + } if (flags.events) writeEvent('setup_step', { step: 'start', target: flags.target }) if (flags.remote) { diff --git a/packages/cli/src/commands/targets/add/desktop.ts b/packages/cli/src/commands/targets/add/desktop.ts index 34af8af4..06d1e4c2 100644 --- a/packages/cli/src/commands/targets/add/desktop.ts +++ b/packages/cli/src/commands/targets/add/desktop.ts @@ -1,7 +1,7 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../../lib/command.js' import { createProfileTarget, readTarget, updateConfig } from '../../../lib/targets.js' -import { printSuccess } from '../../../lib/output.js' +import { printDryRun, printSuccess } from '../../../lib/output.js' export default class TargetsAddDesktop extends BeeperCommand { static override summary = 'Add a managed Beeper Desktop target' @@ -16,6 +16,10 @@ export default class TargetsAddDesktop extends BeeperCommand { ensureWritable(flags) const id = args.name ?? 'desktop' if (await readTarget(id)) throw new Error(`Target "${id}" already exists.`) + if (flags['dry-run']) { + await printDryRun('targets.add.desktop', { id, type: 'desktop', serverEnv: flags['server-env'], port: flags.port, default: flags.default }, flags.json ? 'json' : 'human') + return + } const target = await createProfileTarget('desktop', id, { serverEnv: flags['server-env'], port: flags.port }) if (flags.default) await updateConfig(config => ({ ...config, defaultTarget: target.id })) await printSuccess({ message: `Added target: ${target.id}`, detail: target.baseURL, data: target }, flags.json ? 'json' : 'human') diff --git a/packages/cli/src/commands/targets/add/remote.ts b/packages/cli/src/commands/targets/add/remote.ts index 1036872f..6c21ddcb 100644 --- a/packages/cli/src/commands/targets/add/remote.ts +++ b/packages/cli/src/commands/targets/add/remote.ts @@ -1,7 +1,7 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../../lib/command.js' import { readTarget, updateConfig, writeTarget, type Target } from '../../../lib/targets.js' -import { printSuccess } from '../../../lib/output.js' +import { printDryRun, printSuccess } from '../../../lib/output.js' export default class TargetsAddRemote extends BeeperCommand { static override summary = 'Add a remote Beeper Desktop or Server target' @@ -17,6 +17,10 @@ export default class TargetsAddRemote extends BeeperCommand { ensureWritable(flags) if (await readTarget(args.name)) throw new Error(`Target "${args.name}" already exists.`) const target: Target = { id: args.name, name: args.name, type: 'remote', baseURL: args.url, managed: false } + if (flags['dry-run']) { + await printDryRun('targets.add.remote', { target, default: flags.default }, flags.json ? 'json' : 'human') + return + } await writeTarget(target) if (flags.default) await updateConfig(config => ({ ...config, defaultTarget: target.id })) await printSuccess({ message: `Added target: ${target.id}`, detail: target.baseURL, data: target }, flags.json ? 'json' : 'human') diff --git a/packages/cli/src/commands/targets/add/server.ts b/packages/cli/src/commands/targets/add/server.ts index b5b10aa2..756125c4 100644 --- a/packages/cli/src/commands/targets/add/server.ts +++ b/packages/cli/src/commands/targets/add/server.ts @@ -1,7 +1,7 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../../lib/command.js' import { createProfileTarget, readTarget, updateConfig } from '../../../lib/targets.js' -import { printSuccess } from '../../../lib/output.js' +import { printDryRun, printSuccess } from '../../../lib/output.js' export default class TargetsAddServer extends BeeperCommand { static override summary = 'Add a managed Beeper Server target' @@ -16,6 +16,10 @@ export default class TargetsAddServer extends BeeperCommand { ensureWritable(flags) const id = args.name ?? 'server' if (await readTarget(id)) throw new Error(`Target "${id}" already exists.`) + if (flags['dry-run']) { + await printDryRun('targets.add.server', { id, type: 'server', serverEnv: flags['server-env'], port: flags.port, default: flags.default }, flags.json ? 'json' : 'human') + return + } const target = await createProfileTarget('server', id, { serverEnv: flags['server-env'], port: flags.port }) if (flags.default) await updateConfig(config => ({ ...config, defaultTarget: target.id })) await printSuccess({ message: `Added target: ${target.id}`, detail: target.baseURL, data: target }, flags.json ? 'json' : 'human') diff --git a/packages/cli/src/commands/targets/disable.ts b/packages/cli/src/commands/targets/disable.ts index e0aa652e..80f61a5a 100644 --- a/packages/cli/src/commands/targets/disable.ts +++ b/packages/cli/src/commands/targets/disable.ts @@ -2,7 +2,7 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { readTarget, resolveTarget } from '../../lib/targets.js' import { assertServerProfile, disableProfile } from '../../lib/profiles.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class TargetsDisable extends BeeperCommand { static override summary = 'Disable a local Beeper Server target at login' @@ -13,6 +13,10 @@ export default class TargetsDisable extends BeeperCommand { const target = await resolveTarget({ target: args.name ?? flags.target, baseURL: flags['base-url'] }) if (!target) throw new Error(`Unknown Beeper target "${args.name}".`) assertServerProfile(target) + if (flags['dry-run']) { + await printDryRun('targets.disable', { target }, flags.json ? 'json' : 'human') + return + } const path = await disableProfile(target) await printSuccess({ message: `Disabled target at login: ${target.id}`, detail: path, data: { target, path } }, flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/targets/enable.ts b/packages/cli/src/commands/targets/enable.ts index a709806d..468f747a 100644 --- a/packages/cli/src/commands/targets/enable.ts +++ b/packages/cli/src/commands/targets/enable.ts @@ -2,7 +2,7 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { readTarget, resolveTarget } from '../../lib/targets.js' import { assertServerProfile, enableProfile } from '../../lib/profiles.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class TargetsEnable extends BeeperCommand { static override summary = 'Enable a local Beeper Server target at login' @@ -13,6 +13,10 @@ export default class TargetsEnable extends BeeperCommand { const target = await resolveTarget({ target: args.name ?? flags.target, baseURL: flags['base-url'] }) if (!target) throw new Error(`Unknown Beeper target "${args.name}".`) assertServerProfile(target) + if (flags['dry-run']) { + await printDryRun('targets.enable', { target }, flags.json ? 'json' : 'human') + return + } const path = await enableProfile(target) await printSuccess({ message: `Enabled target at login: ${target.id}`, detail: path, data: { target, path } }, flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/targets/remove.ts b/packages/cli/src/commands/targets/remove.ts index 099ae16f..5aa2c45b 100644 --- a/packages/cli/src/commands/targets/remove.ts +++ b/packages/cli/src/commands/targets/remove.ts @@ -4,7 +4,7 @@ import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createProfileTarget, listTargets, readConfig, readTarget, removeTarget, resolveTarget, updateConfig, writeTarget, type Target } from '../../lib/targets.js' import { disableProfile, enableProfile, profileErrorLogPath, profileLogPath, profileStatus, startProfile, stopProfile } from '../../lib/profiles.js' import { targetLiveStatus } from '../../lib/target-status.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' export default class TargetsRemove extends BeeperCommand { static override summary = 'Remove a target' @@ -12,6 +12,10 @@ export default class TargetsRemove extends BeeperCommand { async run(): Promise { const { args, flags } = await this.parse(TargetsRemove) ensureWritable(flags) + if (flags['dry-run']) { + await printDryRun('targets.remove', { id: args.name }, flags.json ? 'json' : 'human') + return + } await removeTarget(args.name) await printSuccess({ message: `Removed target: ${args.name}`, data: { id: args.name } }, flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/targets/restart.ts b/packages/cli/src/commands/targets/restart.ts index 414e004b..a97ccea5 100644 --- a/packages/cli/src/commands/targets/restart.ts +++ b/packages/cli/src/commands/targets/restart.ts @@ -2,7 +2,7 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { readTarget, resolveTarget } from '../../lib/targets.js' import { assertServerProfile, startProfile, stopProfile } from '../../lib/profiles.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class TargetsRestart extends BeeperCommand { static override summary = 'Restart a local Beeper Server target' @@ -13,6 +13,10 @@ export default class TargetsRestart extends BeeperCommand { const target = await resolveTarget({ target: args.name ?? flags.target, baseURL: flags['base-url'] }) if (!target) throw new Error(`Unknown Beeper target "${args.name}".`) assertServerProfile(target) + if (flags['dry-run']) { + await printDryRun('targets.restart', { target }, flags.json ? 'json' : 'human') + return + } await stopProfile(target).catch(() => undefined) const result = await startProfile(target) await printSuccess({ message: `Restarted target: ${target.id}`, detail: target.baseURL, data: { target, result } }, flags.json ? 'json' : 'human') diff --git a/packages/cli/src/commands/targets/start.ts b/packages/cli/src/commands/targets/start.ts index e51ca9b8..c9cd6a5e 100644 --- a/packages/cli/src/commands/targets/start.ts +++ b/packages/cli/src/commands/targets/start.ts @@ -2,7 +2,7 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { customTargetID, readTarget, resolveTarget } from '../../lib/targets.js' import { launchDesktopApp, startProfile } from '../../lib/profiles.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class TargetsStart extends BeeperCommand { static override summary = 'Start a local Server target or open Beeper Desktop' @@ -13,6 +13,10 @@ export default class TargetsStart extends BeeperCommand { const target = await resolveTarget({ target: args.name ?? flags.target, baseURL: flags['base-url'] }) if (!target) throw new Error(`Unknown Beeper target "${args.name}".`) if (target.type === 'desktop' && target.id !== customTargetID) { + if (flags['dry-run']) { + await printDryRun('targets.start', { target, launchDesktop: true }, flags.json ? 'json' : 'human') + return + } const result = await launchDesktopApp(target.managed ? target : undefined) await printSuccess({ message: 'Opened Beeper Desktop', detail: target.baseURL, data: { target, result } }, flags.json ? 'json' : 'human') return @@ -20,6 +24,10 @@ export default class TargetsStart extends BeeperCommand { if (!target.managed || target.type !== 'server') { throw new Error(`Target "${target.id}" is not a local Beeper Server install.`) } + if (flags['dry-run']) { + await printDryRun('targets.start', { target, startProfile: true }, flags.json ? 'json' : 'human') + return + } const result = await startProfile(target) await printSuccess({ message: `Started target: ${target.id}`, detail: target.baseURL, data: { target, result } }, flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/targets/stop.ts b/packages/cli/src/commands/targets/stop.ts index badd49cc..65444ad0 100644 --- a/packages/cli/src/commands/targets/stop.ts +++ b/packages/cli/src/commands/targets/stop.ts @@ -2,7 +2,7 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { readTarget, resolveTarget } from '../../lib/targets.js' import { assertServerProfile, stopProfile } from '../../lib/profiles.js' -import { printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class TargetsStop extends BeeperCommand { static override summary = 'Stop a local Beeper Server target' @@ -13,6 +13,10 @@ export default class TargetsStop extends BeeperCommand { const target = await resolveTarget({ target: args.name ?? flags.target, baseURL: flags['base-url'] }) if (!target) throw new Error(`Unknown Beeper target "${args.name}".`) assertServerProfile(target) + if (flags['dry-run']) { + await printDryRun('targets.stop', { target }, flags.json ? 'json' : 'human') + return + } await stopProfile(target) await printSuccess({ message: `Stopped target: ${target.id}`, data: { target } }, flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/targets/use.ts b/packages/cli/src/commands/targets/use.ts index cf46e5a3..107a03ea 100644 --- a/packages/cli/src/commands/targets/use.ts +++ b/packages/cli/src/commands/targets/use.ts @@ -4,7 +4,7 @@ import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createProfileTarget, listTargets, readConfig, readTarget, removeTarget, resolveTarget, updateConfig, writeTarget, type Target } from '../../lib/targets.js' import { disableProfile, enableProfile, profileErrorLogPath, profileLogPath, profileStatus, startProfile, stopProfile } from '../../lib/profiles.js' import { targetLiveStatus } from '../../lib/target-status.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData, printDryRun, printSuccess } from '../../lib/output.js' export default class TargetsUse extends BeeperCommand { static override summary = 'Set the default target' @@ -14,6 +14,10 @@ export default class TargetsUse extends BeeperCommand { ensureWritable(flags) const target = await readTarget(args.name) if (!target) throw new Error(`Unknown Beeper target "${args.name}". Run \`beeper targets list\`.`) + if (flags['dry-run']) { + await printDryRun('targets.use', { defaultTarget: target.id, target }, flags.json ? 'json' : 'human') + return + } await updateConfig(config => ({ ...config, defaultTarget: target.id })) await printSuccess({ message: `Using target: ${target.id}`, detail: target.baseURL, data: target }, flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/update.ts b/packages/cli/src/commands/update.ts index 587fda05..5f613f3b 100644 --- a/packages/cli/src/commands/update.ts +++ b/packages/cli/src/commands/update.ts @@ -9,7 +9,7 @@ import { import { profileStatus, startProfile, stopProfile } from '../lib/profiles.js' import { listTargets } from '../lib/targets.js' import { pathSetupHint } from '../lib/env.js' -import { printData } from '../lib/output.js' +import { printData, printDryRun } from '../lib/output.js' import pkg from '../../package.json' with { type: 'json' } export default class Update extends BeeperCommand { @@ -25,6 +25,10 @@ export default class Update extends BeeperCommand { const { flags } = await this.parse(Update) if (!flags.check) ensureWritable(flags) const selected = flags.cli || flags.desktop || flags.server + if (flags['dry-run'] && !flags.check) { + await printDryRun('update', { cli: !selected || flags.cli, desktop: !selected || flags.desktop, server: !selected || flags.server }, flags.json ? 'json' : 'human') + return + } const installations = await readInstallations() const results: Array> = [] diff --git a/packages/cli/src/commands/verify.ts b/packages/cli/src/commands/verify.ts index a95b8ab1..a8e3dd78 100644 --- a/packages/cli/src/commands/verify.ts +++ b/packages/cli/src/commands/verify.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../lib/command.js' import { driveVerification } from '../lib/app-state.js' -import { printData } from '../lib/output.js' +import { printData, printDryRun } from '../lib/output.js' export default class AuthVerify extends BeeperCommand { static override summary = 'Finish setup verification or verify another device' static override flags = { @@ -10,6 +10,10 @@ export default class AuthVerify extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(AuthVerify) ensureWritable(flags) + if (flags['dry-run']) { + await printDryRun('verify', { baseURL: flags['base-url'], target: flags.target, userID: flags.user, yes: flags.yes }, flags.json ? 'json' : 'human') + return + } await printData(await driveVerification({ baseURL: flags['base-url'], target: flags.target, userID: flags.user, yes: flags.yes }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/verify/approve.ts b/packages/cli/src/commands/verify/approve.ts index 489f8bc4..caf4c6e5 100644 --- a/packages/cli/src/commands/verify/approve.ts +++ b/packages/cli/src/commands/verify/approve.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' export default class AuthVerifyApprove extends BeeperCommand { static override summary = 'Approve a pending device verification request' static override flags = { @@ -11,6 +11,10 @@ export default class AuthVerifyApprove extends BeeperCommand { const { flags } = await this.parse(AuthVerifyApprove) ensureWritable(flags) const client = await createClient(flags) + if (flags['dry-run']) { + await printDryRun('verify.approve', { id: flags.id ?? 'active' }, flags.json ? 'json' : 'human') + return + } await printData(await client.app.verifications.accept(flags.id ?? 'active'), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/verify/cancel.ts b/packages/cli/src/commands/verify/cancel.ts index f30ddd33..ce8df653 100644 --- a/packages/cli/src/commands/verify/cancel.ts +++ b/packages/cli/src/commands/verify/cancel.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' export default class AuthVerifyCancel extends BeeperCommand { static override summary = 'Cancel an in-progress device verification' static override flags = { @@ -11,6 +11,10 @@ export default class AuthVerifyCancel extends BeeperCommand { const { flags } = await this.parse(AuthVerifyCancel) ensureWritable(flags) const client = await createClient(flags) + if (flags['dry-run']) { + await printDryRun('verify.cancel', { id: flags.id ?? 'active' }, flags.json ? 'json' : 'human') + return + } await printData(await client.app.verifications.cancel(flags.id ?? 'active', {}), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/verify/qr-confirm.ts b/packages/cli/src/commands/verify/qr-confirm.ts index 0cb190e0..662f4e63 100644 --- a/packages/cli/src/commands/verify/qr-confirm.ts +++ b/packages/cli/src/commands/verify/qr-confirm.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' export default class AuthVerifyQrConfirm extends BeeperCommand { static override summary = 'Confirm that the other device scanned your QR code' static override flags = { @@ -11,6 +11,10 @@ export default class AuthVerifyQrConfirm extends BeeperCommand { const { flags } = await this.parse(AuthVerifyQrConfirm) ensureWritable(flags) const client = await createClient(flags) + if (flags['dry-run']) { + await printDryRun('verify.qr-confirm', { id: flags.id ?? 'active' }, flags.json ? 'json' : 'human') + return + } await printData(await client.app.verifications.qr.confirmScanned(flags.id ?? 'active'), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/verify/qr-scan.ts b/packages/cli/src/commands/verify/qr-scan.ts index baf39624..553b57c8 100644 --- a/packages/cli/src/commands/verify/qr-scan.ts +++ b/packages/cli/src/commands/verify/qr-scan.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' export default class AuthVerifyQrScan extends BeeperCommand { static override summary = 'Submit a scanned QR-code verification payload' static override flags = { @@ -12,6 +12,10 @@ export default class AuthVerifyQrScan extends BeeperCommand { const { flags } = await this.parse(AuthVerifyQrScan) ensureWritable(flags) const client = await createClient(flags) + if (flags['dry-run']) { + await printDryRun('verify.qr-scan', { id: flags.id ?? 'active', payload: flags.payload }, flags.json ? 'json' : 'human') + return + } await printData(await client.app.verifications.qr.scan({ data: flags.payload }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/verify/recovery-key.ts b/packages/cli/src/commands/verify/recovery-key.ts index 1c13b696..b2ef5e3d 100644 --- a/packages/cli/src/commands/verify/recovery-key.ts +++ b/packages/cli/src/commands/verify/recovery-key.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' export default class AuthVerifyRecoveryKey extends BeeperCommand { static override summary = 'Unlock encrypted messages with a recovery key' static override flags = { @@ -11,6 +11,10 @@ export default class AuthVerifyRecoveryKey extends BeeperCommand { const { flags } = await this.parse(AuthVerifyRecoveryKey) ensureWritable(flags) const client = await createClient(flags) + if (flags['dry-run']) { + await printDryRun('verify.recovery-key', { keyProvided: true }, flags.json ? 'json' : 'human') + return + } await printData(await client.app.login.verification.recoveryKey.verify({ recoveryKey: flags.key }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/verify/reset-recovery-key.ts b/packages/cli/src/commands/verify/reset-recovery-key.ts index f2676d98..bc891026 100644 --- a/packages/cli/src/commands/verify/reset-recovery-key.ts +++ b/packages/cli/src/commands/verify/reset-recovery-key.ts @@ -1,6 +1,6 @@ import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { promptYesNoDefaultYes } from '../../lib/app-api.js' export default class AuthVerifyResetRecoveryKey extends BeeperCommand { @@ -10,6 +10,10 @@ export default class AuthVerifyResetRecoveryKey extends BeeperCommand { const { flags } = await this.parse(AuthVerifyResetRecoveryKey) ensureWritable(flags) const client = await createClient(flags) + if (flags['dry-run']) { + await printDryRun('verify.reset-recovery-key', { confirmWithYes: flags.yes }, flags.json ? 'json' : 'human') + return + } const reset = await client.app.login.verification.recoveryKey.reset.create({}) if ((flags.json || !process.stdin.isTTY) && !flags.yes) { diff --git a/packages/cli/src/commands/verify/sas-confirm.ts b/packages/cli/src/commands/verify/sas-confirm.ts index dbd618b0..472b1298 100644 --- a/packages/cli/src/commands/verify/sas-confirm.ts +++ b/packages/cli/src/commands/verify/sas-confirm.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' export default class AuthVerifySasConfirm extends BeeperCommand { static override summary = 'Confirm matching emoji verification' static override flags = { @@ -11,6 +11,10 @@ export default class AuthVerifySasConfirm extends BeeperCommand { const { flags } = await this.parse(AuthVerifySasConfirm) ensureWritable(flags) const client = await createClient(flags) + if (flags['dry-run']) { + await printDryRun('verify.sas-confirm', { id: flags.id ?? 'active' }, flags.json ? 'json' : 'human') + return + } await printData(await client.app.verifications.sas.confirm(flags.id ?? 'active'), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/verify/sas.ts b/packages/cli/src/commands/verify/sas.ts index d184102e..f116f6c6 100644 --- a/packages/cli/src/commands/verify/sas.ts +++ b/packages/cli/src/commands/verify/sas.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' export default class AuthVerifySas extends BeeperCommand { static override summary = 'Start emoji verification' static override flags = { @@ -11,6 +11,10 @@ export default class AuthVerifySas extends BeeperCommand { const { flags } = await this.parse(AuthVerifySas) ensureWritable(flags) const client = await createClient(flags) + if (flags['dry-run']) { + await printDryRun('verify.sas', { id: flags.id ?? 'active' }, flags.json ? 'json' : 'human') + return + } await printData(await client.app.verifications.sas.start(flags.id ?? 'active'), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/verify/start.ts b/packages/cli/src/commands/verify/start.ts index 58f58871..66594609 100644 --- a/packages/cli/src/commands/verify/start.ts +++ b/packages/cli/src/commands/verify/start.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' export default class AuthVerifyStart extends BeeperCommand { static override summary = 'Start a device verification request' static override flags = { @@ -11,6 +11,10 @@ export default class AuthVerifyStart extends BeeperCommand { const { flags } = await this.parse(AuthVerifyStart) ensureWritable(flags) const client = await createClient(flags) + if (flags['dry-run']) { + await printDryRun('verify.start', { userID: flags.user }, flags.json ? 'json' : 'human') + return + } await printData(await client.app.verifications.create({ userID: flags.user }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/watch.ts b/packages/cli/src/commands/watch.ts index 38e28d74..e585a010 100644 --- a/packages/cli/src/commands/watch.ts +++ b/packages/cli/src/commands/watch.ts @@ -4,7 +4,7 @@ import WebSocket from 'ws' import { BeeperCommand, writeEvent } from '../lib/command.js' import { requireToken } from '../lib/client.js' import { getBaseURL } from '../lib/targets.js' -import { startStream } from '../lib/output.js' +import { isMachineReadableOutput, startStream } from '../lib/output.js' type WebhookConfig = { url: string; secret?: string; queue: Array<{ body: string; signature?: string }>; inflight: number; max: number } export type EventFilter = { include?: Set; exclude?: Set } @@ -44,7 +44,7 @@ export default class Watch extends BeeperCommand { ? { url: flags.webhook, secret: flags['webhook-secret'], queue: [], inflight: 0, max: flags['webhook-queue'] } : undefined - if (flags.json) { + if (flags.json || isMachineReadableOutput('human')) { await this.runJSON(ws, subscribed, flags.events, webhook, filter) return } diff --git a/packages/cli/src/lib/command-metadata.ts b/packages/cli/src/lib/command-metadata.ts new file mode 100644 index 00000000..69b27dc1 --- /dev/null +++ b/packages/cli/src/lib/command-metadata.ts @@ -0,0 +1,54 @@ +export type CommandMetadata = { + mutates: boolean + requiresAuth: boolean + selectors: string[] + output: 'data' | 'list' | 'stream' | 'success' | 'send-result' | 'manual' | 'schema' + related: string[] +} + +export function metadataForCommand(command: string): CommandMetadata { + const parts = command.split(' ') + const root = parts[0] ?? '' + const mutatingRoots = new Set(['setup', 'install', 'send', 'update', 'export', 'presence']) + const mutatingVerbs = new Set([ + 'add', 'archive', 'unarchive', 'pin', 'unpin', 'mute', 'unmute', 'mark-read', 'mark-unread', + 'priority', 'notify-anyway', 'rename', 'description', 'avatar', 'draft', 'disappear', 'remind', + 'unremind', 'focus', 'edit', 'delete', 'remove', 'use', 'set', 'reset', 'logout', 'start', 'stop', + 'restart', 'enable', 'disable', 'download', 'export', 'post', 'response', 'approve', 'recovery-key', 'reset-recovery-key', 'cancel', 'sas', + 'sas-confirm', 'qr-scan', 'qr-confirm', + ]) + const mutates = command === 'verify' || command === 'api request' || mutatingRoots.has(root) || parts.some(part => mutatingVerbs.has(part ?? '')) + const localOnly = new Set(['config', 'completion', 'docs', 'version', 'man', 'schema']) + const requiresAuth = !localOnly.has(root) && command !== 'targets list' && !command.startsWith('targets add') && !command.startsWith('install ') + const selectors = [ + command.includes('chats ') || command.includes('messages ') || command.startsWith('send ') || command === 'presence' || command.startsWith('resolve chat') ? 'chat' : undefined, + command.includes('accounts ') || command.includes('contacts ') || command === 'chats start' || command.startsWith('resolve account') || command.startsWith('resolve contact') ? 'account' : undefined, + command.includes('targets ') || command === 'status' || command === 'doctor' || command.startsWith('auth ') || command.startsWith('verify') || command.startsWith('resolve target') ? 'target' : undefined, + command.startsWith('bridges ') || command === 'accounts add' || command.startsWith('resolve bridge') ? 'bridge' : undefined, + command.includes('messages ') || command.startsWith('send react') || command.startsWith('send unreact') ? 'message' : undefined, + ].filter((value): value is string => Boolean(value)) + const output = command === 'schema' ? 'schema' + : command.startsWith('send ') ? 'send-result' + : command === 'watch' || command === 'rpc' ? 'stream' + : command === 'man' ? 'manual' + : command.endsWith('list') || command.includes('search') || command === 'bridges list' || command.startsWith('resolve ') ? 'list' + : mutates ? 'success' + : 'data' + const related = relatedForCommand(command) + return { mutates, requiresAuth, selectors, output, related } +} + +function relatedForCommand(command: string): string[] { + if (command.startsWith('send ')) return ['messages list', 'watch'] + if (command.startsWith('messages ')) return ['chats list', 'send text'] + if (command.startsWith('chats ')) return ['messages list', 'send text'] + if (command.startsWith('bridges ')) return ['accounts add', 'accounts list'] + if (command.startsWith('accounts ')) return ['bridges list', 'chats list'] + if (command.startsWith('targets ')) return ['status', 'doctor'] + if (command.startsWith('resolve ')) return ['chats search', 'accounts list', 'targets list', 'bridges list'] + if (command === 'status') return ['doctor', 'setup'] + if (command === 'doctor') return ['status', 'setup'] + if (command === 'schema') return ['man'] + if (command.startsWith('verify')) return ['setup', 'status'] + return [] +} diff --git a/packages/cli/src/lib/command.ts b/packages/cli/src/lib/command.ts index a5cecd4b..08bb4f37 100644 --- a/packages/cli/src/lib/command.ts +++ b/packages/cli/src/lib/command.ts @@ -6,13 +6,19 @@ export abstract class BeeperCommand extends Command { 'base-url': Flags.string({ description: 'Beeper Desktop API base URL (overrides --target)' }), target: Flags.string({ char: 't', description: 'Named Beeper target to use for this command' }), debug: Flags.boolean({ default: false, description: 'Print SDK debug logging on stderr' }), + 'dry-run': Flags.boolean({ default: false, description: 'Do not make changes; print intended actions when supported' }), events: Flags.boolean({ default: false, description: 'Emit NDJSON lifecycle events on stderr (long-running commands)' }), + force: Flags.boolean({ char: 'f', default: false, description: 'Skip confirmations for destructive commands' }), + format: Flags.string({ options: ['json', 'jsonl', 'table', 'text', 'ids'], description: 'Output format. Defaults to json for agents/non-TTY, table for TTY.' }), full: Flags.boolean({ default: false, description: 'Disable text-output truncation; print full IDs and bodies' }), - json: Flags.boolean({ default: false, description: 'Print machine-readable JSON envelope on stdout' }), + json: Flags.boolean({ default: false, description: 'Alias for --format json' }), + 'no-input': Flags.boolean({ default: false, description: 'Never prompt; fail instead (useful for agents and CI)' }), quiet: Flags.boolean({ char: 'q', default: false, description: 'Suppress spinners and success lines (errors still print). Honored with or without --json.' }), 'read-only': Flags.boolean({ default: false, description: 'Reject commands that would modify Beeper or local CLI state (or set BEEPER_READONLY=1)' }), + 'results-only': Flags.boolean({ default: false, description: 'In JSON mode, emit only the primary result instead of the envelope' }), + select: Flags.string({ description: 'In JSON/JSONL mode, project comma-separated fields; dot paths supported' }), timeout: Flags.string({ description: 'Maximum time to wait, such as 30s, 2m, or 1h' }), - yes: Flags.boolean({ char: 'y', default: false, description: 'Skip interactive confirmation prompts' }), + yes: Flags.boolean({ char: 'y', default: false, description: 'Alias for --force' }), } public override async init(): Promise { @@ -20,22 +26,38 @@ export abstract class BeeperCommand extends Command { if (this.argv.includes('--quiet') || this.argv.includes('-q')) { process.env.BEEPER_QUIET = '1' } + const format = outputFormatFromArgv(this.argv) + if (format) { + process.env.BEEPER_OUTPUT_FORMAT = format + } else if (this.argv.includes('--json')) { + process.env.BEEPER_OUTPUT_FORMAT = 'json' + } else if (process.env.BEEPER_AGENT === '1' || !process.stdout.isTTY) { + process.env.BEEPER_OUTPUT_FORMAT = 'json' + } + const select = stringFlagFromArgv(this.argv, '--select') + if (select) process.env.BEEPER_OUTPUT_SELECT = select + if (this.argv.includes('--results-only')) process.env.BEEPER_OUTPUT_RESULTS_ONLY = '1' + if (this.argv.includes('--no-input') || process.env.BEEPER_AGENT === '1') process.env.BEEPER_NO_INPUT = '1' + if (this.argv.includes('--force') || this.argv.includes('-f') || this.argv.includes('--yes') || this.argv.includes('-y')) process.env.BEEPER_FORCE = '1' } protected override async catch(error: Error & { exitCode?: number }): Promise { - const code = error instanceof CLIError ? error.exitCode : error.exitCode ?? ExitCodes.Generic - process.exitCode = process.exitCode ?? code const message = error.message || String(error) + const inferredCode = error instanceof CLIError ? error.exitCode : inferExitCode(message) + const code = inferredCode ?? error.exitCode ?? ExitCodes.Generic + process.exitCode = process.exitCode ?? code const tryMessage = error instanceof CLIError ? error.tryMessage : undefined - const isBug = !(error instanceof CLIError) || error instanceof BugError + const isBug = error instanceof BugError || (!(error instanceof CLIError) && inferredCode === undefined) if (this.argv.includes('--events')) { writeEvent('error', { message, exitCode: code, kind: isBug ? 'bug' : 'abort', tryMessage }) return } - if (this.argv.includes('--json')) { - process.stderr.write(`${JSON.stringify({ success: false, data: null, error: message, exitCode: code, kind: isBug ? 'bug' : 'abort', tryMessage })}\n`) + if (isMachineOutput(this.argv)) { + const errorCodeValue = error instanceof CLIError && error.code ? error.code : errorCode(code, isBug) + const data = error instanceof CLIError ? error.data : undefined + process.stderr.write(`${JSON.stringify({ ok: false, data: data ?? null, error: { code: errorCodeValue, message, exitCode: code, kind: isBug ? 'bug' : 'abort', hint: tryMessage } })}\n`) return } @@ -49,6 +71,14 @@ export abstract class BeeperCommand extends Command { } } +function inferExitCode(message: string): number | undefined { + if (/\b401\b|unauthorized|invalid token|auth(?:entication)? required/i.test(message)) return ExitCodes.AuthRequired + if (/\b404\b|not\s+found|unknown .*target|no .*matches/i.test(message)) return ExitCodes.NotFound + if (/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|fetch failed|not reachable|not ready/i.test(message)) return ExitCodes.NotReady + if (/usage|invalid|must provide|required|unknown flag|parse/i.test(message)) return ExitCodes.Usage + return undefined +} + function formatBugPanel(error: Error, version: string): string { const bar = '─'.repeat(60) const stack = error.stack?.split('\n').slice(0, 8).join('\n') ?? error.message @@ -72,6 +102,10 @@ export function ensureWritable(flags: { 'read-only'?: boolean }): void { if (readOnly) throw new CLIError('read-only mode: command would modify Beeper or local CLI state', ExitCodes.Usage) } +export function ensureNotDryRun(flags: { 'dry-run'?: boolean }, action: string): void { + if (flags['dry-run']) throw new CLIError(`dry-run: ${action}`, ExitCodes.Success) +} + export function writeEvent(event: string, data: Record = {}): void { process.stderr.write(`${JSON.stringify({ event, data, ts: Date.now() })}\n`) } @@ -79,3 +113,47 @@ export function writeEvent(event: string, data: Record = {}): v export function isQuiet(): boolean { return process.env.BEEPER_QUIET === '1' } + +export function isNoInput(): boolean { + return process.env.BEEPER_NO_INPUT === '1' +} + +export function isForce(flags?: { force?: boolean; yes?: boolean }): boolean { + return Boolean(flags?.force || flags?.yes || process.env.BEEPER_FORCE === '1') +} + +function outputFormatFromArgv(argv: string[]): string | undefined { + for (let i = 0; i < argv.length; i++) { + const arg = argv[i] + if (arg === '--format') return argv[i + 1] + if (arg?.startsWith('--format=')) return arg.slice('--format='.length) + } + return undefined +} + +function stringFlagFromArgv(argv: string[], name: string): string | undefined { + for (let i = 0; i < argv.length; i++) { + const arg = argv[i] + if (arg === name) return argv[i + 1] + if (arg?.startsWith(`${name}=`)) return arg.slice(name.length + 1) + } + return undefined +} + +function isMachineOutput(argv: string[]): boolean { + const format = outputFormatFromArgv(argv) ?? process.env.BEEPER_OUTPUT_FORMAT + return argv.includes('--json') || format === 'json' || format === 'jsonl' +} + +function errorCode(code: number, isBug: boolean): string { + if (isBug) return 'internal_error' + switch (code) { + case ExitCodes.Usage: return 'usage_error' + case ExitCodes.AuthRequired: return 'auth_required' + case ExitCodes.NotReady: return 'not_ready' + case ExitCodes.NotFound: return 'not_found' + case ExitCodes.Ambiguous: return 'ambiguous_selector' + case ExitCodes.CommandNotFound: return 'command_not_found' + default: return 'runtime_error' + } +} diff --git a/packages/cli/src/lib/errors.ts b/packages/cli/src/lib/errors.ts index ce198333..894c93a4 100644 --- a/packages/cli/src/lib/errors.ts +++ b/packages/cli/src/lib/errors.ts @@ -25,10 +25,14 @@ export type ExitCode = typeof ExitCodes[keyof typeof ExitCodes] export class CLIError extends Error { readonly exitCode: ExitCode readonly tryMessage?: string - constructor(message: string, exitCode: ExitCode, tryMessage?: string) { + readonly code?: string + readonly data?: Record + constructor(message: string, exitCode: ExitCode, tryMessage?: string, options: { code?: string; data?: Record } = {}) { super(message) this.exitCode = exitCode this.tryMessage = tryMessage + this.code = options.code + this.data = options.data this.name = 'CLIError' } } @@ -38,8 +42,8 @@ export class CLIError extends Error { * Renders as a single-line red message. Do not include a stack trace. */ export class AbortError extends CLIError { - constructor(message: string, exitCode: ExitCode = ExitCodes.Generic, tryMessage?: string) { - super(message, exitCode, tryMessage) + constructor(message: string, exitCode: ExitCode = ExitCodes.Generic, tryMessage?: string, options: { code?: string; data?: Record } = {}) { + super(message, exitCode, tryMessage, options) this.name = 'AbortError' } } @@ -50,7 +54,7 @@ export class AbortError extends CLIError { */ export class BugError extends CLIError { constructor(message: string, tryMessage?: string) { - super(message, ExitCodes.Generic, tryMessage) + super(message, ExitCodes.Generic, tryMessage, { code: 'internal_error' }) this.name = 'BugError' } } @@ -58,5 +62,5 @@ export class BugError extends CLIError { export const usageError = (message: string) => new AbortError(message, ExitCodes.Usage) export const authRequired = (message: string) => new AbortError(message, ExitCodes.AuthRequired) export const notReady = (message: string) => new AbortError(message, ExitCodes.NotReady) -export const notFound = (message: string) => new AbortError(message, ExitCodes.NotFound) -export const ambiguous = (message: string) => new AbortError(message, ExitCodes.Ambiguous) +export const notFound = (message: string, data?: Record) => new AbortError(message, ExitCodes.NotFound, undefined, { code: 'not_found', data }) +export const ambiguous = (message: string, data?: Record) => new AbortError(message, ExitCodes.Ambiguous, 'Pass an exact ID or --pick N.', { code: 'ambiguous_selector', data }) diff --git a/packages/cli/src/lib/manifest.ts b/packages/cli/src/lib/manifest.ts index 5b7dfe63..2d2aff33 100644 --- a/packages/cli/src/lib/manifest.ts +++ b/packages/cli/src/lib/manifest.ts @@ -236,7 +236,8 @@ export const commandManifest: ManifestCommand[] = [ examples: [ 'beeper chats list', 'beeper chats list --pinned --limit 50', - 'beeper chats list --unread --no-muted --json', + 'beeper chats list --unread --no-muted --format json', + 'beeper ls --format ids', ], }, { @@ -370,6 +371,7 @@ export const commandManifest: ManifestCommand[] = [ description: 'Search messages across chats', examples: [ 'beeper messages search invoice', + 'beeper search invoice --format jsonl --select id,chatID,text', 'beeper messages search --chat 10313 --sender me --media image', 'beeper messages search "flight" --after 2026-01-01 --before 2026-02-01', ], @@ -407,6 +409,7 @@ export const commandManifest: ManifestCommand[] = [ command: 'send text', description: 'Send a text message', examples: [ + 'beeper send --to 10313 --message "on my way" --dry-run --format json', 'beeper send text --to 10313 --message "on my way"', 'beeper send text --to 8951 --message "hi"', 'beeper send text --to "Family" --message "hi" --pick 1', @@ -464,6 +467,31 @@ export const commandManifest: ManifestCommand[] = [ description: 'Show contact details', examples: ['beeper contacts show "Alice" --account whatsapp'], }, + { + command: 'resolve chat', + description: 'Resolve a chat selector to concrete chat candidates', + examples: ['beeper resolve chat Family --format json', 'beeper resolve chat Family --pick 1 --results-only'], + }, + { + command: 'resolve account', + description: 'Resolve an account selector', + examples: ['beeper resolve account whatsapp --format json'], + }, + { + command: 'resolve contact', + description: 'Resolve a contact selector', + examples: ['beeper resolve contact Alice --account whatsapp --format json'], + }, + { + command: 'resolve target', + description: 'Resolve a target selector', + examples: ['beeper resolve target desktop --format json'], + }, + { + command: 'resolve bridge', + description: 'Resolve a bridge selector', + examples: ['beeper resolve bridge whatsapp --format json'], + }, { command: 'media download', description: 'Download message media', @@ -495,7 +523,16 @@ export const commandManifest: ManifestCommand[] = [ { command: 'man', description: 'Print the command manual', - examples: ['beeper man', 'beeper man --json'], + examples: ['beeper man', 'beeper man --format json', 'beeper man --format ids'], + }, + { + command: 'schema', + description: 'Print machine-readable command/flag schema', + examples: [ + 'beeper schema', + 'beeper schema send --results-only', + 'beeper schema --select commands.path,commands.flags.name --results-only', + ], }, { command: 'doctor', diff --git a/packages/cli/src/lib/output.ts b/packages/cli/src/lib/output.ts index f697d2d9..724bfdea 100644 --- a/packages/cli/src/lib/output.ts +++ b/packages/cli/src/lib/output.ts @@ -1,22 +1,28 @@ import type { StreamController, Suggestion } from './ink/render.js' -export type OutputFormat = 'human' | 'json' | 'jsonl' +export type OutputFormat = 'human' | 'json' | 'jsonl' | 'table' | 'text' | 'ids' type RecordValue = Record const writeJSON = (value: unknown, format: 'json' | 'jsonl'): void => { process.stdout.write(`${JSON.stringify(value, null, format === 'json' ? 2 : 0)}\n`) } -const envelope = (data: unknown) => ({ success: true, data, error: null }) +const envelope = (data: unknown, meta: Record = {}) => ({ ok: true, data, error: null, meta }) const loadInk = () => import('./ink/render.js') export async function printData(value: unknown, format: OutputFormat): Promise { + format = effectiveFormat(format) + if (format === 'ids') { + printIDs(Array.isArray(value) ? value : [value]) + return + } if (format === 'json') { - writeJSON(envelope(value), 'json') + writeJSON(jsonPayload(value), 'json') return } if (format === 'jsonl') { + value = projectJSON(value) if (Array.isArray(value)) { for (const item of value) process.stdout.write(`${JSON.stringify(item)}\n`) return @@ -24,21 +30,39 @@ export async function printData(value: unknown, format: OutputFormat): Promise, format: OutputFormat): Promise { + await printData({ dryRun: true, action, request }, format) +} + export async function printList( value: unknown[], format: OutputFormat, empty: { title: string; subtitle?: string; suggestions?: Suggestion[] }, ): Promise { + format = effectiveFormat(format) + if (format === 'ids') { + printIDs(value) + return + } if (format === 'json') { - writeJSON(envelope(value), 'json') + writeJSON(jsonPayload(value), 'json') return } if (format === 'jsonl') { - for (const item of value) process.stdout.write(`${JSON.stringify(item)}\n`) + const projected = projectJSON(value) + for (const item of Array.isArray(projected) ? projected : [projected]) process.stdout.write(`${JSON.stringify(item)}\n`) + return + } + if (format === 'text') { + printText(value) return } const { renderList } = await loadInk() @@ -73,8 +97,17 @@ export async function printSuccess( opts: { message: string; detail?: string; entity?: unknown; data?: Record }, format: OutputFormat, ): Promise { + format = effectiveFormat(format) if (format === 'json' || format === 'jsonl') { - writeJSON(envelope({ message: opts.message, detail: opts.detail, entity: opts.entity, ...(opts.data ?? {}) }), format) + writeJSON(jsonPayload({ message: opts.message, detail: opts.detail, entity: opts.entity, ...(opts.data ?? {}) }), format) + return + } + if (format === 'ids') { + printIDs([opts.entity ?? opts.data ?? {}]) + return + } + if (format === 'text') { + process.stdout.write(`${opts.message}${opts.detail ? `\t${opts.detail}` : ''}\n`) return } if (process.env.BEEPER_QUIET === '1') return @@ -86,8 +119,9 @@ export async function printFailure( opts: { message: string; detail?: string; data?: Record }, format: OutputFormat, ): Promise { + format = effectiveFormat(format) if (format === 'json' || format === 'jsonl') { - writeJSON({ success: false, data: opts.data ?? null, error: opts.message }, format) + writeJSON({ ok: false, data: opts.data ?? null, error: { message: opts.message, detail: opts.detail } }, format) return } const { renderFailure } = await loadInk() @@ -95,8 +129,17 @@ export async function printFailure( } export async function printConfig(data: Record, format: OutputFormat): Promise { + format = effectiveFormat(format) if (format === 'json' || format === 'jsonl') { - writeJSON(envelope(data), format) + writeJSON(jsonPayload(data), format) + return + } + if (format === 'ids') { + printIDs([data]) + return + } + if (format === 'text') { + printText(data) return } const { renderConfig } = await loadInk() @@ -108,8 +151,17 @@ export async function printCommands( format: OutputFormat, opts?: { title?: string; intro?: string[] }, ): Promise { + format = effectiveFormat(format) if (format === 'json' || format === 'jsonl') { - writeJSON(envelope(items), format) + writeJSON(jsonPayload(items, opts ? { title: opts.title } : {}), format) + return + } + if (format === 'ids') { + for (const item of items) process.stdout.write(`${item.command}\n`) + return + } + if (format === 'text') { + for (const item of items) process.stdout.write(`${item.command}\t${item.description}\n`) return } const { renderCommands } = await loadInk() @@ -122,3 +174,111 @@ export async function startStream(opts: { baseURL: string; subscribed: string[] } export type { Suggestion } from './ink/render.js' + +export function isMachineReadableOutput(format?: OutputFormat): boolean { + const effective = effectiveFormat(format ?? 'human') + return effective === 'json' || effective === 'jsonl' || effective === 'ids' || effective === 'text' +} + +function effectiveFormat(format: OutputFormat): OutputFormat { + const env = process.env.BEEPER_OUTPUT_FORMAT as OutputFormat | undefined + if (env && ['json', 'jsonl', 'table', 'text', 'ids'].includes(env)) return env === 'table' ? 'human' : env + return format === 'table' ? 'human' : format +} + +function jsonPayload(value: unknown, meta: Record = {}): unknown { + const projected = projectJSON(value) + if (process.env.BEEPER_OUTPUT_RESULTS_ONLY === '1') return unwrapPrimary(projected) + return envelope(projected, meta) +} + +function projectJSON(value: unknown): unknown { + const fields = (process.env.BEEPER_OUTPUT_SELECT ?? '') + .split(',') + .map(item => item.trim()) + .filter(Boolean) + let output = value + if (process.env.BEEPER_OUTPUT_RESULTS_ONLY === '1') output = unwrapPrimary(output) + if (!fields.length) return output + return selectFields(output, fields) +} + +function unwrapPrimary(value: unknown): unknown { + if (!value || typeof value !== 'object' || Array.isArray(value)) return value + const record = value as Record + if ('items' in record) return record.items + if ('results' in record) return record.results + if ('data' in record) return record.data + const metaKeys = new Set(['nextCursor', 'nextPageToken', 'cursor', 'hasMore', 'count', 'query']) + const keys = Object.keys(record).filter(key => !metaKeys.has(key)) + if (keys.length === 1) return record[keys[0]!] + return value +} + +function selectFields(value: unknown, fields: string[]): unknown { + if (Array.isArray(value)) return value.map(item => selectFields(item, fields)) + if (!value || typeof value !== 'object') return value + const out: Record = {} + for (const field of fields) { + const selected = selectPath(value, field.split('.')) + if (selected !== undefined) mergeSelected(out, selected) + } + return out +} + +function selectPath(value: unknown, parts: string[]): unknown { + if (!parts.length) return value + if (Array.isArray(value)) { + const items = value.map(item => selectPath(item, parts)).filter(item => item !== undefined) + return items.length ? items : undefined + } + if (!value || typeof value !== 'object') return undefined + const [part, ...rest] = parts + if (!part) return undefined + const child = (value as Record)[part] + if (child === undefined) return undefined + const selected = selectPath(child, rest) + return selected === undefined ? undefined : { [part]: selected } +} + +function mergeSelected(target: Record, selected: unknown): void { + if (!selected || typeof selected !== 'object' || Array.isArray(selected)) return + for (const [key, value] of Object.entries(selected as Record)) { + const current = target[key] + if (Array.isArray(value)) { + const currentItems = Array.isArray(current) ? current : [] + target[key] = value.map((item, index) => { + const base = currentItems[index] + if (item && typeof item === 'object' && !Array.isArray(item) && base && typeof base === 'object' && !Array.isArray(base)) { + return mergeObjects(base as Record, item as Record) + } + return item + }) + } else if (value && typeof value === 'object' && !Array.isArray(value) && current && typeof current === 'object' && !Array.isArray(current)) { + target[key] = mergeObjects(current as Record, value as Record) + } else { + target[key] = value + } + } +} + +function mergeObjects(left: Record, right: Record): Record { + const out = { ...left } + mergeSelected(out, right) + return out +} + +function printText(value: unknown): void { + if (Array.isArray(value)) { + for (const item of value) printText(item) + return + } + if (!value || typeof value !== 'object') { + if (value !== undefined) process.stdout.write(`${String(value)}\n`) + return + } + for (const [key, item] of Object.entries(value as Record)) { + if (item == null) continue + process.stdout.write(`${key}\t${typeof item === 'object' ? JSON.stringify(item) : String(item)}\n`) + } +} diff --git a/packages/cli/src/lib/resolve.ts b/packages/cli/src/lib/resolve.ts index d83eb3b7..bf25940e 100644 --- a/packages/cli/src/lib/resolve.ts +++ b/packages/cli/src/lib/resolve.ts @@ -31,9 +31,13 @@ export async function resolveAccountIDs( const resolved: string[] = [] for (const input of effectiveInputs) { const matches = matchAccounts(accounts, input) - if (matches.length === 0) throw notFound(`No account matches "${input}"`) + if (matches.length === 0) throw notFound(`No account matches "${input}"`, { selector: input, kind: 'account' }) if (matches.length > 1 && !options.allowMultiplePerInput) { - throw ambiguous(formatAmbiguous(`account "${input}"`, matches.map(formatAccount))) + throw ambiguous(formatAmbiguous(`account "${input}"`, matches.map(formatAccount)), { + selector: input, + kind: 'account', + candidates: matches.map((account, index) => ({ pick: index + 1, id: String(account.accountID ?? account.id), label: formatAccount(account), raw: account })), + }) } resolved.push(...matches.map(account => String(account.accountID))) } @@ -43,7 +47,7 @@ export async function resolveAccountIDs( export async function resolveAccountID(client: any, input: string): Promise { const [accountID] = await resolveAccountIDs(client, [input]) ?? [] - if (!accountID) throw notFound(`No account matches "${input}"`) + if (!accountID) throw notFound(`No account matches "${input}"`, { selector: input, kind: 'account' }) return accountID } @@ -73,20 +77,33 @@ export async function resolveChatID(client: any, input: string, options: ChatRes if (matches.length === 0) { const suggestion = await suggestChat(client, input, options) if (suggestion) return suggestion - return input + throw notFound(`No chat matches "${input}"`, { selector: input, kind: 'chat' }) } if (matches.length === 1) return chatInputID(matches[0]!) if (options.pick) { const selected = matches[options.pick - 1] - if (!selected) throw notFound(`--pick ${options.pick} is outside the ${matches.length} matching chats`) + if (!selected) throw notFound(`--pick ${options.pick} is outside the ${matches.length} matching chats`, { selector: input, kind: 'chat', pick: options.pick, count: matches.length }) return chatInputID(selected) } - throw ambiguous(formatAmbiguous(`chat "${input}"`, matches.map(formatChat))) + throw ambiguous(formatAmbiguous(`chat "${input}"`, matches.map(formatChat)), { + selector: input, + kind: 'chat', + candidates: matches.map((chat, index) => ({ + pick: index + 1, + id: String(chat.id), + localChatID: chat.localChatID ? String(chat.localChatID) : undefined, + title: chat.title ? String(chat.title) : undefined, + network: chat.network ? String(chat.network) : undefined, + label: formatChat(chat), + raw: chat, + })), + }) } async function suggestChat(client: any, input: string, options: ChatResolutionOptions): Promise { + if (process.env.BEEPER_NO_INPUT === '1') return undefined let pool: AnyRecord[] try { pool = await collect(client.chats.list({ accountIDs: options.accountIDs, limit: 100 }), 100) diff --git a/packages/cli/test/cli-smoke.ts b/packages/cli/test/cli-smoke.ts index 1dd76d51..58a945ec 100644 --- a/packages/cli/test/cli-smoke.ts +++ b/packages/cli/test/cli-smoke.ts @@ -106,11 +106,17 @@ const expectedCommands = [ 'contacts list', 'contacts search', 'contacts show', + 'resolve chat', + 'resolve account', + 'resolve contact', + 'resolve target', + 'resolve bridge', 'media download', 'export', 'watch', 'rpc', 'man', + 'schema', 'doctor', 'status', 'docs', @@ -176,12 +182,12 @@ assert.match(setupHelp, /--email/, 'setup should expose email setup start') assert.doesNotMatch(setupHelp, /--code|--accept-terms/, 'setup must not accept OTP or terms flags in the first command') const man = JSON.parse(ok('man', '--json')) -assert.equal(man.success, true) +assert.equal(man.ok, true) assert.equal(man.error, null) assert.deepEqual(man.data.map(item => item.command), expectedCommands) const availablePlugins = JSON.parse(ok('plugins', 'available', '--json')) -assert.equal(availablePlugins.success, true) +assert.equal(availablePlugins.ok, true) assert.equal(availablePlugins.data[0].name, '@beeper/cli-plugin-cloudflare') assert.equal(availablePlugins.data[0].status, 'not installed') assert.deepEqual(availablePlugins.data[0].commands, ['targets tunnel']) @@ -199,41 +205,41 @@ rmSync(configDir, { recursive: true, force: true }) let result = run('targets', 'add', 'remote', 'work', 'http://127.0.0.1:23373', '--default', '--json') assert.equal(result.status, 0, result.stderr) let envelope = JSON.parse(result.stdout) -assert.equal(envelope.success, true) +assert.equal(envelope.ok, true) assert.equal(envelope.data.id, 'work') assert.equal(envelope.data.type, 'remote') result = run('targets', 'list', '--json') assert.equal(result.status, 0, result.stderr) envelope = JSON.parse(result.stdout) -assert.equal(envelope.success, true) +assert.equal(envelope.ok, true) assert(envelope.data.some(item => item.id === 'work' && item.default)) result = run('auth', 'status', '--json') assert.equal(result.status, 0, result.stderr) envelope = JSON.parse(result.stdout) -assert.equal(envelope.success, true) +assert.equal(envelope.ok, true) assert.equal(envelope.data.authenticated, false) assert.equal(envelope.data.target, 'work') result = run('send', 'text', '--to', 'family', '--message', 'on my way', '--read-only', '--json') assert.notEqual(result.status, 0) envelope = JSON.parse(result.stderr) -assert.equal(envelope.success, false) -assert.match(envelope.error, /read-only mode/) +assert.equal(envelope.ok, false) +assert.match(envelope.error.message, /read-only mode/) result = run('setup', '--remote', 'http://127.0.0.1:9', '--target', 'email-remote', '--email', 'staging-user-123456@example.invalid', '--json') assert.notEqual(result.status, 0) envelope = JSON.parse(result.stderr) -assert.equal(envelope.success, false) -assert.match(envelope.error, /auth email start/) -assert.doesNotMatch(envelope.error, /--code|OTP/i, 'setup must direct automation to the two-step email commands without accepting OTP itself') +assert.equal(envelope.ok, false) +assert.match(envelope.error.message, /auth email start/) +assert.doesNotMatch(envelope.error.message, /--code|OTP/i, 'setup must direct automation to the two-step email commands without accepting OTP itself') result = run('targets', 'show', 'email-remote', '--json') assert.notEqual(result.status, 0) envelope = JSON.parse(result.stderr) -assert.equal(envelope.success, false) -assert.match(envelope.error, /Unknown Beeper target/) +assert.equal(envelope.ok, false) +assert.match(envelope.error.message, /Unknown Beeper target/) rmSync(configDir, { recursive: true, force: true }) const fakeServerPath = join(configDir, 'bin', 'beeper-server') @@ -256,7 +262,7 @@ writeFileSync(join(configDir, 'installations.json'), `${JSON.stringify({ result = run('setup', '--json') assert.equal(result.status, 0, result.stderr) envelope = JSON.parse(result.stdout) -assert.equal(envelope.success, true) +assert.equal(envelope.ok, true) assert(envelope.data.availableActions.some(action => action.id === 'use-installed-server' && action.command === 'beeper setup --server --yes')) assert(!envelope.data.availableActions.some(action => action.id === 'install-server'), 'setup must not offer to reinstall an already installed Server') @@ -273,7 +279,7 @@ assert.equal(rpcResult.status, 0, rpcResult.stderr) const rpcLine = JSON.parse(rpcResult.stdout) assert.equal(rpcLine.id, 1) assert.equal(rpcLine.ok, true) -assert.match(rpcLine.stdout, /"success": true/) +assert.match(rpcLine.stdout, /"ok": true/) const stagingServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'staging', channel: 'stable', platform: 'darwin', arch: 'arm64' }) assert.equal(stagingServerRequest.channel, 'nightly') @@ -314,6 +320,7 @@ assert.equal(await resolveChatID(fakeClient, '10313'), '10313') assert.equal(await resolveChatID(fakeClient, 'Family Work'), '8951') assert.equal(await resolveChatID(fakeClient, 'fam', { pick: 2 }), '8951') await assert.rejects(() => resolveChatID(fakeClient, 'fam'), /Ambiguous chat/) +await assert.rejects(() => resolveChatID(fakeClient, 'missing'), /No chat matches/) function listCommandFiles(dir) { const output = [] diff --git a/packages/cli/test/messages-search-validation.test.ts b/packages/cli/test/messages-search-validation.test.ts index ad053898..bb4c2117 100644 --- a/packages/cli/test/messages-search-validation.test.ts +++ b/packages/cli/test/messages-search-validation.test.ts @@ -17,9 +17,9 @@ describe('messages search query-or-filter requirement', () => { const result = run('messages', 'search', '--json') expect(result.status).toBe(2) const envelope = JSON.parse(result.stderr) - expect(envelope.success).toBe(false) - expect(envelope.exitCode).toBe(2) - expect(envelope.error).toMatch(/Provide a search query or at least one filter flag/) + expect(envelope.ok).toBe(false) + expect(envelope.error.exitCode).toBe(2) + expect(envelope.error.message).toMatch(/Provide a search query or at least one filter flag/) }) it('accepts a bare query', () => { From b8fa69a5cf5bafee0a53a092bf735378ba908aab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:07:50 +0200 Subject: [PATCH 03/26] wip --- packages/cli/README.md | 8 ++++---- packages/cli/src/commands/install/server.ts | 2 +- packages/cli/src/commands/setup.ts | 2 +- .../cli/src/commands/targets/add/desktop.ts | 2 +- .../cli/src/commands/targets/add/server.ts | 2 +- packages/cli/src/lib/installations.ts | 12 ++++------- packages/cli/test/cli-smoke.ts | 20 +++++++++++++++---- 7 files changed, 28 insertions(+), 20 deletions(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index 3173f930..19ee4216 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -449,7 +449,7 @@ Flags: | `--oauth` | boolean | Authorize the target with browser OAuth/PKCE | | `--remote=` | option | Connect to a remote Beeper Desktop or Server URL | | `--server` | boolean | Set up a local Beeper Server target | -| `--server-env=` | option | Server environment. Staging forces nightly. Default: production | +| `--server-env=` | option | Server feed environment Default: production | | `--username=` | option | Username to use if setup creates a new account | Examples: @@ -498,7 +498,7 @@ Flags: | Flag | Type | Description | | --- | --- | --- | | `--channel=` | option | Server release channel Default: stable | -| `--server-env=` | option | Server environment. Staging forces nightly. Default: production | +| `--server-env=` | option | Server feed environment Default: production | Examples: @@ -591,7 +591,7 @@ Flags: | --- | --- | --- | | `--default` | boolean | Set this target as the default after creation | | `--port=` | option | TCP port the managed Desktop will expose its API on | -| `--server-env=` | option | Server environment. Staging forces nightly. Default: production | +| `--server-env=` | option | Server feed environment Default: production | Examples: @@ -620,7 +620,7 @@ Flags: | --- | --- | --- | | `--default` | boolean | Set this target as the default after creation | | `--port=` | option | TCP port the managed Server will expose its API on | -| `--server-env=` | option | Server environment. Staging forces nightly. Default: production | +| `--server-env=` | option | Server feed environment Default: production | Examples: diff --git a/packages/cli/src/commands/install/server.ts b/packages/cli/src/commands/install/server.ts index a2493a83..c109f711 100644 --- a/packages/cli/src/commands/install/server.ts +++ b/packages/cli/src/commands/install/server.ts @@ -8,7 +8,7 @@ export default class SetupInstallServer extends BeeperCommand { static override summary = 'Install Beeper Server locally' static override flags = { channel: Flags.string({ options: ['stable', 'nightly'], default: 'stable', description: 'Server release channel' }), - 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server environment. Staging forces nightly.' }), + 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server feed environment' }), } async run(): Promise { diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index f9a8c864..a0508bdf 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -35,7 +35,7 @@ export default class Setup extends BeeperCommand { desktop: Flags.boolean({ default: false, description: 'Set up a local Beeper Desktop target' }), install: Flags.boolean({ default: false, description: 'Allow installing missing managed runtime' }), channel: Flags.string({ options: ['stable', 'nightly'], default: 'stable', description: 'Install release channel' }), - 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server environment. Staging forces nightly.' }), + 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server feed environment' }), email: Flags.string({ description: 'Sign in with an email address' }), username: Flags.string({ description: 'Username to use if setup creates a new account' }), } diff --git a/packages/cli/src/commands/targets/add/desktop.ts b/packages/cli/src/commands/targets/add/desktop.ts index 06d1e4c2..931f8c2f 100644 --- a/packages/cli/src/commands/targets/add/desktop.ts +++ b/packages/cli/src/commands/targets/add/desktop.ts @@ -9,7 +9,7 @@ export default class TargetsAddDesktop extends BeeperCommand { static override flags = { port: Flags.integer({ description: 'TCP port the managed Desktop will expose its API on' }), default: Flags.boolean({ default: false, description: 'Set this target as the default after creation' }), - 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server environment. Staging forces nightly.' }), + 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server feed environment' }), } async run(): Promise { const { args, flags } = await this.parse(TargetsAddDesktop) diff --git a/packages/cli/src/commands/targets/add/server.ts b/packages/cli/src/commands/targets/add/server.ts index 756125c4..435a76f2 100644 --- a/packages/cli/src/commands/targets/add/server.ts +++ b/packages/cli/src/commands/targets/add/server.ts @@ -9,7 +9,7 @@ export default class TargetsAddServer extends BeeperCommand { static override flags = { port: Flags.integer({ description: 'TCP port the managed Server will expose its API on' }), default: Flags.boolean({ default: false, description: 'Set this target as the default after creation' }), - 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server environment. Staging forces nightly.' }), + 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server feed environment' }), } async run(): Promise { const { args, flags } = await this.parse(TargetsAddServer) diff --git a/packages/cli/src/lib/installations.ts b/packages/cli/src/lib/installations.ts index 19abe049..3ad7338a 100644 --- a/packages/cli/src/lib/installations.ts +++ b/packages/cli/src/lib/installations.ts @@ -87,11 +87,8 @@ export function normalizeInstallRequest(options: { bundleID: string apiBaseURL: string } { - // TODO: switch Server installs back to production once the production download - // endpoint returns a beeper-server artifact instead of the Desktop app bundle. - const serverEnv = options.kind === 'server' ? 'staging' : normalizeServerEnv(options.serverEnv) - let channel = options.channel ?? 'stable' - if (serverEnv === 'staging') channel = 'nightly' + const serverEnv = normalizeServerEnv(options.serverEnv) + const channel = options.channel ?? 'stable' const platform = normalizeDownloadPlatform(options.platform ?? process.platform) const feedPlatform = normalizeFeedPlatform(options.platform ?? process.platform) const arch = normalizeArch(options.arch ?? process.arch) @@ -104,7 +101,7 @@ export function normalizeInstallRequest(options: { feedPlatform, arch, bundleID, - apiBaseURL: options.kind === 'server' || serverEnv === 'staging' ? 'https://api.beeper-staging.com' : 'https://api.beeper.com', + apiBaseURL: serverEnv === 'staging' ? 'https://api.beeper-staging.com' : 'https://api.beeper.com', } } @@ -118,8 +115,7 @@ export function feedURLFor(options: ReturnType): } export function downloadURLFor(options: ReturnType): string { - const channelSegment = options.serverEnv === 'staging' && options.kind === 'server' ? 'stable' : options.channel - return `${options.apiBaseURL}/desktop/download/${options.platform}/${options.arch}/${channelSegment}/${options.bundleID}` + return `${options.apiBaseURL}/desktop/download/${options.platform}/${options.arch}/${options.channel}/${options.bundleID}` } export async function fetchFeed(feedURL: string): Promise { diff --git a/packages/cli/test/cli-smoke.ts b/packages/cli/test/cli-smoke.ts index 58a945ec..1f5828d6 100644 --- a/packages/cli/test/cli-smoke.ts +++ b/packages/cli/test/cli-smoke.ts @@ -282,10 +282,22 @@ assert.equal(rpcLine.ok, true) assert.match(rpcLine.stdout, /"ok": true/) const stagingServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'staging', channel: 'stable', platform: 'darwin', arch: 'arm64' }) -assert.equal(stagingServerRequest.channel, 'nightly') -assert.equal(stagingServerRequest.bundleID, 'com.automattic.beeper.server.nightly') -assert.equal(feedURLFor(stagingServerRequest), 'https://api.beeper-staging.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server.nightly&platform=darwin&channel=nightly&arch=arm64') -assert.equal(downloadURLFor(stagingServerRequest), 'https://api.beeper-staging.com/desktop/download/macos/arm64/stable/com.automattic.beeper.server.nightly') +assert.equal(stagingServerRequest.channel, 'stable') +assert.equal(stagingServerRequest.bundleID, 'com.automattic.beeper.server') +assert.equal(feedURLFor(stagingServerRequest), 'https://api.beeper-staging.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=darwin&channel=stable&arch=arm64') +assert.equal(downloadURLFor(stagingServerRequest), 'https://api.beeper-staging.com/desktop/download/macos/arm64/stable/com.automattic.beeper.server') + +const productionServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'production', channel: 'stable', platform: 'darwin', arch: 'arm64' }) +assert.equal(productionServerRequest.channel, 'stable') +assert.equal(productionServerRequest.bundleID, 'com.automattic.beeper.server') +assert.equal(feedURLFor(productionServerRequest), 'https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=darwin&channel=stable&arch=arm64') +assert.equal(downloadURLFor(productionServerRequest), 'https://api.beeper.com/desktop/download/macos/arm64/stable/com.automattic.beeper.server') + +const stagingNightlyServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'staging', channel: 'nightly', platform: 'darwin', arch: 'arm64' }) +assert.equal(stagingNightlyServerRequest.channel, 'nightly') +assert.equal(stagingNightlyServerRequest.bundleID, 'com.automattic.beeper.server.nightly') +assert.equal(feedURLFor(stagingNightlyServerRequest), 'https://api.beeper-staging.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server.nightly&platform=darwin&channel=nightly&arch=arm64') +assert.equal(downloadURLFor(stagingNightlyServerRequest), 'https://api.beeper-staging.com/desktop/download/macos/arm64/nightly/com.automattic.beeper.server.nightly') const desktopNightlyRequest = normalizeInstallRequest({ kind: 'desktop', channel: 'nightly', platform: 'darwin', arch: 'arm64' }) assert.equal(downloadURLFor(desktopNightlyRequest), 'https://api.beeper.com/desktop/download/macos/arm64/nightly/com.automattic.beeper.desktop.nightly') From 0ec1a225a70529fd69a24ef0ea879d96602a4ca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:13:38 +0200 Subject: [PATCH 04/26] wip --- docs/src/content/docs/targets.mdx | 6 ++--- packages/cli/README.md | 10 ++++---- packages/cli/src/commands/install/server.ts | 3 ++- packages/cli/src/commands/setup.ts | 12 ++++++---- .../cli/src/commands/targets/add/desktop.ts | 3 ++- .../cli/src/commands/targets/add/server.ts | 3 ++- packages/cli/src/lib/installations.ts | 14 ++++------- packages/cli/src/lib/manifest.ts | 2 +- packages/cli/src/lib/server-env.ts | 16 +++++++++++++ packages/cli/src/lib/targets.ts | 3 ++- packages/cli/test/cli-smoke.ts | 23 ++++++++++++++----- 11 files changed, 62 insertions(+), 33 deletions(-) create mode 100644 packages/cli/src/lib/server-env.ts diff --git a/docs/src/content/docs/targets.mdx b/docs/src/content/docs/targets.mdx index 5349538d..64c20cc9 100644 --- a/docs/src/content/docs/targets.mdx +++ b/docs/src/content/docs/targets.mdx @@ -18,8 +18,8 @@ commands use it unless `--target ` overrides. ```sh beeper targets list -beeper targets add desktop [name] [--port N] [--server-env production|staging] [--default] -beeper targets add server [name] [--port N] [--server-env production|staging] [--default] +beeper targets add desktop [name] [--port N] [--server-env local|dev|staging|prod] [--default] +beeper targets add server [name] [--port N] [--server-env local|dev|staging|prod] [--default] beeper targets add remote [--default] beeper targets use beeper targets show [name] @@ -48,7 +48,7 @@ beeper targets remove ```sh beeper targets list --json beeper targets add desktop work --default -beeper targets add server prod --server-env production --default +beeper targets add server prod --server-env prod --default beeper targets add remote office https://desktop.office.example.com --default beeper targets use work beeper targets logs work | less diff --git a/packages/cli/README.md b/packages/cli/README.md index 19ee4216..5cc332b9 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -449,7 +449,7 @@ Flags: | `--oauth` | boolean | Authorize the target with browser OAuth/PKCE | | `--remote=` | option | Connect to a remote Beeper Desktop or Server URL | | `--server` | boolean | Set up a local Beeper Server target | -| `--server-env=` | option | Server feed environment Default: production | +| `--server-env=` | option | Server feed environment Default: prod | | `--username=` | option | Username to use if setup creates a new account | Examples: @@ -498,7 +498,7 @@ Flags: | Flag | Type | Description | | --- | --- | --- | | `--channel=` | option | Server release channel Default: stable | -| `--server-env=` | option | Server feed environment Default: production | +| `--server-env=` | option | Server feed environment Default: prod | Examples: @@ -591,7 +591,7 @@ Flags: | --- | --- | --- | | `--default` | boolean | Set this target as the default after creation | | `--port=` | option | TCP port the managed Desktop will expose its API on | -| `--server-env=` | option | Server feed environment Default: production | +| `--server-env=` | option | Server feed environment Default: prod | Examples: @@ -620,12 +620,12 @@ Flags: | --- | --- | --- | | `--default` | boolean | Set this target as the default after creation | | `--port=` | option | TCP port the managed Server will expose its API on | -| `--server-env=` | option | Server feed environment Default: production | +| `--server-env=` | option | Server feed environment Default: prod | Examples: ```sh -beeper targets add server prod --server-env production --default +beeper targets add server prod --server-env prod --default ``` Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. diff --git a/packages/cli/src/commands/install/server.ts b/packages/cli/src/commands/install/server.ts index c109f711..f417165a 100644 --- a/packages/cli/src/commands/install/server.ts +++ b/packages/cli/src/commands/install/server.ts @@ -3,12 +3,13 @@ import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { installServer, type InstallChannel } from '../../lib/installations.js' import { pathSetupHint } from '../../lib/env.js' import { printDryRun, printSuccess } from '../../lib/output.js' +import { SERVER_ENVIRONMENTS } from '../../lib/server-env.js' export default class SetupInstallServer extends BeeperCommand { static override summary = 'Install Beeper Server locally' static override flags = { channel: Flags.string({ options: ['stable', 'nightly'], default: 'stable', description: 'Server release channel' }), - 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server feed environment' }), + 'server-env': Flags.string({ options: SERVER_ENVIRONMENTS, default: 'prod', description: 'Server feed environment' }), } async run(): Promise { diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index a0508bdf..cc2243a7 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -9,6 +9,7 @@ import { loginWithPKCE } from '../lib/oauth.js' import { findDesktopAppPath, launchDesktopApp, startProfile } from '../lib/profiles.js' import { interactiveEmailSetup } from '../lib/setup-login.js' import { renderStartupLogo } from '../lib/logo.js' +import { SERVER_ENVIRONMENTS, SERVER_ENV_API_BASE_URLS, normalizeServerEnv } from '../lib/server-env.js' import { builtInDesktopTargetID, createProfileTarget, @@ -35,7 +36,7 @@ export default class Setup extends BeeperCommand { desktop: Flags.boolean({ default: false, description: 'Set up a local Beeper Desktop target' }), install: Flags.boolean({ default: false, description: 'Allow installing missing managed runtime' }), channel: Flags.string({ options: ['stable', 'nightly'], default: 'stable', description: 'Install release channel' }), - 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server feed environment' }), + 'server-env': Flags.string({ options: SERVER_ENVIRONMENTS, default: 'prod', description: 'Server feed environment' }), email: Flags.string({ description: 'Sign in with an email address' }), username: Flags.string({ description: 'Username to use if setup creates a new account' }), } @@ -285,7 +286,7 @@ export default class Setup extends BeeperCommand { if (choice === '2') { if (!serverInstalled) { if (!await promptYesNoDefaultYes('Install local Beeper Server stable from beeper.com?')) return - await installWithCopy('server', { ...flags, channel: 'stable', 'server-env': 'production' }) + await installWithCopy('server', { ...flags, channel: 'stable', 'server-env': 'prod' }) } await this.setupManaged('server', { ...flags, install: false, server: true, channel: 'stable' }) return @@ -314,7 +315,7 @@ export default class Setup extends BeeperCommand { if (choice === '3') { if (!serverInstalled) { if (!await promptYesNoDefaultYes('Install local Beeper Server stable from beeper.com?')) return true - await installWithCopy('server', { ...flags, channel: 'stable', 'server-env': 'production' }) + await installWithCopy('server', { ...flags, channel: 'stable', 'server-env': 'prod' }) } await this.setupManaged('server', { ...flags, install: false, server: true, channel: 'stable' }) return true @@ -619,8 +620,9 @@ async function maybeDriveOnboarding(result: SetupResult, flags: SetupFlags): Pro async function installWithCopy(type: 'desktop' | 'server', flags: SetupFlags): Promise { const label = type === 'desktop' ? 'Beeper Desktop' : 'local Beeper Server' const channel = flags.channel === 'nightly' ? 'nightly' : 'stable' - const serverEnv = flags['server-env'] === 'staging' ? 'staging' : 'production' - if (!flags.json && process.stdin.isTTY) process.stdout.write(`Installing ${label} ${channel} from beeper.com...\n`) + const serverEnv = normalizeServerEnv(flags['server-env']) + const source = type === 'server' ? new URL(SERVER_ENV_API_BASE_URLS[serverEnv]).host : 'beeper.com' + if (!flags.json && process.stdin.isTTY) process.stdout.write(`Installing ${label} ${channel} from ${source}...\n`) if (type === 'desktop') await installDesktop({ channel, serverEnv }) else await installServer({ channel, serverEnv }) if (!flags.json && process.stdin.isTTY) process.stdout.write(`Installed ${label} ${channel}.\n\n`) diff --git a/packages/cli/src/commands/targets/add/desktop.ts b/packages/cli/src/commands/targets/add/desktop.ts index 931f8c2f..91a8f6cc 100644 --- a/packages/cli/src/commands/targets/add/desktop.ts +++ b/packages/cli/src/commands/targets/add/desktop.ts @@ -2,6 +2,7 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../../lib/command.js' import { createProfileTarget, readTarget, updateConfig } from '../../../lib/targets.js' import { printDryRun, printSuccess } from '../../../lib/output.js' +import { SERVER_ENVIRONMENTS } from '../../../lib/server-env.js' export default class TargetsAddDesktop extends BeeperCommand { static override summary = 'Add a managed Beeper Desktop target' @@ -9,7 +10,7 @@ export default class TargetsAddDesktop extends BeeperCommand { static override flags = { port: Flags.integer({ description: 'TCP port the managed Desktop will expose its API on' }), default: Flags.boolean({ default: false, description: 'Set this target as the default after creation' }), - 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server feed environment' }), + 'server-env': Flags.string({ options: SERVER_ENVIRONMENTS, default: 'prod', description: 'Server feed environment' }), } async run(): Promise { const { args, flags } = await this.parse(TargetsAddDesktop) diff --git a/packages/cli/src/commands/targets/add/server.ts b/packages/cli/src/commands/targets/add/server.ts index 435a76f2..fed575ad 100644 --- a/packages/cli/src/commands/targets/add/server.ts +++ b/packages/cli/src/commands/targets/add/server.ts @@ -2,6 +2,7 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../../lib/command.js' import { createProfileTarget, readTarget, updateConfig } from '../../../lib/targets.js' import { printDryRun, printSuccess } from '../../../lib/output.js' +import { SERVER_ENVIRONMENTS } from '../../../lib/server-env.js' export default class TargetsAddServer extends BeeperCommand { static override summary = 'Add a managed Beeper Server target' @@ -9,7 +10,7 @@ export default class TargetsAddServer extends BeeperCommand { static override flags = { port: Flags.integer({ description: 'TCP port the managed Server will expose its API on' }), default: Flags.boolean({ default: false, description: 'Set this target as the default after creation' }), - 'server-env': Flags.string({ options: ['production', 'staging'], default: 'production', description: 'Server feed environment' }), + 'server-env': Flags.string({ options: SERVER_ENVIRONMENTS, default: 'prod', description: 'Server feed environment' }), } async run(): Promise { const { args, flags } = await this.parse(TargetsAddServer) diff --git a/packages/cli/src/lib/installations.ts b/packages/cli/src/lib/installations.ts index 3ad7338a..8c7a394b 100644 --- a/packages/cli/src/lib/installations.ts +++ b/packages/cli/src/lib/installations.ts @@ -8,12 +8,14 @@ import type { ReadableStream } from 'node:stream/web' import { execFile } from 'node:child_process' import { promisify } from 'node:util' import { beeperDir } from './targets.js' +import { SERVER_ENV_API_BASE_URLS, normalizeServerEnv, type ServerEnv } from './server-env.js' + +export type { ServerEnv } from './server-env.js' const execFileAsync = promisify(execFile) export type InstallKind = 'desktop' | 'server' export type InstallChannel = 'stable' | 'nightly' -export type ServerEnv = 'production' | 'staging' export type Installation = { kind: InstallKind @@ -101,7 +103,7 @@ export function normalizeInstallRequest(options: { feedPlatform, arch, bundleID, - apiBaseURL: serverEnv === 'staging' ? 'https://api.beeper-staging.com' : 'https://api.beeper.com', + apiBaseURL: SERVER_ENV_API_BASE_URLS[serverEnv], } } @@ -146,7 +148,7 @@ export async function checkInstallationUpdate(installation: Installation): Promi export async function installDesktop(options: { channel?: InstallChannel; serverEnv?: string } = {}): Promise { const request = normalizeInstallRequest({ kind: 'desktop', channel: options.channel, serverEnv: options.serverEnv }) - if (request.serverEnv === 'staging') throw new Error('Desktop staging installs are not supported by the CLI.') + if (request.serverEnv !== 'prod') throw new Error('Desktop non-production installs are not supported by the CLI.') const feedURL = feedURLFor(request) const feed = await fetchFeed(feedURL) const downloadURL = feed.url @@ -322,12 +324,6 @@ async function findServerExecutable(dir: string): Promise { throw new Error('Downloaded Beeper Server artifact did not contain a beeper-server executable.') } -function normalizeServerEnv(value?: string): ServerEnv { - if (!value || value === 'production' || value === 'prod') return 'production' - if (value === 'staging') return 'staging' - throw new Error(`Unsupported server env "${value}". Expected production or staging.`) -} - function normalizeDownloadPlatform(platform: NodeJS.Platform): 'macos' | 'windows' | 'linux' { if (platform === 'darwin') return 'macos' if (platform === 'win32') return 'windows' diff --git a/packages/cli/src/lib/manifest.ts b/packages/cli/src/lib/manifest.ts index 2d2aff33..8ae53c47 100644 --- a/packages/cli/src/lib/manifest.ts +++ b/packages/cli/src/lib/manifest.ts @@ -49,7 +49,7 @@ export const commandManifest: ManifestCommand[] = [ { command: 'targets add server', description: 'Add a managed Beeper Server target', - examples: ['beeper targets add server prod --server-env production --default'], + examples: ['beeper targets add server prod --server-env prod --default'], }, { command: 'targets add remote', diff --git a/packages/cli/src/lib/server-env.ts b/packages/cli/src/lib/server-env.ts new file mode 100644 index 00000000..d4a3bc0c --- /dev/null +++ b/packages/cli/src/lib/server-env.ts @@ -0,0 +1,16 @@ +export const SERVER_ENVIRONMENTS = ['local', 'dev', 'staging', 'prod'] as const + +export type ServerEnv = typeof SERVER_ENVIRONMENTS[number] + +export const SERVER_ENV_API_BASE_URLS: Record = { + local: 'https://api.beeper.localtest.me', + dev: 'https://api.beeper-dev.com', + staging: 'https://api.beeper-staging.com', + prod: 'https://api.beeper.com', +} + +export function normalizeServerEnv(value?: string): ServerEnv { + if (!value || value === 'prod' || value === 'production') return 'prod' + if (value === 'local' || value === 'dev' || value === 'staging') return value + throw new Error(`Unsupported server env "${value}". Expected local, dev, staging, or prod.`) +} diff --git a/packages/cli/src/lib/targets.ts b/packages/cli/src/lib/targets.ts index e16730c5..8c1623ad 100644 --- a/packages/cli/src/lib/targets.ts +++ b/packages/cli/src/lib/targets.ts @@ -3,6 +3,7 @@ import { access, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promise import { homedir } from 'node:os' import { dirname, join } from 'node:path' import { notFound } from './errors.js' +import { normalizeServerEnv } from './server-env.js' export type AuthSource = 'desktop-db' | 'desktop-cache' | 'desktop-oauth' | 'remote-oauth' | 'manual' @@ -201,7 +202,7 @@ function normalizeLocalTarget(target: Target): Target { } export async function createProfileTarget(type: ManagedTargetType, id: string, options: { serverEnv?: string; port?: number } = {}): Promise { - const serverEnv = options.serverEnv ?? 'production' + const serverEnv = normalizeServerEnv(options.serverEnv) const port = options.port ?? await nextPort() const target: Target = { id, diff --git a/packages/cli/test/cli-smoke.ts b/packages/cli/test/cli-smoke.ts index 1f5828d6..43a736f4 100644 --- a/packages/cli/test/cli-smoke.ts +++ b/packages/cli/test/cli-smoke.ts @@ -249,7 +249,7 @@ writeFileSync(join(configDir, 'installations.json'), `${JSON.stringify({ server: { kind: 'server', channel: 'stable', - serverEnv: 'production', + serverEnv: 'prod', bundleID: 'com.automattic.beeper.server', version: 'test', path: fakeServerPath, @@ -287,11 +287,22 @@ assert.equal(stagingServerRequest.bundleID, 'com.automattic.beeper.server') assert.equal(feedURLFor(stagingServerRequest), 'https://api.beeper-staging.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=darwin&channel=stable&arch=arm64') assert.equal(downloadURLFor(stagingServerRequest), 'https://api.beeper-staging.com/desktop/download/macos/arm64/stable/com.automattic.beeper.server') -const productionServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'production', channel: 'stable', platform: 'darwin', arch: 'arm64' }) -assert.equal(productionServerRequest.channel, 'stable') -assert.equal(productionServerRequest.bundleID, 'com.automattic.beeper.server') -assert.equal(feedURLFor(productionServerRequest), 'https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=darwin&channel=stable&arch=arm64') -assert.equal(downloadURLFor(productionServerRequest), 'https://api.beeper.com/desktop/download/macos/arm64/stable/com.automattic.beeper.server') +const prodServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'prod', channel: 'stable', platform: 'darwin', arch: 'arm64' }) +assert.equal(prodServerRequest.channel, 'stable') +assert.equal(prodServerRequest.bundleID, 'com.automattic.beeper.server') +assert.equal(feedURLFor(prodServerRequest), 'https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=darwin&channel=stable&arch=arm64') +assert.equal(downloadURLFor(prodServerRequest), 'https://api.beeper.com/desktop/download/macos/arm64/stable/com.automattic.beeper.server') + +const productionAliasServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'production', channel: 'stable', platform: 'darwin', arch: 'arm64' }) +assert.equal(productionAliasServerRequest.serverEnv, 'prod') + +const localServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'local', channel: 'stable', platform: 'darwin', arch: 'arm64' }) +assert.equal(feedURLFor(localServerRequest), 'https://api.beeper.localtest.me/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=darwin&channel=stable&arch=arm64') +assert.equal(downloadURLFor(localServerRequest), 'https://api.beeper.localtest.me/desktop/download/macos/arm64/stable/com.automattic.beeper.server') + +const devServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'dev', channel: 'stable', platform: 'darwin', arch: 'arm64' }) +assert.equal(feedURLFor(devServerRequest), 'https://api.beeper-dev.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=darwin&channel=stable&arch=arm64') +assert.equal(downloadURLFor(devServerRequest), 'https://api.beeper-dev.com/desktop/download/macos/arm64/stable/com.automattic.beeper.server') const stagingNightlyServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'staging', channel: 'nightly', platform: 'darwin', arch: 'arm64' }) assert.equal(stagingNightlyServerRequest.channel, 'nightly') From 12ed6118a3c998f4223d5c88f94aa4c039951a70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:14:17 +0200 Subject: [PATCH 05/26] wip --- docs/src/content/docs/targets.mdx | 4 ++-- packages/cli/README.md | 8 ++++---- packages/cli/src/commands/install/server.ts | 3 +-- packages/cli/src/commands/setup.ts | 4 ++-- packages/cli/src/commands/targets/add/desktop.ts | 3 +-- packages/cli/src/commands/targets/add/server.ts | 3 +-- 6 files changed, 11 insertions(+), 14 deletions(-) diff --git a/docs/src/content/docs/targets.mdx b/docs/src/content/docs/targets.mdx index 64c20cc9..85cd276e 100644 --- a/docs/src/content/docs/targets.mdx +++ b/docs/src/content/docs/targets.mdx @@ -18,8 +18,8 @@ commands use it unless `--target ` overrides. ```sh beeper targets list -beeper targets add desktop [name] [--port N] [--server-env local|dev|staging|prod] [--default] -beeper targets add server [name] [--port N] [--server-env local|dev|staging|prod] [--default] +beeper targets add desktop [name] [--port N] [--server-env prod|staging] [--default] +beeper targets add server [name] [--port N] [--server-env prod|staging] [--default] beeper targets add remote [--default] beeper targets use beeper targets show [name] diff --git a/packages/cli/README.md b/packages/cli/README.md index 5cc332b9..f0690a0a 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -449,7 +449,7 @@ Flags: | `--oauth` | boolean | Authorize the target with browser OAuth/PKCE | | `--remote=` | option | Connect to a remote Beeper Desktop or Server URL | | `--server` | boolean | Set up a local Beeper Server target | -| `--server-env=` | option | Server feed environment Default: prod | +| `--server-env=` | option | Server feed environment: prod or staging Default: prod | | `--username=` | option | Username to use if setup creates a new account | Examples: @@ -498,7 +498,7 @@ Flags: | Flag | Type | Description | | --- | --- | --- | | `--channel=` | option | Server release channel Default: stable | -| `--server-env=` | option | Server feed environment Default: prod | +| `--server-env=` | option | Server feed environment: prod or staging Default: prod | Examples: @@ -591,7 +591,7 @@ Flags: | --- | --- | --- | | `--default` | boolean | Set this target as the default after creation | | `--port=` | option | TCP port the managed Desktop will expose its API on | -| `--server-env=` | option | Server feed environment Default: prod | +| `--server-env=` | option | Server feed environment: prod or staging Default: prod | Examples: @@ -620,7 +620,7 @@ Flags: | --- | --- | --- | | `--default` | boolean | Set this target as the default after creation | | `--port=` | option | TCP port the managed Server will expose its API on | -| `--server-env=` | option | Server feed environment Default: prod | +| `--server-env=` | option | Server feed environment: prod or staging Default: prod | Examples: diff --git a/packages/cli/src/commands/install/server.ts b/packages/cli/src/commands/install/server.ts index f417165a..55f96306 100644 --- a/packages/cli/src/commands/install/server.ts +++ b/packages/cli/src/commands/install/server.ts @@ -3,13 +3,12 @@ import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { installServer, type InstallChannel } from '../../lib/installations.js' import { pathSetupHint } from '../../lib/env.js' import { printDryRun, printSuccess } from '../../lib/output.js' -import { SERVER_ENVIRONMENTS } from '../../lib/server-env.js' export default class SetupInstallServer extends BeeperCommand { static override summary = 'Install Beeper Server locally' static override flags = { channel: Flags.string({ options: ['stable', 'nightly'], default: 'stable', description: 'Server release channel' }), - 'server-env': Flags.string({ options: SERVER_ENVIRONMENTS, default: 'prod', description: 'Server feed environment' }), + 'server-env': Flags.string({ default: 'prod', description: 'Server feed environment: prod or staging' }), } async run(): Promise { diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index cc2243a7..fa26259c 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -9,7 +9,7 @@ import { loginWithPKCE } from '../lib/oauth.js' import { findDesktopAppPath, launchDesktopApp, startProfile } from '../lib/profiles.js' import { interactiveEmailSetup } from '../lib/setup-login.js' import { renderStartupLogo } from '../lib/logo.js' -import { SERVER_ENVIRONMENTS, SERVER_ENV_API_BASE_URLS, normalizeServerEnv } from '../lib/server-env.js' +import { SERVER_ENV_API_BASE_URLS, normalizeServerEnv } from '../lib/server-env.js' import { builtInDesktopTargetID, createProfileTarget, @@ -36,7 +36,7 @@ export default class Setup extends BeeperCommand { desktop: Flags.boolean({ default: false, description: 'Set up a local Beeper Desktop target' }), install: Flags.boolean({ default: false, description: 'Allow installing missing managed runtime' }), channel: Flags.string({ options: ['stable', 'nightly'], default: 'stable', description: 'Install release channel' }), - 'server-env': Flags.string({ options: SERVER_ENVIRONMENTS, default: 'prod', description: 'Server feed environment' }), + 'server-env': Flags.string({ default: 'prod', description: 'Server feed environment: prod or staging' }), email: Flags.string({ description: 'Sign in with an email address' }), username: Flags.string({ description: 'Username to use if setup creates a new account' }), } diff --git a/packages/cli/src/commands/targets/add/desktop.ts b/packages/cli/src/commands/targets/add/desktop.ts index 91a8f6cc..035127e3 100644 --- a/packages/cli/src/commands/targets/add/desktop.ts +++ b/packages/cli/src/commands/targets/add/desktop.ts @@ -2,7 +2,6 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../../lib/command.js' import { createProfileTarget, readTarget, updateConfig } from '../../../lib/targets.js' import { printDryRun, printSuccess } from '../../../lib/output.js' -import { SERVER_ENVIRONMENTS } from '../../../lib/server-env.js' export default class TargetsAddDesktop extends BeeperCommand { static override summary = 'Add a managed Beeper Desktop target' @@ -10,7 +9,7 @@ export default class TargetsAddDesktop extends BeeperCommand { static override flags = { port: Flags.integer({ description: 'TCP port the managed Desktop will expose its API on' }), default: Flags.boolean({ default: false, description: 'Set this target as the default after creation' }), - 'server-env': Flags.string({ options: SERVER_ENVIRONMENTS, default: 'prod', description: 'Server feed environment' }), + 'server-env': Flags.string({ default: 'prod', description: 'Server feed environment: prod or staging' }), } async run(): Promise { const { args, flags } = await this.parse(TargetsAddDesktop) diff --git a/packages/cli/src/commands/targets/add/server.ts b/packages/cli/src/commands/targets/add/server.ts index fed575ad..59fb4d3a 100644 --- a/packages/cli/src/commands/targets/add/server.ts +++ b/packages/cli/src/commands/targets/add/server.ts @@ -2,7 +2,6 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../../lib/command.js' import { createProfileTarget, readTarget, updateConfig } from '../../../lib/targets.js' import { printDryRun, printSuccess } from '../../../lib/output.js' -import { SERVER_ENVIRONMENTS } from '../../../lib/server-env.js' export default class TargetsAddServer extends BeeperCommand { static override summary = 'Add a managed Beeper Server target' @@ -10,7 +9,7 @@ export default class TargetsAddServer extends BeeperCommand { static override flags = { port: Flags.integer({ description: 'TCP port the managed Server will expose its API on' }), default: Flags.boolean({ default: false, description: 'Set this target as the default after creation' }), - 'server-env': Flags.string({ options: SERVER_ENVIRONMENTS, default: 'prod', description: 'Server feed environment' }), + 'server-env': Flags.string({ default: 'prod', description: 'Server feed environment: prod or staging' }), } async run(): Promise { const { args, flags } = await this.parse(TargetsAddServer) From 3212e03d14e70a77cdf110700f2559c82fc2a4ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:38:59 +0200 Subject: [PATCH 06/26] fix npm launcher download progress --- packages/npm/scripts/build.ts | 41 ++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/packages/npm/scripts/build.ts b/packages/npm/scripts/build.ts index b8dd6405..cd53ffeb 100644 --- a/packages/npm/scripts/build.ts +++ b/packages/npm/scripts/build.ts @@ -45,7 +45,7 @@ const platform = targetPlatform() const artifact = manifest.artifacts.find(item => item.platform === platform) if (!artifact) { - console.error(\`beeper-cli does not ship a binary for \${process.platform}/\${process.arch}.\`) + console.error(`beeper-cli does not ship a binary for ${process.platform}/${process.arch}.`) process.exit(1) } @@ -55,10 +55,10 @@ const binPath = join(cacheDir, 'bin', manifest.command || 'beeper') const expectedBinarySha256 = artifact.binarySha256 || artifact.sha256 if (!existsSync(binPath) || await sha256(binPath).catch(() => '') !== expectedBinarySha256) { - const tempDir = join(tmpdir(), \`beeper-cli-\${manifest.version}-\${process.pid}\`) + const tempDir = join(tmpdir(), `beeper-cli-${manifest.version}-${process.pid}`) const archivePath = join(tempDir, artifact.file) - const downloadURL = \`https://github.com/beeper/cli/releases/download/v\${manifest.version}/\${artifact.file}\` - logStep(\`installing beeper-cli \${manifest.version} for \${platform}\`) + const downloadURL = `https://github.com/beeper/cli/releases/download/v${manifest.version}/${artifact.file}` + logStep(`installing beeper-cli ${manifest.version} for ${platform}`) await rm(tempDir, { recursive: true, force: true }) await mkdir(tempDir, { recursive: true }) await download(downloadURL, archivePath) @@ -66,14 +66,14 @@ if (!existsSync(binPath) || await sha256(binPath).catch(() => '') !== expectedBi const actual = await sha256(archivePath) if (actual !== artifact.sha256) { await rm(tempDir, { recursive: true, force: true }) - console.error(\`beeper-cli binary checksum mismatch for \${artifact.file}.\`) + console.error(`beeper-cli binary checksum mismatch for ${artifact.file}.`) process.exit(1) } logStep('extracting binary') await extract(archivePath, tempDir) const extractedBin = join(tempDir, 'bin', manifest.command || 'beeper') await chmod(extractedBin, 0o755) - logStep(\`caching binary in \${cacheDir}\`) + logStep(`caching binary in ${cacheDir}`) await rm(cacheDir, { recursive: true, force: true }) await mkdir(dirname(binPath), { recursive: true }) await rename(extractedBin, binPath) @@ -81,7 +81,7 @@ if (!existsSync(binPath) || await sha256(binPath).catch(() => '') !== expectedBi logStep('ready') } -if (process.env.BEEPER_CLI_LAUNCHER_DEBUG === '1') logStep(\`starting \${binPath}\`) +if (process.env.BEEPER_CLI_LAUNCHER_DEBUG === '1') logStep(`starting ${binPath}`) const child = spawn(binPath, process.argv.slice(2), { stdio: 'inherit', env: process.env }) child.on('exit', (code, signal) => { if (signal) process.kill(process.pid, signal) @@ -89,7 +89,7 @@ child.on('exit', (code, signal) => { }) function logStep(message) { - console.error(\`beeper-cli: \${message}\`) + console.error(`beeper-cli: ${message}`) } function targetPlatform() { @@ -97,7 +97,7 @@ function targetPlatform() { const cpu = osArch() const normalizedOS = os === 'darwin' || os === 'linux' ? os : os === 'win32' ? 'windows' : os const normalizedArch = cpu === 'x64' || cpu === 'arm64' ? cpu : cpu - return \`\${normalizedOS}-\${normalizedArch}\` + return `${normalizedOS}-${normalizedArch}` } async function sha256(path) { @@ -106,20 +106,22 @@ async function sha256(path) { return hash.digest('hex') } -async function download(url, destination) { - logStep(\`downloading \${artifact.file}\`) +async function download(url, destination, redirects = 0) { + if (redirects > 10) throw new Error(`Too many redirects while downloading ${artifact.file}`) + + logStep(`downloading ${artifact.file}`) await new Promise((resolve, reject) => { get(url, response => { if ([301, 302, 303, 307, 308].includes(response.statusCode ?? 0) && response.headers.location) { response.resume() const nextURL = new URL(response.headers.location, url).toString() - logStep(\`redirecting to \${new URL(nextURL).host}\`) - download(nextURL, destination).then(resolve, reject) + logStep(`redirecting to ${new URL(nextURL).host}`) + download(nextURL, destination, redirects + 1).then(resolve, reject) return } if (response.statusCode !== 200) { response.resume() - reject(new Error(\`Download failed with HTTP \${response.statusCode}: \${url}\`)) + reject(new Error(`Download failed with HTTP ${response.statusCode}: ${url}`)) return } const total = Number(response.headers['content-length'] ?? 0) @@ -130,10 +132,9 @@ async function download(url, destination) { downloaded += chunk.length if (!total) return const percent = Math.floor(downloaded / total * 100) - if (percent >= nextLoggedPercent || percent === 100) { - const milestone = percent === 100 ? 100 : nextLoggedPercent - logStep(\`downloaded \${milestone}%\`) - nextLoggedPercent = milestone + 25 + while (percent >= nextLoggedPercent && nextLoggedPercent <= 100) { + logStep(`downloaded ${nextLoggedPercent}%`) + nextLoggedPercent += 25 } }) response.pipe(file) @@ -152,7 +153,7 @@ async function extract(archivePath, destination) { await run('tar', ['-xzf', archivePath, '-C', destination]) return } - throw new Error(\`Unsupported beeper-cli archive: \${artifact.file}\`) + throw new Error(`Unsupported beeper-cli archive: ${artifact.file}`) } async function run(command, args) { @@ -161,7 +162,7 @@ async function run(command, args) { child.on('error', reject) child.on('exit', code => { if (code === 0) resolve() - else reject(new Error(\`\${command} \${args.join(' ')} exited with \${code}\`)) + else reject(new Error(`${command} ${args.join(' ')} exited with ${code}`)) }) }) } From c5cfb050e9d9e24659910664fe2826cd86a52581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:39:03 +0200 Subject: [PATCH 07/26] fix schema command filtering and shapes --- packages/cli/src/commands/schema.ts | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/commands/schema.ts b/packages/cli/src/commands/schema.ts index 94e872c0..24eab64d 100644 --- a/packages/cli/src/commands/schema.ts +++ b/packages/cli/src/commands/schema.ts @@ -16,15 +16,17 @@ type RawCommand = { } export default class Schema extends BeeperCommand { + static override strict = false static override summary = 'Print machine-readable command/flag schema' static override description = 'Agent-first schema for commands, flags, args, examples, mutation metadata, selectors, output shapes, and related commands.' static override args = { - command: Args.string({ required: false, description: 'Optional command path, such as "messages search"', multiple: true }), + command: Args.string({ required: false, description: 'Optional command path, such as "messages search"' }), } async run(): Promise { - const { args } = await this.parse(Schema) - const requested = Array.isArray(args.command) ? args.command.join(' ') : undefined + await this.parse(Schema) + const pathArgs = this.argv.filter(arg => !arg.startsWith('-')) + const requested = pathArgs.length > 0 ? pathArgs.join(' ') : undefined const manifestByCommand = new Map(commandManifest.map(item => [item.command, item])) const commands = (this.config.commands as RawCommand[]) .filter(command => !command.hidden) @@ -116,10 +118,20 @@ function typeName(record: Record): string { function outputShape(kind: string): Record { const envelope = { ok: true, data: '', error: null, meta: '' } switch (kind) { - case 'list': return { kind, envelope, data: 'array' } - case 'stream': return { kind, data: 'jsonl events or RPC lines' } - case 'success': return { kind, envelope, data: { message: 'string', detail: 'string?', entity: 'object?' } } - case 'send-result': return { kind, envelope, data: { chatID: 'string', pendingMessageID: 'string?', state: 'string?' } } - default: return { kind, envelope, data: 'object' } + case 'list': { + return { kind, envelope, data: 'array' } + } + case 'send-result': { + return { kind, envelope, data: { chatID: 'string', pendingMessageID: 'string?', state: 'string?' } } + } + case 'stream': { + return { kind, data: 'jsonl events or RPC lines' } + } + case 'success': { + return { kind, envelope, data: { message: 'string', detail: 'string?', data: 'object?' } } + } + default: { + return { kind, envelope, data: 'object' } + } } } From ffcff36b42217a27a74984a9ed6c6eb1d6bc1c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:39:06 +0200 Subject: [PATCH 08/26] fix command error classification --- packages/cli/src/lib/command.ts | 34 +++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/cli/src/lib/command.ts b/packages/cli/src/lib/command.ts index 08bb4f37..2f9243e6 100644 --- a/packages/cli/src/lib/command.ts +++ b/packages/cli/src/lib/command.ts @@ -47,7 +47,7 @@ export abstract class BeeperCommand extends Command { const code = inferredCode ?? error.exitCode ?? ExitCodes.Generic process.exitCode = process.exitCode ?? code const tryMessage = error instanceof CLIError ? error.tryMessage : undefined - const isBug = error instanceof BugError || (!(error instanceof CLIError) && inferredCode === undefined) + const isBug = error instanceof BugError || !(error instanceof CLIError) if (this.argv.includes('--events')) { writeEvent('error', { message, exitCode: code, kind: isBug ? 'bug' : 'abort', tryMessage }) @@ -102,10 +102,6 @@ export function ensureWritable(flags: { 'read-only'?: boolean }): void { if (readOnly) throw new CLIError('read-only mode: command would modify Beeper or local CLI state', ExitCodes.Usage) } -export function ensureNotDryRun(flags: { 'dry-run'?: boolean }, action: string): void { - if (flags['dry-run']) throw new CLIError(`dry-run: ${action}`, ExitCodes.Success) -} - export function writeEvent(event: string, data: Record = {}): void { process.stderr.write(`${JSON.stringify({ event, data, ts: Date.now() })}\n`) } @@ -148,12 +144,26 @@ function isMachineOutput(argv: string[]): boolean { function errorCode(code: number, isBug: boolean): string { if (isBug) return 'internal_error' switch (code) { - case ExitCodes.Usage: return 'usage_error' - case ExitCodes.AuthRequired: return 'auth_required' - case ExitCodes.NotReady: return 'not_ready' - case ExitCodes.NotFound: return 'not_found' - case ExitCodes.Ambiguous: return 'ambiguous_selector' - case ExitCodes.CommandNotFound: return 'command_not_found' - default: return 'runtime_error' + case ExitCodes.Ambiguous: { + return 'ambiguous_selector' + } + case ExitCodes.AuthRequired: { + return 'auth_required' + } + case ExitCodes.CommandNotFound: { + return 'command_not_found' + } + case ExitCodes.NotFound: { + return 'not_found' + } + case ExitCodes.NotReady: { + return 'not_ready' + } + case ExitCodes.Usage: { + return 'usage_error' + } + default: { + return 'runtime_error' + } } } From a42c57be1c685754e5ec5f928ccb4d4031b60f5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:39:09 +0200 Subject: [PATCH 09/26] fix machine-readable failure output --- packages/cli/src/lib/output.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/lib/output.ts b/packages/cli/src/lib/output.ts index 724bfdea..2f836aaf 100644 --- a/packages/cli/src/lib/output.ts +++ b/packages/cli/src/lib/output.ts @@ -99,7 +99,7 @@ export async function printSuccess( ): Promise { format = effectiveFormat(format) if (format === 'json' || format === 'jsonl') { - writeJSON(jsonPayload({ message: opts.message, detail: opts.detail, entity: opts.entity, ...(opts.data ?? {}) }), format) + writeJSON(jsonPayload({ message: opts.message, detail: opts.detail, entity: opts.entity, ...opts.data }), format) return } if (format === 'ids') { @@ -124,6 +124,14 @@ export async function printFailure( writeJSON({ ok: false, data: opts.data ?? null, error: { message: opts.message, detail: opts.detail } }, format) return } + if (format === 'text') { + process.stdout.write(`${opts.message}${opts.detail ? `\t${opts.detail}` : ''}\n`) + return + } + if (format === 'ids') { + if (opts.data) printIDs([opts.data]) + return + } const { renderFailure } = await loadInk() await renderFailure(opts) } @@ -278,7 +286,7 @@ function printText(value: unknown): void { return } for (const [key, item] of Object.entries(value as Record)) { - if (item == null) continue + if (item === null || item === undefined) continue process.stdout.write(`${key}\t${typeof item === 'object' ? JSON.stringify(item) : String(item)}\n`) } } From cb259c98187e3ec2f5ae1686182533c8d0f1681f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:39:15 +0200 Subject: [PATCH 10/26] fix resolver error payloads and pick validation --- packages/cli/src/commands/resolve/account.ts | 5 +++-- packages/cli/src/commands/resolve/bridge.ts | 5 +++-- packages/cli/src/commands/resolve/contact.ts | 18 +++++++++++++----- packages/cli/src/lib/resolve.ts | 3 +-- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/cli/src/commands/resolve/account.ts b/packages/cli/src/commands/resolve/account.ts index a62ce1b9..ade03dc9 100644 --- a/packages/cli/src/commands/resolve/account.ts +++ b/packages/cli/src/commands/resolve/account.ts @@ -22,8 +22,9 @@ export default class ResolveAccount extends BeeperCommand { const ids = await resolveAccountIDs(client, [args.selector], { allowMultiplePerInput: true, applyDefault: false }) const candidates = rows.filter((row: any) => ids?.includes(String(row.accountID ?? row.id))) if (!candidates.length) throw notFound(`No account matches "${args.selector}"`, { selector: args.selector, kind: 'account' }) - const selected = flags.pick ? candidates[flags.pick - 1] : candidates.length === 1 ? candidates[0] : undefined - if (flags.pick && !selected) throw notFound(`--pick ${flags.pick} is outside the ${candidates.length} matching accounts`, { selector: args.selector, pick: flags.pick, count: candidates.length }) + const pick = flags.pick + const selected = pick !== undefined ? candidates[pick - 1] : candidates.length === 1 ? candidates[0] : undefined + if (pick !== undefined && !selected) throw notFound(`--pick ${pick} is outside the ${candidates.length} matching accounts`, { selector: args.selector, pick, count: candidates.length }) await printData({ selector: args.selector, kind: 'account', diff --git a/packages/cli/src/commands/resolve/bridge.ts b/packages/cli/src/commands/resolve/bridge.ts index eb0a42f6..44781dfe 100644 --- a/packages/cli/src/commands/resolve/bridge.ts +++ b/packages/cli/src/commands/resolve/bridge.ts @@ -29,8 +29,9 @@ export default class ResolveBridge extends BeeperCommand { normalize(bridge.displayName).includes(normalized) ) if (!candidates.length) throw notFound(`No bridge matches "${args.selector}"`, { selector: args.selector, kind: 'bridge' }) - const selected = flags.pick ? candidates[flags.pick - 1] : candidates.length === 1 ? candidates[0] : undefined - if (flags.pick && !selected) throw notFound(`--pick ${flags.pick} is outside the ${candidates.length} matching bridges`, { selector: args.selector, pick: flags.pick, count: candidates.length }) + const pick = flags.pick + const selected = pick !== undefined ? candidates[pick - 1] : candidates.length === 1 ? candidates[0] : undefined + if (pick !== undefined && !selected) throw notFound(`--pick ${pick} is outside the ${candidates.length} matching bridges`, { selector: args.selector, pick, count: candidates.length }) await printData({ selector: args.selector, kind: 'bridge', diff --git a/packages/cli/src/commands/resolve/contact.ts b/packages/cli/src/commands/resolve/contact.ts index f85f1bd4..8cfd2491 100644 --- a/packages/cli/src/commands/resolve/contact.ts +++ b/packages/cli/src/commands/resolve/contact.ts @@ -25,13 +25,15 @@ export default class ResolveContact extends BeeperCommand { try { const result = await client.accounts.contacts.search(accountID, { query: args.selector }) candidates.push(...result.items.slice(0, flags.limit).map((item: unknown) => ({ ...(item as Record), accountID }))) - } catch { - // Keep searching accounts that support lookup for this selector shape. + } catch (error) { + if (shouldIgnoreLookupError(error)) continue + throw error } } if (!candidates.length) throw notFound(`No contact matches "${args.selector}"`, { selector: args.selector, kind: 'contact' }) - const selected = flags.pick ? candidates[flags.pick - 1] : candidates.length === 1 ? candidates[0] : undefined - if (flags.pick && !selected) throw notFound(`--pick ${flags.pick} is outside the ${candidates.length} matching contacts`, { selector: args.selector, pick: flags.pick, count: candidates.length }) + const pick = flags.pick + const selected = pick !== undefined ? candidates[pick - 1] : candidates.length === 1 ? candidates[0] : undefined + if (pick !== undefined && !selected) throw notFound(`--pick ${pick} is outside the ${candidates.length} matching contacts`, { selector: args.selector, pick, count: candidates.length }) await printData({ selector: args.selector, kind: 'contact', @@ -50,6 +52,12 @@ function contactCandidate(contact: Record, pick: number): Recor username: contact.username, phoneNumber: contact.phoneNumber, email: contact.email, - raw: contact, } } + +function shouldIgnoreLookupError(error: unknown): boolean { + if (!(error instanceof Error)) return false + const status = (error as Error & { status?: number; statusCode?: number }).status ?? (error as Error & { status?: number; statusCode?: number }).statusCode + if (status === 400 || status === 404) return true + return /\b(400|404)\b|not supported|not found/i.test(error.message) +} diff --git a/packages/cli/src/lib/resolve.ts b/packages/cli/src/lib/resolve.ts index bf25940e..a3f11a32 100644 --- a/packages/cli/src/lib/resolve.ts +++ b/packages/cli/src/lib/resolve.ts @@ -36,7 +36,7 @@ export async function resolveAccountIDs( throw ambiguous(formatAmbiguous(`account "${input}"`, matches.map(formatAccount)), { selector: input, kind: 'account', - candidates: matches.map((account, index) => ({ pick: index + 1, id: String(account.accountID ?? account.id), label: formatAccount(account), raw: account })), + candidates: matches.map((account, index) => ({ pick: index + 1, id: String(account.accountID ?? account.id), label: formatAccount(account) })), }) } resolved.push(...matches.map(account => String(account.accountID))) @@ -97,7 +97,6 @@ export async function resolveChatID(client: any, input: string, options: ChatRes title: chat.title ? String(chat.title) : undefined, network: chat.network ? String(chat.network) : undefined, label: formatChat(chat), - raw: chat, })), }) } From 619c53e2248b6131e491a4e120c9dc4da890fe20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:39:18 +0200 Subject: [PATCH 11/26] fix command metadata lint issues --- packages/cli/src/lib/command-metadata.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/lib/command-metadata.ts b/packages/cli/src/lib/command-metadata.ts index 69b27dc1..3d3f3c4a 100644 --- a/packages/cli/src/lib/command-metadata.ts +++ b/packages/cli/src/lib/command-metadata.ts @@ -9,16 +9,16 @@ export type CommandMetadata = { export function metadataForCommand(command: string): CommandMetadata { const parts = command.split(' ') const root = parts[0] ?? '' - const mutatingRoots = new Set(['setup', 'install', 'send', 'update', 'export', 'presence']) + const mutatingRoots = new Set(['export', 'install', 'presence', 'send', 'setup', 'update']) const mutatingVerbs = new Set([ - 'add', 'archive', 'unarchive', 'pin', 'unpin', 'mute', 'unmute', 'mark-read', 'mark-unread', - 'priority', 'notify-anyway', 'rename', 'description', 'avatar', 'draft', 'disappear', 'remind', - 'unremind', 'focus', 'edit', 'delete', 'remove', 'use', 'set', 'reset', 'logout', 'start', 'stop', - 'restart', 'enable', 'disable', 'download', 'export', 'post', 'response', 'approve', 'recovery-key', 'reset-recovery-key', 'cancel', 'sas', - 'sas-confirm', 'qr-scan', 'qr-confirm', + 'add', 'approve', 'archive', 'avatar', 'cancel', 'delete', 'description', 'disable', 'disappear', 'download', + 'draft', 'edit', 'enable', 'export', 'focus', 'logout', 'mark-read', 'mark-unread', 'mute', 'notify-anyway', + 'pin', 'post', 'priority', 'qr-confirm', 'qr-scan', 'recovery-key', 'remind', 'remove', 'rename', 'reset', + 'reset-recovery-key', 'response', 'restart', 'sas', 'sas-confirm', 'set', 'start', 'stop', 'unarchive', + 'unmute', 'unpin', 'unremind', 'use', ]) const mutates = command === 'verify' || command === 'api request' || mutatingRoots.has(root) || parts.some(part => mutatingVerbs.has(part ?? '')) - const localOnly = new Set(['config', 'completion', 'docs', 'version', 'man', 'schema']) + const localOnly = new Set(['completion', 'config', 'docs', 'man', 'schema', 'version']) const requiresAuth = !localOnly.has(root) && command !== 'targets list' && !command.startsWith('targets add') && !command.startsWith('install ') const selectors = [ command.includes('chats ') || command.includes('messages ') || command.startsWith('send ') || command === 'presence' || command.startsWith('resolve chat') ? 'chat' : undefined, @@ -26,7 +26,7 @@ export function metadataForCommand(command: string): CommandMetadata { command.includes('targets ') || command === 'status' || command === 'doctor' || command.startsWith('auth ') || command.startsWith('verify') || command.startsWith('resolve target') ? 'target' : undefined, command.startsWith('bridges ') || command === 'accounts add' || command.startsWith('resolve bridge') ? 'bridge' : undefined, command.includes('messages ') || command.startsWith('send react') || command.startsWith('send unreact') ? 'message' : undefined, - ].filter((value): value is string => Boolean(value)) + ].filter(Boolean) as string[] const output = command === 'schema' ? 'schema' : command.startsWith('send ') ? 'send-result' : command === 'watch' || command === 'rpc' ? 'stream' From 3cebc504fc242821a92c1178ef917291bd8af445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:39:22 +0200 Subject: [PATCH 12/26] remove unused output imports --- packages/cli/src/commands/chats/archive.ts | 2 +- packages/cli/src/commands/chats/avatar.ts | 2 +- packages/cli/src/commands/chats/description.ts | 2 +- packages/cli/src/commands/chats/draft.ts | 2 +- packages/cli/src/commands/chats/focus.ts | 2 +- packages/cli/src/commands/chats/mark-read.ts | 2 +- packages/cli/src/commands/chats/mark-unread.ts | 2 +- packages/cli/src/commands/chats/notify-anyway.ts | 2 +- packages/cli/src/commands/chats/pin.ts | 2 +- packages/cli/src/commands/chats/remind.ts | 2 +- packages/cli/src/commands/chats/unarchive.ts | 2 +- packages/cli/src/commands/chats/unmute.ts | 2 +- packages/cli/src/commands/chats/unpin.ts | 2 +- packages/cli/src/commands/chats/unremind.ts | 2 +- packages/cli/src/commands/targets/remove.ts | 2 +- packages/cli/src/commands/targets/use.ts | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/cli/src/commands/chats/archive.ts b/packages/cli/src/commands/chats/archive.ts index cf690869..150a2883 100644 --- a/packages/cli/src/commands/chats/archive.ts +++ b/packages/cli/src/commands/chats/archive.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsArchive extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/avatar.ts b/packages/cli/src/commands/chats/avatar.ts index c511dbfc..01da3592 100644 --- a/packages/cli/src/commands/chats/avatar.ts +++ b/packages/cli/src/commands/chats/avatar.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsAvatar extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/description.ts b/packages/cli/src/commands/chats/description.ts index 6c382ae8..1f081774 100644 --- a/packages/cli/src/commands/chats/description.ts +++ b/packages/cli/src/commands/chats/description.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsDescription extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/draft.ts b/packages/cli/src/commands/chats/draft.ts index 2db6abb3..ec4f22ed 100644 --- a/packages/cli/src/commands/chats/draft.ts +++ b/packages/cli/src/commands/chats/draft.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsDraft extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/focus.ts b/packages/cli/src/commands/chats/focus.ts index e8b68036..3903edf8 100644 --- a/packages/cli/src/commands/chats/focus.ts +++ b/packages/cli/src/commands/chats/focus.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsFocus extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/mark-read.ts b/packages/cli/src/commands/chats/mark-read.ts index 176ff556..72ddf591 100644 --- a/packages/cli/src/commands/chats/mark-read.ts +++ b/packages/cli/src/commands/chats/mark-read.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsMarkRead extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/mark-unread.ts b/packages/cli/src/commands/chats/mark-unread.ts index e70043ff..73acc5c7 100644 --- a/packages/cli/src/commands/chats/mark-unread.ts +++ b/packages/cli/src/commands/chats/mark-unread.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsMarkUnread extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/notify-anyway.ts b/packages/cli/src/commands/chats/notify-anyway.ts index 1ffdf72e..418d5237 100644 --- a/packages/cli/src/commands/chats/notify-anyway.ts +++ b/packages/cli/src/commands/chats/notify-anyway.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsNotifyAnyway extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/pin.ts b/packages/cli/src/commands/chats/pin.ts index 26543935..642265e9 100644 --- a/packages/cli/src/commands/chats/pin.ts +++ b/packages/cli/src/commands/chats/pin.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsPin extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/remind.ts b/packages/cli/src/commands/chats/remind.ts index fce00d4e..4bd61a01 100644 --- a/packages/cli/src/commands/chats/remind.ts +++ b/packages/cli/src/commands/chats/remind.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsRemind extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/unarchive.ts b/packages/cli/src/commands/chats/unarchive.ts index 8df63c14..2da004c4 100644 --- a/packages/cli/src/commands/chats/unarchive.ts +++ b/packages/cli/src/commands/chats/unarchive.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsUnarchive extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/unmute.ts b/packages/cli/src/commands/chats/unmute.ts index 3c7d1ac0..46148e78 100644 --- a/packages/cli/src/commands/chats/unmute.ts +++ b/packages/cli/src/commands/chats/unmute.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsUnmute extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/unpin.ts b/packages/cli/src/commands/chats/unpin.ts index 750f6348..2839626d 100644 --- a/packages/cli/src/commands/chats/unpin.ts +++ b/packages/cli/src/commands/chats/unpin.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printData, printDryRun } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsUnpin extends BeeperCommand { diff --git a/packages/cli/src/commands/chats/unremind.ts b/packages/cli/src/commands/chats/unremind.ts index 15b28aad..92dd5a73 100644 --- a/packages/cli/src/commands/chats/unremind.ts +++ b/packages/cli/src/commands/chats/unremind.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' import { resolveChatID } from '../../lib/resolve.js' export default class ChatsUnremind extends BeeperCommand { diff --git a/packages/cli/src/commands/targets/remove.ts b/packages/cli/src/commands/targets/remove.ts index 5aa2c45b..82cbb948 100644 --- a/packages/cli/src/commands/targets/remove.ts +++ b/packages/cli/src/commands/targets/remove.ts @@ -4,7 +4,7 @@ import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createProfileTarget, listTargets, readConfig, readTarget, removeTarget, resolveTarget, updateConfig, writeTarget, type Target } from '../../lib/targets.js' import { disableProfile, enableProfile, profileErrorLogPath, profileLogPath, profileStatus, startProfile, stopProfile } from '../../lib/profiles.js' import { targetLiveStatus } from '../../lib/target-status.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class TargetsRemove extends BeeperCommand { static override summary = 'Remove a target' diff --git a/packages/cli/src/commands/targets/use.ts b/packages/cli/src/commands/targets/use.ts index 107a03ea..92e385d2 100644 --- a/packages/cli/src/commands/targets/use.ts +++ b/packages/cli/src/commands/targets/use.ts @@ -4,7 +4,7 @@ import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createProfileTarget, listTargets, readConfig, readTarget, removeTarget, resolveTarget, updateConfig, writeTarget, type Target } from '../../lib/targets.js' import { disableProfile, enableProfile, profileErrorLogPath, profileLogPath, profileStatus, startProfile, stopProfile } from '../../lib/profiles.js' import { targetLiveStatus } from '../../lib/target-status.js' -import { printData, printDryRun, printSuccess } from '../../lib/output.js' +import { printDryRun, printSuccess } from '../../lib/output.js' export default class TargetsUse extends BeeperCommand { static override summary = 'Set the default target' From 7b17068a98aaca2ca394e49e20da2cfeae5cf7d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:39:26 +0200 Subject: [PATCH 13/26] fix dry-run guard ordering --- packages/cli/src/commands/accounts/add.ts | 2 +- packages/cli/src/commands/auth/logout.ts | 8 ++++---- packages/cli/src/commands/chats/disappear.ts | 8 ++++---- packages/cli/src/commands/messages/export.ts | 9 +++++---- packages/cli/src/commands/presence.ts | 8 ++++---- packages/cli/src/commands/send/voice.ts | 16 +++++++++------- packages/cli/src/commands/update.ts | 2 +- packages/cli/src/commands/verify/approve.ts | 4 ++-- 8 files changed, 30 insertions(+), 27 deletions(-) diff --git a/packages/cli/src/commands/accounts/add.ts b/packages/cli/src/commands/accounts/add.ts index a4b1865e..d28138e0 100644 --- a/packages/cli/src/commands/accounts/add.ts +++ b/packages/cli/src/commands/accounts/add.ts @@ -29,7 +29,6 @@ export default class AccountsAdd extends BeeperCommand { async run(): Promise { const { args, flags } = await this.parse(AccountsAdd) - ensureWritable(flags) const client = await createClient(flags) if (!args.bridge) { @@ -83,6 +82,7 @@ export default class AccountsAdd extends BeeperCommand { return } + ensureWritable(flags) const step = await client.bridges.loginSessions.create(accountType.id, { flowID, loginID: flags['login-id'], diff --git a/packages/cli/src/commands/auth/logout.ts b/packages/cli/src/commands/auth/logout.ts index df2e90b1..d4e9e8ab 100644 --- a/packages/cli/src/commands/auth/logout.ts +++ b/packages/cli/src/commands/auth/logout.ts @@ -7,16 +7,16 @@ export default class AuthLogout extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(AuthLogout) - ensureWritable(flags) + if (!flags['dry-run']) ensureWritable(flags) const target = await resolveTarget({ target: flags.target, baseURL: flags['base-url'] }) - if (process.env.BEEPER_ACCESS_TOKEN && !target.auth?.accessToken) { - throw new Error('auth logout cannot clear BEEPER_ACCESS_TOKEN from the environment; unset it in the calling process.') - } const token = target.auth?.accessToken if (flags['dry-run']) { await printDryRun('auth.logout', { target: target.id, baseURL: target.baseURL, hadToken: Boolean(token), revokeToken: Boolean(token) }, flags.json ? 'json' : 'human') return } + if (process.env.BEEPER_ACCESS_TOKEN && !target.auth?.accessToken) { + throw new Error('auth logout cannot clear BEEPER_ACCESS_TOKEN from the environment; unset it in the calling process.') + } let revoked = false if (token) { const response = await fetch(new URL('/oauth/revoke', target.baseURL), { diff --git a/packages/cli/src/commands/chats/disappear.ts b/packages/cli/src/commands/chats/disappear.ts index 07c858fe..6e9237c7 100644 --- a/packages/cli/src/commands/chats/disappear.ts +++ b/packages/cli/src/commands/chats/disappear.ts @@ -17,15 +17,15 @@ export default class ChatsDisappear extends BeeperCommand { } async run(): Promise { const { flags } = await this.parse(ChatsDisappear) - ensureWritable(flags) - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) const expiry = flags.seconds.toLowerCase() === 'off' ? null : Number(flags.seconds) if (expiry !== null && (!Number.isInteger(expiry) || expiry < 0)) throw new Error('--seconds must be a positive integer or "off"') if (flags['dry-run']) { - await printDryRun('chats.disappear', { chatID, messageExpirySeconds: expiry }, flags.json ? 'json' : 'human') + await printDryRun('chats.disappear', { chat: flags.chat, pick: flags.pick, messageExpirySeconds: expiry }, flags.json ? 'json' : 'human') return } + ensureWritable(flags) + const client = await createClient(flags) + const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) await printData(await client.chats.update(chatID, { messageExpirySeconds: expiry }), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/messages/export.ts b/packages/cli/src/commands/messages/export.ts index 9615fd16..03d8ac38 100644 --- a/packages/cli/src/commands/messages/export.ts +++ b/packages/cli/src/commands/messages/export.ts @@ -23,12 +23,10 @@ export default class MessagesExport extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(MessagesExport) if (flags['before-cursor'] && flags['after-cursor']) throw new Error('Use only one of --before-cursor or --after-cursor') - if (flags.output !== '-') ensureWritable(flags) - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) if (flags['dry-run']) { await printDryRun('messages.export', { - chatID, + chat: flags.chat, + pick: flags.pick, output: flags.output, beforeCursor: flags['before-cursor'], afterCursor: flags['after-cursor'], @@ -39,6 +37,9 @@ export default class MessagesExport extends BeeperCommand { }, flags.json ? 'json' : 'human') return } + if (flags.output !== '-') ensureWritable(flags) + const client = await createClient(flags) + const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) const cursor = flags['before-cursor'] ?? flags['after-cursor'] const direction = flags['before-cursor'] ? 'before' : flags['after-cursor'] ? 'after' : undefined const afterTs = flags.after ? Date.parse(flags.after) : undefined diff --git a/packages/cli/src/commands/presence.ts b/packages/cli/src/commands/presence.ts index e914148f..709f927b 100644 --- a/packages/cli/src/commands/presence.ts +++ b/packages/cli/src/commands/presence.ts @@ -17,15 +17,15 @@ export default class Presence extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(Presence) - ensureWritable(flags) if (flags.duration !== undefined && flags.duration <= 0) throw new Error('--duration must be a positive integer (seconds)') if (flags.duration !== undefined && flags.state !== 'typing') throw new Error('--duration only applies when --state is typing') - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) if (flags['dry-run']) { - await printDryRun('presence', { chatID, state: flags.state, durationSeconds: flags.duration }, flags.json ? 'json' : 'human') + await printDryRun('presence', { chat: flags.chat, pick: flags.pick, state: flags.state, durationSeconds: flags.duration }, flags.json ? 'json' : 'human') return } + ensureWritable(flags) + const client = await createClient(flags) + const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) const post = (state: 'typing' | 'paused') => client.post(`/v1/chats/${encodeURIComponent(chatID)}/typing`, { body: { state } }) diff --git a/packages/cli/src/commands/send/voice.ts b/packages/cli/src/commands/send/voice.ts index 3682957a..e89f8aa9 100644 --- a/packages/cli/src/commands/send/voice.ts +++ b/packages/cli/src/commands/send/voice.ts @@ -21,11 +21,9 @@ export default class SendVoice extends BeeperCommand { } async run(): Promise { const { flags } = await this.parse(SendVoice) - ensureWritable(flags) - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.to, { pick: flags.pick }) - const request = { - chatID, + const dryRunRequest = { + chat: flags.to, + pick: flags.pick, file: flags.file, fileName: flags.filename, mimeType: flags.mime, @@ -37,11 +35,15 @@ export default class SendVoice extends BeeperCommand { waitTimeoutMs: flags['wait-timeout'], } if (flags['dry-run']) { - await printDryRun('send.voice', request, flags.json ? 'json' : 'human') + await printDryRun('send.voice', dryRunRequest, flags.json ? 'json' : 'human') return } + + ensureWritable(flags) + const client = await createClient(flags) + const chatID = await resolveChatID(client, flags.to, { pick: flags.pick }) await printData( - await sendMessage(client, request), + await sendMessage(client, { ...dryRunRequest, chatID }), flags.json ? 'json' : 'human', ) } diff --git a/packages/cli/src/commands/update.ts b/packages/cli/src/commands/update.ts index 5f613f3b..f14f9f61 100644 --- a/packages/cli/src/commands/update.ts +++ b/packages/cli/src/commands/update.ts @@ -23,7 +23,7 @@ export default class Update extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(Update) - if (!flags.check) ensureWritable(flags) + if (!flags.check && !flags['dry-run']) ensureWritable(flags) const selected = flags.cli || flags.desktop || flags.server if (flags['dry-run'] && !flags.check) { await printDryRun('update', { cli: !selected || flags.cli, desktop: !selected || flags.desktop, server: !selected || flags.server }, flags.json ? 'json' : 'human') diff --git a/packages/cli/src/commands/verify/approve.ts b/packages/cli/src/commands/verify/approve.ts index caf4c6e5..dc99e52a 100644 --- a/packages/cli/src/commands/verify/approve.ts +++ b/packages/cli/src/commands/verify/approve.ts @@ -9,12 +9,12 @@ export default class AuthVerifyApprove extends BeeperCommand { } async run(): Promise { const { flags } = await this.parse(AuthVerifyApprove) - ensureWritable(flags) - const client = await createClient(flags) if (flags['dry-run']) { await printDryRun('verify.approve', { id: flags.id ?? 'active' }, flags.json ? 'json' : 'human') return } + ensureWritable(flags) + const client = await createClient(flags) await printData(await client.app.verifications.accept(flags.id ?? 'active'), flags.json ? 'json' : 'human') } } From f14e0abb0f2f42b73018f86bba84973caeba24c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:39:29 +0200 Subject: [PATCH 14/26] fix dry-run padding lint --- packages/cli/src/commands/send/file.ts | 1 + packages/cli/src/commands/send/text.ts | 1 + packages/cli/src/commands/targets/start.ts | 2 ++ 3 files changed, 4 insertions(+) diff --git a/packages/cli/src/commands/send/file.ts b/packages/cli/src/commands/send/file.ts index d021ccb8..2e5442fc 100644 --- a/packages/cli/src/commands/send/file.ts +++ b/packages/cli/src/commands/send/file.ts @@ -34,6 +34,7 @@ export default class SendFile extends BeeperCommand { await printDryRun('send.file', request, flags.json ? 'json' : 'human') return } + await printData(await sendMessage(client, request), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/send/text.ts b/packages/cli/src/commands/send/text.ts index d5bffad8..3363907a 100644 --- a/packages/cli/src/commands/send/text.ts +++ b/packages/cli/src/commands/send/text.ts @@ -33,6 +33,7 @@ export default class SendText extends BeeperCommand { await printDryRun('send.text', request, flags.json ? 'json' : 'human') return } + await printData(await sendMessage(client, request), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/targets/start.ts b/packages/cli/src/commands/targets/start.ts index c9cd6a5e..956d621f 100644 --- a/packages/cli/src/commands/targets/start.ts +++ b/packages/cli/src/commands/targets/start.ts @@ -21,9 +21,11 @@ export default class TargetsStart extends BeeperCommand { await printSuccess({ message: 'Opened Beeper Desktop', detail: target.baseURL, data: { target, result } }, flags.json ? 'json' : 'human') return } + if (!target.managed || target.type !== 'server') { throw new Error(`Target "${target.id}" is not a local Beeper Server install.`) } + if (flags['dry-run']) { await printDryRun('targets.start', { target, startProfile: true }, flags.json ? 'json' : 'human') return From 337c94c8bfde4577aecdcc3960f6cd994d213895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:39:35 +0200 Subject: [PATCH 15/26] fix server environment setup handling --- README.md | 24 +++++++------- packages/cli/README.md | 32 +++++++++---------- packages/cli/src/commands/install/server.ts | 3 +- packages/cli/src/commands/setup.ts | 11 ++++--- .../cli/src/commands/targets/add/desktop.ts | 3 +- .../cli/src/commands/targets/add/server.ts | 3 +- packages/cli/src/lib/installations.ts | 9 ++++-- 7 files changed, 47 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 30abd8d7..385ff825 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ **One CLI for all your chats.** Built for you and your agent — batteries included. [![npm](https://img.shields.io/npm/v/beeper-cli.svg?label=npm&color=6E56F8)](https://www.npmjs.com/package/beeper-cli) -[![license](https://img.shields.io/badge/license-MIT-6E56F8.svg)](https://github.com/beeper/desktop-api-cli/blob/main/packages/cli/LICENSE) -[![docs](https://img.shields.io/badge/docs-online-6E56F8.svg)](https://example.com) +[![license](https://img.shields.io/badge/license-MIT-6E56F8.svg)](https://github.com/beeper/cli/blob/main/packages/cli/LICENSE) +[![docs](https://img.shields.io/badge/docs-online-6E56F8.svg)](https://beeper.github.io/cli) [![built with Bun](https://img.shields.io/badge/built%20with-Bun-6E56F8.svg)](https://bun.sh) @@ -22,7 +22,7 @@ Facebook Messenger · X (Twitter) DMs · LinkedIn · Slack · Google Messages (RCS/SMS) · Google Chat · Matrix · IRC · Bluesky. Run `beeper bridges list` for the live list on your target. -📖 **[Read the docs](https://example.com)** · command manual: `beeper man` · open docs: `beeper docs` +📖 **[Read the docs](https://beeper.github.io/cli)** · command manual: `beeper man` · open docs: `beeper docs` ## Features @@ -202,19 +202,19 @@ WhatsApp, Telegram, Discord, iMessage, and the rest show up under `accounts list ## Documentation -Full documentation lives at **[example.com](https://example.com)** +Full documentation lives at **[beeper.github.io/cli](https://beeper.github.io/cli)** (built from [`docs/`](docs/) with Astro Starlight — a fully static site). | Topic | Page | Commands | | --- | --- | --- | -| **Setup + install** | [connect](https://example.com/connect/) · [install](https://example.com/install/) · [auth](https://example.com/auth/) | `setup` · `install desktop` · `install server` · `verify` · `status` · `doctor` · `auth status` | -| **Targets** | [targets](https://example.com/targets/) | `targets list` · `targets add desktop` · `targets add server` · `targets add remote` · `targets use` · `targets status` · `targets logs` | -| **Bridges + accounts** | [accounts](https://example.com/accounts/) | `bridges list` · `bridges show` · `accounts list` · `accounts add` · `accounts show` · `accounts use` · `accounts remove` | -| **Chats** | [chats](https://example.com/chats/) | `chats list` · `chats search` · `chats show` · `chats start` · `chats archive` · `chats pin` · `chats mute` · `chats priority` · `chats remind` · `chats rename` · `chats draft` · `chats focus` | -| **Messages** | [messages](https://example.com/messages/) · [send](https://example.com/send/) · [presence](https://example.com/presence/) | `messages list` · `messages search` · `messages export` · `send text` · `send file` · `send sticker` · `send voice` · `send react` · `presence` | -| **Contacts + media** | [contacts](https://example.com/contacts/) · [media](https://example.com/media/) · [export](https://example.com/export/) | `contacts list` · `contacts search` · `media download` · `export` | -| **Automation** | [scripting](https://example.com/scripting/) · [watch](https://example.com/watch/) · [rpc](https://example.com/rpc/) · [api](https://example.com/api/) | `watch` · `watch --webhook` · `rpc` · `man` · `api get` · `api post` · `api request` | -| **Maintenance** | [config](https://example.com/config/) · [update](https://example.com/update/) | `update` · `config` · `completion` · `docs` · `version` | +| **Setup + install** | [connect](https://beeper.github.io/cli/connect/) · [install](https://beeper.github.io/cli/install/) · [auth](https://beeper.github.io/cli/auth/) | `setup` · `install desktop` · `install server` · `verify` · `status` · `doctor` · `auth status` | +| **Targets** | [targets](https://beeper.github.io/cli/targets/) | `targets list` · `targets add desktop` · `targets add server` · `targets add remote` · `targets use` · `targets status` · `targets logs` | +| **Bridges + accounts** | [accounts](https://beeper.github.io/cli/accounts/) | `bridges list` · `bridges show` · `accounts list` · `accounts add` · `accounts show` · `accounts use` · `accounts remove` | +| **Chats** | [chats](https://beeper.github.io/cli/chats/) | `chats list` · `chats search` · `chats show` · `chats start` · `chats archive` · `chats pin` · `chats mute` · `chats priority` · `chats remind` · `chats rename` · `chats draft` · `chats focus` | +| **Messages** | [messages](https://beeper.github.io/cli/messages/) · [send](https://beeper.github.io/cli/send/) · [presence](https://beeper.github.io/cli/presence/) | `messages list` · `messages search` · `messages export` · `send text` · `send file` · `send sticker` · `send voice` · `send react` · `presence` | +| **Contacts + media** | [contacts](https://beeper.github.io/cli/contacts/) · [media](https://beeper.github.io/cli/media/) · [export](https://beeper.github.io/cli/export/) | `contacts list` · `contacts search` · `media download` · `export` | +| **Automation** | [scripting](https://beeper.github.io/cli/scripting/) · [watch](https://beeper.github.io/cli/watch/) · [rpc](https://beeper.github.io/cli/rpc/) · [api](https://beeper.github.io/cli/api/) | `watch` · `watch --webhook` · `rpc` · `man` · `api get` · `api post` · `api request` | +| **Maintenance** | [config](https://beeper.github.io/cli/config/) · [update](https://beeper.github.io/cli/update/) | `update` · `config` · `completion` · `docs` · `version` | Use `beeper docs` to open the CLI docs and `beeper man` to print the local command manual. To work on the docs site locally: `cd docs && bun install && bun run dev`. diff --git a/packages/cli/README.md b/packages/cli/README.md index f0690a0a..2d1e574d 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -5,8 +5,8 @@ **One CLI for all your chats.** Built for you and your agent — batteries included. [![npm](https://img.shields.io/npm/v/beeper-cli.svg?label=npm&color=6E56F8)](https://www.npmjs.com/package/beeper-cli) -[![license](https://img.shields.io/badge/license-MIT-6E56F8.svg)](https://github.com/beeper/desktop-api-cli/blob/main/packages/cli/LICENSE) -[![docs](https://img.shields.io/badge/docs-online-6E56F8.svg)](https://example.com) +[![license](https://img.shields.io/badge/license-MIT-6E56F8.svg)](https://github.com/beeper/cli/blob/main/packages/cli/LICENSE) +[![docs](https://img.shields.io/badge/docs-online-6E56F8.svg)](https://beeper.github.io/cli) [![built with Bun](https://img.shields.io/badge/built%20with-Bun-6E56F8.svg)](https://bun.sh) @@ -22,7 +22,7 @@ Facebook Messenger · X (Twitter) DMs · LinkedIn · Slack · Google Messages (RCS/SMS) · Google Chat · Matrix · IRC · Bluesky. Run `beeper bridges list` for the live list on your target. -📖 **[Read the docs](https://example.com)** · command manual: `beeper man` · open docs: `beeper docs` +📖 **[Read the docs](https://beeper.github.io/cli)** · command manual: `beeper man` · open docs: `beeper docs` ## Features @@ -202,19 +202,19 @@ WhatsApp, Telegram, Discord, iMessage, and the rest show up under `accounts list ## Documentation -Full documentation lives at **[example.com](https://example.com)** +Full documentation lives at **[beeper.github.io/cli](https://beeper.github.io/cli)** (built from [`docs/`](docs/) with Astro Starlight — a fully static site). | Topic | Page | Commands | | --- | --- | --- | -| **Setup + install** | [connect](https://example.com/connect/) · [install](https://example.com/install/) · [auth](https://example.com/auth/) | `setup` · `install desktop` · `install server` · `verify` · `status` · `doctor` · `auth status` | -| **Targets** | [targets](https://example.com/targets/) | `targets list` · `targets add desktop` · `targets add server` · `targets add remote` · `targets use` · `targets status` · `targets logs` | -| **Bridges + accounts** | [accounts](https://example.com/accounts/) | `bridges list` · `bridges show` · `accounts list` · `accounts add` · `accounts show` · `accounts use` · `accounts remove` | -| **Chats** | [chats](https://example.com/chats/) | `chats list` · `chats search` · `chats show` · `chats start` · `chats archive` · `chats pin` · `chats mute` · `chats priority` · `chats remind` · `chats rename` · `chats draft` · `chats focus` | -| **Messages** | [messages](https://example.com/messages/) · [send](https://example.com/send/) · [presence](https://example.com/presence/) | `messages list` · `messages search` · `messages export` · `send text` · `send file` · `send sticker` · `send voice` · `send react` · `presence` | -| **Contacts + media** | [contacts](https://example.com/contacts/) · [media](https://example.com/media/) · [export](https://example.com/export/) | `contacts list` · `contacts search` · `media download` · `export` | -| **Automation** | [scripting](https://example.com/scripting/) · [watch](https://example.com/watch/) · [rpc](https://example.com/rpc/) · [api](https://example.com/api/) | `watch` · `watch --webhook` · `rpc` · `man` · `api get` · `api post` · `api request` | -| **Maintenance** | [config](https://example.com/config/) · [update](https://example.com/update/) | `update` · `config` · `completion` · `docs` · `version` | +| **Setup + install** | [connect](https://beeper.github.io/cli/connect/) · [install](https://beeper.github.io/cli/install/) · [auth](https://beeper.github.io/cli/auth/) | `setup` · `install desktop` · `install server` · `verify` · `status` · `doctor` · `auth status` | +| **Targets** | [targets](https://beeper.github.io/cli/targets/) | `targets list` · `targets add desktop` · `targets add server` · `targets add remote` · `targets use` · `targets status` · `targets logs` | +| **Bridges + accounts** | [accounts](https://beeper.github.io/cli/accounts/) | `bridges list` · `bridges show` · `accounts list` · `accounts add` · `accounts show` · `accounts use` · `accounts remove` | +| **Chats** | [chats](https://beeper.github.io/cli/chats/) | `chats list` · `chats search` · `chats show` · `chats start` · `chats archive` · `chats pin` · `chats mute` · `chats priority` · `chats remind` · `chats rename` · `chats draft` · `chats focus` | +| **Messages** | [messages](https://beeper.github.io/cli/messages/) · [send](https://beeper.github.io/cli/send/) · [presence](https://beeper.github.io/cli/presence/) | `messages list` · `messages search` · `messages export` · `send text` · `send file` · `send sticker` · `send voice` · `send react` · `presence` | +| **Contacts + media** | [contacts](https://beeper.github.io/cli/contacts/) · [media](https://beeper.github.io/cli/media/) · [export](https://beeper.github.io/cli/export/) | `contacts list` · `contacts search` · `media download` · `export` | +| **Automation** | [scripting](https://beeper.github.io/cli/scripting/) · [watch](https://beeper.github.io/cli/watch/) · [rpc](https://beeper.github.io/cli/rpc/) · [api](https://beeper.github.io/cli/api/) | `watch` · `watch --webhook` · `rpc` · `man` · `api get` · `api post` · `api request` | +| **Maintenance** | [config](https://beeper.github.io/cli/config/) · [update](https://beeper.github.io/cli/update/) | `update` · `config` · `completion` · `docs` · `version` | Use `beeper docs` to open the CLI docs and `beeper man` to print the local command manual. To work on the docs site locally: `cd docs && bun install && bun run dev`. @@ -449,7 +449,7 @@ Flags: | `--oauth` | boolean | Authorize the target with browser OAuth/PKCE | | `--remote=` | option | Connect to a remote Beeper Desktop or Server URL | | `--server` | boolean | Set up a local Beeper Server target | -| `--server-env=` | option | Server feed environment: prod or staging Default: prod | +| `--server-env=` | option | Server environment: local, dev, staging, or prod Default: prod | | `--username=` | option | Username to use if setup creates a new account | Examples: @@ -498,7 +498,7 @@ Flags: | Flag | Type | Description | | --- | --- | --- | | `--channel=` | option | Server release channel Default: stable | -| `--server-env=` | option | Server feed environment: prod or staging Default: prod | +| `--server-env=` | option | Server environment: local, dev, staging, or prod Default: prod | Examples: @@ -591,7 +591,7 @@ Flags: | --- | --- | --- | | `--default` | boolean | Set this target as the default after creation | | `--port=` | option | TCP port the managed Desktop will expose its API on | -| `--server-env=` | option | Server feed environment: prod or staging Default: prod | +| `--server-env=` | option | Server environment: local, dev, staging, or prod Default: prod | Examples: @@ -620,7 +620,7 @@ Flags: | --- | --- | --- | | `--default` | boolean | Set this target as the default after creation | | `--port=` | option | TCP port the managed Server will expose its API on | -| `--server-env=` | option | Server feed environment: prod or staging Default: prod | +| `--server-env=` | option | Server environment: local, dev, staging, or prod Default: prod | Examples: diff --git a/packages/cli/src/commands/install/server.ts b/packages/cli/src/commands/install/server.ts index 55f96306..78c7bc65 100644 --- a/packages/cli/src/commands/install/server.ts +++ b/packages/cli/src/commands/install/server.ts @@ -3,12 +3,13 @@ import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { installServer, type InstallChannel } from '../../lib/installations.js' import { pathSetupHint } from '../../lib/env.js' import { printDryRun, printSuccess } from '../../lib/output.js' +import { SERVER_ENVIRONMENTS, normalizeServerEnv } from '../../lib/server-env.js' export default class SetupInstallServer extends BeeperCommand { static override summary = 'Install Beeper Server locally' static override flags = { channel: Flags.string({ options: ['stable', 'nightly'], default: 'stable', description: 'Server release channel' }), - 'server-env': Flags.string({ default: 'prod', description: 'Server feed environment: prod or staging' }), + 'server-env': Flags.string({ default: 'prod', description: 'Server environment: local, dev, staging, or prod', options: [...SERVER_ENVIRONMENTS], parse: async value => normalizeServerEnv(value) }), } async run(): Promise { diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index fa26259c..6b9ded05 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -9,7 +9,7 @@ import { loginWithPKCE } from '../lib/oauth.js' import { findDesktopAppPath, launchDesktopApp, startProfile } from '../lib/profiles.js' import { interactiveEmailSetup } from '../lib/setup-login.js' import { renderStartupLogo } from '../lib/logo.js' -import { SERVER_ENV_API_BASE_URLS, normalizeServerEnv } from '../lib/server-env.js' +import { SERVER_ENVIRONMENTS, SERVER_ENV_API_BASE_URLS, normalizeServerEnv } from '../lib/server-env.js' import { builtInDesktopTargetID, createProfileTarget, @@ -36,7 +36,7 @@ export default class Setup extends BeeperCommand { desktop: Flags.boolean({ default: false, description: 'Set up a local Beeper Desktop target' }), install: Flags.boolean({ default: false, description: 'Allow installing missing managed runtime' }), channel: Flags.string({ options: ['stable', 'nightly'], default: 'stable', description: 'Install release channel' }), - 'server-env': Flags.string({ default: 'prod', description: 'Server feed environment: prod or staging' }), + 'server-env': Flags.string({ default: 'prod', description: 'Server environment: local, dev, staging, or prod', options: [...SERVER_ENVIRONMENTS], parse: async value => normalizeServerEnv(value) }), email: Flags.string({ description: 'Sign in with an email address' }), username: Flags.string({ description: 'Username to use if setup creates a new account' }), } @@ -170,7 +170,8 @@ export default class Setup extends BeeperCommand { const readiness = await evaluateReadiness({ baseURL: target.baseURL, target: target.id }) if (readiness.state === 'target-unreachable' && target.type !== 'desktop') { if (flags.json || !process.stdin.isTTY) { - await printData(currentTargetBrokenOutput(target, readiness), flags.json ? 'json' : 'human') + const serverInstalled = await isServerInstalled() + await printData(currentTargetBrokenOutput(target, readiness, serverInstalled), flags.json ? 'json' : 'human') return } if (await this.handleBrokenCurrentTarget(target, readiness, flags)) return @@ -707,7 +708,7 @@ function installedServerAction(installed: boolean): { id: string; command: strin : action('install-server', 'beeper setup --server --install --yes') } -function currentTargetBrokenOutput(target: Target, readiness: Readiness): Record { +function currentTargetBrokenOutput(target: Target, readiness: Readiness, serverInstalled: boolean): Record { return { state: 'current-target-unreachable', message: `Beeper CLI is set up for ${target.name ?? target.id}, but it is not reachable.`, @@ -717,7 +718,7 @@ function currentTargetBrokenOutput(target: Target, readiness: Readiness): Record availableActions: [ action('retry-current', `beeper setup -t ${target.id}`), action('use-desktop', 'beeper setup --desktop'), - action('install-server', 'beeper setup --server --install --yes'), + installedServerAction(serverInstalled), action('connect-remote', 'beeper setup --remote '), ], } diff --git a/packages/cli/src/commands/targets/add/desktop.ts b/packages/cli/src/commands/targets/add/desktop.ts index 035127e3..2c1732d5 100644 --- a/packages/cli/src/commands/targets/add/desktop.ts +++ b/packages/cli/src/commands/targets/add/desktop.ts @@ -2,6 +2,7 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../../lib/command.js' import { createProfileTarget, readTarget, updateConfig } from '../../../lib/targets.js' import { printDryRun, printSuccess } from '../../../lib/output.js' +import { SERVER_ENVIRONMENTS, normalizeServerEnv } from '../../../lib/server-env.js' export default class TargetsAddDesktop extends BeeperCommand { static override summary = 'Add a managed Beeper Desktop target' @@ -9,7 +10,7 @@ export default class TargetsAddDesktop extends BeeperCommand { static override flags = { port: Flags.integer({ description: 'TCP port the managed Desktop will expose its API on' }), default: Flags.boolean({ default: false, description: 'Set this target as the default after creation' }), - 'server-env': Flags.string({ default: 'prod', description: 'Server feed environment: prod or staging' }), + 'server-env': Flags.string({ default: 'prod', description: 'Server environment: local, dev, staging, or prod', options: [...SERVER_ENVIRONMENTS], parse: async value => normalizeServerEnv(value) }), } async run(): Promise { const { args, flags } = await this.parse(TargetsAddDesktop) diff --git a/packages/cli/src/commands/targets/add/server.ts b/packages/cli/src/commands/targets/add/server.ts index 59fb4d3a..ae3f4a3e 100644 --- a/packages/cli/src/commands/targets/add/server.ts +++ b/packages/cli/src/commands/targets/add/server.ts @@ -2,6 +2,7 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../../lib/command.js' import { createProfileTarget, readTarget, updateConfig } from '../../../lib/targets.js' import { printDryRun, printSuccess } from '../../../lib/output.js' +import { SERVER_ENVIRONMENTS, normalizeServerEnv } from '../../../lib/server-env.js' export default class TargetsAddServer extends BeeperCommand { static override summary = 'Add a managed Beeper Server target' @@ -9,7 +10,7 @@ export default class TargetsAddServer extends BeeperCommand { static override flags = { port: Flags.integer({ description: 'TCP port the managed Server will expose its API on' }), default: Flags.boolean({ default: false, description: 'Set this target as the default after creation' }), - 'server-env': Flags.string({ default: 'prod', description: 'Server feed environment: prod or staging' }), + 'server-env': Flags.string({ default: 'prod', description: 'Server environment: local, dev, staging, or prod', options: [...SERVER_ENVIRONMENTS], parse: async value => normalizeServerEnv(value) }), } async run(): Promise { const { args, flags } = await this.parse(TargetsAddServer) diff --git a/packages/cli/src/lib/installations.ts b/packages/cli/src/lib/installations.ts index 8c7a394b..ad7b9995 100644 --- a/packages/cli/src/lib/installations.ts +++ b/packages/cli/src/lib/installations.ts @@ -148,8 +148,13 @@ export async function checkInstallationUpdate(installation: Installation): Promi export async function installDesktop(options: { channel?: InstallChannel; serverEnv?: string } = {}): Promise { const request = normalizeInstallRequest({ kind: 'desktop', channel: options.channel, serverEnv: options.serverEnv }) - if (request.serverEnv !== 'prod') throw new Error('Desktop non-production installs are not supported by the CLI.') - const feedURL = feedURLFor(request) + const feedRequest = request.serverEnv === 'prod' + ? request + : { ...request, serverEnv: 'prod' as const, apiBaseURL: SERVER_ENV_API_BASE_URLS.prod } + if (request.serverEnv !== feedRequest.serverEnv) { + process.stderr.write(`Desktop ${request.serverEnv} installs use the production update feed; the app will still launch against ${request.serverEnv}.\n`) + } + const feedURL = feedURLFor(feedRequest) const feed = await fetchFeed(feedURL) const downloadURL = feed.url if (!downloadURL) throw new Error('Desktop update feed did not include a download URL.') From f22ce037b3d69efc9e4aeaff9605fb6ad9ce159c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:39:51 +0200 Subject: [PATCH 16/26] fix documentation links --- docs/astro.config.mjs | 10 +++++----- packages/cli/scripts/generate-readme.ts | 9 ++++----- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 2cf60acd..313d8f39 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -9,8 +9,8 @@ import starlight from '@astrojs/starlight'; // When you pick a home for the docs, set `site` to the canonical origin (used // for sitemap + canonical URLs) and, if serving from a sub-path, set `base`. export default defineConfig({ - site: 'https://example.com', - base: '/', + site: 'https://beeper.github.io', + base: '/cli/', output: 'static', trailingSlash: 'always', integrations: [ @@ -27,12 +27,12 @@ export default defineConfig({ { icon: 'github', label: 'GitHub', - href: 'https://github.com/beeper/desktop-api-cli', + href: 'https://github.com/beeper/cli', }, ], editLink: { baseUrl: - 'https://github.com/beeper/desktop-api-cli/edit/main/docs/', + 'https://github.com/beeper/cli/edit/main/docs/', }, customCss: ['./src/styles/theme.css'], // Starlight ships full-text search (Pagefind) and dark mode by default. @@ -84,7 +84,7 @@ export default defineConfig({ { label: 'Updating', link: '/update/' }, { label: 'Full command reference', - link: 'https://github.com/beeper/desktop-api-cli/blob/main/packages/cli/README.md', + link: 'https://github.com/beeper/cli/blob/main/packages/cli/README.md', attrs: { target: '_blank' }, }, { diff --git a/packages/cli/scripts/generate-readme.ts b/packages/cli/scripts/generate-readme.ts index 60b7a47a..cfc17595 100644 --- a/packages/cli/scripts/generate-readme.ts +++ b/packages/cli/scripts/generate-readme.ts @@ -33,11 +33,10 @@ const commandList = commands.map(command => { const examplesByID = new Map(commandManifest.map(item => [item.command, item.examples ?? []])); const commandSections = commands.map(command => commandSection(command)).join('\n\n'); -// Origin where the Astro docs site (in `docs/`) is published. Keep this in sync -// with `site` in `docs/astro.config.mjs`. Until the docs have a permanent home -// this is a placeholder; flip both in one commit when you pick a host. -const docsUrl = 'https://example.com'; -const repoUrl = 'https://github.com/beeper/desktop-api-cli'; +// Public URL where the Astro docs site (in `docs/`) is published. Keep this in +// sync with `site` + `base` in `docs/astro.config.mjs`. +const docsUrl = 'https://beeper.github.io/cli'; +const repoUrl = 'https://github.com/beeper/cli'; const intro = `
From db163a24f6feb77682cba30b706e989f16ea5f1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 18:51:43 +0200 Subject: [PATCH 17/26] cleanup --- .claude/scheduled_tasks.lock | 1 + packages/cli/src/commands/api/get.ts | 5 +++-- packages/cli/src/commands/api/post.ts | 7 ++++--- packages/cli/src/commands/api/request.ts | 7 ++++--- .../cli/src/commands/auth/email/response.ts | 5 +++-- packages/cli/src/commands/auth/logout.ts | 5 +++-- packages/cli/src/commands/bridges/show.ts | 16 ++++------------ packages/cli/src/commands/chats/archive.ts | 1 - packages/cli/src/commands/chats/avatar.ts | 1 - .../cli/src/commands/chats/description.ts | 1 - packages/cli/src/commands/chats/focus.ts | 1 - packages/cli/src/commands/chats/mark-read.ts | 1 - .../cli/src/commands/chats/mark-unread.ts | 1 - .../cli/src/commands/chats/notify-anyway.ts | 1 - packages/cli/src/commands/chats/pin.ts | 1 - packages/cli/src/commands/chats/remind.ts | 1 - packages/cli/src/commands/chats/unarchive.ts | 1 - packages/cli/src/commands/chats/unmute.ts | 1 - packages/cli/src/commands/chats/unpin.ts | 1 - packages/cli/src/commands/chats/unremind.ts | 1 - packages/cli/src/commands/config/reset.ts | 5 +++-- packages/cli/src/commands/config/set.ts | 5 +++-- packages/cli/src/commands/doctor.ts | 6 +++++- packages/cli/src/commands/media/download.ts | 5 +++-- packages/cli/src/commands/messages/search.ts | 5 +++-- .../cli/src/commands/plugins/available.ts | 15 +++++++++------ packages/cli/src/commands/resolve/chat.ts | 13 ++----------- packages/cli/src/commands/setup.ts | 11 +++++------ packages/cli/src/commands/targets/disable.ts | 2 +- packages/cli/src/commands/targets/enable.ts | 2 +- packages/cli/src/commands/targets/list.ts | 9 +++------ packages/cli/src/commands/targets/logs.ts | 5 ++--- packages/cli/src/commands/targets/remove.ts | 7 ++----- packages/cli/src/commands/targets/restart.ts | 2 +- packages/cli/src/commands/targets/show.ts | 11 ++++------- packages/cli/src/commands/targets/start.ts | 2 +- packages/cli/src/commands/targets/status.ts | 10 ++++------ packages/cli/src/commands/targets/stop.ts | 2 +- packages/cli/src/commands/targets/use.ts | 7 ++----- packages/cli/src/commands/verify/cancel.ts | 2 +- packages/cli/src/lib/command-metadata.ts | 19 ++++++++++--------- packages/cli/src/lib/command.ts | 7 +------ packages/cli/src/lib/export/index.ts | 2 -- packages/cli/src/lib/ink/render.tsx | 7 +++---- packages/cli/src/lib/ink/theme.ts | 3 +-- packages/cli/src/lib/installations.ts | 4 +--- packages/cli/src/lib/profiles.ts | 17 +++-------------- packages/cli/src/lib/resolve.ts | 14 +++----------- packages/cli/src/lib/setup-login.ts | 5 +++-- 49 files changed, 103 insertions(+), 160 deletions(-) create mode 100644 .claude/scheduled_tasks.lock diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock new file mode 100644 index 00000000..b9663f66 --- /dev/null +++ b/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"2da33c33-52d4-4916-961e-72cd6ae9594a","pid":18713,"procStart":"Sat May 30 16:14:01 2026","acquiredAt":1780159461036} \ No newline at end of file diff --git a/packages/cli/src/commands/api/get.ts b/packages/cli/src/commands/api/get.ts index a36515d4..850156d4 100644 --- a/packages/cli/src/commands/api/get.ts +++ b/packages/cli/src/commands/api/get.ts @@ -16,11 +16,12 @@ export default class ApiGet extends BeeperCommand { async run(): Promise { const { args, flags } = await this.parse(ApiGet) + const format = flags.json ? 'json' : 'human' if (flags['no-auth']) { - await printData(await appRequest('GET', args.path, { baseURL: flags['base-url'], target: flags.target, token: false }), flags.json ? 'json' : 'human') + await printData(await appRequest('GET', args.path, { baseURL: flags['base-url'], target: flags.target, token: false }), format) return } const client = await createClient(flags) - await printData(await client.get(args.path), flags.json ? 'json' : 'human') + await printData(await client.get(args.path), format) } } diff --git a/packages/cli/src/commands/api/post.ts b/packages/cli/src/commands/api/post.ts index 429f9e22..c381a3bf 100644 --- a/packages/cli/src/commands/api/post.ts +++ b/packages/cli/src/commands/api/post.ts @@ -18,6 +18,7 @@ export default class ApiPost extends BeeperCommand { async run(): Promise { const { args, flags } = await this.parse(ApiPost) ensureWritable(flags) + const format = flags.json ? 'json' : 'human' let body: Record try { body = JSON.parse(flags.body) as Record @@ -25,14 +26,14 @@ export default class ApiPost extends BeeperCommand { throw new Error(`--body is not valid JSON: ${flags.body}`) } if (flags['dry-run']) { - await printDryRun('api.post', { method: 'POST', path: args.path, body, noAuth: flags['no-auth'], target: flags.target, baseURL: flags['base-url'] }, flags.json ? 'json' : 'human') + await printDryRun('api.post', { method: 'POST', path: args.path, body, noAuth: flags['no-auth'], target: flags.target, baseURL: flags['base-url'] }, format) return } if (flags['no-auth']) { - await printData(await appRequest('POST', args.path, { baseURL: flags['base-url'], body, target: flags.target, token: false }), flags.json ? 'json' : 'human') + await printData(await appRequest('POST', args.path, { baseURL: flags['base-url'], body, target: flags.target, token: false }), format) return } const client = await createClient(flags) - await printData(await client.post(args.path, { body }), flags.json ? 'json' : 'human') + await printData(await client.post(args.path, { body }), format) } } diff --git a/packages/cli/src/commands/api/request.ts b/packages/cli/src/commands/api/request.ts index f75cd3d5..c551e450 100644 --- a/packages/cli/src/commands/api/request.ts +++ b/packages/cli/src/commands/api/request.ts @@ -19,15 +19,16 @@ export default class ApiRequest extends BeeperCommand { const { args, flags } = await this.parse(ApiRequest) const method = args.method as AppRequestMethod if (method !== 'GET') ensureWritable(flags) + const format = flags.json ? 'json' : 'human' const body = flags.body ? JSON.parse(flags.body) as Record : undefined if (flags['dry-run'] && method !== 'GET') { - await printDryRun('api.request', { method, path: args.path, body, noAuth: flags['no-auth'], target: flags.target, baseURL: flags['base-url'] }, flags.json ? 'json' : 'human') + await printDryRun('api.request', { method, path: args.path, body, noAuth: flags['no-auth'], target: flags.target, baseURL: flags['base-url'] }, format) return } if (flags['no-auth']) { - await printData(await appRequest(method, args.path, { baseURL: flags['base-url'], body, target: flags.target, token: false }), flags.json ? 'json' : 'human') + await printData(await appRequest(method, args.path, { baseURL: flags['base-url'], body, target: flags.target, token: false }), format) return } - await printData(await appRequest(method, args.path, { baseURL: flags['base-url'], body, target: flags.target }), flags.json ? 'json' : 'human') + await printData(await appRequest(method, args.path, { baseURL: flags['base-url'], body, target: flags.target }), format) } } diff --git a/packages/cli/src/commands/auth/email/response.ts b/packages/cli/src/commands/auth/email/response.ts index f4a34d9d..3268af76 100644 --- a/packages/cli/src/commands/auth/email/response.ts +++ b/packages/cli/src/commands/auth/email/response.ts @@ -16,9 +16,10 @@ export default class AuthEmailResponse extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(AuthEmailResponse) ensureWritable(flags) + const format = flags.json ? 'json' : 'human' const target = await resolveTarget({ target: flags.target, baseURL: flags['base-url'] }) if (flags['dry-run']) { - await printDryRun('auth.email.response', { target: target.id, baseURL: target.baseURL, setupRequestID: flags['setup-request-id'], username: flags.username, yes: flags.yes }, flags.json ? 'json' : 'human') + await printDryRun('auth.email.response', { target: target.id, baseURL: target.baseURL, setupRequestID: flags['setup-request-id'], username: flags.username, yes: flags.yes }, format) return } const data = await finishEmailSetup(target, { @@ -28,6 +29,6 @@ export default class AuthEmailResponse extends BeeperCommand { username: flags.username, yes: flags.yes, }) - await printData(data, flags.json ? 'json' : 'human') + await printData(data, format) } } diff --git a/packages/cli/src/commands/auth/logout.ts b/packages/cli/src/commands/auth/logout.ts index d4e9e8ab..fc8756d5 100644 --- a/packages/cli/src/commands/auth/logout.ts +++ b/packages/cli/src/commands/auth/logout.ts @@ -8,10 +8,11 @@ export default class AuthLogout extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(AuthLogout) if (!flags['dry-run']) ensureWritable(flags) + const format = flags.json ? 'json' : 'human' const target = await resolveTarget({ target: flags.target, baseURL: flags['base-url'] }) const token = target.auth?.accessToken if (flags['dry-run']) { - await printDryRun('auth.logout', { target: target.id, baseURL: target.baseURL, hadToken: Boolean(token), revokeToken: Boolean(token) }, flags.json ? 'json' : 'human') + await printDryRun('auth.logout', { target: target.id, baseURL: target.baseURL, hadToken: Boolean(token), revokeToken: Boolean(token) }, format) return } if (process.env.BEEPER_ACCESS_TOKEN && !target.auth?.accessToken) { @@ -28,6 +29,6 @@ export default class AuthLogout extends BeeperCommand { revoked = Boolean(response?.ok) await clearTargetAuth(target) } - await printSuccess({ message: 'Logged out', detail: token ? 'local token cleared' : 'no token was stored', data: { revoked, hadToken: Boolean(token) } }, flags.json ? 'json' : 'human') + await printSuccess({ message: 'Logged out', detail: token ? 'local token cleared' : 'no token was stored', data: { revoked, hadToken: Boolean(token) } }, format) } } diff --git a/packages/cli/src/commands/bridges/show.ts b/packages/cli/src/commands/bridges/show.ts index 76840af4..592a32eb 100644 --- a/packages/cli/src/commands/bridges/show.ts +++ b/packages/cli/src/commands/bridges/show.ts @@ -30,21 +30,13 @@ export default class BridgesShow extends BeeperCommand { function resolveBridge(items: Array>, input: string): Record { const normalizedInput = normalize(input) - const exact = items.filter(item => [ - item.id, - item.displayName, - item.network, - item.type, - ].some(value => normalize(value) === normalizedInput)) + const fields = (item: Record): unknown[] => [item.id, item.displayName, item.network, item.type] + + const exact = items.filter(item => fields(item).some(value => normalize(value) === normalizedInput)) if (exact.length === 1) return exact[0]! if (exact.length > 1) throw ambiguousBridge(input, exact) - const partial = items.filter(item => [ - item.id, - item.displayName, - item.network, - item.type, - ].some(value => normalize(value).includes(normalizedInput))) + const partial = items.filter(item => fields(item).some(value => normalize(value).includes(normalizedInput))) if (partial.length === 1) return partial[0]! if (partial.length > 1) throw ambiguousBridge(input, partial) diff --git a/packages/cli/src/commands/chats/archive.ts b/packages/cli/src/commands/chats/archive.ts index 150a2883..da099632 100644 --- a/packages/cli/src/commands/chats/archive.ts +++ b/packages/cli/src/commands/chats/archive.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printData, printDryRun } from '../../lib/output.js' diff --git a/packages/cli/src/commands/chats/avatar.ts b/packages/cli/src/commands/chats/avatar.ts index 01da3592..7c97c2bc 100644 --- a/packages/cli/src/commands/chats/avatar.ts +++ b/packages/cli/src/commands/chats/avatar.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printData, printDryRun } from '../../lib/output.js' diff --git a/packages/cli/src/commands/chats/description.ts b/packages/cli/src/commands/chats/description.ts index 1f081774..0cb2022a 100644 --- a/packages/cli/src/commands/chats/description.ts +++ b/packages/cli/src/commands/chats/description.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printData, printDryRun } from '../../lib/output.js' diff --git a/packages/cli/src/commands/chats/focus.ts b/packages/cli/src/commands/chats/focus.ts index 3903edf8..c9e811b8 100644 --- a/packages/cli/src/commands/chats/focus.ts +++ b/packages/cli/src/commands/chats/focus.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printData, printDryRun } from '../../lib/output.js' diff --git a/packages/cli/src/commands/chats/mark-read.ts b/packages/cli/src/commands/chats/mark-read.ts index 72ddf591..82dceed9 100644 --- a/packages/cli/src/commands/chats/mark-read.ts +++ b/packages/cli/src/commands/chats/mark-read.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printData, printDryRun } from '../../lib/output.js' diff --git a/packages/cli/src/commands/chats/mark-unread.ts b/packages/cli/src/commands/chats/mark-unread.ts index 73acc5c7..e27c9839 100644 --- a/packages/cli/src/commands/chats/mark-unread.ts +++ b/packages/cli/src/commands/chats/mark-unread.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printData, printDryRun } from '../../lib/output.js' diff --git a/packages/cli/src/commands/chats/notify-anyway.ts b/packages/cli/src/commands/chats/notify-anyway.ts index 418d5237..4174098d 100644 --- a/packages/cli/src/commands/chats/notify-anyway.ts +++ b/packages/cli/src/commands/chats/notify-anyway.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printData, printDryRun } from '../../lib/output.js' diff --git a/packages/cli/src/commands/chats/pin.ts b/packages/cli/src/commands/chats/pin.ts index 642265e9..7ee34d16 100644 --- a/packages/cli/src/commands/chats/pin.ts +++ b/packages/cli/src/commands/chats/pin.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printData, printDryRun } from '../../lib/output.js' diff --git a/packages/cli/src/commands/chats/remind.ts b/packages/cli/src/commands/chats/remind.ts index 4bd61a01..f2599030 100644 --- a/packages/cli/src/commands/chats/remind.ts +++ b/packages/cli/src/commands/chats/remind.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printDryRun, printSuccess } from '../../lib/output.js' diff --git a/packages/cli/src/commands/chats/unarchive.ts b/packages/cli/src/commands/chats/unarchive.ts index 2da004c4..17f55f9e 100644 --- a/packages/cli/src/commands/chats/unarchive.ts +++ b/packages/cli/src/commands/chats/unarchive.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printData, printDryRun } from '../../lib/output.js' diff --git a/packages/cli/src/commands/chats/unmute.ts b/packages/cli/src/commands/chats/unmute.ts index 46148e78..6466e892 100644 --- a/packages/cli/src/commands/chats/unmute.ts +++ b/packages/cli/src/commands/chats/unmute.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printData, printDryRun } from '../../lib/output.js' diff --git a/packages/cli/src/commands/chats/unpin.ts b/packages/cli/src/commands/chats/unpin.ts index 2839626d..1053c681 100644 --- a/packages/cli/src/commands/chats/unpin.ts +++ b/packages/cli/src/commands/chats/unpin.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printData, printDryRun } from '../../lib/output.js' diff --git a/packages/cli/src/commands/chats/unremind.ts b/packages/cli/src/commands/chats/unremind.ts index 92dd5a73..5fa5e9ef 100644 --- a/packages/cli/src/commands/chats/unremind.ts +++ b/packages/cli/src/commands/chats/unremind.ts @@ -1,5 +1,4 @@ import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printDryRun, printSuccess } from '../../lib/output.js' diff --git a/packages/cli/src/commands/config/reset.ts b/packages/cli/src/commands/config/reset.ts index e3066dad..8d3b9636 100644 --- a/packages/cli/src/commands/config/reset.ts +++ b/packages/cli/src/commands/config/reset.ts @@ -8,11 +8,12 @@ export default class ConfigReset extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(ConfigReset) ensureWritable(flags) + const format = flags.json ? 'json' : 'human' if (flags['dry-run']) { - await printDryRun('config.reset', {}, flags.json ? 'json' : 'human') + await printDryRun('config.reset', {}, format) return } await resetConfig() - await printSuccess({ message: 'Config reset' }, flags.json ? 'json' : 'human') + await printSuccess({ message: 'Config reset' }, format) } } diff --git a/packages/cli/src/commands/config/set.ts b/packages/cli/src/commands/config/set.ts index 1bc9e5b2..2cd95099 100644 --- a/packages/cli/src/commands/config/set.ts +++ b/packages/cli/src/commands/config/set.ts @@ -13,9 +13,10 @@ export default class ConfigSet extends BeeperCommand { async run(): Promise { const { args, flags } = await this.parse(ConfigSet) ensureWritable(flags) + const format = flags.json ? 'json' : 'human' const nextValue = args.value === '' ? undefined : args.value if (flags['dry-run']) { - await printDryRun('config.set', { [args.key]: nextValue }, flags.json ? 'json' : 'human') + await printDryRun('config.set', { [args.key]: nextValue }, format) return } await updateConfig(config => ({ ...config, [args.key]: nextValue })) @@ -23,6 +24,6 @@ export default class ConfigSet extends BeeperCommand { message: nextValue === undefined ? `Cleared ${args.key}` : `Set ${args.key}`, detail: nextValue, data: { [args.key]: nextValue }, - }, flags.json ? 'json' : 'human') + }, format) } } diff --git a/packages/cli/src/commands/doctor.ts b/packages/cli/src/commands/doctor.ts index e0b428d1..a07a5581 100644 --- a/packages/cli/src/commands/doctor.ts +++ b/packages/cli/src/commands/doctor.ts @@ -10,7 +10,11 @@ export default class Doctor extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(Doctor) const target = await resolveTarget({ target: flags.target, baseURL: flags['base-url'] }) - const checks = { target: await targetLiveStatus(target), readiness: await evaluateReadiness({ baseURL: target.baseURL, target: target.id }) } + const [targetStatus, readiness] = await Promise.all([ + targetLiveStatus(target), + evaluateReadiness({ baseURL: target.baseURL, target: target.id }), + ]) + const checks = { target: targetStatus, readiness } await printData({ ok: checks.readiness.state === 'ready', checks }, flags.json ? 'json' : 'human') if (checks.readiness.state !== 'ready') this.exit(ExitCodes.NotReady) } diff --git a/packages/cli/src/commands/media/download.ts b/packages/cli/src/commands/media/download.ts index a6c01001..3c86c8d7 100644 --- a/packages/cli/src/commands/media/download.ts +++ b/packages/cli/src/commands/media/download.ts @@ -12,9 +12,10 @@ export default class MediaDownload extends BeeperCommand { } async run(): Promise { const { args, flags } = await this.parse(MediaDownload) + const format = flags.json ? 'json' : 'human' if (flags['dry-run'] && flags.out !== '-') { ensureWritable(flags) - await printDryRun('media.download', { url: args.url, out: flags.out }, flags.json ? 'json' : 'human') + await printDryRun('media.download', { url: args.url, out: flags.out }, format) return } const client = await createClient(flags) @@ -28,6 +29,6 @@ export default class MediaDownload extends BeeperCommand { await mkdir(flags.out, { recursive: true }) const path = join(flags.out, basename(new URL(args.url).pathname) || 'media') await writeFile(path, buffer) - await printSuccess({ message: 'Downloaded media', detail: path, data: { path, bytes: buffer.length } }, flags.json ? 'json' : 'human') + await printSuccess({ message: 'Downloaded media', detail: path, data: { path, bytes: buffer.length } }, format) } } diff --git a/packages/cli/src/commands/messages/search.ts b/packages/cli/src/commands/messages/search.ts index fe82ed54..2515087b 100644 --- a/packages/cli/src/commands/messages/search.ts +++ b/packages/cli/src/commands/messages/search.ts @@ -60,11 +60,12 @@ export default class MessagesSearch extends BeeperCommand { } const useSpinner = !isMachineReadableOutput(flags.ids ? 'ids' : flags.json ? 'json' : 'human') const label = args.query ? `Searching messages for "${args.query}"…` : 'Searching messages…' + const collect = () => collectPage(client.messages.search(params), flags.limit) const items = useSpinner - ? await withSpinner(label, () => collectPage(client.messages.search(params), flags.limit), { + ? await withSpinner(label, collect, { done: value => `${value.length} match${value.length === 1 ? '' : 'es'}`, }) - : await collectPage(client.messages.search(params), flags.limit) + : await collect() if (flags.ids) { printIDs(items) return diff --git a/packages/cli/src/commands/plugins/available.ts b/packages/cli/src/commands/plugins/available.ts index 4147b30b..e2f16dd6 100644 --- a/packages/cli/src/commands/plugins/available.ts +++ b/packages/cli/src/commands/plugins/available.ts @@ -9,12 +9,15 @@ export default class PluginsAvailable extends BeeperCommand { const { flags } = await this.parse(PluginsAvailable) const installed = new Set(this.config.plugins.keys()) const corePlugins = new Set((this.config.pjson.oclif.plugins ?? []) as string[]) - const plugins = recommendedPlugins.map(plugin => ({ - ...plugin, - installed: installed.has(plugin.name), - status: installed.has(plugin.name) ? 'installed' : 'not installed', - core: corePlugins.has(plugin.name), - })) + const plugins = recommendedPlugins.map(plugin => { + const isInstalled = installed.has(plugin.name) + return { + ...plugin, + installed: isInstalled, + status: isInstalled ? 'installed' : 'not installed', + core: corePlugins.has(plugin.name), + } + }) await printData(plugins, flags.json ? 'json' : 'human') } diff --git a/packages/cli/src/commands/resolve/chat.ts b/packages/cli/src/commands/resolve/chat.ts index 82240f61..4586e190 100644 --- a/packages/cli/src/commands/resolve/chat.ts +++ b/packages/cli/src/commands/resolve/chat.ts @@ -2,7 +2,7 @@ import { Args, Flags } from '@oclif/core' import { BeeperCommand } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { notFound } from '../../lib/errors.js' -import { printData } from '../../lib/output.js' +import { collectPage, printData } from '../../lib/output.js' import { resolveAccountIDs } from '../../lib/resolve.js' export default class ResolveChat extends BeeperCommand { @@ -20,7 +20,7 @@ export default class ResolveChat extends BeeperCommand { const { args, flags } = await this.parse(ResolveChat) const client = await createClient(flags) const accountIDs = await resolveAccountIDs(client, flags.account, { allowMultiplePerInput: true }) - const candidates = await collect(client.chats.search({ accountIDs, query: args.selector, scope: 'titles' }), flags.limit) + const candidates = await collectPage(client.chats.search({ accountIDs, query: args.selector, scope: 'titles' }), flags.limit) const normalized = normalize(args.selector) const exact = candidates.filter(chat => normalize(chat.id) === normalized || @@ -42,15 +42,6 @@ export default class ResolveChat extends BeeperCommand { type Chat = Record -async function collect(iterable: AsyncIterable, limit: number): Promise { - const items: Chat[] = [] - for await (const item of iterable) { - items.push(item as Chat) - if (items.length >= limit) break - } - return items -} - function chatCandidate(chat: Chat, pick: number): Record { return { pick, diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index 6b9ded05..f1ab07a3 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -137,21 +137,21 @@ export default class Setup extends BeeperCommand { } else if (flags.json || !process.stdin.isTTY) { await printData(setupStateOutput(detected, target), flags.json ? 'json' : 'human') return - } else if (detected.kind === 'installed-not-running' && !flags.json && process.stdin.isTTY) { + } else if (detected.kind === 'installed-not-running') { printStatus('Found Beeper Desktop on this device.', 'installed, not running') const shouldLaunch = flags.yes || await promptYesNoDefaultYes('Launch Beeper Desktop now?') if (shouldLaunch) { await launchAndPoll(target, setupCmd, flags) return } - } else if (detected.kind === 'running-signed-out' && !flags.json && process.stdin.isTTY) { + } else if (detected.kind === 'running-signed-out') { printStatus('Found Beeper Desktop on this device.', 'running, signed out') const shouldOpen = flags.yes || await promptYesNoDefaultYes('Open Beeper Desktop so you can sign in?') if (shouldOpen) { await launchAndPoll(target, setupCmd, flags) return } - } else if (detected.kind === 'session-unreadable' && !flags.json && process.stdin.isTTY) { + } else if (detected.kind === 'session-unreadable') { printStatus('Found Beeper Desktop on this device.', 'signed in, but CLI could not read the local session') process.stdout.write('You can still connect through Beeper Desktop.\n') if (flags.debug) process.stdout.write(`\n${detected.reason}\n`) @@ -161,7 +161,7 @@ export default class Setup extends BeeperCommand { await this.setupOAuth(target, flags) return } - } else if (detected.kind === 'not-installed' && !flags.json && process.stdin.isTTY) { + } else if (detected.kind === 'not-installed') { await this.setupFromChoice(flags) return } @@ -234,8 +234,7 @@ export default class Setup extends BeeperCommand { private async setupManaged(type: 'desktop' | 'server', flags: SetupFlags): Promise { if (flags.install) { if ((flags.json || !process.stdin.isTTY) && !flags.yes) throw new Error('Install requires --install --yes in non-interactive mode.') - if (type === 'desktop') await installWithCopy('desktop', flags) - else await installWithCopy('server', flags) + await installWithCopy(type, flags) } const id = flags.target ?? type const target = await readTarget(id) ?? await createProfileTarget(type, id, { serverEnv: flags['server-env'], port: undefined }) diff --git a/packages/cli/src/commands/targets/disable.ts b/packages/cli/src/commands/targets/disable.ts index 80f61a5a..b2afabc0 100644 --- a/packages/cli/src/commands/targets/disable.ts +++ b/packages/cli/src/commands/targets/disable.ts @@ -1,6 +1,6 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { readTarget, resolveTarget } from '../../lib/targets.js' +import { resolveTarget } from '../../lib/targets.js' import { assertServerProfile, disableProfile } from '../../lib/profiles.js' import { printDryRun, printSuccess } from '../../lib/output.js' diff --git a/packages/cli/src/commands/targets/enable.ts b/packages/cli/src/commands/targets/enable.ts index 468f747a..89ea1e68 100644 --- a/packages/cli/src/commands/targets/enable.ts +++ b/packages/cli/src/commands/targets/enable.ts @@ -1,6 +1,6 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { readTarget, resolveTarget } from '../../lib/targets.js' +import { resolveTarget } from '../../lib/targets.js' import { assertServerProfile, enableProfile } from '../../lib/profiles.js' import { printDryRun, printSuccess } from '../../lib/output.js' diff --git a/packages/cli/src/commands/targets/list.ts b/packages/cli/src/commands/targets/list.ts index e8721e76..9b05baa2 100644 --- a/packages/cli/src/commands/targets/list.ts +++ b/packages/cli/src/commands/targets/list.ts @@ -1,10 +1,7 @@ -import { Args, Flags } from '@oclif/core' -import { readFile } from 'node:fs/promises' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { builtInDesktopTargetID, createProfileTarget, listTargets, readConfig, readTarget, removeTarget, resolveTarget, updateConfig, writeTarget, type Target } from '../../lib/targets.js' -import { disableProfile, enableProfile, profileErrorLogPath, profileLogPath, profileStatus, startProfile, stopProfile } from '../../lib/profiles.js' +import { BeeperCommand } from '../../lib/command.js' +import { builtInDesktopTargetID, listTargets, readConfig, type Target } from '../../lib/targets.js' import { targetLiveStatus } from '../../lib/target-status.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData } from '../../lib/output.js' export default class TargetsList extends BeeperCommand { static override summary = 'List configured Beeper targets' diff --git a/packages/cli/src/commands/targets/logs.ts b/packages/cli/src/commands/targets/logs.ts index 21c23dc2..2844eb8b 100644 --- a/packages/cli/src/commands/targets/logs.ts +++ b/packages/cli/src/commands/targets/logs.ts @@ -2,7 +2,7 @@ import { Args, Flags } from '@oclif/core' import { readdir, readFile, stat } from 'node:fs/promises' import { join } from 'node:path' import { BeeperCommand } from '../../lib/command.js' -import { customTargetID, readTarget, resolveTarget } from '../../lib/targets.js' +import { customTargetID, resolveTarget } from '../../lib/targets.js' import { desktopLogDir, profileErrorLogPath, profileLogPath } from '../../lib/profiles.js' export default class TargetsLogs extends BeeperCommand { @@ -26,8 +26,7 @@ export default class TargetsLogs extends BeeperCommand { } const files = await listLogFiles(desktopLogDir(target.managed ? target : undefined)) const selected = flags.all ? files : files.slice(0, flags.files) - for (const file of files) { - if (!selected.includes(file)) continue + for (const file of selected) { await printLogFile(file, flags.lines) } } diff --git a/packages/cli/src/commands/targets/remove.ts b/packages/cli/src/commands/targets/remove.ts index 82cbb948..988e25ad 100644 --- a/packages/cli/src/commands/targets/remove.ts +++ b/packages/cli/src/commands/targets/remove.ts @@ -1,9 +1,6 @@ -import { Args, Flags } from '@oclif/core' -import { readFile } from 'node:fs/promises' +import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createProfileTarget, listTargets, readConfig, readTarget, removeTarget, resolveTarget, updateConfig, writeTarget, type Target } from '../../lib/targets.js' -import { disableProfile, enableProfile, profileErrorLogPath, profileLogPath, profileStatus, startProfile, stopProfile } from '../../lib/profiles.js' -import { targetLiveStatus } from '../../lib/target-status.js' +import { removeTarget } from '../../lib/targets.js' import { printDryRun, printSuccess } from '../../lib/output.js' export default class TargetsRemove extends BeeperCommand { diff --git a/packages/cli/src/commands/targets/restart.ts b/packages/cli/src/commands/targets/restart.ts index a97ccea5..30966768 100644 --- a/packages/cli/src/commands/targets/restart.ts +++ b/packages/cli/src/commands/targets/restart.ts @@ -1,6 +1,6 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { readTarget, resolveTarget } from '../../lib/targets.js' +import { resolveTarget } from '../../lib/targets.js' import { assertServerProfile, startProfile, stopProfile } from '../../lib/profiles.js' import { printDryRun, printSuccess } from '../../lib/output.js' diff --git a/packages/cli/src/commands/targets/show.ts b/packages/cli/src/commands/targets/show.ts index 2fc64d07..892672c4 100644 --- a/packages/cli/src/commands/targets/show.ts +++ b/packages/cli/src/commands/targets/show.ts @@ -1,10 +1,7 @@ -import { Args, Flags } from '@oclif/core' -import { readFile } from 'node:fs/promises' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createProfileTarget, listTargets, readConfig, readTarget, removeTarget, resolveTarget, updateConfig, writeTarget, type Target } from '../../lib/targets.js' -import { disableProfile, enableProfile, profileErrorLogPath, profileLogPath, profileStatus, startProfile, stopProfile } from '../../lib/profiles.js' -import { targetLiveStatus } from '../../lib/target-status.js' -import { printData, printSuccess } from '../../lib/output.js' +import { Args } from '@oclif/core' +import { BeeperCommand } from '../../lib/command.js' +import { resolveTarget } from '../../lib/targets.js' +import { printData } from '../../lib/output.js' export default class TargetsShow extends BeeperCommand { static override summary = 'Show target details' diff --git a/packages/cli/src/commands/targets/start.ts b/packages/cli/src/commands/targets/start.ts index 956d621f..b659aaaf 100644 --- a/packages/cli/src/commands/targets/start.ts +++ b/packages/cli/src/commands/targets/start.ts @@ -1,6 +1,6 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { customTargetID, readTarget, resolveTarget } from '../../lib/targets.js' +import { customTargetID, resolveTarget } from '../../lib/targets.js' import { launchDesktopApp, startProfile } from '../../lib/profiles.js' import { printDryRun, printSuccess } from '../../lib/output.js' diff --git a/packages/cli/src/commands/targets/status.ts b/packages/cli/src/commands/targets/status.ts index 531ce441..a8cf970d 100644 --- a/packages/cli/src/commands/targets/status.ts +++ b/packages/cli/src/commands/targets/status.ts @@ -1,10 +1,8 @@ -import { Args, Flags } from '@oclif/core' -import { readFile } from 'node:fs/promises' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createProfileTarget, listTargets, readConfig, readTarget, removeTarget, resolveTarget, updateConfig, writeTarget, type Target } from '../../lib/targets.js' -import { disableProfile, enableProfile, profileErrorLogPath, profileLogPath, profileStatus, startProfile, stopProfile } from '../../lib/profiles.js' +import { Args } from '@oclif/core' +import { BeeperCommand } from '../../lib/command.js' +import { resolveTarget } from '../../lib/targets.js' import { targetLiveStatus } from '../../lib/target-status.js' -import { printData, printSuccess } from '../../lib/output.js' +import { printData } from '../../lib/output.js' export default class TargetsStatus extends BeeperCommand { static override summary = 'Check endpoint and process reachability for a target' diff --git a/packages/cli/src/commands/targets/stop.ts b/packages/cli/src/commands/targets/stop.ts index 65444ad0..31d96a95 100644 --- a/packages/cli/src/commands/targets/stop.ts +++ b/packages/cli/src/commands/targets/stop.ts @@ -1,6 +1,6 @@ import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { readTarget, resolveTarget } from '../../lib/targets.js' +import { resolveTarget } from '../../lib/targets.js' import { assertServerProfile, stopProfile } from '../../lib/profiles.js' import { printDryRun, printSuccess } from '../../lib/output.js' diff --git a/packages/cli/src/commands/targets/use.ts b/packages/cli/src/commands/targets/use.ts index 92e385d2..3dbeca6b 100644 --- a/packages/cli/src/commands/targets/use.ts +++ b/packages/cli/src/commands/targets/use.ts @@ -1,9 +1,6 @@ -import { Args, Flags } from '@oclif/core' -import { readFile } from 'node:fs/promises' +import { Args } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createProfileTarget, listTargets, readConfig, readTarget, removeTarget, resolveTarget, updateConfig, writeTarget, type Target } from '../../lib/targets.js' -import { disableProfile, enableProfile, profileErrorLogPath, profileLogPath, profileStatus, startProfile, stopProfile } from '../../lib/profiles.js' -import { targetLiveStatus } from '../../lib/target-status.js' +import { readTarget, updateConfig } from '../../lib/targets.js' import { printDryRun, printSuccess } from '../../lib/output.js' export default class TargetsUse extends BeeperCommand { diff --git a/packages/cli/src/commands/verify/cancel.ts b/packages/cli/src/commands/verify/cancel.ts index ce8df653..a2c50f02 100644 --- a/packages/cli/src/commands/verify/cancel.ts +++ b/packages/cli/src/commands/verify/cancel.ts @@ -10,11 +10,11 @@ export default class AuthVerifyCancel extends BeeperCommand { async run(): Promise { const { flags } = await this.parse(AuthVerifyCancel) ensureWritable(flags) - const client = await createClient(flags) if (flags['dry-run']) { await printDryRun('verify.cancel', { id: flags.id ?? 'active' }, flags.json ? 'json' : 'human') return } + const client = await createClient(flags) await printData(await client.app.verifications.cancel(flags.id ?? 'active', {}), flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/lib/command-metadata.ts b/packages/cli/src/lib/command-metadata.ts index 3d3f3c4a..214abf19 100644 --- a/packages/cli/src/lib/command-metadata.ts +++ b/packages/cli/src/lib/command-metadata.ts @@ -6,19 +6,20 @@ export type CommandMetadata = { related: string[] } +const mutatingRoots = new Set(['export', 'install', 'presence', 'send', 'setup', 'update']) +const mutatingVerbs = new Set([ + 'add', 'approve', 'archive', 'avatar', 'cancel', 'delete', 'description', 'disable', 'disappear', 'download', + 'draft', 'edit', 'enable', 'export', 'focus', 'logout', 'mark-read', 'mark-unread', 'mute', 'notify-anyway', + 'pin', 'post', 'priority', 'qr-confirm', 'qr-scan', 'recovery-key', 'remind', 'remove', 'rename', 'reset', + 'reset-recovery-key', 'response', 'restart', 'sas', 'sas-confirm', 'set', 'start', 'stop', 'unarchive', + 'unmute', 'unpin', 'unremind', 'use', +]) +const localOnly = new Set(['completion', 'config', 'docs', 'man', 'schema', 'version']) + export function metadataForCommand(command: string): CommandMetadata { const parts = command.split(' ') const root = parts[0] ?? '' - const mutatingRoots = new Set(['export', 'install', 'presence', 'send', 'setup', 'update']) - const mutatingVerbs = new Set([ - 'add', 'approve', 'archive', 'avatar', 'cancel', 'delete', 'description', 'disable', 'disappear', 'download', - 'draft', 'edit', 'enable', 'export', 'focus', 'logout', 'mark-read', 'mark-unread', 'mute', 'notify-anyway', - 'pin', 'post', 'priority', 'qr-confirm', 'qr-scan', 'recovery-key', 'remind', 'remove', 'rename', 'reset', - 'reset-recovery-key', 'response', 'restart', 'sas', 'sas-confirm', 'set', 'start', 'stop', 'unarchive', - 'unmute', 'unpin', 'unremind', 'use', - ]) const mutates = command === 'verify' || command === 'api request' || mutatingRoots.has(root) || parts.some(part => mutatingVerbs.has(part ?? '')) - const localOnly = new Set(['completion', 'config', 'docs', 'man', 'schema', 'version']) const requiresAuth = !localOnly.has(root) && command !== 'targets list' && !command.startsWith('targets add') && !command.startsWith('install ') const selectors = [ command.includes('chats ') || command.includes('messages ') || command.startsWith('send ') || command === 'presence' || command.startsWith('resolve chat') ? 'chat' : undefined, diff --git a/packages/cli/src/lib/command.ts b/packages/cli/src/lib/command.ts index 2f9243e6..5bafba7b 100644 --- a/packages/cli/src/lib/command.ts +++ b/packages/cli/src/lib/command.ts @@ -119,12 +119,7 @@ export function isForce(flags?: { force?: boolean; yes?: boolean }): boolean { } function outputFormatFromArgv(argv: string[]): string | undefined { - for (let i = 0; i < argv.length; i++) { - const arg = argv[i] - if (arg === '--format') return argv[i + 1] - if (arg?.startsWith('--format=')) return arg.slice('--format='.length) - } - return undefined + return stringFlagFromArgv(argv, '--format') } function stringFlagFromArgv(argv: string[], name: string): string | undefined { diff --git a/packages/cli/src/lib/export/index.ts b/packages/cli/src/lib/export/index.ts index 5422f73d..dca90d82 100644 --- a/packages/cli/src/lib/export/index.ts +++ b/packages/cli/src/lib/export/index.ts @@ -6,8 +6,6 @@ import { fileURLToPath } from 'node:url' import type { Chat } from '@beeper/desktop-api/resources/chats/chats' import type { Attachment, Message } from '@beeper/desktop-api/resources/shared' -type AnyRecord = Record - export type ExportOptions = { accountIDs?: string[] chatIDs?: string[] diff --git a/packages/cli/src/lib/ink/render.tsx b/packages/cli/src/lib/ink/render.tsx index 19110b9b..007c629f 100644 --- a/packages/cli/src/lib/ink/render.tsx +++ b/packages/cli/src/lib/ink/render.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { Box, render as inkRender, Static, Text, useApp, useInput } from 'ink' import Spinner from 'ink-spinner' import { @@ -28,7 +28,7 @@ import { UserRow, } from './components.js' import type { RecordValue } from './format.js' -import { glyphs, theme } from './theme.js' +import { theme } from './theme.js' const App: React.FC<{ children: React.ReactNode }> = ({ children }) => { const { exit } = useApp() @@ -127,7 +127,7 @@ export async function renderValue(value: unknown): Promise { await renderOnce() return case 'doctor': { - const rawChecks = Array.isArray(record.checks) + const checks = Array.isArray(record.checks) ? record.checks as Array<{ ok: boolean; name: string; detail?: string }> : record.checks && typeof record.checks === 'object' ? Object.entries(record.checks as Record).map(([name, value]) => { @@ -140,7 +140,6 @@ export async function renderValue(value: unknown): Promise { return { ok, name, detail } }) : [] - const checks = rawChecks await renderOnce() return } diff --git a/packages/cli/src/lib/ink/theme.ts b/packages/cli/src/lib/ink/theme.ts index 32986f4d..31bed95c 100644 --- a/packages/cli/src/lib/ink/theme.ts +++ b/packages/cli/src/lib/ink/theme.ts @@ -76,8 +76,7 @@ export function senderColor(id: string | undefined | null): string { if (!id) return theme.text let hash = 0 for (let i = 0; i < id.length; i++) hash = ((hash << 5) - hash + id.charCodeAt(i)) | 0 - const palette = groupSenderPalette - return palette[Math.abs(hash) % palette.length]! + return groupSenderPalette[Math.abs(hash) % groupSenderPalette.length]! } // Glyphs — every visual cue we use sits in this map so a single audit covers them. diff --git a/packages/cli/src/lib/installations.ts b/packages/cli/src/lib/installations.ts index ad7b9995..6f3a2b58 100644 --- a/packages/cli/src/lib/installations.ts +++ b/packages/cli/src/lib/installations.ts @@ -1,5 +1,5 @@ import { createWriteStream } from 'node:fs' -import { chmod, cp, mkdir, readFile, rename, rm, symlink, writeFile } from 'node:fs/promises' +import { chmod, cp, mkdir, readFile, readdir, rename, rm, stat, symlink, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { basename, dirname, extname, join } from 'node:path' import { Readable } from 'node:stream' @@ -299,7 +299,6 @@ async function copyPath(source: string, destination: string): Promise { } async function findAppBundle(dir: string): Promise { - const { readdir, stat } = await import('node:fs/promises') const entries = await readdir(dir) for (const entry of entries) { const path = join(dir, entry) @@ -314,7 +313,6 @@ async function findAppBundle(dir: string): Promise { } async function findServerExecutable(dir: string): Promise { - const { readdir, stat } = await import('node:fs/promises') const entries = await readdir(dir) for (const entry of entries) { const path = join(dir, entry) diff --git a/packages/cli/src/lib/profiles.ts b/packages/cli/src/lib/profiles.ts index 6599bcad..38437c03 100644 --- a/packages/cli/src/lib/profiles.ts +++ b/packages/cli/src/lib/profiles.ts @@ -1,11 +1,11 @@ import { spawn } from 'node:child_process' import { execFile } from 'node:child_process' import { closeSync, openSync } from 'node:fs' -import { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises' +import { mkdir, readFile, rm, writeFile } from 'node:fs/promises' import { homedir } from 'node:os' import { join } from 'node:path' import { promisify } from 'node:util' -import { beeperDir, type Target } from './targets.js' +import { beeperDir, pathExists, type Target } from './targets.js' import { readInstallations } from './installations.js' import { usageError } from './errors.js' @@ -148,9 +148,7 @@ export async function stopProfile(target: Target): Promise { export async function profileStatus(target: Target): Promise> { assertProfile(target) const run = await readRun(target.id) - const reachable = await fetch(new URL('/v1/info', target.baseURL), { signal: AbortSignal.timeout(1000) }) - .then(response => response.ok) - .catch(() => false) + const reachable = await isReachable(target) return { id: target.id, type: target.type, @@ -381,12 +379,3 @@ async function waitForExit(pid: number, timeoutMs: number): Promise { async function sleep(ms: number): Promise { await new Promise(resolve => setTimeout(resolve, ms)) } - -async function pathExists(path: string): Promise { - try { - await access(path) - return true - } catch { - return false - } -} diff --git a/packages/cli/src/lib/resolve.ts b/packages/cli/src/lib/resolve.ts index a3f11a32..4414df5b 100644 --- a/packages/cli/src/lib/resolve.ts +++ b/packages/cli/src/lib/resolve.ts @@ -1,6 +1,7 @@ import { readConfig } from './targets.js' import { ambiguous, notFound } from './errors.js' import { confirmSuggestion, declineWithExit127, rankSuggestions } from './did-you-mean.js' +import { collectPage } from './output.js' type AnyRecord = Record @@ -61,7 +62,7 @@ export async function resolveChatID(client: any, input: string, options: ChatRes const exact = await retrieveChat(client, input) if (exact) return chatInputID(exact) - const candidates = await collect(client.chats.search({ + const candidates = await collectPage(client.chats.search({ accountIDs: options.accountIDs, query: input, scope: 'titles', @@ -105,7 +106,7 @@ async function suggestChat(client: any, input: string, options: ChatResolutionOp if (process.env.BEEPER_NO_INPUT === '1') return undefined let pool: AnyRecord[] try { - pool = await collect(client.chats.list({ accountIDs: options.accountIDs, limit: 100 }), 100) + pool = await collectPage(client.chats.list({ accountIDs: options.accountIDs, limit: 100 }), 100) } catch { return undefined } @@ -163,15 +164,6 @@ async function retrieveChat(client: any, input: string): Promise(iterable: AsyncIterable, limit: number): Promise { - const items: T[] = [] - for await (const item of iterable) { - items.push(item) - if (items.length >= limit) break - } - return items -} - function normalize(value: unknown): string { return String(value ?? '').trim().toLowerCase().replace(/[\s._-]+/g, '') } diff --git a/packages/cli/src/lib/setup-login.ts b/packages/cli/src/lib/setup-login.ts index 41fab57f..1768c885 100644 --- a/packages/cli/src/lib/setup-login.ts +++ b/packages/cli/src/lib/setup-login.ts @@ -29,8 +29,9 @@ export async function finishEmailSetup(target: Target, options: { const client = setupClient(target) let output = await client.app.login.response({ setupRequestID: options.setupRequestID, response: options.code }) if (isRegistrationRequired(output)) { - if ((options.json || !process.stdin.isTTY) && !options.yes) throw new Error('Registration requires --yes to accept the Beeper terms in non-interactive setup.') - const username = options.username ?? (options.json || !process.stdin.isTTY ? undefined : await promptUsername(output.usernameSuggestions)) + const nonInteractive = options.json || !process.stdin.isTTY + if (nonInteractive && !options.yes) throw new Error('Registration requires --yes to accept the Beeper terms in non-interactive setup.') + const username = options.username ?? (nonInteractive ? undefined : await promptUsername(output.usernameSuggestions)) if (!username) throw new Error('Registration requires --username.') if (!options.yes && !await promptYesNoDefaultYes('Accept the Beeper terms and create this account?')) throw new Error('Registration cancelled.') output = await client.app.login.register({ From a819a5ae006a57adfb3a7bba6dee811fa07c08a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 19:08:20 +0200 Subject: [PATCH 18/26] fix npm launcher source quoting --- packages/npm/scripts/build.ts | 139 +--------------------------------- 1 file changed, 2 insertions(+), 137 deletions(-) diff --git a/packages/npm/scripts/build.ts b/packages/npm/scripts/build.ts index cd53ffeb..fa7be4c8 100644 --- a/packages/npm/scripts/build.ts +++ b/packages/npm/scripts/build.ts @@ -1,4 +1,5 @@ #!/usr/bin/env bun +/* eslint-disable no-template-curly-in-string */ import { chmod, cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' import { existsSync } from 'node:fs' import { join } from 'node:path' @@ -29,142 +30,6 @@ await writeFile(join(root, 'bin', 'beeper.js'), launcher()) await chmod(join(root, 'bin', 'beeper.js'), 0o755) function launcher() { - return `#!/usr/bin/env node -import { createHash } from 'node:crypto' -import { createWriteStream, existsSync } from 'node:fs' -import { chmod, mkdir, readFile, rename, rm } from 'node:fs/promises' -import { get } from 'node:https' -import { homedir, platform as osPlatform, arch as osArch, tmpdir } from 'node:os' -import { dirname, join } from 'node:path' -import { fileURLToPath } from 'node:url' -import { spawn } from 'node:child_process' - -const packageRoot = join(dirname(fileURLToPath(import.meta.url)), '..') -const manifest = JSON.parse(await readFile(join(packageRoot, 'binaries.json'), 'utf8')) -const platform = targetPlatform() -const artifact = manifest.artifacts.find(item => item.platform === platform) - -if (!artifact) { - console.error(`beeper-cli does not ship a binary for ${process.platform}/${process.arch}.`) - process.exit(1) + return "#!/usr/bin/env node\nimport { createHash } from 'node:crypto'\nimport { createWriteStream, existsSync } from 'node:fs'\nimport { chmod, mkdir, readFile, rename, rm } from 'node:fs/promises'\nimport { get } from 'node:https'\nimport { homedir, platform as osPlatform, arch as osArch, tmpdir } from 'node:os'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { spawn } from 'node:child_process'\n\nconst packageRoot = join(dirname(fileURLToPath(import.meta.url)), '..')\nconst manifest = JSON.parse(await readFile(join(packageRoot, 'binaries.json'), 'utf8'))\nconst platform = targetPlatform()\nconst artifact = manifest.artifacts.find(item => item.platform === platform)\n\nif (!artifact) {\n console.error(`beeper-cli does not ship a binary for ${process.platform}/${process.arch}.`)\n process.exit(1)\n}\n\nconst cacheDir = process.env.BEEPER_CLI_BINARY_CACHE_DIR || join(homedir(), '.cache', 'beeper-cli', manifest.version)\nconst binPath = join(cacheDir, 'bin', manifest.command || 'beeper')\n\nconst expectedBinarySha256 = artifact.binarySha256 || artifact.sha256\n\nif (!existsSync(binPath) || await sha256(binPath).catch(() => '') !== expectedBinarySha256) {\n const tempDir = join(tmpdir(), `beeper-cli-${manifest.version}-${process.pid}`)\n const archivePath = join(tempDir, artifact.file)\n const downloadURL = `https://github.com/beeper/cli/releases/download/v${manifest.version}/${artifact.file}`\n logStep(`installing beeper-cli ${manifest.version} for ${platform}`)\n await rm(tempDir, { recursive: true, force: true })\n await mkdir(tempDir, { recursive: true })\n await download(downloadURL, archivePath)\n logStep('verifying download')\n const actual = await sha256(archivePath)\n if (actual !== artifact.sha256) {\n await rm(tempDir, { recursive: true, force: true })\n console.error(`beeper-cli binary checksum mismatch for ${artifact.file}.`)\n process.exit(1)\n }\n logStep('extracting binary')\n await extract(archivePath, tempDir)\n const extractedBin = join(tempDir, 'bin', manifest.command || 'beeper')\n await chmod(extractedBin, 0o755)\n logStep(`caching binary in ${cacheDir}`)\n await rm(cacheDir, { recursive: true, force: true })\n await mkdir(dirname(binPath), { recursive: true })\n await rename(extractedBin, binPath)\n await rm(tempDir, { recursive: true, force: true })\n logStep('ready')\n}\n\nif (process.env.BEEPER_CLI_LAUNCHER_DEBUG === '1') logStep(`starting ${binPath}`)\nconst child = spawn(binPath, process.argv.slice(2), { stdio: 'inherit', env: process.env })\nchild.on('exit', (code, signal) => {\n if (signal) process.kill(process.pid, signal)\n process.exit(code ?? 1)\n})\n\nfunction logStep(message) {\n console.error(`beeper-cli: ${message}`)\n}\n\nfunction targetPlatform() {\n const os = osPlatform()\n const cpu = osArch()\n const normalizedOS = os === 'darwin' || os === 'linux' ? os : os === 'win32' ? 'windows' : os\n const normalizedArch = cpu === 'x64' || cpu === 'arm64' ? cpu : cpu\n return `${normalizedOS}-${normalizedArch}`\n}\n\nasync function sha256(path) {\n const hash = createHash('sha256')\n hash.update(await readFile(path))\n return hash.digest('hex')\n}\n\nasync function download(url, destination, redirects = 0) {\n if (redirects > 10) throw new Error(`Too many redirects while downloading ${artifact.file}`)\n\n logStep(`downloading ${artifact.file}`)\n await new Promise((resolve, reject) => {\n get(url, response => {\n if ([301, 302, 303, 307, 308].includes(response.statusCode ?? 0) && response.headers.location) {\n response.resume()\n const nextURL = new URL(response.headers.location, url).toString()\n logStep(`redirecting to ${new URL(nextURL).host}`)\n download(nextURL, destination, redirects + 1).then(resolve, reject)\n return\n }\n if (response.statusCode !== 200) {\n response.resume()\n reject(new Error(`Download failed with HTTP ${response.statusCode}: ${url}`))\n return\n }\n const total = Number(response.headers['content-length'] ?? 0)\n let downloaded = 0\n let nextLoggedPercent = 25\n const file = createWriteStream(destination, { mode: 0o755 })\n response.on('data', chunk => {\n downloaded += chunk.length\n if (!total) return\n const percent = Math.floor(downloaded / total * 100)\n while (percent >= nextLoggedPercent && nextLoggedPercent <= 100) {\n logStep(`downloaded ${nextLoggedPercent}%`)\n nextLoggedPercent += 25\n }\n })\n response.pipe(file)\n file.on('finish', () => file.close(resolve))\n file.on('error', reject)\n }).on('error', reject)\n })\n}\n\nasync function extract(archivePath, destination) {\n if (artifact.file.endsWith('.zip')) {\n await run('/usr/bin/ditto', ['-x', '-k', archivePath, destination])\n return\n }\n if (artifact.file.endsWith('.tar.gz')) {\n await run('tar', ['-xzf', archivePath, '-C', destination])\n return\n }\n throw new Error(`Unsupported beeper-cli archive: ${artifact.file}`)\n}\n\nasync function run(command, args) {\n await new Promise((resolve, reject) => {\n const child = spawn(command, args, { stdio: 'ignore' })\n child.on('error', reject)\n child.on('exit', code => {\n if (code === 0) resolve()\n else reject(new Error(`${command} ${args.join(' ')} exited with ${code}`))\n })\n })\n}" } -const cacheDir = process.env.BEEPER_CLI_BINARY_CACHE_DIR || join(homedir(), '.cache', 'beeper-cli', manifest.version) -const binPath = join(cacheDir, 'bin', manifest.command || 'beeper') - -const expectedBinarySha256 = artifact.binarySha256 || artifact.sha256 - -if (!existsSync(binPath) || await sha256(binPath).catch(() => '') !== expectedBinarySha256) { - const tempDir = join(tmpdir(), `beeper-cli-${manifest.version}-${process.pid}`) - const archivePath = join(tempDir, artifact.file) - const downloadURL = `https://github.com/beeper/cli/releases/download/v${manifest.version}/${artifact.file}` - logStep(`installing beeper-cli ${manifest.version} for ${platform}`) - await rm(tempDir, { recursive: true, force: true }) - await mkdir(tempDir, { recursive: true }) - await download(downloadURL, archivePath) - logStep('verifying download') - const actual = await sha256(archivePath) - if (actual !== artifact.sha256) { - await rm(tempDir, { recursive: true, force: true }) - console.error(`beeper-cli binary checksum mismatch for ${artifact.file}.`) - process.exit(1) - } - logStep('extracting binary') - await extract(archivePath, tempDir) - const extractedBin = join(tempDir, 'bin', manifest.command || 'beeper') - await chmod(extractedBin, 0o755) - logStep(`caching binary in ${cacheDir}`) - await rm(cacheDir, { recursive: true, force: true }) - await mkdir(dirname(binPath), { recursive: true }) - await rename(extractedBin, binPath) - await rm(tempDir, { recursive: true, force: true }) - logStep('ready') -} - -if (process.env.BEEPER_CLI_LAUNCHER_DEBUG === '1') logStep(`starting ${binPath}`) -const child = spawn(binPath, process.argv.slice(2), { stdio: 'inherit', env: process.env }) -child.on('exit', (code, signal) => { - if (signal) process.kill(process.pid, signal) - process.exit(code ?? 1) -}) - -function logStep(message) { - console.error(`beeper-cli: ${message}`) -} - -function targetPlatform() { - const os = osPlatform() - const cpu = osArch() - const normalizedOS = os === 'darwin' || os === 'linux' ? os : os === 'win32' ? 'windows' : os - const normalizedArch = cpu === 'x64' || cpu === 'arm64' ? cpu : cpu - return `${normalizedOS}-${normalizedArch}` -} - -async function sha256(path) { - const hash = createHash('sha256') - hash.update(await readFile(path)) - return hash.digest('hex') -} - -async function download(url, destination, redirects = 0) { - if (redirects > 10) throw new Error(`Too many redirects while downloading ${artifact.file}`) - - logStep(`downloading ${artifact.file}`) - await new Promise((resolve, reject) => { - get(url, response => { - if ([301, 302, 303, 307, 308].includes(response.statusCode ?? 0) && response.headers.location) { - response.resume() - const nextURL = new URL(response.headers.location, url).toString() - logStep(`redirecting to ${new URL(nextURL).host}`) - download(nextURL, destination, redirects + 1).then(resolve, reject) - return - } - if (response.statusCode !== 200) { - response.resume() - reject(new Error(`Download failed with HTTP ${response.statusCode}: ${url}`)) - return - } - const total = Number(response.headers['content-length'] ?? 0) - let downloaded = 0 - let nextLoggedPercent = 25 - const file = createWriteStream(destination, { mode: 0o755 }) - response.on('data', chunk => { - downloaded += chunk.length - if (!total) return - const percent = Math.floor(downloaded / total * 100) - while (percent >= nextLoggedPercent && nextLoggedPercent <= 100) { - logStep(`downloaded ${nextLoggedPercent}%`) - nextLoggedPercent += 25 - } - }) - response.pipe(file) - file.on('finish', () => file.close(resolve)) - file.on('error', reject) - }).on('error', reject) - }) -} - -async function extract(archivePath, destination) { - if (artifact.file.endsWith('.zip')) { - await run('/usr/bin/ditto', ['-x', '-k', archivePath, destination]) - return - } - if (artifact.file.endsWith('.tar.gz')) { - await run('tar', ['-xzf', archivePath, '-C', destination]) - return - } - throw new Error(`Unsupported beeper-cli archive: ${artifact.file}`) -} - -async function run(command, args) { - await new Promise((resolve, reject) => { - const child = spawn(command, args, { stdio: 'ignore' }) - child.on('error', reject) - child.on('exit', code => { - if (code === 0) resolve() - else reject(new Error(`${command} ${args.join(' ')} exited with ${code}`)) - }) - }) -} -` -} From 31c9d558cd984f79ca04acaf3cbc2bcc22c4cb96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 19:08:23 +0200 Subject: [PATCH 19/26] fix schema positional parsing --- packages/cli/src/commands/schema.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/schema.ts b/packages/cli/src/commands/schema.ts index 24eab64d..f990d858 100644 --- a/packages/cli/src/commands/schema.ts +++ b/packages/cli/src/commands/schema.ts @@ -24,8 +24,8 @@ export default class Schema extends BeeperCommand { } async run(): Promise { - await this.parse(Schema) - const pathArgs = this.argv.filter(arg => !arg.startsWith('-')) + const { argv } = await this.parse(Schema) + const pathArgs = argv as string[] const requested = pathArgs.length > 0 ? pathArgs.join(' ') : undefined const manifestByCommand = new Map(commandManifest.map(item => [item.command, item])) const commands = (this.config.commands as RawCommand[]) @@ -121,15 +121,19 @@ function outputShape(kind: string): Record { case 'list': { return { kind, envelope, data: 'array' } } + case 'send-result': { return { kind, envelope, data: { chatID: 'string', pendingMessageID: 'string?', state: 'string?' } } } + case 'stream': { return { kind, data: 'jsonl events or RPC lines' } } + case 'success': { return { kind, envelope, data: { message: 'string', detail: 'string?', data: 'object?' } } } + default: { return { kind, envelope, data: 'object' } } From 5e5e695f05fe25eff05dd997f91ce82fd749aefe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 19:08:27 +0200 Subject: [PATCH 20/26] fix dry-run padding lint --- packages/cli/src/commands/presence.ts | 2 ++ packages/cli/src/commands/verify/approve.ts | 3 +++ 2 files changed, 5 insertions(+) diff --git a/packages/cli/src/commands/presence.ts b/packages/cli/src/commands/presence.ts index 709f927b..fe85d783 100644 --- a/packages/cli/src/commands/presence.ts +++ b/packages/cli/src/commands/presence.ts @@ -23,6 +23,7 @@ export default class Presence extends BeeperCommand { await printDryRun('presence', { chat: flags.chat, pick: flags.pick, state: flags.state, durationSeconds: flags.duration }, flags.json ? 'json' : 'human') return } + ensureWritable(flags) const client = await createClient(flags) const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) @@ -36,6 +37,7 @@ export default class Presence extends BeeperCommand { await printSuccess({ message: `Sent typing then paused after ${flags.duration}s`, data: { chatID, state: 'paused', durationSeconds: flags.duration } }, flags.json ? 'json' : 'human') return } + await printSuccess({ message: `Sent ${flags.state} indicator`, data: { chatID, state: flags.state } }, flags.json ? 'json' : 'human') } } diff --git a/packages/cli/src/commands/verify/approve.ts b/packages/cli/src/commands/verify/approve.ts index dc99e52a..115937fa 100644 --- a/packages/cli/src/commands/verify/approve.ts +++ b/packages/cli/src/commands/verify/approve.ts @@ -2,17 +2,20 @@ import { Flags } from '@oclif/core' import { BeeperCommand, ensureWritable } from '../../lib/command.js' import { createClient } from '../../lib/client.js' import { printData, printDryRun } from '../../lib/output.js' + export default class AuthVerifyApprove extends BeeperCommand { static override summary = 'Approve a pending device verification request' static override flags = { id: Flags.string({ description: 'Verification request ID. Defaults to the active request.' }), } + async run(): Promise { const { flags } = await this.parse(AuthVerifyApprove) if (flags['dry-run']) { await printDryRun('verify.approve', { id: flags.id ?? 'active' }, flags.json ? 'json' : 'human') return } + ensureWritable(flags) const client = await createClient(flags) await printData(await client.app.verifications.accept(flags.id ?? 'active'), flags.json ? 'json' : 'human') From 3bf94b773635186d7eeed89ca02a21cd1cd10b9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 19:08:31 +0200 Subject: [PATCH 21/26] fix command error classification lint --- packages/cli/src/lib/command.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/lib/command.ts b/packages/cli/src/lib/command.ts index 5bafba7b..ac7faa13 100644 --- a/packages/cli/src/lib/command.ts +++ b/packages/cli/src/lib/command.ts @@ -26,6 +26,7 @@ export abstract class BeeperCommand extends Command { if (this.argv.includes('--quiet') || this.argv.includes('-q')) { process.env.BEEPER_QUIET = '1' } + const format = outputFormatFromArgv(this.argv) if (format) { process.env.BEEPER_OUTPUT_FORMAT = format @@ -34,6 +35,7 @@ export abstract class BeeperCommand extends Command { } else if (process.env.BEEPER_AGENT === '1' || !process.stdout.isTTY) { process.env.BEEPER_OUTPUT_FORMAT = 'json' } + const select = stringFlagFromArgv(this.argv, '--select') if (select) process.env.BEEPER_OUTPUT_SELECT = select if (this.argv.includes('--results-only')) process.env.BEEPER_OUTPUT_RESULTS_ONLY = '1' @@ -47,7 +49,7 @@ export abstract class BeeperCommand extends Command { const code = inferredCode ?? error.exitCode ?? ExitCodes.Generic process.exitCode = process.exitCode ?? code const tryMessage = error instanceof CLIError ? error.tryMessage : undefined - const isBug = error instanceof BugError || !(error instanceof CLIError) + const isBug = error instanceof BugError || (!(error instanceof CLIError) && inferredCode === undefined) if (this.argv.includes('--events')) { writeEvent('error', { message, exitCode: code, kind: isBug ? 'bug' : 'abort', tryMessage }) @@ -75,7 +77,7 @@ function inferExitCode(message: string): number | undefined { if (/\b401\b|unauthorized|invalid token|auth(?:entication)? required/i.test(message)) return ExitCodes.AuthRequired if (/\b404\b|not\s+found|unknown .*target|no .*matches/i.test(message)) return ExitCodes.NotFound if (/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|fetch failed|not reachable|not ready/i.test(message)) return ExitCodes.NotReady - if (/usage|invalid|must provide|required|unknown flag|parse/i.test(message)) return ExitCodes.Usage + if (/\busage\b|\binvalid (?:argument|option|flag|value|input)\b|\bmust provide\b|\brequired (?:flag|argument|option|value)\b|\bunknown flag\b|\bparse error\b/i.test(message)) return ExitCodes.Usage return undefined } @@ -98,7 +100,7 @@ function formatBugPanel(error: Error, version: string): string { export function ensureWritable(flags: { 'read-only'?: boolean }): void { const env = process.env.BEEPER_READONLY - const readOnly = flags['read-only'] || ['1', 'true', 'yes', 'on'].includes(String(env ?? '').toLowerCase()) + const readOnly = flags['read-only'] || ['1', 'on', 'true', 'yes'].includes(String(env ?? '').toLowerCase()) if (readOnly) throw new CLIError('read-only mode: command would modify Beeper or local CLI state', ExitCodes.Usage) } @@ -128,6 +130,7 @@ function stringFlagFromArgv(argv: string[], name: string): string | undefined { if (arg === name) return argv[i + 1] if (arg?.startsWith(`${name}=`)) return arg.slice(name.length + 1) } + return undefined } @@ -142,21 +145,27 @@ function errorCode(code: number, isBug: boolean): string { case ExitCodes.Ambiguous: { return 'ambiguous_selector' } + case ExitCodes.AuthRequired: { return 'auth_required' } + case ExitCodes.CommandNotFound: { return 'command_not_found' } + case ExitCodes.NotFound: { return 'not_found' } + case ExitCodes.NotReady: { return 'not_ready' } + case ExitCodes.Usage: { return 'usage_error' } + default: { return 'runtime_error' } From fefbf4e26048b04a0360e1e1fb01b4b6a2e34e8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Sat, 30 May 2026 19:08:34 +0200 Subject: [PATCH 22/26] remove local claude runtime lock --- .claude/scheduled_tasks.lock | 1 - .gitignore | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 .claude/scheduled_tasks.lock diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock deleted file mode 100644 index b9663f66..00000000 --- a/.claude/scheduled_tasks.lock +++ /dev/null @@ -1 +0,0 @@ -{"sessionId":"2da33c33-52d4-4916-961e-72cd6ae9594a","pid":18713,"procStart":"Sat May 30 16:14:01 2026","acquiredAt":1780159461036} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 13b643ec..bf7d3f55 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ node_modules/ /beeper-desktop-cli .upstream/ *.exe + +.claude/ From cf9a5bdcf360e10dedbf95f25c843bcc60b46f45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Wed, 3 Jun 2026 04:11:09 +0200 Subject: [PATCH 23/26] Port bridge-manager flows to TypeScript --- .DS_Store | Bin 0 -> 6148 bytes packages/cli/package.json | 1 + packages/cli/scripts/generate-command-map.ts | 7 + .../cli/scripts/sync-bridge-manager-config.ts | 78 ++ packages/cli/src/commands.generated.ts | 417 +++++----- packages/cli/src/commands/bridges/config.ts | 50 ++ packages/cli/src/commands/bridges/delete.ts | 68 ++ packages/cli/src/commands/bridges/list.ts | 33 +- .../src/commands/bridges/login-password.ts | 37 + packages/cli/src/commands/bridges/login.ts | 36 + packages/cli/src/commands/bridges/logout.ts | 20 + packages/cli/src/commands/bridges/proxy.ts | 23 + packages/cli/src/commands/bridges/register.ts | 40 + packages/cli/src/commands/bridges/run.ts | 179 ++++ packages/cli/src/commands/bridges/show.ts | 68 +- packages/cli/src/commands/bridges/whoami.ts | 78 ++ packages/cli/src/lib/bridges/catalog.ts | 85 ++ packages/cli/src/lib/bridges/command.ts | 9 + packages/cli/src/lib/bridges/generated.ts | 178 ++++ packages/cli/src/lib/bridges/go-template.ts | 183 +++++ packages/cli/src/lib/bridges/manager.ts | 761 ++++++++++++++++++ .../cli/src/lib/bridges/websocket-proxy.ts | 152 ++++ packages/cli/src/types/ws.d.ts | 23 + 23 files changed, 2274 insertions(+), 252 deletions(-) create mode 100644 .DS_Store create mode 100644 packages/cli/scripts/sync-bridge-manager-config.ts create mode 100644 packages/cli/src/commands/bridges/config.ts create mode 100644 packages/cli/src/commands/bridges/delete.ts create mode 100644 packages/cli/src/commands/bridges/login-password.ts create mode 100644 packages/cli/src/commands/bridges/login.ts create mode 100644 packages/cli/src/commands/bridges/logout.ts create mode 100644 packages/cli/src/commands/bridges/proxy.ts create mode 100644 packages/cli/src/commands/bridges/register.ts create mode 100644 packages/cli/src/commands/bridges/run.ts create mode 100644 packages/cli/src/commands/bridges/whoami.ts create mode 100644 packages/cli/src/lib/bridges/catalog.ts create mode 100644 packages/cli/src/lib/bridges/command.ts create mode 100644 packages/cli/src/lib/bridges/generated.ts create mode 100644 packages/cli/src/lib/bridges/go-template.ts create mode 100644 packages/cli/src/lib/bridges/manager.ts create mode 100644 packages/cli/src/lib/bridges/websocket-proxy.ts create mode 100644 packages/cli/src/types/ws.d.ts diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..6b53cabfa5c84fb4e9d16fc341a22d61681ffebd GIT binary patch literal 6148 zcmeHLF;5#Y6n-vu$pNLxz`({aAef*?>^-9J3nFIP5Og3CE)rzFwQIMoRl9U8bJeBl z!oUIx5)2Hb-8!-Iy=N=2xeH=KQROGupU>~v@4L@;b}mGu>izZ_QH_Wq6vki*Rf(~m zOTk{4o;jeBIS%QVjymaDb0;fA`wp*wS76)};CHu5EA)<1+9m7#?e>If_~~~sqMJ1u ztxnd0Cwg^WnZLdKwNVz;enM1lk9P}`ttvSK@JVT(j`dDrd<$U;@4#nsm^r=q_u)!7 zSpH%5GQX2|QS!15l9%?VPc0`qO@1|ZXhql@K5jWWI8j3;qoW)?^qL6;)P@R24|E7q zUXg*%=I~|x?(UCX3(G&uUgmf5&i>%MFYz2?$h}M3)Rk7BI=C`ByIzmr=bO)u#oD~S zog~ZAwXDy`xx3kr=TqWs-j#Vizi>$P@Eq^Zfq9x8z7&36EsL>vE?wNb|Eu*Y%w_X? zEY9XdQ)kR;Tn%S7TQsfF*IogyfLCCk0Phbm3S(k0*C<;DD)|He2Iv+;TlQjL%pJhQ zV6G88Fr|kA^-yI#F_a#T{@BDN26K&iI4Q}Dc}!+yzEG58M}I8gq!Nw3_6m3fTm>fd zWsT4OO}xV+zW?1p{>m%h75J|dP(h>FsN%lV9{>OV literal 0 HcmV?d00001 diff --git a/packages/cli/package.json b/packages/cli/package.json index b35a43b9..bf9a7345 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -23,6 +23,7 @@ "scripts": { "build": "bun run clean && bun scripts/prepare-desktop-api.ts && bun scripts/generate-command-map.ts && tsc -p tsconfig.json", "binary": "bun run build && bun scripts/build-binaries.ts", + "bridges:sync-config": "bun scripts/sync-bridge-manager-config.ts", "release:local": "bun run build && bun scripts/build-binaries.ts && BEEPER_CLI_REQUIRE_MACOS_SIGNING=1 bun scripts/sign-macos-binaries.ts && bun scripts/build-homebrew-archive.ts && (cd ../npm && bun run build) && bun scripts/publish-local-release.ts", "check:api-copy": "bun run build && bun scripts/check-api-copy.ts", "check:readme": "bun run build && bun scripts/generate-readme.ts --check", diff --git a/packages/cli/scripts/generate-command-map.ts b/packages/cli/scripts/generate-command-map.ts index e809e4e9..ef0fb4e4 100644 --- a/packages/cli/scripts/generate-command-map.ts +++ b/packages/cli/scripts/generate-command-map.ts @@ -9,7 +9,14 @@ const outPath = join(root, 'src', 'commands.generated.ts') const listAliases: Record = { 'accounts:list': ['accounts'], + 'bridges:config': ['bridges:c'], + 'bridges:delete': ['bridges:d'], 'bridges:list': ['bridges'], + 'bridges:login': ['bridges:l'], + 'bridges:login-password': ['bridges:p'], + 'bridges:proxy': ['bridges:x'], + 'bridges:register': ['bridges:r'], + 'bridges:whoami': ['bridges:w'], 'chats:list': ['chats', 'accounts:chats', 'ls'], 'contacts:list': ['contacts'], 'messages:search': ['search'], diff --git a/packages/cli/scripts/sync-bridge-manager-config.ts b/packages/cli/scripts/sync-bridge-manager-config.ts new file mode 100644 index 00000000..ca5796a9 --- /dev/null +++ b/packages/cli/scripts/sync-bridge-manager-config.ts @@ -0,0 +1,78 @@ +#!/usr/bin/env bun +import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises' +import { homedir } from 'node:os' +import { basename, dirname, join } from 'node:path' + +type OfficialBridge = { typeName: string; names: string[] } + +const repo = process.argv[2] ?? process.env.BRIDGE_MANAGER_REPO ?? join(homedir(), 'projects', 'bridge-manager') +const root = join(import.meta.dir, '..') +const outputPath = join(root, 'src', 'lib', 'bridges', 'generated.ts') + +const bridgeutil = await readFile(join(repo, 'cmd', 'bbctl', 'bridgeutil.go'), 'utf8') +const config = await readFile(join(repo, 'cmd', 'bbctl', 'config.go'), 'utf8') +const templateDir = join(repo, 'bridgeconfig') +const templateFiles = (await readdir(templateDir)).filter(file => file.endsWith('.tpl.yaml')).sort() +const templates: Record = {} +for (const file of templateFiles) templates[file] = await readFile(join(templateDir, file), 'utf8') + +await mkdir(dirname(outputPath), { recursive: true }) +await writeFile(outputPath, `${renderGenerated({ + bridgeIPSuffix: parseStringMap(config, 'bridgeIPSuffix'), + officialBridges: parseOfficialBridges(bridgeutil), + templates, + websocketBridges: parseBoolMap(bridgeutil, 'websocketBridges'), +})}\n`) + +process.stderr.write(`Synced ${templateFiles.length} bridge templates from ${repo} to ${outputPath}\n`) + +function renderGenerated(data: { + bridgeIPSuffix: Record + officialBridges: OfficialBridge[] + templates: Record + websocketBridges: Record +}): string { + const supportedBridges = Object.keys(data.templates).map(file => basename(file, '.tpl.yaml')).sort() + return `// Generated by scripts/sync-bridge-manager-config.ts from bridge-manager. Do not edit by hand. + +export type GeneratedOfficialBridge = { typeName: string; names: string[] } + +export const generatedTemplates: Record = ${JSON.stringify(data.templates, null, 2)} as const + +export const generatedSupportedBridges = ${JSON.stringify(supportedBridges, null, 2)} as const + +export const generatedOfficialBridges: GeneratedOfficialBridge[] = ${JSON.stringify(data.officialBridges, null, 2)} + +export const generatedWebsocketBridges: Record = ${JSON.stringify(data.websocketBridges, null, 2)} + +export const generatedBridgeIPSuffix: Record = ${JSON.stringify(data.bridgeIPSuffix, null, 2)} +` +} + +function parseOfficialBridges(source: string): OfficialBridge[] { + const body = mustMatch(source, /var officialBridges = \[\]bridgeTypeToNames\{([\s\S]*?)\n\}/, 'officialBridges') + return [...body.matchAll(/\{"([^"]+)",\s*\[\]string\{([^}]*)\}\}/g)].map(match => ({ + typeName: match[1]!, + names: [...match[2]!.matchAll(/"([^"]+)"/g)].map(name => name[1]!), + })) +} + +function parseBoolMap(source: string, name: string): Record { + const body = mustMatch(source, new RegExp(`var ${name} = map\\[string\\]bool\\{([\\s\\S]*?)\\n\\}`), name) + const out: Record = {} + for (const match of body.matchAll(/"([^"]+)"\s*:\s*(true|false)/g)) out[match[1]!] = match[2] === 'true' + return out +} + +function parseStringMap(source: string, name: string): Record { + const body = mustMatch(source, new RegExp(`var ${name} = map\\[string\\]string\\{([\\s\\S]*?)\\n\\}`), name) + const out: Record = {} + for (const match of body.matchAll(/"([^"]+)"\s*:\s*"([^"]*)"/g)) out[match[1]!] = match[2]! + return out +} + +function mustMatch(source: string, pattern: RegExp, name: string): string { + const match = source.match(pattern) + if (!match) throw new Error(`Failed to parse ${name} from bridge-manager Go source`) + return match[1]! +} diff --git a/packages/cli/src/commands.generated.ts b/packages/cli/src/commands.generated.ts index f8170748..b327a90e 100644 --- a/packages/cli/src/commands.generated.ts +++ b/packages/cli/src/commands.generated.ts @@ -11,105 +11,114 @@ import Command9 from './commands/auth/email/start.js' import Command10 from './commands/auth/logout.js' import Command11 from './commands/auth/status.js' import Command12 from './commands/autocomplete.js' -import Command13 from './commands/bridges/list.js' -import Command14 from './commands/bridges/show.js' -import Command15 from './commands/chats/archive.js' -import Command16 from './commands/chats/avatar.js' -import Command17 from './commands/chats/description.js' -import Command18 from './commands/chats/disappear.js' -import Command19 from './commands/chats/draft.js' -import Command20 from './commands/chats/focus.js' -import Command21 from './commands/chats/list.js' -import Command22 from './commands/chats/mark-read.js' -import Command23 from './commands/chats/mark-unread.js' -import Command24 from './commands/chats/mute.js' -import Command25 from './commands/chats/notify-anyway.js' -import Command26 from './commands/chats/pin.js' -import Command27 from './commands/chats/priority.js' -import Command28 from './commands/chats/remind.js' -import Command29 from './commands/chats/rename.js' -import Command30 from './commands/chats/search.js' -import Command31 from './commands/chats/show.js' -import Command32 from './commands/chats/start.js' -import Command33 from './commands/chats/unarchive.js' -import Command34 from './commands/chats/unmute.js' -import Command35 from './commands/chats/unpin.js' -import Command36 from './commands/chats/unremind.js' -import Command37 from './commands/completion.js' -import Command38 from './commands/config/get.js' -import Command39 from './commands/config/path.js' -import Command40 from './commands/config/reset.js' -import Command41 from './commands/config/set.js' -import Command42 from './commands/contacts/list.js' -import Command43 from './commands/contacts/search.js' -import Command44 from './commands/contacts/show.js' -import Command45 from './commands/docs.js' -import Command46 from './commands/doctor.js' -import Command47 from './commands/export.js' -import Command48 from './commands/install/desktop.js' -import Command49 from './commands/install/server.js' -import Command50 from './commands/man.js' -import Command51 from './commands/media/download.js' -import Command52 from './commands/messages/context.js' -import Command53 from './commands/messages/delete.js' -import Command54 from './commands/messages/edit.js' -import Command55 from './commands/messages/export.js' -import Command56 from './commands/messages/list.js' -import Command57 from './commands/messages/search.js' -import Command58 from './commands/messages/show.js' -import Command59 from './commands/plugins.js' -import Command60 from './commands/plugins/available.js' -import Command61 from './commands/presence.js' -import Command62 from './commands/resolve/account.js' -import Command63 from './commands/resolve/bridge.js' -import Command64 from './commands/resolve/chat.js' -import Command65 from './commands/resolve/contact.js' -import Command66 from './commands/resolve/target.js' -import Command67 from './commands/rpc.js' -import Command68 from './commands/schema.js' -import Command69 from './commands/send/file.js' -import Command70 from './commands/send/react.js' -import Command71 from './commands/send/sticker.js' -import Command72 from './commands/send/text.js' -import Command73 from './commands/send/unreact.js' -import Command74 from './commands/send/voice.js' -import Command75 from './commands/setup.js' -import Command76 from './commands/status.js' -import Command77 from './commands/targets/add/desktop.js' -import Command78 from './commands/targets/add/remote.js' -import Command79 from './commands/targets/add/server.js' -import Command80 from './commands/targets/disable.js' -import Command81 from './commands/targets/enable.js' -import Command82 from './commands/targets/list.js' -import Command83 from './commands/targets/logs.js' -import Command84 from './commands/targets/remove.js' -import Command85 from './commands/targets/restart.js' -import Command86 from './commands/targets/show.js' -import Command87 from './commands/targets/start.js' -import Command88 from './commands/targets/status.js' -import Command89 from './commands/targets/stop.js' -import Command90 from './commands/targets/use.js' -import Command91 from './commands/update.js' -import Command92 from './commands/verify.js' -import Command93 from './commands/verify/approve.js' -import Command94 from './commands/verify/cancel.js' -import Command95 from './commands/verify/list.js' -import Command96 from './commands/verify/qr-confirm.js' -import Command97 from './commands/verify/qr-scan.js' -import Command98 from './commands/verify/recovery-key.js' -import Command99 from './commands/verify/reset-recovery-key.js' -import Command100 from './commands/verify/sas.js' -import Command101 from './commands/verify/sas-confirm.js' -import Command102 from './commands/verify/show.js' -import Command103 from './commands/verify/start.js' -import Command104 from './commands/verify/status.js' -import Command105 from './commands/version.js' -import Command106 from './commands/watch.js' +import Command13 from './commands/bridges/config.js' +import Command14 from './commands/bridges/delete.js' +import Command15 from './commands/bridges/list.js' +import Command16 from './commands/bridges/login.js' +import Command17 from './commands/bridges/login-password.js' +import Command18 from './commands/bridges/logout.js' +import Command19 from './commands/bridges/proxy.js' +import Command20 from './commands/bridges/register.js' +import Command21 from './commands/bridges/run.js' +import Command22 from './commands/bridges/show.js' +import Command23 from './commands/bridges/whoami.js' +import Command24 from './commands/chats/archive.js' +import Command25 from './commands/chats/avatar.js' +import Command26 from './commands/chats/description.js' +import Command27 from './commands/chats/disappear.js' +import Command28 from './commands/chats/draft.js' +import Command29 from './commands/chats/focus.js' +import Command30 from './commands/chats/list.js' +import Command31 from './commands/chats/mark-read.js' +import Command32 from './commands/chats/mark-unread.js' +import Command33 from './commands/chats/mute.js' +import Command34 from './commands/chats/notify-anyway.js' +import Command35 from './commands/chats/pin.js' +import Command36 from './commands/chats/priority.js' +import Command37 from './commands/chats/remind.js' +import Command38 from './commands/chats/rename.js' +import Command39 from './commands/chats/search.js' +import Command40 from './commands/chats/show.js' +import Command41 from './commands/chats/start.js' +import Command42 from './commands/chats/unarchive.js' +import Command43 from './commands/chats/unmute.js' +import Command44 from './commands/chats/unpin.js' +import Command45 from './commands/chats/unremind.js' +import Command46 from './commands/completion.js' +import Command47 from './commands/config/get.js' +import Command48 from './commands/config/path.js' +import Command49 from './commands/config/reset.js' +import Command50 from './commands/config/set.js' +import Command51 from './commands/contacts/list.js' +import Command52 from './commands/contacts/search.js' +import Command53 from './commands/contacts/show.js' +import Command54 from './commands/docs.js' +import Command55 from './commands/doctor.js' +import Command56 from './commands/export.js' +import Command57 from './commands/install/desktop.js' +import Command58 from './commands/install/server.js' +import Command59 from './commands/man.js' +import Command60 from './commands/media/download.js' +import Command61 from './commands/messages/context.js' +import Command62 from './commands/messages/delete.js' +import Command63 from './commands/messages/edit.js' +import Command64 from './commands/messages/export.js' +import Command65 from './commands/messages/list.js' +import Command66 from './commands/messages/search.js' +import Command67 from './commands/messages/show.js' +import Command68 from './commands/plugins.js' +import Command69 from './commands/plugins/available.js' +import Command70 from './commands/presence.js' +import Command71 from './commands/resolve/account.js' +import Command72 from './commands/resolve/bridge.js' +import Command73 from './commands/resolve/chat.js' +import Command74 from './commands/resolve/contact.js' +import Command75 from './commands/resolve/target.js' +import Command76 from './commands/rpc.js' +import Command77 from './commands/schema.js' +import Command78 from './commands/send/file.js' +import Command79 from './commands/send/react.js' +import Command80 from './commands/send/sticker.js' +import Command81 from './commands/send/text.js' +import Command82 from './commands/send/unreact.js' +import Command83 from './commands/send/voice.js' +import Command84 from './commands/setup.js' +import Command85 from './commands/status.js' +import Command86 from './commands/targets/add/desktop.js' +import Command87 from './commands/targets/add/remote.js' +import Command88 from './commands/targets/add/server.js' +import Command89 from './commands/targets/disable.js' +import Command90 from './commands/targets/enable.js' +import Command91 from './commands/targets/list.js' +import Command92 from './commands/targets/logs.js' +import Command93 from './commands/targets/remove.js' +import Command94 from './commands/targets/restart.js' +import Command95 from './commands/targets/show.js' +import Command96 from './commands/targets/start.js' +import Command97 from './commands/targets/status.js' +import Command98 from './commands/targets/stop.js' +import Command99 from './commands/targets/use.js' +import Command100 from './commands/update.js' +import Command101 from './commands/verify.js' +import Command102 from './commands/verify/approve.js' +import Command103 from './commands/verify/cancel.js' +import Command104 from './commands/verify/list.js' +import Command105 from './commands/verify/qr-confirm.js' +import Command106 from './commands/verify/qr-scan.js' +import Command107 from './commands/verify/recovery-key.js' +import Command108 from './commands/verify/reset-recovery-key.js' +import Command109 from './commands/verify/sas.js' +import Command110 from './commands/verify/sas-confirm.js' +import Command111 from './commands/verify/show.js' +import Command112 from './commands/verify/start.js' +import Command113 from './commands/verify/status.js' +import Command114 from './commands/version.js' +import Command115 from './commands/watch.js' export const commands = { 'accounts': Command1, 'accounts:add': Command0, - 'accounts:chats': Command21, + 'accounts:chats': Command30, 'accounts:list': Command1, 'accounts:remove': Command2, 'accounts:show': Command3, @@ -122,105 +131,121 @@ export const commands = { 'auth:logout': Command10, 'auth:status': Command11, 'autocomplete': Command12, - 'bridges': Command13, - 'bridges:list': Command13, - 'bridges:show': Command14, - 'chats': Command21, - 'chats:archive': Command15, - 'chats:avatar': Command16, - 'chats:description': Command17, - 'chats:disappear': Command18, - 'chats:draft': Command19, - 'chats:focus': Command20, - 'chats:list': Command21, - 'chats:mark-read': Command22, - 'chats:mark-unread': Command23, - 'chats:mute': Command24, - 'chats:notify-anyway': Command25, - 'chats:pin': Command26, - 'chats:priority': Command27, - 'chats:remind': Command28, - 'chats:rename': Command29, - 'chats:search': Command30, - 'chats:show': Command31, - 'chats:start': Command32, - 'chats:unarchive': Command33, - 'chats:unmute': Command34, - 'chats:unpin': Command35, - 'chats:unremind': Command36, - 'completion': Command37, - 'config:get': Command38, - 'config:path': Command39, - 'config:reset': Command40, - 'config:set': Command41, - 'contacts': Command42, - 'contacts:list': Command42, - 'contacts:search': Command43, - 'contacts:show': Command44, - 'docs': Command45, - 'doctor': Command46, - 'export': Command47, - 'install:desktop': Command48, - 'install:server': Command49, - 'ls': Command21, - 'man': Command50, - 'media:download': Command51, - 'messages:context': Command52, - 'messages:delete': Command53, - 'messages:edit': Command54, - 'messages:export': Command55, - 'messages:list': Command56, - 'messages:search': Command57, - 'messages:show': Command58, - 'plugins': Command59, - 'plugins:available': Command60, - 'presence': Command61, - 'resolve:account': Command62, - 'resolve:bridge': Command63, - 'resolve:chat': Command64, - 'resolve:contact': Command65, - 'resolve:target': Command66, - 'rpc': Command67, - 'schema': Command68, - 'search': Command57, - 'send': Command72, - 'send:file': Command69, - 'send:react': Command70, - 'send:sticker': Command71, - 'send:text': Command72, - 'send:unreact': Command73, - 'send:voice': Command74, - 'setup': Command75, - 'status': Command76, - 'targets': Command82, - 'targets:add:desktop': Command77, - 'targets:add:remote': Command78, - 'targets:add:server': Command79, - 'targets:disable': Command80, - 'targets:enable': Command81, - 'targets:list': Command82, - 'targets:logs': Command83, - 'targets:remove': Command84, - 'targets:restart': Command85, - 'targets:show': Command86, - 'targets:start': Command87, - 'targets:status': Command88, - 'targets:stop': Command89, - 'targets:use': Command90, - 'update': Command91, - 'verify': Command92, - 'verify:approve': Command93, - 'verify:cancel': Command94, - 'verify:list': Command95, - 'verify:qr-confirm': Command96, - 'verify:qr-scan': Command97, - 'verify:recovery-key': Command98, - 'verify:reset-recovery-key': Command99, - 'verify:sas': Command100, - 'verify:sas-confirm': Command101, - 'verify:show': Command102, - 'verify:start': Command103, - 'verify:status': Command104, - 'version': Command105, - 'watch': Command106, + 'bridges': Command15, + 'bridges:c': Command13, + 'bridges:config': Command13, + 'bridges:d': Command14, + 'bridges:delete': Command14, + 'bridges:l': Command16, + 'bridges:list': Command15, + 'bridges:login': Command16, + 'bridges:login-password': Command17, + 'bridges:logout': Command18, + 'bridges:p': Command17, + 'bridges:proxy': Command19, + 'bridges:r': Command20, + 'bridges:register': Command20, + 'bridges:run': Command21, + 'bridges:show': Command22, + 'bridges:w': Command23, + 'bridges:whoami': Command23, + 'bridges:x': Command19, + 'chats': Command30, + 'chats:archive': Command24, + 'chats:avatar': Command25, + 'chats:description': Command26, + 'chats:disappear': Command27, + 'chats:draft': Command28, + 'chats:focus': Command29, + 'chats:list': Command30, + 'chats:mark-read': Command31, + 'chats:mark-unread': Command32, + 'chats:mute': Command33, + 'chats:notify-anyway': Command34, + 'chats:pin': Command35, + 'chats:priority': Command36, + 'chats:remind': Command37, + 'chats:rename': Command38, + 'chats:search': Command39, + 'chats:show': Command40, + 'chats:start': Command41, + 'chats:unarchive': Command42, + 'chats:unmute': Command43, + 'chats:unpin': Command44, + 'chats:unremind': Command45, + 'completion': Command46, + 'config:get': Command47, + 'config:path': Command48, + 'config:reset': Command49, + 'config:set': Command50, + 'contacts': Command51, + 'contacts:list': Command51, + 'contacts:search': Command52, + 'contacts:show': Command53, + 'docs': Command54, + 'doctor': Command55, + 'export': Command56, + 'install:desktop': Command57, + 'install:server': Command58, + 'ls': Command30, + 'man': Command59, + 'media:download': Command60, + 'messages:context': Command61, + 'messages:delete': Command62, + 'messages:edit': Command63, + 'messages:export': Command64, + 'messages:list': Command65, + 'messages:search': Command66, + 'messages:show': Command67, + 'plugins': Command68, + 'plugins:available': Command69, + 'presence': Command70, + 'resolve:account': Command71, + 'resolve:bridge': Command72, + 'resolve:chat': Command73, + 'resolve:contact': Command74, + 'resolve:target': Command75, + 'rpc': Command76, + 'schema': Command77, + 'search': Command66, + 'send': Command81, + 'send:file': Command78, + 'send:react': Command79, + 'send:sticker': Command80, + 'send:text': Command81, + 'send:unreact': Command82, + 'send:voice': Command83, + 'setup': Command84, + 'status': Command85, + 'targets': Command91, + 'targets:add:desktop': Command86, + 'targets:add:remote': Command87, + 'targets:add:server': Command88, + 'targets:disable': Command89, + 'targets:enable': Command90, + 'targets:list': Command91, + 'targets:logs': Command92, + 'targets:remove': Command93, + 'targets:restart': Command94, + 'targets:show': Command95, + 'targets:start': Command96, + 'targets:status': Command97, + 'targets:stop': Command98, + 'targets:use': Command99, + 'update': Command100, + 'verify': Command101, + 'verify:approve': Command102, + 'verify:cancel': Command103, + 'verify:list': Command104, + 'verify:qr-confirm': Command105, + 'verify:qr-scan': Command106, + 'verify:recovery-key': Command107, + 'verify:reset-recovery-key': Command108, + 'verify:sas': Command109, + 'verify:sas-confirm': Command110, + 'verify:show': Command111, + 'verify:start': Command112, + 'verify:status': Command113, + 'version': Command114, + 'watch': Command115, } diff --git a/packages/cli/src/commands/bridges/config.ts b/packages/cli/src/commands/bridges/config.ts new file mode 100644 index 00000000..0574e4df --- /dev/null +++ b/packages/cli/src/commands/bridges/config.ts @@ -0,0 +1,50 @@ +import { Args, Flags } from '@oclif/core' +import { ensureWritable } from '../../lib/command.js' +import { BridgeCommand } from '../../lib/bridges/command.js' +import { generateBridgeConfig, outputFile, prepareBridgeEnv } from '../../lib/bridges/manager.js' + +export default class BridgesConfig extends BridgeCommand { + static override summary = 'Generate a config for an official Beeper bridge' + static override aliases = ['bridges:c'] + static override args = { bridge: Args.string({ required: true, description: 'Self-hosted bridge name, usually sh-...' }) } + static override flags = { + env: Flags.string({ description: 'Beeper environment or domain (prod, staging, dev, local, or a domain)' }), + 'no-state': Flags.boolean({ default: false, description: "Don't send a bridge state update" }), + output: Flags.string({ char: 'o', default: process.env.BEEPER_BRIDGE_CONFIG_FILE ?? '-', description: 'Path to save generated config file to. Use - for stdout.' }), + param: Flags.string({ char: 'p', multiple: true, description: 'Bridge-specific config option in key=value form. Repeatable.' }), + type: Flags.string({ char: 't', default: process.env.BEEPER_BRIDGE_TYPE, description: 'The type of bridge being registered.' }), + } + + async run(): Promise { + const { args, flags } = await this.parse(BridgesConfig) + ensureWritable(flags) + const env = await prepareBridgeEnv(flags) + const cfg = await generateBridgeConfig(env, args.bridge, { + force: flags.force, + noState: flags['no-state'], + params: flags.param, + type: flags.type, + }) + await outputFile('Config', cfg.config ?? '', flags.output) + printStartupHint(cfg.bridgeType, flags.output, cfg.homeserver_url, cfg.your_user_id) + } +} + +function printStartupHint(bridgeType: string, outputPath: string, homeserverURL: string, userID: string): void { + const configPath = outputPath === '-' || !outputPath ? '' : outputPath + let startupCommand = '' + let installInstructions = '' + if (['imessage', 'whatsapp', 'discord', 'slack', 'gmessages', 'gvoice', 'signal', 'meta', 'twitter', 'bluesky', 'linkedin'].includes(bridgeType)) { + startupCommand = `mautrix-${bridgeType}` + if (configPath !== 'config.yaml' && configPath !== '') startupCommand += ` -c ${configPath}` + installInstructions = `https://docs.mau.fi/bridges/go/setup.html?bridge=${bridgeType}#installation` + } else if (bridgeType === 'imessagego') { + startupCommand = 'beeper-imessage' + if (configPath !== 'config.yaml' && configPath !== '') startupCommand += ` -c ${configPath}` + } else if (bridgeType === 'heisenbridge') { + startupCommand = `python -m heisenbridge -c ${configPath} -o ${userID} ${homeserverURL.replace('https://', 'wss://')}` + installInstructions = 'https://github.com/beeper/bridge-manager/wiki/Heisenbridge' + } + if (startupCommand) process.stderr.write(`\nStartup command: ${startupCommand}\n`) + if (installInstructions) process.stderr.write(`See ${installInstructions} for bridge installation instructions\n`) +} diff --git a/packages/cli/src/commands/bridges/delete.ts b/packages/cli/src/commands/bridges/delete.ts new file mode 100644 index 00000000..5f873299 --- /dev/null +++ b/packages/cli/src/commands/bridges/delete.ts @@ -0,0 +1,68 @@ +import { Args, Flags } from '@oclif/core' +import { createInterface } from 'node:readline/promises' +import { stdin as input, stdout as output } from 'node:process' +import { readdir, rm } from 'node:fs/promises' +import { join } from 'node:path' +import { ensureWritable, isForce } from '../../lib/command.js' +import { BridgeCommand } from '../../lib/bridges/command.js' +import { bridgeDataDir, deleteBridge, prepareBridgeEnv, validateBridgeID, whoami } from '../../lib/bridges/manager.js' + +export default class BridgesDelete extends BridgeCommand { + static override summary = 'Delete a bridge and all associated rooms on the Beeper servers' + static override aliases = ['bridges:d'] + static override args = { bridge: Args.string({ required: true, description: 'Bridge name' }) } + static override flags = { + env: Flags.string({ description: 'Beeper environment or domain (prod, staging, dev, local, or a domain)' }), + 'local-dev': Flags.boolean({ char: 'l', default: process.env.BEEPER_BRIDGE_LOCAL === '1', description: 'Delete bridge database and config from the current working directory' }), + } + + async run(): Promise { + const { args, flags } = await this.parse(BridgesDelete) + ensureWritable(flags) + if (args.bridge === 'hungryserv') throw new Error("You really shouldn't do that") + validateBridgeID(args.bridge) + const env = await prepareBridgeEnv(flags) + const bridgeDir = flags['local-dev'] ? process.cwd() : join(bridgeDataDir(env.envName), args.bridge) + + if (!flags.force) { + const info = await whoami(env) + const bridgeInfo = info.user.bridges?.[args.bridge] + if (!bridgeInfo) throw new Error(`You don't have a ${args.bridge} bridge.`) + if (!bridgeInfo.bridgeState?.isSelfHosted) throw new Error(`Your ${args.bridge} bridge is not self-hosted.`) + } + + if (!isForce(flags)) { + const confirmed = await confirm(`Are you sure you want to permanently delete ${args.bridge}?`) + if (!confirmed) throw new Error('bridge delete cancelled') + } + await deleteBridge(env, args.bridge) + process.stdout.write('Started deleting bridge\n') + await deleteLocalBridgeData(bridgeDir, !flags['local-dev']) + } +} + +async function confirm(message: string): Promise { + const rl = createInterface({ input, output }) + try { + const answer = (await rl.question(`${message} [y/N] `)).trim().toLowerCase() + return answer === 'y' || answer === 'yes' + } finally { + rl.close() + } +} + +async function deleteLocalBridgeData(bridgeDir: string, deleteWholeDir: boolean): Promise { + if (deleteWholeDir) { + await rm(bridgeDir, { force: true, recursive: true }) + process.stderr.write(`Deleted local bridge data from ${bridgeDir}\n`) + return + } + for (const item of await readdir(bridgeDir).catch(() => [])) { + if (isLocalBridgeFile(item)) await rm(join(bridgeDir, item), { force: true }) + } + process.stderr.write(`Deleted local bridge data from ${bridgeDir}\n`) +} + +function isLocalBridgeFile(name: string): boolean { + return name === 'config.yaml' || name.endsWith('.db') || name.endsWith('.db-shm') || name.endsWith('.db-wal') +} diff --git a/packages/cli/src/commands/bridges/list.ts b/packages/cli/src/commands/bridges/list.ts index 1d85a477..ac34a86a 100644 --- a/packages/cli/src/commands/bridges/list.ts +++ b/packages/cli/src/commands/bridges/list.ts @@ -1,28 +1,33 @@ import { Flags } from '@oclif/core' -import { BeeperCommand } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' +import { BridgeCommand } from '../../lib/bridges/command.js' import { printList } from '../../lib/output.js' +import { prepareBridgeEnv } from '../../lib/bridges/manager.js' -export default class BridgesList extends BeeperCommand { - static override summary = 'List bridges that can connect chat accounts' - static override description = '`bridges list` is the scriptable bridge catalog. Use `accounts add` without an argument for the guided account connection flow.' +export default class BridgesList extends BridgeCommand { + static override summary = 'List self-hosted bridge types' + static override description = '`bridges list` lists the bridge-manager templates available for `beeper bridges config` and `beeper bridges run`.' static override flags = { - provider: Flags.string({ options: ['local', 'cloud', 'self-hosted'], description: 'Limit to bridge provider' }), - available: Flags.boolean({ allowNo: true, description: 'Only bridges available to add (--no-available to exclude)' }), + env: Flags.string({ description: 'Beeper environment or domain (prod, staging, dev, local, or a domain)' }), } async run(): Promise { const { flags } = await this.parse(BridgesList) - const client = await createClient(flags) - const response = await client.bridges.list() - const items = ((response as unknown as { items?: Array> }).items ?? []) - .filter(item => !flags.provider || item.provider === flags.provider) - .filter(item => flags.available === undefined || (item.status === 'available') === flags.available) + const env = await prepareBridgeEnv(flags) + const items = env.catalog.supportedBridges.map(type => { + const official = env.catalog.officialBridges.find(item => item.typeName === type) + return { + id: type, + bridgeType: type, + names: official?.names ?? [], + websocket: Boolean(env.catalog.websocketBridges[type]), + template: `${type}.tpl.yaml`, + } + }) await printList(items, flags.json ? 'json' : 'human', { title: 'No bridges matched', - subtitle: 'Try removing provider or availability filters.', - suggestions: [{ command: 'beeper accounts add', hint: 'choose a bridge to connect an account' }], + subtitle: 'Add templates with BEEPER_BRIDGE_TEMPLATE_DIR or ~/.beeper/bridges/templates.', + suggestions: [{ command: 'beeper bridges config sh-discord --type discord', hint: 'generate a self-hosted bridge config' }], }) } } diff --git a/packages/cli/src/commands/bridges/login-password.ts b/packages/cli/src/commands/bridges/login-password.ts new file mode 100644 index 00000000..b7ed8d20 --- /dev/null +++ b/packages/cli/src/commands/bridges/login-password.ts @@ -0,0 +1,37 @@ +import { Flags } from '@oclif/core' +import { createInterface } from 'node:readline/promises' +import { stdin as input, stdout as output } from 'node:process' +import { ensureWritable, isNoInput } from '../../lib/command.js' +import { BridgeCommand } from '../../lib/bridges/command.js' +import { loginWithPassword, prepareBridgeTargetEnv } from '../../lib/bridges/manager.js' +import { printSuccess } from '../../lib/output.js' + +export default class BridgesLoginPassword extends BridgeCommand { + static override summary = 'Log into the Beeper server using username and password' + static override aliases = ['bridges:p'] + static override flags = { + env: Flags.string({ description: 'Beeper environment or domain (prod, staging, dev, local, or a domain)' }), + password: Flags.string({ char: 'p', default: process.env.BEEPER_PASSWORD, description: 'The Beeper password' }), + username: Flags.string({ char: 'u', default: process.env.BEEPER_USERNAME, description: 'The Beeper username to log in as' }), + } + + async run(): Promise { + const { flags } = await this.parse(BridgesLoginPassword) + ensureWritable(flags) + const env = await prepareBridgeTargetEnv(flags) + const username = flags.username || await prompt('Username:') + const password = flags.password || await prompt('Password:') + const login = await loginWithPassword(env, username, password) + await printSuccess({ message: `Successfully logged in as ${login.userID}`, data: { target: env.target.id, userID: login.userID } }, flags.json ? 'json' : 'human') + } +} + +async function prompt(message: string): Promise { + if (isNoInput() || !process.stdin.isTTY) throw new Error(`${message.replace(/:$/, '')} is required.`) + const rl = createInterface({ input, output }) + try { + return (await rl.question(`${message} `)).trim() + } finally { + rl.close() + } +} diff --git a/packages/cli/src/commands/bridges/login.ts b/packages/cli/src/commands/bridges/login.ts new file mode 100644 index 00000000..12dee329 --- /dev/null +++ b/packages/cli/src/commands/bridges/login.ts @@ -0,0 +1,36 @@ +import { Flags } from '@oclif/core' +import { createInterface } from 'node:readline/promises' +import { stdin as input, stdout as output } from 'node:process' +import { ensureWritable, isNoInput } from '../../lib/command.js' +import { BridgeCommand } from '../../lib/bridges/command.js' +import { loginWithEmail, prepareBridgeTargetEnv } from '../../lib/bridges/manager.js' +import { printSuccess } from '../../lib/output.js' + +export default class BridgesLogin extends BridgeCommand { + static override summary = 'Log into the Beeper server for bridge-manager APIs' + static override aliases = ['bridges:l'] + static override flags = { + email: Flags.string({ default: process.env.BEEPER_EMAIL, description: 'The Beeper account email to log in with' }), + env: Flags.string({ description: 'Beeper environment or domain (prod, staging, dev, local, or a domain)' }), + 'no-desktop': Flags.boolean({ default: process.env.BBCTL_NO_DESKTOP_LOGIN === '1', description: 'Accepted for bbctl compatibility; Desktop login is not used by this command' }), + } + + async run(): Promise { + const { flags } = await this.parse(BridgesLogin) + ensureWritable(flags) + const env = await prepareBridgeTargetEnv(flags) + const email = flags.email || await prompt('Email:') + const login = await loginWithEmail(env, email, () => prompt('Enter login code sent to your email:')) + await printSuccess({ message: `Successfully logged in as ${login.userID}`, data: { target: env.target.id, userID: login.userID } }, flags.json ? 'json' : 'human') + } +} + +async function prompt(message: string): Promise { + if (isNoInput() || !process.stdin.isTTY) throw new Error(`${message.replace(/:$/, '')} is required.`) + const rl = createInterface({ input, output }) + try { + return (await rl.question(`${message} `)).trim() + } finally { + rl.close() + } +} diff --git a/packages/cli/src/commands/bridges/logout.ts b/packages/cli/src/commands/bridges/logout.ts new file mode 100644 index 00000000..4e46ad38 --- /dev/null +++ b/packages/cli/src/commands/bridges/logout.ts @@ -0,0 +1,20 @@ +import { Flags } from '@oclif/core' +import { ensureWritable, isForce } from '../../lib/command.js' +import { BridgeCommand } from '../../lib/bridges/command.js' +import { logoutBridgeTarget, prepareBridgeEnv } from '../../lib/bridges/manager.js' +import { printSuccess } from '../../lib/output.js' + +export default class BridgesLogout extends BridgeCommand { + static override summary = 'Log out from the Beeper server for bridge-manager APIs' + static override flags = { + env: Flags.string({ description: 'Beeper environment or domain (prod, staging, dev, local, or a domain)' }), + } + + async run(): Promise { + const { flags } = await this.parse(BridgesLogout) + ensureWritable(flags) + const env = await prepareBridgeEnv(flags) + await logoutBridgeTarget(env, isForce(flags)) + await printSuccess({ message: 'Logged out successfully', data: { target: env.target.id } }, flags.json ? 'json' : 'human') + } +} diff --git a/packages/cli/src/commands/bridges/proxy.ts b/packages/cli/src/commands/bridges/proxy.ts new file mode 100644 index 00000000..27b3af97 --- /dev/null +++ b/packages/cli/src/commands/bridges/proxy.ts @@ -0,0 +1,23 @@ +import { Flags } from '@oclif/core' +import { BridgeCommand } from '../../lib/bridges/command.js' +import { prepareBridgeEnv, whoami } from '../../lib/bridges/manager.js' +import { proxyAppserviceWebsocket } from '../../lib/bridges/websocket-proxy.js' + +export default class BridgesProxy extends BridgeCommand { + static override summary = 'Connect to an appservice websocket and proxy it to a local appservice HTTP server' + static override aliases = ['bridges:x'] + static override flags = { + env: Flags.string({ description: 'Beeper environment or domain (prod, staging, dev, local, or a domain)' }), + registration: Flags.string({ char: 'r', required: true, default: process.env.BEEPER_BRIDGE_REGISTRATION_FILE, description: 'Registration file containing as_token, hs_token, and local appservice URL' }), + } + + async run(): Promise { + const { flags } = await this.parse(BridgesProxy) + const env = await prepareBridgeEnv(flags) + const info = await whoami(env) + await proxyAppserviceWebsocket({ + homeserverURL: `https://matrix.${env.domain}/_hungryserv/${encodeURIComponent(info.userInfo.username)}`, + registrationPath: flags.registration, + }) + } +} diff --git a/packages/cli/src/commands/bridges/register.ts b/packages/cli/src/commands/bridges/register.ts new file mode 100644 index 00000000..789fbb65 --- /dev/null +++ b/packages/cli/src/commands/bridges/register.ts @@ -0,0 +1,40 @@ +import { Args, Flags } from '@oclif/core' +import { ensureWritable } from '../../lib/command.js' +import { BridgeCommand } from '../../lib/bridges/command.js' +import { outputFile, prepareBridgeEnv, registerBridge, registrationToYAML, validateBridgeName, writeRegistrationJSON } from '../../lib/bridges/manager.js' + +export default class BridgesRegister extends BridgeCommand { + static override summary = 'Register a third-party bridge and print the appservice registration' + static override aliases = ['bridges:r'] + static override args = { bridge: Args.string({ required: true, description: 'Self-hosted bridge name, usually sh-...' }) } + static override flags = { + address: Flags.string({ char: 'a', default: process.env.BEEPER_BRIDGE_ADDRESS, description: 'HTTPS address where Beeper can push events. Omit to use websocket.' }), + env: Flags.string({ description: 'Beeper environment or domain (prod, staging, dev, local, or a domain)' }), + get: Flags.boolean({ char: 'g', default: false, description: "Only get existing registrations, don't create if missing" }), + json: Flags.boolean({ char: 'j', default: process.env.BEEPER_BRIDGE_REGISTRATION_JSON === '1', description: 'Return all data as JSON instead of registration YAML' }), + 'no-state': Flags.boolean({ default: false, description: "Don't send a bridge state update" }), + output: Flags.string({ char: 'o', default: process.env.BEEPER_BRIDGE_REGISTRATION_FILE ?? '-', description: 'Path to save generated registration file to. Use - for stdout.' }), + } + + async run(): Promise { + const { args, flags } = await this.parse(BridgesRegister) + ensureWritable(flags) + validateBridgeName(args.bridge, flags.force) + const env = await prepareBridgeEnv(flags) + const output = await registerBridge(env, args.bridge, { + address: flags.address, + force: flags.force, + get: flags.get, + noState: flags['no-state'], + }) + if (flags.json) { + await writeRegistrationJSON(output) + return + } + await outputFile('Registration', registrationToYAML(output.registration), flags.output) + process.stderr.write('\nAdditional bridge configuration details:\n') + process.stderr.write(`* Homeserver domain: ${output.homeserver_domain}\n`) + process.stderr.write(`* Homeserver URL: ${output.homeserver_url}\n`) + process.stderr.write(`* Your user ID: ${output.your_user_id}\n`) + } +} diff --git a/packages/cli/src/commands/bridges/run.ts b/packages/cli/src/commands/bridges/run.ts new file mode 100644 index 00000000..c1ce3049 --- /dev/null +++ b/packages/cli/src/commands/bridges/run.ts @@ -0,0 +1,179 @@ +import { Args, Flags } from '@oclif/core' +import { constants as fsConstants } from 'node:fs' +import { access, mkdir, writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import { platform } from 'node:os' +import { spawn } from 'node:child_process' +import { once } from 'node:events' +import { ensureWritable } from '../../lib/command.js' +import { BridgeCommand } from '../../lib/bridges/command.js' +import { + bridgeDataDir, + compileGoBridge, + generateBridgeConfig, + prepareBridgeEnv, + registerBridge, + runCommand, + setupPythonVenv, + updateGoBridge, + whoami, + type GeneratedBridgeConfig, +} from '../../lib/bridges/manager.js' +import { runProxyLoop } from '../../lib/bridges/websocket-proxy.js' + +export default class BridgesRun extends BridgeCommand { + static override summary = 'Run an official Beeper bridge' + static override args = { bridge: Args.string({ required: true, description: 'Self-hosted bridge name, usually sh-...' }) } + static override flags = { + compile: Flags.boolean({ default: process.env.BEEPER_BRIDGE_COMPILE === '1', description: 'Clone the bridge repository and compile locally instead of downloading a CI binary' }), + 'config-file': Flags.string({ char: 'c', default: process.env.BEEPER_BRIDGE_CONFIG_FILE ?? 'config.yaml', description: 'File name to save the config to' }), + 'custom-startup-command': Flags.string({ default: process.env.BEEPER_BRIDGE_CUSTOM_STARTUP_COMMAND, description: 'Custom binary or script to run for startup. Disables update checks.' }), + env: Flags.string({ description: 'Beeper environment or domain (prod, staging, dev, local, or a domain)' }), + 'local-dev': Flags.boolean({ char: 'l', default: process.env.BEEPER_BRIDGE_LOCAL === '1', description: 'Run the bridge in the current working directory' }), + 'no-override-config': Flags.boolean({ default: process.env.BEEPER_BRIDGE_NO_OVERRIDE_CONFIG === '1', description: "Don't override config file if it already exists" }), + 'no-state': Flags.boolean({ default: false, description: "Don't send a bridge state update" }), + 'no-update': Flags.boolean({ char: 'n', default: process.env.BEEPER_BRIDGE_NO_UPDATE === '1', description: "Don't update the bridge even if it is out of date" }), + param: Flags.string({ char: 'p', multiple: true, description: 'Bridge-specific config option in key=value form. Repeatable.' }), + type: Flags.string({ char: 't', default: process.env.BEEPER_BRIDGE_TYPE, description: 'The type of bridge to run.' }), + } + + async run(): Promise { + const { args, flags } = await this.parse(BridgesRun) + ensureWritable(flags) + const env = await prepareBridgeEnv(flags) + const dataDir = bridgeDataDir(env.envName) + const bridgeDir = flags['local-dev'] ? process.cwd() : join(dataDir, args.bridge) + await mkdir(join(bridgeDir, 'logs'), { recursive: true, mode: 0o700 }) + + const configPath = join(bridgeDir, flags['config-file']) + const shouldWriteConfig = !(flags['no-override-config'] || flags['local-dev']) || !await exists(configPath) + let cfg: GeneratedBridgeConfig + if (shouldWriteConfig) { + cfg = await generateBridgeConfig(env, args.bridge, { + force: flags.force, + noState: flags['no-state'], + params: flags.param, + type: flags.type, + }) + await writeFile(configPath, cfg.config ?? '', { mode: 0o600 }) + } else { + const info = await whoami(env) + const bridgeType = info.user.bridges?.[args.bridge]?.bridgeState?.bridgeType + if (!bridgeType) { + cfg = await generateBridgeConfig(env, args.bridge, { + force: flags.force, + noState: flags['no-state'], + params: flags.param, + type: flags.type, + }) + await writeFile(configPath, cfg.config ?? '', { mode: 0o600 }) + } else { + const reg = await registerBridge(env, args.bridge, { bridgeType, force: flags.force, get: true, noState: flags['no-state'] }) + cfg = { ...reg, bridgeType } + } + process.stderr.write(`Config already exists, not overriding - delete ${configPath} to regenerate it\n`) + } + + const startup = await prepareStartup({ + bridgeDir, + cfg, + compile: flags.compile, + configFile: flags['config-file'], + customStartupCommand: flags['custom-startup-command'], + dataDir, + localDev: flags['local-dev'], + noUpdate: flags['no-update'], + }) + process.stderr.write(`Starting ${cfg.bridgeType}\n`) + await runBridgeProcess({ ...startup, bridgeDir, cfg, env }) + } +} + +async function prepareStartup(options: { + bridgeDir: string + cfg: GeneratedBridgeConfig + compile: boolean + configFile: string + customStartupCommand?: string + dataDir: string + localDev: boolean + noUpdate: boolean +}): Promise<{ command: string; args: string[]; needsWebsocketProxy: boolean }> { + const goBridges = ['imessage', 'imessagego', 'whatsapp', 'discord', 'slack', 'gmessages', 'gvoice', 'signal', 'meta', 'twitter', 'bluesky', 'linkedin', 'telegram'] + if (goBridges.includes(options.cfg.bridgeType)) { + const binaryName = options.cfg.bridgeType === 'imessagego' ? 'beeper-imessage' : `mautrix-${options.cfg.bridgeType}` + let command = join(options.dataDir, 'binaries', binaryName) + if (options.customStartupCommand) { + command = options.customStartupCommand + } else if (options.localDev) { + command = join(options.bridgeDir, binaryName) + await runCommand('./build.sh', [], options.bridgeDir) + } else if (options.compile) { + const buildDir = join(options.dataDir, 'compile', binaryName) + command = join(buildDir, binaryName) + await compileGoBridge(buildDir, command, options.cfg.bridgeType, options.noUpdate) + } else { + await updateGoBridge(command, options.cfg.bridgeType, options.noUpdate) + } + return { command, args: ['-c', options.configFile], needsWebsocketProxy: false } + } + if (options.cfg.bridgeType === 'googlechat') { + const command = options.customStartupCommand ?? join(await setupPythonVenv(options.bridgeDir, options.cfg.bridgeType, options.localDev), 'bin', 'python3') + return { command, args: ['-m', 'mautrix_googlechat', '-c', options.configFile], needsWebsocketProxy: true } + } + if (options.cfg.bridgeType === 'heisenbridge') { + const command = options.customStartupCommand ?? join(await setupPythonVenv(options.bridgeDir, options.cfg.bridgeType, options.localDev), 'bin', 'python3') + return { + command, + args: ['-m', 'heisenbridge', '-c', options.configFile, '-o', options.cfg.your_user_id, options.cfg.homeserver_url.replace('https://', 'wss://')], + needsWebsocketProxy: false, + } + } + throw new Error('Unsupported bridge type for beeper bridges run') +} + +async function runBridgeProcess(options: { + args: string[] + bridgeDir: string + cfg: GeneratedBridgeConfig + command: string + env: { domain: string } + needsWebsocketProxy: boolean +}): Promise { + const controller = new AbortController() + const child = spawn(options.command, options.args, { + cwd: options.bridgeDir, + detached: platform() === 'linux', + stdio: 'inherit', + }) + let proxyDone: Promise | undefined + if (options.needsWebsocketProxy) { + proxyDone = runProxyLoop(controller.signal, options.cfg.homeserver_url, options.cfg.registration) + proxyDone.catch(error => { + process.stderr.write(`Websocket proxy exited: ${(error as Error).message}\n`) + child.kill('SIGTERM') + }) + } + const shutdown = () => { + controller.abort() + if (platform() === 'linux' && child.pid) process.kill(-child.pid, 'SIGTERM') + else child.kill('SIGTERM') + setTimeout(() => child.kill('SIGKILL'), 3000).unref() + } + process.once('SIGINT', shutdown) + process.once('SIGTERM', shutdown) + const [code, signal] = await once(child, 'exit') as [number | null, NodeJS.Signals | null] + controller.abort() + if (proxyDone) await proxyDone.catch(() => undefined) + if (code !== 0) throw new Error(`${options.command} exited with ${signal ?? code}`) + process.stderr.write('Bridge exited\n') +} + +async function exists(path: string): Promise { + try { + await access(path, fsConstants.F_OK) + return true + } catch { + return false + } +} diff --git a/packages/cli/src/commands/bridges/show.ts b/packages/cli/src/commands/bridges/show.ts index 592a32eb..b2d5055e 100644 --- a/packages/cli/src/commands/bridges/show.ts +++ b/packages/cli/src/commands/bridges/show.ts @@ -1,53 +1,37 @@ -import { Args } from '@oclif/core' -import { BeeperCommand } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' +import { Args, Flags } from '@oclif/core' +import { BridgeCommand } from '../../lib/bridges/command.js' import { printData } from '../../lib/output.js' +import { prepareBridgeEnv } from '../../lib/bridges/manager.js' -export default class BridgesShow extends BeeperCommand { - static override summary = 'Show bridge details, login flows, and connected accounts' +export default class BridgesShow extends BridgeCommand { + static override summary = 'Show self-hosted bridge type details' static override args = { - bridge: Args.string({ required: true, description: 'Bridge ID, display name, network, or type' }), + bridge: Args.string({ required: true, description: 'Bridge type' }), + } + static override flags = { + env: Flags.string({ description: 'Beeper environment or domain (prod, staging, dev, local, or a domain)' }), + template: Flags.boolean({ default: false, description: 'Print the raw template' }), } async run(): Promise { const { args, flags } = await this.parse(BridgesShow) - const client = await createClient(flags) - const response = await client.bridges.list() - const listBridge = resolveBridge(((response as unknown as { items?: Array> }).items ?? []), args.bridge) - const bridgeID = String(listBridge.id) - const [bridge, loginFlows, capabilities] = await Promise.all([ - client.bridges.retrieve(bridgeID).catch(() => listBridge), - client.bridges.loginFlows.list(bridgeID).catch(() => undefined), - client.bridges.retrieveCapabilities(bridgeID).catch(() => undefined), - ]) + const env = await prepareBridgeEnv(flags) + const templateName = `${args.bridge}.tpl.yaml` + const template = env.catalog.templates[templateName] + if (!template) throw new Error(`Unknown bridge type "${args.bridge}".`) + if (flags.template && !flags.json) { + process.stdout.write(template) + return + } + const official = env.catalog.officialBridges.find(item => item.typeName === args.bridge) await printData({ - ...bridge, - loginFlows: loginFlows ? (loginFlows as { items?: unknown[] }).items ?? loginFlows : undefined, - capabilities, + id: args.bridge, + bridgeType: args.bridge, + names: official?.names ?? [], + websocket: Boolean(env.catalog.websocketBridges[args.bridge]), + ipSuffix: env.catalog.bridgeIPSuffix[args.bridge], + template: templateName, + templateBody: flags.template ? template : undefined, }, flags.json ? 'json' : 'human') } } - -function resolveBridge(items: Array>, input: string): Record { - const normalizedInput = normalize(input) - const fields = (item: Record): unknown[] => [item.id, item.displayName, item.network, item.type] - - const exact = items.filter(item => fields(item).some(value => normalize(value) === normalizedInput)) - if (exact.length === 1) return exact[0]! - if (exact.length > 1) throw ambiguousBridge(input, exact) - - const partial = items.filter(item => fields(item).some(value => normalize(value).includes(normalizedInput))) - if (partial.length === 1) return partial[0]! - if (partial.length > 1) throw ambiguousBridge(input, partial) - - throw new Error(`Unknown bridge "${input}". Run \`beeper bridges list\`.`) -} - -function ambiguousBridge(input: string, matches: Array>): Error { - const options = matches.map(item => `${String(item.displayName ?? item.id)} (${String(item.id)})`).join(', ') - return new Error(`Bridge "${input}" is ambiguous. Use one of: ${options}`) -} - -function normalize(value: unknown): string { - return String(value ?? '').toLowerCase().replaceAll(/[^a-z0-9]+/g, '') -} diff --git a/packages/cli/src/commands/bridges/whoami.ts b/packages/cli/src/commands/bridges/whoami.ts new file mode 100644 index 00000000..da087bf9 --- /dev/null +++ b/packages/cli/src/commands/bridges/whoami.ts @@ -0,0 +1,78 @@ +import { Flags } from '@oclif/core' +import { BridgeCommand } from '../../lib/bridges/command.js' +import { hungryURL, prepareBridgeEnv, whoami, type WhoamiBridge, type WhoamiResponse } from '../../lib/bridges/manager.js' +import { printData } from '../../lib/output.js' + +export default class BridgesWhoami extends BridgeCommand { + static override summary = 'Show Beeper account details for bridge-manager APIs' + static override aliases = ['bridges:w'] + static override flags = { + env: Flags.string({ description: 'Beeper environment or domain (prod, staging, dev, local, or a domain)' }), + raw: Flags.boolean({ char: 'r', default: process.env.BEEPER_WHOAMI_RAW === '1', description: 'Get raw JSON output instead of pretty-printed bridge status' }), + } + + async run(): Promise { + const { flags } = await this.parse(BridgesWhoami) + const env = await prepareBridgeEnv(flags) + const data = await whoami(env) + if (flags.raw) { + process.stdout.write(`${JSON.stringify(data, null, 2)}\n`) + return + } + if (flags.json) { + await printData(data, 'json') + return + } + printWhoami(data, env.domain) + } +} + +function printWhoami(data: WhoamiResponse, domain: string): void { + const user = data.userInfo + process.stdout.write(`User ID: @${user.username}:${domain}\n`) + if (user.isAdmin) process.stdout.write('Admin: true\n') + if (user.isFree) process.stdout.write('Free: true\n') + if (user.fullName) process.stdout.write(`Name: ${user.fullName}\n`) + if (user.email) process.stdout.write(`Email: ${user.email}\n`) + if (user.supportRoomId) process.stdout.write(`Support room ID: ${user.supportRoomId}\n`) + if (user.createdAt) process.stdout.write(`Registered at: ${new Date(user.createdAt).toLocaleString()}\n`) + process.stdout.write('Cloud bridge details:\n') + if (user.channel) process.stdout.write(` Update channel: ${user.channel}\n`) + if (user.bridgeClusterId) process.stdout.write(` Cluster ID: ${user.bridgeClusterId}\n`) + process.stdout.write(` Hungryserv URL: ${hungryURL(domain, user.username)}\n`) + process.stdout.write('Bridges:\n') + if (data.user.hungryserv) process.stdout.write(` ${formatBridge('hungryserv', data.user.hungryserv)}\n`) + for (const name of Object.keys(data.user.bridges ?? {}).sort()) { + process.stdout.write(` ${formatBridge(name, data.user.bridges![name]!)}\n`) + } +} + +function formatBridge(name: string, bridge: WhoamiBridge): string { + const parts = [name] + const version = parseBridgeImage(name, bridge.version) + if (version) parts.push(`(version: ${version})`) + if (bridge.bridgeState?.isSelfHosted) { + const typeName = bridge.bridgeState.bridgeType && !name.includes(bridge.bridgeState.bridgeType) ? `${bridge.bridgeState.bridgeType}, ` : '' + parts.push(`(${typeName}self-hosted)`) + } + parts.push(`- ${bridge.bridgeState?.stateEvent ?? 'UNKNOWN'}`) + const remote = formatBridgeRemotes(name, bridge) + if (remote) parts.push(`- ${remote}`) + return parts.join(' ') +} + +function formatBridgeRemotes(name: string, bridge: WhoamiBridge): string { + if (['hungryserv', 'androidsms', 'imessage'].includes(name)) return '' + const states = Object.values(bridge.remoteState ?? {}) + if (!states.length) return bridge.bridgeState?.isSelfHosted ? '' : 'not logged in' + if (states.length > 1) return 'multiple remotes' + const state = states[0]! + return `remote: ${state.stateEvent ?? 'UNKNOWN'} (${state.remoteName ?? ''} / ${state.remoteID ?? ''})` +} + +function parseBridgeImage(name: string, image: string | undefined): string { + if (!image || image === '?') return '' + if (name === 'imessagecloud') return image.slice(0, 8) + const match = image.match(/^docker\.beeper-tools\.com\/(?:bridge\/)?([a-z]+):(v2-)?([0-9a-f]{40})(?:-amd64)?$/) + return match ? `${match[2] ?? ''}${match[3]!.slice(0, 8)}` : image +} diff --git a/packages/cli/src/lib/bridges/catalog.ts b/packages/cli/src/lib/bridges/catalog.ts new file mode 100644 index 00000000..562d7ff8 --- /dev/null +++ b/packages/cli/src/lib/bridges/catalog.ts @@ -0,0 +1,85 @@ +import { constants as fsConstants } from 'node:fs' +import { access, readFile, readdir } from 'node:fs/promises' +import { basename, delimiter, join } from 'node:path' +import { beeperDir } from '../targets.js' +import { + generatedBridgeIPSuffix, + generatedOfficialBridges, + generatedSupportedBridges, + generatedTemplates, + generatedWebsocketBridges, + type GeneratedOfficialBridge, +} from './generated.js' + +export type BridgeCatalog = { + bridgeIPSuffix: Record + officialBridges: GeneratedOfficialBridge[] + supportedBridges: string[] + templates: Record + websocketBridges: Record +} + +type CatalogPlugin = Partial> + +export async function loadBridgeCatalog(): Promise { + const catalog: BridgeCatalog = { + bridgeIPSuffix: { ...generatedBridgeIPSuffix }, + officialBridges: [...generatedOfficialBridges], + supportedBridges: [...generatedSupportedBridges], + templates: { ...generatedTemplates }, + websocketBridges: { ...generatedWebsocketBridges }, + } + + for (const dir of pluginTemplateDirs()) await loadTemplateDir(catalog, dir) + for (const path of pluginCatalogPaths()) await loadCatalogPlugin(catalog, path) + catalog.supportedBridges = Object.keys(catalog.templates).map(file => basename(file, '.tpl.yaml')).sort() + return catalog +} + +export function templateName(bridgeType: string): string { + return `${bridgeType}.tpl.yaml` +} + +export function isSupported(catalog: BridgeCatalog, bridgeType: string): boolean { + return Boolean(catalog.templates[templateName(bridgeType)]) +} + +function pluginTemplateDirs(): string[] { + const dirs = [join(beeperDir(), 'bridges', 'templates')] + const envDirs = process.env.BEEPER_BRIDGE_TEMPLATE_DIR?.split(delimiter).filter(Boolean) ?? [] + return [...dirs, ...envDirs] +} + +function pluginCatalogPaths(): string[] { + const paths = [join(beeperDir(), 'bridges', 'bridges.json')] + const envPaths = process.env.BEEPER_BRIDGE_CATALOG?.split(delimiter).filter(Boolean) ?? [] + return [...paths, ...envPaths] +} + +async function loadTemplateDir(catalog: BridgeCatalog, dir: string): Promise { + if (!await exists(dir)) return + const files = (await readdir(dir)).filter(file => file.endsWith('.tpl.yaml')) + for (const file of files) catalog.templates[file] = await readFile(join(dir, file), 'utf8') +} + +async function loadCatalogPlugin(catalog: BridgeCatalog, path: string): Promise { + if (!await exists(path)) return + const plugin = JSON.parse(await readFile(path, 'utf8')) as CatalogPlugin + if (plugin.bridgeIPSuffix) Object.assign(catalog.bridgeIPSuffix, plugin.bridgeIPSuffix) + if (plugin.websocketBridges) Object.assign(catalog.websocketBridges, plugin.websocketBridges) + if (plugin.templates) Object.assign(catalog.templates, plugin.templates) + if (plugin.officialBridges) { + const byType = new Map(catalog.officialBridges.map(item => [item.typeName, item])) + for (const item of plugin.officialBridges) byType.set(item.typeName, item) + catalog.officialBridges = [...byType.values()] + } +} + +async function exists(path: string): Promise { + try { + await access(path, fsConstants.F_OK) + return true + } catch { + return false + } +} diff --git a/packages/cli/src/lib/bridges/command.ts b/packages/cli/src/lib/bridges/command.ts new file mode 100644 index 00000000..9e88a6e7 --- /dev/null +++ b/packages/cli/src/lib/bridges/command.ts @@ -0,0 +1,9 @@ +import { Flags } from '@oclif/core' +import { BeeperCommand } from '../command.js' + +export abstract class BridgeCommand extends BeeperCommand { + static override baseFlags = { + ...BeeperCommand.baseFlags, + target: Flags.string({ description: 'Named Beeper target to use for this command' }), + } +} diff --git a/packages/cli/src/lib/bridges/generated.ts b/packages/cli/src/lib/bridges/generated.ts new file mode 100644 index 00000000..6c25e6fe --- /dev/null +++ b/packages/cli/src/lib/bridges/generated.ts @@ -0,0 +1,178 @@ +// Generated by scripts/sync-bridge-manager-config.ts from bridge-manager. Do not edit by hand. + +export type GeneratedOfficialBridge = { typeName: string; names: string[] } + +export const generatedTemplates: Record = { + "bluesky.tpl.yaml": "# Network-specific config options\nnetwork:\n # Displayname template for Bluesky users. Available variables:\n # .DisplayName - displayname set by the user. Not required, may be empty.\n # .Handle - username (domain) of the user. Always present.\n # .DID - internal user ID starting with `did:`. Always present.\n displayname_template: {{ `\"{{or .DisplayName .Handle}}\"` }}\n\n{{ setfield . \"CommandPrefix\" \"!bsky\" -}}\n{{ setfield . \"DatabaseFileName\" \"mautrix-bluesky\" -}}\n{{ setfield . \"BridgeTypeName\" \"Bluesky\" -}}\n{{ setfield . \"BridgeTypeIcon\" \"mxc://maunium.net/ezAjjDxhiJWGEohmhkpfeHYf\" -}}\n{{ setfield . \"DefaultPickleKey\" \"go.mau.fi/mautrix-bluesky\" -}}\n{{ template \"bridgev2.tpl.yaml\" . }}\n", + "bridgev2.tpl.yaml": "# Config options that affect the central bridge module.\nbridge:\n {{ if .CommandPrefix -}}\n # The prefix for commands. Only required in non-management rooms.\n command_prefix: '{{ .CommandPrefix }}'\n {{ end -}}\n # Should the bridge create a space for each login containing the rooms that account is in?\n personal_filtering_spaces: true\n # Whether the bridge should set names and avatars explicitly for DM portals.\n # This is only necessary when using clients that don't support MSC4171.\n private_chat_portal_meta: false\n # Should events be handled asynchronously within portal rooms?\n # If true, events may end up being out of order, but slow events won't block other ones.\n # This is not yet safe to use.\n async_events: false\n # Should every user have their own portals rather than sharing them?\n # By default, users who are in the same group on the remote network will be\n # in the same Matrix room bridged to that group. If this is set to true,\n # every user will get their own Matrix room instead.\n split_portals: true\n # Should the bridge resend `m.bridge` events to all portals on startup?\n resend_bridge_info: false\n # Should `m.bridge` events be sent without a state key?\n # By default, the bridge uses a unique key that won't conflict with other bridges.\n no_bridge_info_state_key: false\n # Should bridge connection status be sent to the management room as `m.notice` events?\n # These contain the same data that can be posted to an external HTTP server using homeserver -> status_endpoint.\n # Allowed values: none, errors, all\n bridge_status_notices: none\n # How long after an unknown error should the bridge attempt a full reconnect?\n # Must be at least 1 minute. The bridge will add an extra ±20% jitter to this value.\n unknown_error_auto_reconnect: null\n\n # Should leaving Matrix rooms be bridged as leaving groups on the remote network?\n bridge_matrix_leave: false\n # Should `m.notice` messages be bridged?\n bridge_notices: false\n # Should room tags only be synced when creating the portal? Tags mean things like favorite/pin and archive/low priority.\n # Tags currently can't be synced back to the remote network, so a continuous sync means tagging from Matrix will be undone.\n tag_only_on_create: true\n # List of tags to allow bridging. If empty, no tags will be bridged.\n only_bridge_tags: [m.favourite, m.lowpriority]\n # Should room mute status only be synced when creating the portal?\n # Like tags, mutes can't currently be synced back to the remote network.\n mute_only_on_create: true\n # Should the bridge check the db to ensure that incoming events haven't been handled before\n deduplicate_matrix_messages: false\n # Should cross-room reply metadata be bridged?\n # Most Matrix clients don't support this and servers may reject such messages too.\n cross_room_replies: true\n\n # What should be done to portal rooms when a user logs out or is logged out?\n # Permitted values:\n # nothing - Do nothing, let the user stay in the portals\n # kick - Remove the user from the portal rooms, but don't delete them\n # unbridge - Remove all ghosts in the room and disassociate it from the remote chat\n # delete - Remove all ghosts and users from the room (i.e. delete it)\n cleanup_on_logout:\n # Should cleanup on logout be enabled at all?\n enabled: true\n # Settings for manual logouts (explicitly initiated by the Matrix user)\n manual:\n # Action for private portals which will never be shared with other Matrix users.\n private: delete\n # Action for portals with a relay user configured.\n relayed: delete\n # Action for portals which may be shared, but don't currently have any other Matrix users.\n shared_no_users: delete\n # Action for portals which have other logged-in Matrix users.\n shared_has_users: delete\n # Settings for credentials being invalidated (initiated by the remote network, possibly through user action).\n # Keys have the same meanings as in the manual section.\n bad_credentials:\n private: nothing\n relayed: nothing\n shared_no_users: nothing\n shared_has_users: nothing\n\n # Settings for relay mode\n relay:\n # Whether relay mode should be allowed. If allowed, the set-relay command can be used to turn any\n # authenticated user into a relaybot for that chat.\n enabled: false\n # Should only admins be allowed to set themselves as relay users?\n admin_only: true\n # List of user login IDs which anyone can set as a relay, as long as the relay user is in the room.\n default_relays: []\n\n # Permissions for using the bridge.\n # Permitted values:\n # relay - Talk through the relaybot (if enabled), no access otherwise\n # commands - Access to use commands in the bridge, but not login.\n # user - Access to use the bridge with puppeting.\n # admin - Full access, user level with some additional administration tools.\n # Permitted keys:\n # * - All Matrix users\n # domain - All users on that homeserver\n # mxid - Specific user\n permissions:\n \"{{ .UserID }}\": admin\n\n# Config for the bridge's database.\ndatabase:\n # The database type. \"sqlite3-fk-wal\" and \"postgres\" are supported.\n type: sqlite3-fk-wal\n # The database URI.\n # SQLite: A raw file path is supported, but `file:?_txlock=immediate` is recommended.\n # https://github.com/mattn/go-sqlite3#connection-string\n # Postgres: Connection string. For example, postgres://user:password@host/database?sslmode=disable\n # To connect via Unix socket, use something like postgres:///dbname?host=/var/run/postgresql\n uri: file:{{.DatabasePrefix}}{{or .DatabaseFileName .BridgeName}}.db?_txlock=immediate\n # Maximum number of connections.\n max_open_conns: 5\n max_idle_conns: 2\n # Maximum connection idle time and lifetime before they're closed. Disabled if null.\n # Parsed with https://pkg.go.dev/time#ParseDuration\n max_conn_idle_time: null\n max_conn_lifetime: null\n\n# Homeserver details.\nhomeserver:\n # The address that this appservice can use to connect to the homeserver.\n # Local addresses without HTTPS are generally recommended when the bridge is running on the same machine,\n # but https also works if they run on different machines.\n address: {{ .HungryAddress }}\n # The domain of the homeserver (also known as server_name, used for MXIDs, etc).\n domain: beeper.local\n\n # What software is the homeserver running?\n # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use \"standard\" here.\n software: hungry\n # The URL to push real-time bridge status to.\n # If set, the bridge will make POST requests to this URL whenever a user's remote network connection state changes.\n # The bridge will use the appservice as_token to authorize requests.\n status_endpoint: null\n # Endpoint for reporting per-message status.\n # If set, the bridge will make POST requests to this URL when processing a message from Matrix.\n # It will make one request when receiving the message (step BRIDGE), one after decrypting if applicable\n # (step DECRYPTED) and one after sending to the remote network (step REMOTE). Errors will also be reported.\n # The bridge will use the appservice as_token to authorize requests.\n message_send_checkpoint_endpoint: null\n # Does the homeserver support https://github.com/matrix-org/matrix-spec-proposals/pull/2246?\n async_media: true\n\n # Should the bridge use a websocket for connecting to the homeserver?\n # The server side is currently not documented anywhere and is only implemented by mautrix-wsproxy,\n # mautrix-asmux (deprecated), and hungryserv (proprietary).\n websocket: {{ .Websocket }}\n # How often should the websocket be pinged? Pinging will be disabled if this is zero.\n ping_interval_seconds: 180\n\n# Application service host/registration related details.\n# Changing these values requires regeneration of the registration.\nappservice:\n # The address that the homeserver can use to connect to this appservice.\n address: irrelevant\n # A public address that external services can use to reach this appservice.\n # This value doesn't affect the registration file.\n public_address:\n\n # The hostname and port where this appservice should listen.\n # For Docker, you generally have to change the hostname to 0.0.0.0.\n hostname: 0.0.0.0\n port: 4000\n\n # The unique ID of this appservice.\n id: {{ .AppserviceID }}\n # Appservice bot details.\n bot:\n # Username of the appservice bot.\n username: {{ .BridgeName }}bot\n # Display name and avatar for bot. Set to \"remove\" to remove display name/avatar, leave empty\n # to leave display name/avatar as-is.\n {{ if .BridgeTypeName -}}\n displayname: {{ .BridgeTypeName }} bridge bot\n {{- end }}\n {{ if .BridgeTypeIcon -}}\n avatar: {{ .BridgeTypeIcon }}\n {{- end }}\n\n # Whether to receive ephemeral events via appservice transactions.\n ephemeral_events: true\n # Should incoming events be handled asynchronously?\n # This may be necessary for large public instances with lots of messages going through.\n # However, messages will not be guaranteed to be bridged in the same order they were sent in.\n async_transactions: false\n\n # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify.\n as_token: {{ .ASToken }}\n hs_token: {{ .HSToken }}\n\n # Localpart template of MXIDs for remote users.\n username_template: {{ .BridgeName }}_{{ \"{{.}}\" }}\n\n# Config options that affect the Matrix connector of the bridge.\nmatrix:\n # Whether the bridge should send the message status as a custom com.beeper.message_send_status event.\n message_status_events: true\n # Whether the bridge should send a read receipt after successfully bridging a message.\n delivery_receipts: false\n # Whether the bridge should send error notices via m.notice events when a message fails to bridge.\n message_error_notices: false\n # Whether the bridge should update the m.direct account data event when double puppeting is enabled.\n sync_direct_chat_list: false\n # Whether created rooms should have federation enabled. If false, created portal rooms\n # will never be federated. Changing this option requires recreating rooms.\n federate_rooms: false\n # The threshold as bytes after which the bridge should roundtrip uploads via the disk\n # rather than keeping the whole file in memory.\n upload_file_threshold: 5242880\n\n# Settings for provisioning API\nprovisioning:\n # Prefix for the provisioning API paths.\n prefix: /_matrix/provision\n # Shared secret for authentication. If set to \"generate\" or null, a random secret will be generated,\n # or if set to \"disable\", the provisioning API will be disabled.\n shared_secret: {{ .ProvisioningSecret }}\n # Whether to allow provisioning API requests to be authed using Matrix access tokens.\n # This follows the same rules as double puppeting to determine which server to contact to check the token,\n # which means that by default, it only works for users on the same server as the bridge.\n allow_matrix_auth: true\n # Enable debug API at /debug with provisioning authentication.\n debug_endpoints: true\n\n# Some networks require publicly accessible media download links (e.g. for user avatars when using Discord webhooks).\n# These settings control whether the bridge will provide such public media access.\npublic_media:\n # Should public media be enabled at all?\n # The public_address field under the appservice section MUST be set when enabling public media.\n enabled: false\n # A key for signing public media URLs.\n # If set to \"generate\", a random key will be generated.\n signing_key: {{ .ProvisioningSecret }}\n # Number of seconds that public media URLs are valid for.\n # If set to 0, URLs will never expire.\n expiry: 0\n # Length of hash to use for public media URLs. Must be between 0 and 32.\n hash_length: 32\n\n# Settings for converting remote media to custom mxc:// URIs instead of reuploading.\n# More details can be found at https://docs.mau.fi/bridges/go/discord/direct-media.html\ndirect_media:\n # Should custom mxc:// URIs be used instead of reuploading media?\n enabled: false\n # The server name to use for the custom mxc:// URIs.\n # This server name will effectively be a real Matrix server, it just won't implement anything other than media.\n # You must either set up .well-known delegation from this domain to the bridge, or proxy the domain directly to the bridge.\n server_name: discord-media.example.com\n # Optionally a custom .well-known response. This defaults to `server_name:443`\n well_known_response:\n # Optionally specify a custom prefix for the media ID part of the MXC URI.\n media_id_prefix:\n # If the remote network supports media downloads over HTTP, then the bridge will use MSC3860/MSC3916\n # media download redirects if the requester supports it. Optionally, you can force redirects\n # and not allow proxying at all by setting this to false.\n # This option does nothing if the remote network does not support media downloads over HTTP.\n allow_proxy: true\n # Matrix server signing key to make the federation tester pass, same format as synapse's .signing.key file.\n # This key is also used to sign the mxc:// URIs to ensure only the bridge can generate them.\n server_key: ed25519 AAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n\n# Settings for backfilling messages.\n# Note that the exact way settings are applied depends on the network connector.\n# See https://docs.mau.fi/bridges/general/backfill.html for more details.\nbackfill:\n # Whether to do backfilling at all.\n enabled: true\n # Maximum number of messages to backfill in empty rooms.\n max_initial_messages: {{ or .MaxInitialMessages 50 }}\n # Maximum number of missed messages to backfill after bridge restarts.\n max_catchup_messages: 500\n # If a backfilled chat is older than this number of hours,\n # mark it as read even if it's unread on the remote network.\n unread_hours_threshold: 720\n # Settings for backfilling threads within other backfills.\n threads:\n # Maximum number of messages to backfill in a new thread.\n max_initial_messages: 50\n # Settings for the backwards backfill queue. This only applies when connecting to\n # Beeper as standard Matrix servers don't support inserting messages into history.\n queue:\n # Should the backfill queue be enabled?\n enabled: true\n # Number of messages to backfill in one batch.\n batch_size: {{ or .MaxBackwardMessages 50 }}\n # Delay between batches in seconds.\n batch_delay: 20\n # Maximum number of batches to backfill per portal.\n # If set to -1, all available messages will be backfilled.\n max_batches: 0\n # Optional network-specific overrides for max batches.\n # Interpretation of this field depends on the network connector.\n max_batches_override:\n channel: 10\n supergroup: 10\n dm: -1\n group_dm: -1\n\n# Settings for enabling double puppeting\ndouble_puppet:\n # Servers to always allow double puppeting from.\n # This is only for other servers and should NOT contain the server the bridge is on.\n servers:\n {{ .BeeperDomain }}: {{ .HungryAddress }}\n # Whether to allow client API URL discovery for other servers. When using this option,\n # users on other servers can use double puppeting even if their server URLs aren't\n # explicitly added to the servers map above.\n allow_discovery: false\n # Shared secrets for automatic double puppeting.\n # See https://docs.mau.fi/bridges/general/double-puppeting.html for instructions.\n secrets:\n {{ .BeeperDomain }}: \"as_token:{{ .ASToken }}\"\n\n# End-to-bridge encryption support options.\n#\n# See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html for more info.\nencryption:\n # Whether to enable encryption at all. If false, the bridge will not function in encrypted rooms.\n allow: true\n # Whether to force-enable encryption in all bridged rooms.\n default: true\n # Whether to require all messages to be encrypted and drop any unencrypted messages.\n require: true\n # Whether to use MSC2409/MSC3202 instead of /sync long polling for receiving encryption-related data.\n # This option is not yet compatible with standard Matrix servers like Synapse and should not be used.\n appservice: true\n # Whether to use MSC4190 instead of appservice login to create the bridge bot device.\n # Requires the homeserver to support MSC4190 and the device masquerading parts of MSC3202.\n # Only relevant when using end-to-bridge encryption, required when using encryption with next-gen auth (MSC3861).\n # Changing this option requires updating the appservice registration file.\n msc4190: false\n # Should the bridge bot generate a recovery key and cross-signing keys and verify itself?\n # Note that without the latest version of MSC4190, this will fail if you reset the bridge database.\n # The generated recovery key will be saved in the kv_store table under `recovery_key`.\n self_sign: false\n # Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled.\n # You must use a client that supports requesting keys from other users to use this feature.\n allow_key_sharing: true\n # Pickle key for encrypting encryption keys in the bridge database.\n # If set to generate, a random key will be generated.\n pickle_key: {{ or .Params.pickle_key .DefaultPickleKey \"bbctl\" }}\n # Options for deleting megolm sessions from the bridge.\n delete_keys:\n # Beeper-specific: delete outbound sessions when hungryserv confirms\n # that the user has uploaded the key to key backup.\n delete_outbound_on_ack: true\n # Don't store outbound sessions in the inbound table.\n dont_store_outbound: false\n # Ratchet megolm sessions forward after decrypting messages.\n ratchet_on_decrypt: true\n # Delete fully used keys (index >= max_messages) after decrypting messages.\n delete_fully_used_on_decrypt: true\n # Delete previous megolm sessions from same device when receiving a new one.\n delete_prev_on_new_session: true\n # Delete megolm sessions received from a device when the device is deleted.\n delete_on_device_delete: true\n # Periodically delete megolm sessions when 2x max_age has passed since receiving the session.\n periodically_delete_expired: true\n # Delete inbound megolm sessions that don't have the received_at field used for\n # automatic ratcheting and expired session deletion. This is meant as a migration\n # to delete old keys prior to the bridge update.\n delete_outdated_inbound: false\n # What level of device verification should be required from users?\n #\n # Valid levels:\n # unverified - Send keys to all device in the room.\n # cross-signed-untrusted - Require valid cross-signing, but trust all cross-signing keys.\n # cross-signed-tofu - Require valid cross-signing, trust cross-signing keys on first use (and reject changes).\n # cross-signed-verified - Require valid cross-signing, plus a valid user signature from the bridge bot.\n # Note that creating user signatures from the bridge bot is not currently possible.\n # verified - Require manual per-device verification\n # (currently only possible by modifying the `trust` column in the `crypto_device` database table).\n verification_levels:\n # Minimum level for which the bridge should send keys to when bridging messages from the remote network to Matrix.\n receive: cross-signed-tofu\n # Minimum level that the bridge should accept for incoming Matrix messages.\n send: cross-signed-tofu\n # Minimum level that the bridge should require for accepting key requests.\n share: cross-signed-tofu\n # Options for Megolm room key rotation. These options allow you to configure the m.room.encryption event content.\n # See https://spec.matrix.org/v1.10/client-server-api/#mroomencryption for more information about that event.\n rotation:\n # Enable custom Megolm room key rotation settings. Note that these\n # settings will only apply to rooms created after this option is set.\n enable_custom: true\n # The maximum number of milliseconds a session should be used\n # before changing it. The Matrix spec recommends 604800000 (a week)\n # as the default.\n milliseconds: 2592000000\n # The maximum number of messages that should be sent with a given a\n # session before changing it. The Matrix spec recommends 100 as the\n # default.\n messages: 10000\n # Disable rotating keys when a user's devices change?\n # You should not enable this option unless you understand all the implications.\n disable_device_change_key_rotation: true\n\n# Logging config. See https://github.com/tulir/zeroconfig for details.\nlogging:\n min_level: debug\n writers:\n - type: stdout\n format: pretty-colored\n - type: file\n format: json\n filename: ./logs/bridge.log\n max_size: 100\n max_backups: 10\n compress: false\n", + "discord.tpl.yaml": "# Homeserver details.\nhomeserver:\n # The address that this appservice can use to connect to the homeserver.\n address: {{ .HungryAddress }}\n # Publicly accessible base URL for media, used for avatars in relay mode.\n # If not set, the connection address above will be used.\n public_address: https://matrix.{{ .BeeperDomain }}\n # The domain of the homeserver (also known as server_name, used for MXIDs, etc).\n domain: beeper.local\n\n # What software is the homeserver running?\n # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use \"standard\" here.\n software: hungry\n # The URL to push real-time bridge status to.\n # If set, the bridge will make POST requests to this URL whenever a user's discord connection state changes.\n # The bridge will use the appservice as_token to authorize requests.\n status_endpoint: null\n # Endpoint for reporting per-message status.\n message_send_checkpoint_endpoint: null\n # Does the homeserver support https://github.com/matrix-org/matrix-spec-proposals/pull/2246?\n async_media: true\n\n # Should the bridge use a websocket for connecting to the homeserver?\n # The server side is currently not documented anywhere and is only implemented by mautrix-wsproxy,\n # mautrix-asmux (deprecated), and hungryserv (proprietary).\n websocket: {{ .Websocket }}\n # How often should the websocket be pinged? Pinging will be disabled if this is zero.\n ping_interval_seconds: 180\n\n# Application service host/registration related details.\n# Changing these values requires regeneration of the registration.\nappservice:\n # The address that the homeserver can use to connect to this appservice.\n address: null\n\n # The hostname and port where this appservice should listen.\n hostname: {{ if .Websocket }}null{{ else }}{{ .ListenAddr }}{{ end }}\n port: {{ if .Websocket }}null{{ else }}{{ .ListenPort }}{{ end }}\n\n # Database config.\n database:\n # The database type. \"sqlite3-fk-wal\" and \"postgres\" are supported.\n type: sqlite3-fk-wal\n # The database URI.\n # SQLite: A raw file path is supported, but `file:?_txlock=immediate` is recommended.\n # https://github.com/mattn/go-sqlite3#connection-string\n # Postgres: Connection string. For example, postgres://user:password@host/database?sslmode=disable\n # To connect via Unix socket, use something like postgres:///dbname?host=/var/run/postgresql\n uri: file:{{.DatabasePrefix}}mautrix-discord.db?_txlock=immediate\n # Maximum number of connections. Mostly relevant for Postgres.\n max_open_conns: 5\n max_idle_conns: 2\n # Maximum connection idle time and lifetime before they're closed. Disabled if null.\n # Parsed with https://pkg.go.dev/time#ParseDuration\n max_conn_idle_time: null\n max_conn_lifetime: null\n\n # The unique ID of this appservice.\n id: {{ .AppserviceID }}\n # Appservice bot details.\n bot:\n # Username of the appservice bot.\n username: {{ .BridgeName }}bot\n # Display name and avatar for bot. Set to \"remove\" to remove display name/avatar, leave empty\n # to leave display name/avatar as-is.\n displayname: Discord bridge bot\n avatar: mxc://maunium.net/nIdEykemnwdisvHbpxflpDlC\n\n # Whether or not to receive ephemeral events via appservice transactions.\n # Requires MSC2409 support (i.e. Synapse 1.22+).\n ephemeral_events: true\n\n # Should incoming events be handled asynchronously?\n # This may be necessary for large public instances with lots of messages going through.\n # However, messages will not be guaranteed to be bridged in the same order they were sent in.\n async_transactions: false\n\n # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify.\n as_token: {{ .ASToken }}\n hs_token: {{ .HSToken }}\n\n# Bridge config\nbridge:\n # Localpart template of MXIDs for Discord users.\n # {{.}} is replaced with the internal ID of the Discord user.\n username_template: {{ .BridgeName }}_{{ \"{{.}}\" }}\n # Displayname template for Discord users. This is also used as the room name in DMs if private_chat_portal_meta is enabled.\n # Available variables:\n # .ID - Internal user ID\n # .Username - Legacy display/username on Discord\n # .GlobalName - New displayname on Discord\n # .Discriminator - The 4 numbers after the name on Discord\n # .Bot - Whether the user is a bot\n # .System - Whether the user is an official system user\n # .Webhook - Whether the user is a webhook and is not an application\n # .Application - Whether the user is an application\n displayname_template: {{ `\"{{if .Webhook}}Webhook{{else}}{{or .GlobalName .Username}}{{end}}\"` }}\n # Displayname template for Discord channels (bridged as rooms, or spaces when type=4).\n # Available variables:\n # .Name - Channel name, or user displayname (pre-formatted with displayname_template) in DMs.\n # .ParentName - Parent channel name (used for categories).\n # .GuildName - Guild name.\n # .NSFW - Whether the channel is marked as NSFW.\n # .Type - Channel type (see values at https://github.com/bwmarrin/discordgo/blob/v0.25.0/structs.go#L251-L267)\n channel_name_template: {{ `\"{{if or (eq .Type 3) (eq .Type 4)}}{{.Name}}{{else}}#{{.Name}}{{end}}\"` }}\n # Displayname template for Discord guilds (bridged as spaces).\n # Available variables:\n # .Name - Guild name\n guild_name_template: {{ `\"{{.Name}}\"` }}\n # Whether to explicitly set the avatar and room name for private chat portal rooms.\n # If set to `default`, this will be enabled in encrypted rooms and disabled in unencrypted rooms.\n # If set to `always`, all DM rooms will have explicit names and avatars set.\n # If set to `never`, DM rooms will never have names and avatars set.\n private_chat_portal_meta: never\n\n portal_message_buffer: 128\n\n # Number of private channel portals to create on bridge startup.\n # Other portals will be created when receiving messages.\n startup_private_channel_create_limit: 5\n # Should the bridge send a read receipt from the bridge bot when a message has been sent to Discord?\n delivery_receipts: false\n # Whether the bridge should send the message status as a custom com.beeper.message_send_status event.\n message_status_events: true\n # Whether the bridge should send error notices via m.notice events when a message fails to bridge.\n message_error_notices: false\n # Should the bridge use space-restricted join rules instead of invite-only for guild rooms?\n # This can avoid unnecessary invite events in guild rooms when members are synced in.\n restricted_rooms: false\n # Should the bridge automatically join the user to threads on Discord when the thread is opened on Matrix?\n # This only works with clients that support thread read receipts (MSC3771 added in Matrix v1.4).\n autojoin_thread_on_open: true\n # Should inline fields in Discord embeds be bridged as HTML tables to Matrix?\n # Tables aren't supported in all clients, but are the only way to emulate the Discord inline field UI.\n embed_fields_as_tables: true\n # Should guild channels be muted when the portal is created? This only meant for single-user instances,\n # it won't mute it for all users if there are multiple Matrix users in the same Discord guild.\n mute_channels_on_create: true\n # Should the bridge update the m.direct account data event when double puppeting is enabled.\n # Note that updating the m.direct event is not atomic (except with mautrix-asmux)\n # and is therefore prone to race conditions.\n sync_direct_chat_list: false\n # Set this to true to tell the bridge to re-send m.bridge events to all rooms on the next run.\n # This field will automatically be changed back to false after it, except if the config file is not writable.\n resend_bridge_info: false\n # Should incoming custom emoji reactions be bridged as mxc:// URIs?\n # If set to false, custom emoji reactions will be bridged as the shortcode instead, and the image won't be available.\n custom_emoji_reactions: true\n # Should the bridge attempt to completely delete portal rooms when a channel is deleted on Discord?\n # If true, the bridge will try to kick Matrix users from the room. Otherwise, the bridge only makes ghosts leave.\n delete_portal_on_channel_delete: true\n # Should the bridge delete all portal rooms when you leave a guild on Discord?\n # This only applies if the guild has no other Matrix users on this bridge instance.\n delete_guild_on_leave: true\n # Whether or not created rooms should have federation enabled.\n # If false, created portal rooms will never be federated.\n federate_rooms: false\n # Prefix messages from webhooks with the profile info? This can be used along with a custom displayname_template\n # to better handle webhooks that change their name all the time (like ones used by bridges).\n prefix_webhook_messages: true\n # Bridge webhook avatars?\n enable_webhook_avatars: false\n # Should the bridge upload media to the Discord CDN directly before sending the message when using a user token,\n # like the official client does? The other option is sending the media in the message send request as a form part\n # (which is always used by bots and webhooks).\n use_discord_cdn_upload: true\n # Should mxc uris copied from Discord be cached?\n # This can be `never` to never cache, `unencrypted` to only cache unencrypted mxc uris, or `always` to cache everything.\n # If you have a media repo that generates non-unique mxc uris, you should set this to never.\n cache_media: unencrypted\n # Patterns for converting Discord media to custom mxc:// URIs instead of reuploading.\n # Each of the patterns can be set to null to disable custom URIs for that type of media.\n # More details can be found at https://docs.mau.fi/bridges/go/discord/direct-media.html\n media_patterns:\n # Should custom mxc:// URIs be used instead of reuploading media?\n enabled: false\n # Pattern for normal message attachments.\n attachments: {{ `mxc://discord-media.mau.dev/attachments|{{.ChannelID}}|{{.AttachmentID}}|{{.FileName}}` }}\n # Pattern for custom emojis.\n emojis: {{ `mxc://discord-media.mau.dev/emojis|{{.ID}}.{{.Ext}}` }}\n # Pattern for stickers. Note that animated lottie stickers will not be converted if this is enabled.\n stickers: {{ `mxc://discord-media.mau.dev/stickers|{{.ID}}.{{.Ext}}` }}\n # Pattern for static user avatars.\n avatars: {{ `mxc://discord-media.mau.dev/avatars|{{.UserID}}|{{.AvatarID}}.{{.Ext}}` }}\n # Settings for converting animated stickers.\n animated_sticker:\n # Format to which animated stickers should be converted.\n # disable - No conversion, send as-is (lottie JSON)\n # png - converts to non-animated png (fastest)\n # gif - converts to animated gif\n # webm - converts to webm video, requires ffmpeg executable with vp9 codec and webm container support\n # webp - converts to animated webp, requires ffmpeg executable with webp codec/container support\n target: webp\n # Arguments for converter. All converters take width and height.\n args:\n width: 320\n height: 320\n fps: 25 # only for webm, webp and gif (2, 5, 10, 20 or 25 recommended)\n # Servers to always allow double puppeting from\n double_puppet_server_map:\n {{ .BeeperDomain }}: {{ .HungryAddress }}\n # Allow using double puppeting from any server with a valid client .well-known file.\n double_puppet_allow_discovery: false\n # Shared secrets for https://github.com/devture/matrix-synapse-shared-secret-auth\n #\n # If set, double puppeting will be enabled automatically for local users\n # instead of users having to find an access token and run `login-matrix`\n # manually.\n login_shared_secret_map:\n {{ .BeeperDomain }}: \"as_token:{{ .ASToken }}\"\n\n # The prefix for commands. Only required in non-management rooms.\n command_prefix: '!discord'\n # Messages sent upon joining a management room.\n # Markdown is supported. The defaults are listed below.\n management_room_text:\n # Sent when joining a room.\n welcome: \"Hello, I'm a Discord bridge bot.\"\n # Sent when joining a management room and the user is already logged in.\n welcome_connected: \"Use `help` for help.\"\n # Sent when joining a management room and the user is not logged in.\n welcome_unconnected: \"Use `help` for help or `login` to log in.\"\n # Optional extra text sent when joining a management room.\n additional_help: \"\"\n\n # Settings for backfilling messages.\n backfill:\n # Limits for forward backfilling.\n forward_limits:\n # Initial backfill (when creating portal). 0 means backfill is disabled.\n # A special unlimited value is not supported, you must set a limit. Initial backfill will\n # fetch all messages first before backfilling anything, so high limits can take a lot of time.\n initial:\n dm: 50\n channel: 0\n thread: 0\n # Missed message backfill (on startup).\n # 0 means backfill is disabled, -1 means fetch all messages since last bridged message.\n # When using unlimited backfill (-1), messages are backfilled as they are fetched.\n # With limits, all messages up to the limit are fetched first and backfilled afterwards.\n missed:\n dm: -1\n channel: 50\n thread: 0\n # Maximum members in a guild to enable backfilling. Set to -1 to disable limit.\n # This can be used as a rough heuristic to disable backfilling in channels that are too active.\n # Currently only applies to missed message backfill.\n max_guild_members: 500\n\n # End-to-bridge encryption support options.\n #\n # See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html for more info.\n encryption:\n # Allow encryption, work in group chat rooms with e2ee enabled\n allow: true\n # Default to encryption, force-enable encryption in all portals the bridge creates\n # This will cause the bridge bot to be in private chats for the encryption to work properly.\n default: true\n # Whether to use MSC2409/MSC3202 instead of /sync long polling for receiving encryption-related data.\n appservice: true\n # Require encryption, drop any unencrypted messages.\n require: true\n # Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled.\n # You must use a client that supports requesting keys from other users to use this feature.\n allow_key_sharing: true\n # Should users mentions be in the event wire content to enable the server to send push notifications?\n plaintext_mentions: true\n # Options for deleting megolm sessions from the bridge.\n delete_keys:\n # Beeper-specific: delete outbound sessions when hungryserv confirms\n # that the user has uploaded the key to key backup.\n delete_outbound_on_ack: true\n # Don't store outbound sessions in the inbound table.\n dont_store_outbound: false\n # Ratchet megolm sessions forward after decrypting messages.\n ratchet_on_decrypt: true\n # Delete fully used keys (index >= max_messages) after decrypting messages.\n delete_fully_used_on_decrypt: true\n # Delete previous megolm sessions from same device when receiving a new one.\n delete_prev_on_new_session: true\n # Delete megolm sessions received from a device when the device is deleted.\n delete_on_device_delete: true\n # Periodically delete megolm sessions when 2x max_age has passed since receiving the session.\n periodically_delete_expired: true\n # Delete inbound megolm sessions that don't have the received_at field used for\n # automatic ratcheting and expired session deletion. This is meant as a migration\n # to delete old keys prior to the bridge update.\n delete_outdated_inbound: false\n # What level of device verification should be required from users?\n #\n # Valid levels:\n # unverified - Send keys to all device in the room.\n # cross-signed-untrusted - Require valid cross-signing, but trust all cross-signing keys.\n # cross-signed-tofu - Require valid cross-signing, trust cross-signing keys on first use (and reject changes).\n # cross-signed-verified - Require valid cross-signing, plus a valid user signature from the bridge bot.\n # Note that creating user signatures from the bridge bot is not currently possible.\n # verified - Require manual per-device verification\n # (currently only possible by modifying the `trust` column in the `crypto_device` database table).\n verification_levels:\n # Minimum level for which the bridge should send keys to when bridging messages from WhatsApp to Matrix.\n receive: cross-signed-tofu\n # Minimum level that the bridge should accept for incoming Matrix messages.\n send: cross-signed-tofu\n # Minimum level that the bridge should require for accepting key requests.\n share: cross-signed-tofu\n # Options for Megolm room key rotation. These options allow you to\n # configure the m.room.encryption event content. See:\n # https://spec.matrix.org/v1.3/client-server-api/#mroomencryption for\n # more information about that event.\n rotation:\n # Enable custom Megolm room key rotation settings. Note that these\n # settings will only apply to rooms created after this option is\n # set.\n enable_custom: true\n # The maximum number of milliseconds a session should be used\n # before changing it. The Matrix spec recommends 604800000 (a week)\n # as the default.\n milliseconds: 2592000000\n # The maximum number of messages that should be sent with a given a\n # session before changing it. The Matrix spec recommends 100 as the\n # default.\n messages: 10000\n\n # Disable rotating keys when a user's devices change?\n # You should not enable this option unless you understand all the implications.\n disable_device_change_key_rotation: true\n\n # Settings for provisioning API\n provisioning:\n # Prefix for the provisioning API paths.\n prefix: /_matrix/provision\n # Shared secret for authentication. If set to \"generate\", a random secret will be generated,\n # or if set to \"disable\", the provisioning API will be disabled.\n shared_secret: {{ .ProvisioningSecret }}\n\n # Permissions for using the bridge.\n # Permitted values:\n # relay - Talk through the relaybot (if enabled), no access otherwise\n # user - Access to use the bridge to chat with a Discord account.\n # admin - User level and some additional administration tools\n # Permitted keys:\n # * - All Matrix users\n # domain - All users on that homeserver\n # mxid - Specific user\n permissions:\n \"{{ .UserID }}\": admin\n\n# Logging config. See https://github.com/tulir/zeroconfig for details.\nlogging:\n min_level: debug\n writers:\n - type: stdout\n format: pretty-colored\n - type: file\n format: json\n filename: ./logs/mautrix-discord.log\n max_size: 100\n max_backups: 10\n compress: true\n", + "gmessages.tpl.yaml": "# Network-specific config options\nnetwork:\n # Displayname template for SMS users.\n displayname_template: {{ `\"{{or .FullName .PhoneNumber}}\"` }}\n # Settings for how the bridge appears to the phone.\n device_meta:\n # OS name to tell the phone. This is the name that shows up in the paired devices list.\n os: Beeper (self-hosted)\n # Browser type to tell the phone. This decides which icon is shown.\n # Valid types: OTHER, CHROME, FIREFOX, SAFARI, OPERA, IE, EDGE\n browser: OTHER\n # Device type to tell the phone. This also affects the icon, as well as how many sessions are allowed simultaneously.\n # One web, two tablets and one PWA should be able to connect at the same time.\n # Valid types: WEB, TABLET, PWA\n type: TABLET\n # Should the bridge aggressively set itself as the active device if the user opens Google Messages in a browser?\n # If this is disabled, the user must manually use the `set-active` command to reactivate the bridge.\n aggressive_reconnect: true\n # Number of chats to sync when connecting to Google Messages.\n initial_chat_sync_count: 25\n\n{{ setfield . \"CommandPrefix\" \"!gm\" -}}\n{{ setfield . \"DatabaseFileName\" \"mautrix-gmessages\" -}}\n{{ setfield . \"BridgeTypeName\" \"Google Messages\" -}}\n{{ setfield . \"BridgeTypeIcon\" \"mxc://maunium.net/yGOdcrJcwqARZqdzbfuxfhzb\" -}}\n{{ setfield . \"DefaultPickleKey\" \"go.mau.fi/mautrix-gmessages\" -}}\n{{ template \"bridgev2.tpl.yaml\" . }}\n", + "googlechat.tpl.yaml": "# Homeserver details\nhomeserver:\n # The address that this appservice can use to connect to the homeserver.\n address: {{ .HungryAddress }}\n # The domain of the homeserver (for MXIDs, etc).\n domain: beeper.local\n # Whether or not to verify the SSL certificate of the homeserver.\n # Only applies if address starts with https://\n verify_ssl: true\n # What software is the homeserver running?\n # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use \"standard\" here.\n software: hungry\n # Number of retries for all HTTP requests if the homeserver isn't reachable.\n http_retry_count: 4\n # The URL to push real-time bridge status to.\n # If set, the bridge will make POST requests to this URL whenever a user's Google Chat connection state changes.\n # The bridge will use the appservice as_token to authorize requests.\n status_endpoint: null\n # Endpoint for reporting per-message status.\n message_send_checkpoint_endpoint: null\n # Whether asynchronous uploads via MSC2246 should be enabled for media.\n # Requires a media repo that supports MSC2246.\n async_media: true\n\n# Application service host/registration related details\n# Changing these values requires regeneration of the registration.\nappservice:\n # The address that the homeserver can use to connect to this appservice.\n address: \"http://{{ .ListenAddr }}:{{ .ListenPort }}\"\n\n # The hostname and port where this appservice should listen.\n hostname: {{ .ListenAddr }}\n port: {{ .ListenPort }}\n # The maximum body size of appservice API requests (from the homeserver) in mebibytes\n # Usually 1 is enough, but on high-traffic bridges you might need to increase this to avoid 413s\n max_body_size: 1\n\n # The full URI to the database. SQLite and Postgres are supported.\n # Format examples:\n # SQLite: sqlite:filename.db\n # Postgres: postgres://username:password@hostname/dbname\n database: sqlite:{{.DatabasePrefix}}mautrix-googlechat.db\n # Additional arguments for asyncpg.create_pool() or sqlite3.connect()\n # https://magicstack.github.io/asyncpg/current/api/index.html#asyncpg.pool.create_pool\n # https://docs.python.org/3/library/sqlite3.html#sqlite3.connect\n # For sqlite, min_size is used as the connection thread pool size and max_size is ignored.\n # Additionally, SQLite supports init_commands as an array of SQL queries to run on connect (e.g. to set PRAGMAs).\n database_opts:\n min_size: 1\n max_size: 1\n\n # The unique ID of this appservice.\n id: {{ .AppserviceID }}\n # Username of the appservice bot.\n bot_username: {{ .BridgeName}}bot\n # Display name and avatar for bot. Set to \"remove\" to remove display name/avatar, leave empty\n # to leave display name/avatar as-is.\n bot_displayname: Google Chat bridge bot\n bot_avatar: mxc://maunium.net/BDIWAQcbpPGASPUUBuEGWXnQ\n\n # Whether or not to receive ephemeral events via appservice transactions.\n # Requires MSC2409 support (i.e. Synapse 1.22+).\n # You should disable bridge -> sync_with_custom_puppets when this is enabled.\n ephemeral_events: true\n\n # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify.\n as_token: {{ .ASToken }}\n hs_token: {{ .HSToken }}\n\n# Prometheus telemetry config. Requires prometheus-client to be installed.\nmetrics:\n enabled: false\n listen_port: 8000\n\n# Manhole config.\nmanhole:\n # Whether or not opening the manhole is allowed.\n enabled: false\n # The path for the unix socket.\n path: /var/tmp/mautrix-googlechat.manhole\n # The list of UIDs who can be added to the whitelist.\n # If empty, any UIDs can be specified in the open-manhole command.\n whitelist:\n - 0\n\n# Bridge config\nbridge:\n # Localpart template of MXIDs for Google Chat users.\n # {userid} is replaced with the user ID of the Google Chat user.\n username_template: \"{{ .BridgeName }}_{userid}\"\n # Displayname template for Google Chat users.\n # {full_name}, {first_name}, {last_name} and {email} are replaced with names.\n displayname_template: \"{full_name}\"\n\n # The prefix for commands. Only required in non-management rooms.\n command_prefix: \"!gc\"\n\n # Number of chats to sync (and create portals for) on startup/login.\n # Set 0 to disable automatic syncing.\n initial_chat_sync: 10\n # Whether or not the Google Chat users of logged in Matrix users should be\n # invited to private chats when the user sends a message from another client.\n invite_own_puppet_to_pm: false\n # Whether or not to use /sync to get presence, read receipts and typing notifications\n # when double puppeting is enabled\n sync_with_custom_puppets: false\n # Whether or not to update the m.direct account data event when double puppeting is enabled.\n # Note that updating the m.direct event is not atomic (except with mautrix-asmux)\n # and is therefore prone to race conditions.\n sync_direct_chat_list: false\n # Servers to always allow double puppeting from\n double_puppet_server_map:\n {{ .BeeperDomain }}: {{ .HungryAddress }}\n # Allow using double puppeting from any server with a valid client .well-known file.\n double_puppet_allow_discovery: false\n # Shared secret for https://github.com/devture/matrix-synapse-shared-secret-auth\n #\n # If set, custom puppets will be enabled automatically for local users\n # instead of users having to find an access token and run `login-matrix`\n # manually.\n # If using this for other servers than the bridge's server,\n # you must also set the URL in the double_puppet_server_map.\n login_shared_secret_map:\n {{ .BeeperDomain }}: \"as_token:{{ .ASToken }}\"\n # Whether or not to update avatars when syncing all contacts at startup.\n update_avatar_initial_sync: true\n # End-to-bridge encryption support options.\n #\n # See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html for more info.\n encryption:\n # Allow encryption, work in group chat rooms with e2ee enabled\n allow: true\n # Default to encryption, force-enable encryption in all portals the bridge creates\n # This will cause the bridge bot to be in private chats for the encryption to work properly.\n default: true\n # Whether to use MSC2409/MSC3202 instead of /sync long polling for receiving encryption-related data.\n appservice: true\n # Require encryption, drop any unencrypted messages.\n require: true\n # Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled.\n # You must use a client that supports requesting keys from other users to use this feature.\n allow_key_sharing: true\n # Options for deleting megolm sessions from the bridge.\n delete_keys:\n # Beeper-specific: delete outbound sessions when hungryserv confirms\n # that the user has uploaded the key to key backup.\n delete_outbound_on_ack: true\n # Don't store outbound sessions in the inbound table.\n dont_store_outbound: false\n # Ratchet megolm sessions forward after decrypting messages.\n ratchet_on_decrypt: true\n # Delete fully used keys (index >= max_messages) after decrypting messages.\n delete_fully_used_on_decrypt: true\n # Delete previous megolm sessions from same device when receiving a new one.\n delete_prev_on_new_session: true\n # Delete megolm sessions received from a device when the device is deleted.\n delete_on_device_delete: true\n # Periodically delete megolm sessions when 2x max_age has passed since receiving the session.\n periodically_delete_expired: true\n # Delete inbound megolm sessions that don't have the received_at field used for\n # automatic ratcheting and expired session deletion. This is meant as a migration\n # to delete old keys prior to the bridge update.\n delete_outdated_inbound: true\n # What level of device verification should be required from users?\n #\n # Valid levels:\n # unverified - Send keys to all device in the room.\n # cross-signed-untrusted - Require valid cross-signing, but trust all cross-signing keys.\n # cross-signed-tofu - Require valid cross-signing, trust cross-signing keys on first use (and reject changes).\n # cross-signed-verified - Require valid cross-signing, plus a valid user signature from the bridge bot.\n # Note that creating user signatures from the bridge bot is not currently possible.\n # verified - Require manual per-device verification\n # (currently only possible by modifying the `trust` column in the `crypto_device` database table).\n verification_levels:\n # Minimum level for which the bridge should send keys to when bridging messages from Telegram to Matrix.\n receive: cross-signed-tofu\n # Minimum level that the bridge should accept for incoming Matrix messages.\n send: cross-signed-tofu\n # Minimum level that the bridge should require for accepting key requests.\n share: cross-signed-tofu\n # Options for Megolm room key rotation. These options allow you to\n # configure the m.room.encryption event content. See:\n # https://spec.matrix.org/v1.3/client-server-api/#mroomencryption for\n # more information about that event.\n rotation:\n # Enable custom Megolm room key rotation settings. Note that these\n # settings will only apply to rooms created after this option is\n # set.\n enable_custom: true\n # The maximum number of milliseconds a session should be used\n # before changing it. The Matrix spec recommends 604800000 (a week)\n # as the default.\n milliseconds: 2592000000\n # The maximum number of messages that should be sent with a given a\n # session before changing it. The Matrix spec recommends 100 as the\n # default.\n messages: 10000\n\n # Disable rotating keys when a user's devices change?\n # You should not enable this option unless you understand all the implications.\n disable_device_change_key_rotation: true\n\n # Whether or not the bridge should send a read receipt from the bridge bot when a message has\n # been sent to Google Chat.\n delivery_receipts: false\n # Whether or not delivery errors should be reported as messages in the Matrix room.\n delivery_error_reports: false\n # Whether the bridge should send the message status as a custom com.beeper.message_send_status event.\n message_status_events: true\n # Whether or not created rooms should have federation enabled.\n # If false, created portal rooms will never be federated.\n federate_rooms: false\n # Settings for backfilling messages from Google Chat.\n backfill:\n # Whether or not the Google Chat users of logged in Matrix users should be\n # invited to private chats when backfilling history from Google Chat. This is\n # usually needed to prevent rate limits and to allow timestamp massaging.\n invite_own_puppet: false\n # Number of threads to backfill in threaded spaces in initial backfill.\n initial_thread_limit: 0\n # Number of replies to backfill in each thread in initial backfill.\n initial_thread_reply_limit: 500\n # Number of messages to backfill in non-threaded spaces and DMs in initial backfill.\n initial_nonthread_limit: 1\n # Number of events to backfill in catchup backfill.\n missed_event_limit: 200\n # How many events to request from Google Chat at once in catchup backfill?\n missed_event_page_size: 100\n # If using double puppeting, should notifications be disabled\n # while the initial backfill is in progress?\n disable_notifications: true\n\n # Set this to true to tell the bridge to re-send m.bridge events to all rooms on the next run.\n # This field will automatically be changed back to false after it,\n # except if the config file is not writable.\n resend_bridge_info: false\n # Whether or not unimportant bridge notices should be sent to the bridge notice room.\n unimportant_bridge_notices: false\n # Whether or not bridge notices should be disabled entirely.\n disable_bridge_notices: true\n # Whether to explicitly set the avatar and room name for private chat portal rooms.\n # If set to `default`, this will be enabled in encrypted rooms and disabled in unencrypted rooms.\n # If set to `always`, all DM rooms will have explicit names and avatars set.\n # If set to `never`, DM rooms will never have names and avatars set.\n private_chat_portal_meta: never\n\n provisioning:\n # Internal prefix in the appservice web server for the login endpoints.\n prefix: /_matrix/provision\n # Shared secret for integration managers such as mautrix-manager.\n # If set to \"generate\", a random string will be generated on the next startup.\n # If null, integration manager access to the API will not be possible.\n shared_secret: {{ .ProvisioningSecret }}\n\n # Permissions for using the bridge.\n # Permitted values:\n # user - Use the bridge with puppeting.\n # admin - Use and administrate the bridge.\n # Permitted keys:\n # * - All Matrix users\n # domain - All users on that homeserver\n # mxid - Specific user\n permissions:\n \"{{ .UserID }}\": \"admin\"\n\n# Python logging configuration.\n#\n# See section 16.7.2 of the Python documentation for more info:\n# https://docs.python.org/3.6/library/logging.config.html#configuration-dictionary-schema\nlogging:\n version: 1\n formatters:\n colored:\n (): mautrix_googlechat.util.ColorFormatter\n format: \"[%(asctime)s] [%(levelname)s@%(name)s] %(message)s\"\n normal:\n format: \"[%(asctime)s] [%(levelname)s@%(name)s] %(message)s\"\n handlers:\n file:\n class: logging.handlers.RotatingFileHandler\n formatter: normal\n filename: ./logs/mautrix-googlechat.log\n maxBytes: 10485760\n backupCount: 10\n console:\n class: logging.StreamHandler\n formatter: colored\n loggers:\n mau:\n level: DEBUG\n maugclib:\n level: INFO\n aiohttp:\n level: INFO\n root:\n level: DEBUG\n handlers: [file, console]\n", + "gvoice.tpl.yaml": "# Network-specific config options\nnetwork:\n # Displayname template for SMS users. Available variables:\n # .Name - same as phone number in most cases\n # .Contact.Name - name from contact list\n # .Contact.FirstName - first name from contact list\n # .PhoneNumber\n displayname_template: {{ `\"{{ or .Contact.Name .Name }}\"` }}\n\n{{ setfield . \"CommandPrefix\" \"!gv\" -}}\n{{ setfield . \"DatabaseFileName\" \"mautrix-gvoice\" -}}\n{{ setfield . \"BridgeTypeName\" \"Google Voice\" -}}\n{{ setfield . \"BridgeTypeIcon\" \"mxc://maunium.net/VOPtYGBzHLRfPTEzGgNMpeKo\" -}}\n{{ setfield . \"DefaultPickleKey\" \"go.mau.fi/mautrix-gvoice\" -}}\n{{ setfield . \"MaxInitialMessages\" 10 -}}\n{{ setfield . \"MaxBackwardMessages\" 100 -}}\n{{ template \"bridgev2.tpl.yaml\" . }}\n", + "heisenbridge.tpl.yaml": "id: {{ .AppserviceID }}\nurl: {{ if .Websocket }}websocket{{ else }}http://{{ .ListenAddr }}:{{ .ListenPort }}{{ end }}\nas_token: {{ .ASToken }}\nhs_token: {{ .HSToken }}\nsender_localpart: {{ .BridgeName }}bot\nnamespaces:\n users:\n - regex: '@{{ .BridgeName }}_.+:beeper\\.local'\n exclusive: true\npush_ephemeral: true\nheisenbridge:\n media_url: https://matrix.{{ .BeeperDomain }}\n displayname: Heisenbridge\n", + "imessage.tpl.yaml": "# Homeserver details.\nhomeserver:\n # The address that this appservice can use to connect to the homeserver.\n address: {{ .HungryAddress }}\n # The address to mautrix-wsproxy (which should usually be next to the homeserver behind a reverse proxy).\n # Only the /_matrix/client/unstable/fi.mau.as_sync websocket endpoint is used on this address.\n #\n # Set to null to disable using the websocket. When not using the websocket, make sure hostname and port are set in the appservice section.\n websocket_proxy: {{ if .Websocket }}{{ replace .HungryAddress \"https\" \"wss\" }}{{ else }}null{{ end }}\n # How often should the websocket be pinged? Pinging will be disabled if this is zero.\n ping_interval_seconds: 180\n # The domain of the homeserver (also known as server_name, used for MXIDs, etc).\n domain: beeper.local\n\n # What software is the homeserver running?\n # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use \"standard\" here.\n software: hungry\n # Does the homeserver support https://github.com/matrix-org/matrix-spec-proposals/pull/2246?\n async_media: true\n\n# Application service host/registration related details.\n# Changing these values requires regeneration of the registration.\nappservice:\n # The hostname and port where this appservice should listen.\n # The default method of deploying mautrix-imessage is using a websocket proxy, so it doesn't need a http server\n # To use a http server instead of a websocket, set websocket_proxy to null in the homeserver section,\n # and set the port below to a real port.\n hostname: {{ if .Websocket }}null{{ else }}{{ .ListenAddr }}{{ end }}\n port: {{ if .Websocket }}null{{ else }}{{ .ListenPort }}{{ end }}\n # Optional TLS certificates to listen for https instead of http connections.\n tls_key: null\n tls_cert: null\n\n # Database config.\n database:\n # The database type. Only \"sqlite3-fk-wal\" is supported.\n type: sqlite3-fk-wal\n # SQLite database path. A raw file path is supported, but `file:?_txlock=immediate` is recommended.\n uri: file:{{.DatabasePrefix}}mautrix-imessage.db?_txlock=immediate\n\n # The unique ID of this appservice.\n id: {{ .AppserviceID }}\n # Appservice bot details.\n bot:\n # Username of the appservice bot.\n username: {{ .BridgeName }}bot\n # Display name and avatar for bot. Set to \"remove\" to remove display name/avatar, leave empty\n # to leave display name/avatar as-is.\n displayname: iMessage bridge bot\n avatar: mxc://maunium.net/tManJEpANASZvDVzvRvhILdX\n\n # Whether or not to receive ephemeral events via appservice transactions.\n # Requires MSC2409 support (i.e. Synapse 1.22+).\n # You should disable bridge -> sync_with_custom_puppets when this is enabled.\n ephemeral_events: true\n\n # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify.\n as_token: {{ .ASToken }}\n hs_token: {{ .HSToken }}\n\n# iMessage connection config\nimessage:\n # Available platforms:\n # * mac: Standard Mac connector, requires full disk access and will ask for AppleScript and contacts permission.\n # * ios: Jailbreak iOS connector when using with Brooklyn.\n # * android: Equivalent to ios, but for use with the Android SMS wrapper app.\n # * mac-nosip: Mac without SIP connector, runs Barcelona as a subprocess.\n platform: {{ .Params.imessage_platform }}\n # Path to the Barcelona executable for the mac-nosip connector\n imessage_rest_path: \"{{ or .Params.barcelona_path \"darwin-barcelona-mautrix\" }}\"\n # Additional arguments to pass to the mac-nosip connector\n imessage_rest_args: []\n # The mode for fetching contacts in the no-SIP connector.\n # The default mode is `ipc` which will ask Barcelona. However, recent versions of Barcelona have removed contact support.\n # You can specify `mac` to use Contacts.framework directly instead of through Barcelona.\n # You can also specify `disable` to not try to use contacts at all.\n contacts_mode: mac\n # Whether to log the contents of IPC payloads\n log_ipc_payloads: false\n # For the no-SIP connector, hackily set the user account locale before starting Barcelona.\n hacky_set_locale: null\n # A list of environment variables to add for the Barcelona process (as NAME=value strings)\n environment: []\n # Path to unix socket for Barcelona communication.\n unix_socket: mautrix-imessage.sock\n # Interval to ping Barcelona at. The process will exit if Barcelona doesn't respond in time.\n ping_interval_seconds: 15\n\n bluebubbles_url: {{ .Params.bluebubbles_url }}\n bluebubbles_password: {{ .Params.bluebubbles_password }}\n\n# Segment settings for collecting some debug data.\nsegment:\n key: null\n user_id: null\n\nhacky_startup_test:\n identifier: null\n message: null\n response_message: null\n key: null\n echo_mode: false\n\n# Bridge config\nbridge:\n # The user of the bridge.\n user: \"{{ .UserID }}\"\n\n # Localpart template of MXIDs for iMessage users.\n # {{ \"{{.}}\" }} is replaced with the phone number or email of the iMessage user.\n username_template: {{ .BridgeName }}_{{ \"{{.}}\" }}\n # Displayname template for iMessage users.\n # {{ \"{{.}}\" }} is replaced with the contact list name (if available) or username (phone number or email) of the iMessage user.\n displayname_template: \"{{ \"{{.}}\" }}\"\n # Should the bridge create a space and add bridged rooms to it?\n personal_filtering_spaces: true\n\n # Whether or not the bridge should send a read receipt from the bridge bot when a message has been\n # sent to iMessage.\n delivery_receipts: false\n # Whether or not the bridge should send the message status as a custom\n # com.beeper.message_send_status event.\n message_status_events: true\n # Whether or not the bridge should send error notices via m.notice events\n # when a message fails to bridge.\n send_error_notices: false\n # The maximum number of seconds between the message arriving at the\n # homeserver and the bridge attempting to send the message. This can help\n # prevent messages from being bridged a long time after arriving at the\n # homeserver which could cause confusion in the chat history on the remote\n # network. Set to 0 to disable.\n max_handle_seconds: 60\n # Device ID to include in m.bridge data, read by client-integrated Android SMS.\n # Not relevant for standalone bridges nor iMessage.\n device_id: null\n # Whether or not to sync with custom puppets to receive EDUs that are not normally sent to appservices.\n sync_with_custom_puppets: false\n # Whether or not to update the m.direct account data event when double puppeting is enabled.\n # Note that updating the m.direct event is not atomic (except with mautrix-asmux)\n # and is therefore prone to race conditions.\n sync_direct_chat_list: false\n # Shared secret for https://github.com/devture/matrix-synapse-shared-secret-auth\n #\n # If set, double puppeting will be enabled automatically instead of the user\n # having to find an access token and run `login-matrix` manually.\n login_shared_secret: appservice\n # Homeserver URL for the double puppet. If null, will use the URL set in homeserver -> address\n double_puppet_server_url: null\n # Backfill settings\n backfill:\n # Should backfilling be enabled at all?\n enable: true\n # Maximum number of messages to backfill for new portal rooms.\n initial_limit: 100\n # Maximum age of chats to sync in days.\n initial_sync_max_age: 7\n # If a backfilled chat is older than this number of hours, mark it as read even if it's unread on iMessage.\n # Set to -1 to let any chat be unread.\n unread_hours_threshold: 720\n # Use MSC2716 for backfilling?\n #\n # This requires a server with MSC2716 support, which is currently an experimental feature in Synapse.\n # It can be enabled by setting experimental_features -> msc2716_enabled to true in homeserver.yaml.\n msc2716: true\n # Whether or not the bridge should periodically resync chat and contact info.\n periodic_sync: true\n # Should the bridge look through joined rooms to find existing portals if the database has none?\n # This can be used to recover from bridge database loss.\n find_portals_if_db_empty: false\n # Media viewer settings. See https://gitlab.com/beeper/media-viewer for more info.\n # Used to send media viewer links instead of full files for attachments that are too big for MMS.\n media_viewer:\n # The address to the media viewer. If null, media viewer links will not be used.\n url: https://media.beeper.com\n # The homeserver domain to pass to the media viewer to use for downloading media.\n # If null, will use the server name configured in the homeserver section.\n homeserver: {{ .BeeperDomain }}\n # The minimum number of bytes in a file before the bridge switches to using the media viewer when sending MMS.\n # Note that for unencrypted files, this will use a direct link to the homeserver rather than the media viewer.\n sms_min_size: 409600\n # Same as above, but for iMessages.\n imessage_min_size: 52428800\n # Template text when inserting media viewer URLs.\n # %s is replaced with the actual URL.\n template: \"Full size attachment: %s\"\n # Should we convert heif images to jpeg before re-uploading? This increases\n # compatibility, but adds generation loss (reduces quality).\n convert_heif: false\n # Should we convert tiff images to jpeg before re-uploading? This increases\n # compatibility, but adds generation loss (reduces quality).\n convert_tiff: true\n # Modern Apple devices tend to use h265 encoding for video, which is a licensed standard and therefore not\n # supported by most major browsers. If enabled, all video attachments will be converted according to the\n # ffmpeg args.\n convert_video:\n enabled: false\n # Convert to h264 format (supported by all major browsers) at decent quality while retaining original\n # audio. Modify these args to do whatever encoding/quality you want.\n ffmpeg_args: [\"-c:v\", \"libx264\", \"-preset\", \"faster\", \"-crf\", \"22\", \"-c:a\", \"copy\"]\n extension: \"mp4\"\n mime_type: \"video/mp4\"\n # The prefix for commands.\n command_prefix: \"!im\"\n # Should we rewrite the sender in a DM to match the chat GUID?\n # This is helpful when the sender ID shifts depending on the device they use, since\n # the bridge is unable to add participants to the chat post-creation.\n force_uniform_dm_senders: true\n # Should SMS chats always be in the same room as iMessage chats with the same phone number?\n disable_sms_portals: false\n # iMessage has weird IDs for group chats, so getting all messages in the same MMS group chat into the same Matrix room\n # may require rerouting some messages based on the fake ReplyToGUID that iMessage adds.\n reroute_mms_group_replies: false\n # Whether or not created rooms should have federation enabled.\n # If false, created portal rooms will never be federated.\n federate_rooms: false\n # Send captions in the same message as images using MSC2530?\n # This is currently not supported in most clients.\n caption_in_message: true\n # Should the bridge explicitly set the avatar and room name for private chat portal rooms?\n # This is implicitly enabled in encrypted rooms.\n private_chat_portal_meta: never\n\n # End-to-bridge encryption support options.\n # See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html\n encryption:\n # Allow encryption, work in group chat rooms with e2ee enabled\n allow: true\n # Default to encryption, force-enable encryption in all portals the bridge creates\n # This will cause the bridge bot to be in private chats for the encryption to work properly.\n default: true\n # Whether or not to use MSC2409/MSC3202 instead of /sync long polling for receiving encryption-related data.\n appservice: true\n # Require encryption, drop any unencrypted messages.\n require: true\n # Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled.\n # You must use a client that supports requesting keys from other users to use this feature.\n allow_key_sharing: true\n # Options for deleting megolm sessions from the bridge.\n delete_keys:\n # Beeper-specific: delete outbound sessions when hungryserv confirms\n # that the user has uploaded the key to key backup.\n delete_outbound_on_ack: true\n # Don't store outbound sessions in the inbound table.\n dont_store_outbound: false\n # Ratchet megolm sessions forward after decrypting messages.\n ratchet_on_decrypt: true\n # Delete fully used keys (index >= max_messages) after decrypting messages.\n delete_fully_used_on_decrypt: true\n # Delete previous megolm sessions from same device when receiving a new one.\n delete_prev_on_new_session: true\n # Delete megolm sessions received from a device when the device is deleted.\n delete_on_device_delete: true\n # Periodically delete megolm sessions when 2x max_age has passed since receiving the session.\n periodically_delete_expired: true\n # What level of device verification should be required from users?\n #\n # Valid levels:\n # unverified - Send keys to all device in the room.\n # cross-signed-untrusted - Require valid cross-signing, but trust all cross-signing keys.\n # cross-signed-tofu - Require valid cross-signing, trust cross-signing keys on first use (and reject changes).\n # cross-signed-verified - Require valid cross-signing, plus a valid user signature from the bridge bot.\n # Note that creating user signatures from the bridge bot is not currently possible.\n # verified - Require manual per-device verification\n # (currently only possible by modifying the `trust` column in the `crypto_device` database table).\n verification_levels:\n # Minimum level for which the bridge should send keys to when bridging messages from WhatsApp to Matrix.\n receive: cross-signed-tofu\n # Minimum level that the bridge should accept for incoming Matrix messages.\n send: cross-signed-tofu\n # Minimum level that the bridge should require for accepting key requests.\n share: cross-signed-tofu\n # Options for Megolm room key rotation. These options allow you to\n # configure the m.room.encryption event content. See:\n # https://spec.matrix.org/v1.3/client-server-api/#mroomencryption for\n # more information about that event.\n rotation:\n # Enable custom Megolm room key rotation settings. Note that these\n # settings will only apply to rooms created after this option is\n # set.\n enable_custom: true\n # The maximum number of milliseconds a session should be used\n # before changing it. The Matrix spec recommends 604800000 (a week)\n # as the default.\n milliseconds: 2592000000\n # The maximum number of messages that should be sent with a given a\n # session before changing it. The Matrix spec recommends 100 as the\n # default.\n messages: 10000\n\n # Disable rotating keys when a user's devices change?\n # You should not enable this option unless you understand all the implications.\n disable_device_change_key_rotation: true\n\n # Settings for relay mode\n relay:\n # Whether relay mode should be allowed.\n enabled: false\n # A list of user IDs and server names who are allowed to be relayed through this bridge. Use * to allow everyone.\n whitelist: []\n\n# Logging config. See https://github.com/tulir/zeroconfig for details.\nlogging:\n min_level: debug\n writers:\n - type: stdout\n format: pretty-colored\n - type: file\n format: json\n filename: ./logs/mautrix-imessage.log\n max_size: 100\n max_backups: 10\n compress: false\n\n# This may be used by external config managers. mautrix-imessage does not read it, but will carry it across configuration migrations.\nrevision: 0\n", + "imessagego.tpl.yaml": "# Homeserver details.\nhomeserver:\n # The address that this appservice can use to connect to the homeserver.\n address: {{ .HungryAddress }}\n # The domain of the homeserver (also known as server_name, used for MXIDs, etc).\n domain: beeper.local\n\n # What software is the homeserver running?\n # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use \"standard\" here.\n software: hungry\n # The URL to push real-time bridge status to.\n # If set, the bridge will make POST requests to this URL whenever a user's discord connection state changes.\n # The bridge will use the appservice as_token to authorize requests.\n status_endpoint: null\n # Endpoint for reporting per-message status.\n message_send_checkpoint_endpoint: null\n # Does the homeserver support https://github.com/matrix-org/matrix-spec-proposals/pull/2246?\n async_media: true\n\n # Should the bridge use a websocket for connecting to the homeserver?\n # The server side is currently not documented anywhere and is only implemented by mautrix-wsproxy,\n # mautrix-asmux (deprecated), and hungryserv (proprietary).\n websocket: {{ .Websocket }}\n # How often should the websocket be pinged? Pinging will be disabled if this is zero.\n ping_interval_seconds: 180\n\n# Application service host/registration related details.\n# Changing these values requires regeneration of the registration.\nappservice:\n # The address that the homeserver can use to connect to this appservice.\n address: null\n\n # The hostname and port where this appservice should listen.\n hostname: {{ if .Websocket }}null{{ else }}{{ .ListenAddr }}{{ end }}\n port: {{ if .Websocket }}null{{ else }}{{ .ListenPort }}{{ end }}\n\n # Database config.\n database:\n # The database type. Only \"sqlite3-fk-wal\" is supported.\n type: sqlite3-fk-wal\n # SQLite database path. A raw file path is supported, but `file:?_txlock=immediate` is recommended.\n uri: file:{{.DatabasePrefix}}beeper-imessage.db?_txlock=immediate\n\n # The unique ID of this appservice.\n id: {{ .AppserviceID }}\n # Appservice bot details.\n bot:\n # Username of the appservice bot.\n username: {{ .BridgeName }}bot\n # Display name and avatar for bot. Set to \"remove\" to remove display name/avatar, leave empty\n # to leave display name/avatar as-is.\n displayname: iMessage bridge bot\n avatar: mxc://maunium.net/tManJEpANASZvDVzvRvhILdX\n\n # Whether or not to receive ephemeral events via appservice transactions.\n # Requires MSC2409 support (i.e. Synapse 1.22+).\n # You should disable bridge -> sync_with_custom_puppets when this is enabled.\n ephemeral_events: true\n\n # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify.\n as_token: {{ .ASToken }}\n hs_token: {{ .HSToken }}\n\n# Segment-compatible analytics endpoint for tracking some events, like provisioning API login and encryption errors.\nanalytics:\n # Hostname of the tracking server. The path is hardcoded to /v1/track\n host: api.segment.io\n # API key to send with tracking requests. Tracking is disabled if this is null.\n token: null\n # Optional user ID for tracking events. If null, defaults to using Matrix user ID.\n user_id: null\n\nimessage:\n device_name: {{ or .Params.device_name \"Beeper (self-hosted)\" }}\n\n# Bridge config\nbridge:\n # Localpart template of MXIDs for iMessage users.\n username_template: {{ .BridgeName }}_{{ \"{{.}}\" }}\n # Displayname template for iMessage users.\n displayname_template: \"{{ \"{{.}}\" }}\"\n\n # A URL to fetch validation data from. Use this option or the nac_plist option\n nac_validation_data_url: {{ or .Params.nac_url \"https://registration-relay.beeper.com\" }}\n # Optional auth token to use when fetching validation data. If null, defaults to passing the as_token.\n nac_validation_data_token: {{ .Params.nac_token }}\n nac_validation_is_relay: true\n\n # Servers to always allow double puppeting from\n double_puppet_server_map:\n {{ .BeeperDomain }}: {{ .HungryAddress }}\n # Allow using double puppeting from any server with a valid client .well-known file.\n double_puppet_allow_discovery: false\n # Shared secrets for https://github.com/devture/matrix-synapse-shared-secret-auth\n #\n # If set, double puppeting will be enabled automatically for local users\n # instead of users having to find an access token and run `login-matrix`\n # manually.\n login_shared_secret_map:\n {{ .BeeperDomain }}: \"as_token:{{ .ASToken }}\"\n\n # Should the bridge create a space and add bridged rooms to it?\n personal_filtering_spaces: true\n # Whether or not the bridge should send a read receipt from the bridge bot when a message has been\n # sent to iMessage.\n delivery_receipts: false\n # Whether or not the bridge should send the message status as a custom\n # com.beeper.message_send_status event.\n message_status_events: true\n # Whether or not the bridge should send error notices via m.notice events\n # when a message fails to bridge.\n send_error_notices: false\n # Enable notices about various things in the bridge management room?\n enable_bridge_notices: true\n # Enable less important notices (sent with m.notice) in the bridge management room?\n unimportant_bridge_notices: true\n # The maximum number of seconds between the message arriving at the\n # homeserver and the bridge attempting to send the message. This can help\n # prevent messages from being bridged a long time after arriving at the\n # homeserver which could cause confusion in the chat history on the remote\n # network. Set to 0 to disable.\n max_handle_seconds: 0\n # Should we convert heif images to jpeg before re-uploading? This increases\n # compatibility, but adds generation loss (reduces quality).\n convert_heif: false\n # Should we convert tiff images to jpeg before re-uploading? This increases\n # compatibility, but adds generation loss (reduces quality).\n convert_tiff: true\n # Modern Apple devices tend to use h265 encoding for video, which is a licensed standard and therefore not\n # supported by most major browsers. If enabled, all video attachments will be converted according to the\n # ffmpeg args.\n convert_mov: true\n # The prefix for commands.\n command_prefix: \"!im\"\n # Whether or not created rooms should have federation enabled.\n # If false, created portal rooms will never be federated.\n federate_rooms: false\n # Whether to explicitly set the avatar and room name for private chat portal rooms.\n # If set to `default`, this will be enabled in encrypted rooms and disabled in unencrypted rooms.\n # If set to `always`, all DM rooms will have explicit names and avatars set.\n # If set to `never`, DM rooms will never have names and avatars set.\n private_chat_portal_meta: never\n # Should iMessage reply threads be mapped to Matrix threads? If false, iMessage reply threads will be bridged\n # as replies to the previous message in the thread.\n matrix_threads: false\n\n # End-to-bridge encryption support options.\n # See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html\n encryption:\n # Allow encryption, work in group chat rooms with e2ee enabled\n allow: true\n # Default to encryption, force-enable encryption in all portals the bridge creates\n # This will cause the bridge bot to be in private chats for the encryption to work properly.\n default: true\n # Whether or not to use MSC2409/MSC3202 instead of /sync long polling for receiving encryption-related data.\n appservice: true\n # Require encryption, drop any unencrypted messages.\n require: true\n # Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled.\n # You must use a client that supports requesting keys from other users to use this feature.\n allow_key_sharing: true\n # Options for deleting megolm sessions from the bridge.\n delete_keys:\n # Beeper-specific: delete outbound sessions when hungryserv confirms\n # that the user has uploaded the key to key backup.\n delete_outbound_on_ack: true\n # Don't store outbound sessions in the inbound table.\n dont_store_outbound: false\n # Ratchet megolm sessions forward after decrypting messages.\n ratchet_on_decrypt: true\n # Delete fully used keys (index >= max_messages) after decrypting messages.\n delete_fully_used_on_decrypt: true\n # Delete previous megolm sessions from same device when receiving a new one.\n delete_prev_on_new_session: true\n # Delete megolm sessions received from a device when the device is deleted.\n delete_on_device_delete: true\n # Periodically delete megolm sessions when 2x max_age has passed since receiving the session.\n periodically_delete_expired: true\n # What level of device verification should be required from users?\n #\n # Valid levels:\n # unverified - Send keys to all device in the room.\n # cross-signed-untrusted - Require valid cross-signing, but trust all cross-signing keys.\n # cross-signed-tofu - Require valid cross-signing, trust cross-signing keys on first use (and reject changes).\n # cross-signed-verified - Require valid cross-signing, plus a valid user signature from the bridge bot.\n # Note that creating user signatures from the bridge bot is not currently possible.\n # verified - Require manual per-device verification\n # (currently only possible by modifying the `trust` column in the `crypto_device` database table).\n verification_levels:\n # Minimum level for which the bridge should send keys to when bridging messages from iMessage to Matrix.\n receive: cross-signed-tofu\n # Minimum level that the bridge should accept for incoming Matrix messages.\n send: cross-signed-tofu\n # Minimum level that the bridge should require for accepting key requests.\n share: cross-signed-tofu\n # Options for Megolm room key rotation. These options allow you to\n # configure the m.room.encryption event content. See:\n # https://spec.matrix.org/v1.3/client-server-api/#mroomencryption for\n # more information about that event.\n rotation:\n # Enable custom Megolm room key rotation settings. Note that these\n # settings will only apply to rooms created after this option is\n # set.\n enable_custom: true\n # The maximum number of milliseconds a session should be used\n # before changing it. The Matrix spec recommends 604800000 (a week)\n # as the default.\n milliseconds: 2592000000\n # The maximum number of messages that should be sent with a given a\n # session before changing it. The Matrix spec recommends 100 as the\n # default.\n messages: 10000\n\n # Disable rotating keys when a user's devices change?\n # You should not enable this option unless you understand all the implications.\n disable_device_change_key_rotation: true\n\n # Settings for provisioning API\n provisioning:\n # Prefix for the provisioning API paths.\n prefix: /_matrix/provision\n # Shared secret for authentication. If set to \"generate\", a random secret will be generated,\n # or if set to \"disable\", the provisioning API will be disabled.\n shared_secret: {{ .ProvisioningSecret }}\n\n # Permissions for using the bridge.\n # Permitted values:\n # user - Access to use the bridge to chat with a WhatsApp account.\n # admin - User level and some additional administration tools\n # Permitted keys:\n # * - All Matrix users\n # domain - All users on that homeserver\n # mxid - Specific user\n permissions:\n \"{{ .UserID }}\": admin\n\n# Logging config. See https://github.com/tulir/zeroconfig for details.\nlogging:\n min_level: debug\n writers:\n - type: stdout\n format: pretty-colored\n - type: file\n format: json\n filename: ./logs/beeper-imessage.log\n max_size: 100\n max_backups: 10\n compress: false\n", + "linkedin.tpl.yaml": "# Network-specific config options\nnetwork:\n # Displayname template for LinkedIn users.\n # .FirstName is replaced with the first name\n # .LastName is replaced with the last name\n # .Organization is replaced with the organization name\n displayname_template: {{ `\"{{ with .Organization }}{{ . }}{{ else }}{{ .FirstName }} {{ .LastName }}{{ end }}\"` }}\n\n sync:\n # Number of most recently active dialogs to check when syncing chats.\n # Set to 0 to remove limit.\n update_limit: 0\n # Number of most recently active dialogs to create portals for when syncing\n # chats.\n # Set to 0 to remove limit.\n create_limit: 10\n\n{{ setfield . \"CommandPrefix\" \"!linkedin\" -}}\n{{ setfield . \"DatabaseFileName\" \"mautrix-linkedin\" -}}\n{{ setfield . \"BridgeTypeName\" \"LinkedIn\" -}}\n{{ setfield . \"BridgeTypeIcon\" \"mxc://nevarro.space/cwsWnmeMpWSMZLUNblJHaIvP\" -}}\n{{ setfield . \"DefaultPickleKey\" \"mautrix.bridge.e2ee\" -}}\n{{ template \"bridgev2.tpl.yaml\" . }}\n", + "meta.tpl.yaml": "# Network-specific config options\nnetwork:\n # Which service is this bridge for? Available options:\n # * unset - allow users to pick any service when logging in (except facebook-tor)\n # * facebook - connect to FB Messenger via facebook.com\n # * facebook-tor - connect to FB Messenger via facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd.onion\n # (note: does not currently proxy media downloads)\n # * messenger - connect to FB Messenger via messenger.com (can be used with the facebook side deactivated)\n # * messenger-lite - connect to FB Messenger via Messenger iOS API\n # * instagram - connect to Instagram DMs via instagram.com\n #\n # Remember to change the appservice id, bot profile info, bridge username_template and management_room_text too.\n mode: {{ .Params.meta_platform }}\n # Should users be allowed to pick messenger.com login when mode is set to `facebook`?\n allow_messenger_com_on_fb: true\n # When in Instagram mode, should the bridge connect to WhatsApp servers for encrypted chats?\n # In FB/Messenger mode encryption is always enabled, this option only affects Instagram mode.\n ig_e2ee: false\n # Displayname template for FB/IG users. Available variables:\n # .DisplayName - The display name set by the user.\n # .Username - The username set by the user.\n # .ID - The internal user ID of the user.\n displayname_template: {{ `'{{or .DisplayName .Username \"Unknown user\"}}'` }}\n # Static proxy address (HTTP or SOCKS5) for connecting to Meta.\n proxy:\n # HTTP endpoint to request new proxy address from, for dynamically assigned proxies.\n # The endpoint must return a JSON body with a string field called proxy_url.\n get_proxy_from:\n # Minimum interval between full reconnects in seconds, default is 1 hour\n min_full_reconnect_interval_seconds: 3600\n # Interval to force refresh the connection (full reconnect), default is 20 hours. Set 0 to disable force refreshes.\n force_refresh_interval_seconds: 72000\n # Should connection state be cached to allow quicker restarts?\n cache_connection_state: false\n # Disable fetching XMA media (reels, stories, etc) when backfilling.\n disable_xma_backfill: true\n # Disable fetching XMA media entirely.\n disable_xma_always: false\n\n{{ setfield . \"DatabaseFileName\" \"mautrix-meta\" -}}\n{{ setfield . \"DefaultPickleKey\" \"mautrix.bridge.e2ee\" -}}\n{{ if eq .Params.meta_platform \"facebook\" \"facebook-tor\" \"messenger\" \"messenger-lite\" -}}\n {{ setfield . \"CommandPrefix\" \"!fb\" -}}\n {{ setfield . \"BridgeTypeName\" \"Facebook\" -}}\n {{ setfield . \"BridgeTypeIcon\" \"mxc://maunium.net/ygtkteZsXnGJLJHRchUwYWak\" -}}\n{{ else if eq .Params.meta_platform \"instagram\" -}}\n {{ setfield . \"CommandPrefix\" \"!ig\" -}}\n {{ setfield . \"BridgeTypeName\" \"Instagram\" -}}\n {{ setfield . \"BridgeTypeIcon\" \"mxc://maunium.net/JxjlbZUlCPULEeHZSwleUXQv\" -}}\n{{ else -}}\n {{ setfield . \"CommandPrefix\" \"!meta\" -}}\n {{ setfield . \"BridgeTypeName\" \"Meta\" -}}\n {{ setfield . \"BridgeTypeIcon\" \"mxc://maunium.net/DxpVrwwzPUwaUSazpsjXgcKB\" -}}\n{{ end -}}\n{{ template \"bridgev2.tpl.yaml\" . }}\n", + "signal.tpl.yaml": "# Network-specific config options\nnetwork:\n # Displayname template for Signal users.\n displayname_template: {{ `'{{or .Nickname .ContactName .ProfileName .PhoneNumber \"Unknown user\" }}'` }}\n # Should avatars from the user's contact list be used? This is not safe on multi-user instances.\n use_contact_avatars: true\n # Should the bridge sync ghost user info even if profile fetching fails? This is not safe on multi-user instances.\n use_outdated_profiles: true\n # Should the Signal user's phone number be included in the room topic in private chat portal rooms?\n number_in_topic: true\n # Default device name that shows up in the Signal app.\n device_name: {{ or .Params.device_name \"Beeper (self-hosted)\" }}\n # Avatar image for the Note to Self room.\n note_to_self_avatar: mxc://maunium.net/REBIVrqjZwmaWpssCZpBlmlL\n # Format for generating URLs from location messages for sending to Signal.\n # Google Maps: 'https://www.google.com/maps/place/%[1]s,%[2]s'\n # OpenStreetMap: 'https://www.openstreetmap.org/?mlat=%[1]s&mlon=%[2]s'\n location_format: 'https://www.google.com/maps/place/%[1]s,%[2]s'\n # Should view-once messages disappear shortly after sending a read receipt on Matrix?\n disappear_view_once: true\n\n{{ setfield . \"CommandPrefix\" \"!signal\" -}}\n{{ setfield . \"DatabaseFileName\" \"mautrix-signal\" -}}\n{{ setfield . \"BridgeTypeName\" \"Signal\" -}}\n{{ setfield . \"BridgeTypeIcon\" \"mxc://maunium.net/wPJgTQbZOtpBFmDNkiNEMDUp\" -}}\n{{ setfield . \"DefaultPickleKey\" \"mautrix.bridge.e2ee\" -}}\n{{ template \"bridgev2.tpl.yaml\" . }}\n", + "slack.tpl.yaml": "network:\n # Displayname template for Slack users. Available variables:\n # .Name - The username of the user\n # .ID - The internal ID of the user\n # .IsBot - Whether the user is a bot\n # .Profile.DisplayName - The username or real name of the user (depending on settings)\n # Variables only available for users (not bots):\n # .TeamID - The internal ID of the workspace the user is in\n # .TZ - The timezone region of the user (e.g. Europe/London)\n # .TZLabel - The label of the timezone of the user (e.g. Greenwich Mean Time)\n # .TZOffset - The UTC offset of the timezone of the user (e.g. 0)\n # .Profile.RealName - The real name of the user\n # .Profile.FirstName - The first name of the user\n # .Profile.LastName - The last name of the user\n # .Profile.Title - The job title of the user\n # .Profile.Pronouns - The pronouns of the user\n # .Profile.Email - The email address of the user\n # .Profile.Phone - The formatted phone number of the user\n displayname_template: '{{ `{{or .Profile.DisplayName .Profile.RealName .Name}}{{if .IsBot}} (bot){{end}}` }}'\n # Channel name template for Slack channels (all types). Available variables:\n # .Name - The name of the channel\n # .TeamName - The name of the team the channel is in\n # .TeamDomain - The Slack subdomain of the team the channel is in\n # .ID - The internal ID of the channel\n # .IsNoteToSelf - Whether the channel is a DM with yourself\n # .IsGeneral - Whether the channel is the #general channel\n # .IsChannel - Whether the channel is a channel (rather than a DM)\n # .IsPrivate - Whether the channel is private\n # .IsIM - Whether the channel is a one-to-one DM\n # .IsMpIM - Whether the channel is a group DM\n # .IsShared - Whether the channel is shared with another workspace.\n # .IsExtShared - Whether the channel is shared with an external organization.\n # .IsOrgShared - Whether the channel is shared with an organization in the same enterprise grid.\n channel_name_template: '{{ `{{if or .IsNoteToSelf (and (not .IsIM) (not .IsMpIM))}}{{if and .IsChannel (not .IsPrivate)}}#{{end}}{{.Name}}{{if .IsNoteToSelf}} (you){{end}}{{end}}` }}'\n # Displayname template for Slack workspaces. Available variables:\n # .Name - The name of the team\n # .Domain - The Slack subdomain of the team\n # .ID - The internal ID of the team\n team_name_template: '{{ `{{.Name}}` }}'\n # Should incoming custom emoji reactions be bridged as mxc:// URIs?\n # If set to false, custom emoji reactions will be bridged as the shortcode instead, and the image won't be available.\n custom_emoji_reactions: true\n # Should channels and group DMs have the workspace icon as the Matrix room avatar?\n workspace_avatar_in_rooms: false\n # Number of participants to sync in channels (doesn't affect group DMs)\n participant_sync_count: 5\n # Should channel participants only be synced when creating the room?\n # If you want participants to always be accurately synced, set participant_sync_count to a high value and this to false.\n participant_sync_only_on_create: true\n # Should channel portals be muted by default?\n mute_channels_by_default: true\n # Options for backfilling messages from Slack.\n backfill:\n # Number of conversations to fetch from Slack when syncing workspace.\n # This option applies even if message backfill is disabled below.\n # If set to -1, all chats in the client.boot response will be bridged, and nothing will be fetched separately.\n conversation_count: -1\n\n{{ setfield . \"CommandPrefix\" \"!slack\" -}}\n{{ setfield . \"DatabaseFileName\" \"mautrix-slack\" -}}\n{{ setfield . \"BridgeTypeName\" \"Slack\" -}}\n{{ setfield . \"BridgeTypeIcon\" \"mxc://maunium.net/pVtzLmChZejGxLqmXtQjFxem\" -}}\n{{ template \"bridgev2.tpl.yaml\" . }}\n", + "telegram.tpl.yaml": "# Network-specific config options\nnetwork:\n # Get your own API keys at https://my.telegram.org/apps\n api_id: {{ .Params.api_id }}\n api_hash: {{ .Params.api_hash }}\n\n # Device info shown in the Telegram device list.\n device_info:\n device_model: {{ or .Params.device_name \"Beeper (self-hosted)\" }}\n system_version:\n app_version: auto\n lang_code: en\n system_lang_code: en\n\n # Settings for converting animated stickers.\n animated_sticker:\n # Format to which animated stickers should be converted.\n #\n # disable - no conversion, send as-is (gzipped lottie)\n # png - converts to non-animated png (fastest),\n # gif - converts to animated gif\n # webm - converts to webm video, requires ffmpeg executable with vp9 codec\n # and webm container support\n # webp - converts to animated webp, requires ffmpeg executable with webp\n # codec/container support\n target: webp\n # Should video stickers be converted to the specified format as well?\n convert_from_webm: false\n # Arguments for converter. All converters take width and height.\n args:\n width: 256\n height: 256\n fps: 25 # only for webm, webp and gif (2, 5, 10, 20 or 25 recommended)\n\n # Maximum number of pixels in an image before sending to Telegram as a\n # document. Defaults to 4096x4096 = 16777216.\n image_as_file_pixels: 16777216\n\n # Should view-once messages be disabled entirely?\n disable_view_once: false\n # Should disappearing messages be disabled entirely?\n disable_disappearing: false\n\n # Settings for syncing the member list for portals.\n member_list:\n # Maximum number of members to sync per portal when starting up. Other\n # members will be synced when they send messages. The maximum is 10000,\n # after which the Telegram server will not send any more members.\n #\n # -1 means no limit (which means it's limited to 10000 by the server)\n max_initial_sync: 20\n # Whether or not to sync the member list in broadcast channels. If\n # disabled, members will still be synced when they send messages.\n #\n # If no channel admins have logged into the bridge, the bridge won't be\n # able to sync the member list regardless of this setting.\n sync_broadcast_channels: false\n # Whether or not to skip deleted members when syncing members.\n skip_deleted: true\n # Maximum number of participants in chats to bridge. Only applies when the\n # portal is being created. If there are more members when trying to create a\n # room, the room creation will be cancelled.\n #\n # -1 means no limit (which means all chats can be bridged)\n max_member_count: 10000\n\n # Settings for pings to the Telegram server.\n ping:\n # The interval (in seconds) between pings.\n interval_seconds: 30\n # The timeout (in seconds) for a single ping.\n timeout_seconds: 10\n\n sync:\n # Number of most recently active dialogs to check when syncing chats.\n # Set to 0 to remove limit.\n update_limit: 0\n # Number of most recently active dialogs to create portals for when syncing\n # chats.\n # Set to 0 to remove limit.\n create_limit: 15\n # Whether or not to sync and create portals for direct chats at startup.\n direct_chats: true\n\n # Should the bridge send all unicode reactions as custom emoji reactions to\n # Telegram? By default, the bridge only uses custom emojis for unicode emojis\n # that aren't allowed in reactions.\n always_custom_emoji_reaction: false\n\n # The avatar to use for the Telegram Saved Messages chat\n saved_message_avatar: mxc://maunium.net/XhhfHoPejeneOngMyBbtyWDk\n\n # Create a new room and tombstone the old one when upgrading rooms\n always_tombstone_on_supergroup_migration: false\n\n{{ setfield . \"CommandPrefix\" \"!tg\" -}}\n{{ setfield . \"DatabaseFileName\" \"mautrix-telegram\" -}}\n{{ setfield . \"BridgeTypeName\" \"Telegram\" -}}\n{{ setfield . \"BridgeTypeIcon\" \"mxc://maunium.net/tJCRmUyJDsgRNgqhOgoiHWbX\" -}}\n{{ setfield . \"DefaultPickleKey\" \"mautrix.bridge.e2ee\" -}}\n{{ template \"bridgev2.tpl.yaml\" . }}\n", + "twitter.tpl.yaml": "# Network-specific config options\nnetwork:\n # Displayname template for Twitter users.\n # .DisplayName is replaced with the display name of the Twitter user.\n # .Username is replaced with the username of the Twitter user.\n displayname_template: {{ `\"{{ .DisplayName }}\"` }}\n\n # Maximum number of conversations to sync on startup\n conversation_sync_limit: 20\n\n # Should the bridge cache sessions instead of resyncing chats on every restart?\n cache_session: true\n\n # Should the bridge use \"X\" instead of \"Twitter\" in certain places,\n # such as the management room welcome message and MSC2346 bridge info?\n x: false\n\n{{ setfield . \"CommandPrefix\" \"!tw\" -}}\n{{ setfield . \"DatabaseFileName\" \"mautrix-twitter\" -}}\n{{ setfield . \"BridgeTypeName\" \"Twitter\" -}}\n{{ setfield . \"BridgeTypeIcon\" \"mxc://maunium.net/HVHcnusJkQcpVcsVGZRELLCn\" -}}\n{{ setfield . \"DefaultPickleKey\" \"mautrix.bridge.e2ee\" -}}\n{{ template \"bridgev2.tpl.yaml\" . }}\n", + "whatsapp.tpl.yaml": "# Network-specific config options\nnetwork:\n # Device name that's shown in the \"WhatsApp Web\" section in the mobile app.\n os_name: Beeper (self-hosted)\n # Browser name that determines the logo shown in the mobile app.\n # Must be \"unknown\" for a generic icon or a valid browser name if you want a specific icon.\n # List of valid browser names: https://github.com/tulir/whatsmeow/blob/efc632c008604016ddde63bfcfca8de4e5304da9/binary/proto/def.proto#L43-L64\n browser_name: unknown\n\n # Proxy to use for all WhatsApp connections.\n proxy: null\n # Alternative to proxy: an HTTP endpoint that returns the proxy URL to use for WhatsApp connections.\n get_proxy_url: null\n # Whether the proxy options should only apply to the login websocket and not to authenticated connections.\n proxy_only_login: false\n\n # Displayname template for WhatsApp users.\n # .PushName - nickname set by the WhatsApp user\n # .BusinessName - validated WhatsApp business name\n # .Phone - phone number (international format)\n # .FullName - Name you set in the contacts list\n displayname_template: {{ `'{{or .FullName .BusinessName .PushName .Phone .RedactedPhone \"Unknown user\"}}'` }}\n\n # Should incoming calls send a message to the Matrix room?\n call_start_notices: true\n # Should another user's cryptographic identity changing send a message to Matrix?\n identity_change_notices: false\n # Send the presence as \"available\" to whatsapp when users start typing on a portal.\n # This works as a workaround for homeservers that do not support presence, and allows\n # users to see when the whatsapp user on the other side is typing during a conversation.\n send_presence_on_typing: false\n # Should WhatsApp status messages be bridged into a Matrix room?\n # Disabling this won't affect already created status broadcast rooms.\n enable_status_broadcast: true\n # Should sending WhatsApp status messages be allowed?\n # This can cause issues if the user has lots of contacts, so it's disabled by default.\n disable_status_broadcast_send: true\n # Should the status broadcast room be muted and moved into low priority by default?\n # This is only applied when creating the room, the user can unmute it later.\n mute_status_broadcast: true\n # Tag to apply to the status broadcast room.\n status_broadcast_tag: m.lowpriority\n # Should the bridge use thumbnails from WhatsApp?\n # They're disabled by default due to very low resolution.\n whatsapp_thumbnail: false\n # Should the bridge detect URLs in outgoing messages, ask the homeserver to generate a preview,\n # and send it to WhatsApp? URL previews can always be sent using the `com.beeper.linkpreviews`\n # key in the event content even if this is disabled.\n url_previews: false\n # Should the bridge always send \"active\" delivery receipts (two gray ticks on WhatsApp)\n # even if the user isn't marked as online (e.g. when presence bridging isn't enabled)?\n #\n # By default, the bridge acts like WhatsApp web, which only sends active delivery\n # receipts when it's in the foreground.\n force_active_delivery_receipts: false\n # Settings for converting animated stickers.\n animated_sticker:\n # Format to which animated stickers should be converted.\n # disable - No conversion, just unzip and send raw lottie JSON\n # png - converts to non-animated png (fastest)\n # gif - converts to animated gif\n # webm - converts to webm video, requires ffmpeg executable with vp9 codec and webm container support\n # webp - converts to animated webp, requires ffmpeg executable with webp codec/container support\n target: webp\n # Arguments for converter. All converters take width and height.\n args:\n width: 320\n height: 320\n fps: 25 # only for webm, webp and gif (2, 5, 10, 20 or 25 recommended)\n\n # Settings for handling history sync payloads.\n history_sync:\n # How many conversations should the bridge create after login?\n # If -1, all conversations received from history sync will be bridged.\n # Other conversations will be backfilled on demand when receiving a message.\n max_initial_conversations: -1\n # Should the bridge request a full sync from the phone when logging in?\n # This bumps the size of history syncs from 3 months to 1 year.\n request_full_sync: true\n # Configuration parameters that are sent to the phone along with the request full sync flag.\n # By default, (when the values are null or 0), the config isn't sent at all.\n full_sync_config:\n # Number of days of history to request.\n # The limit seems to be around 3 years, but using higher values doesn't break.\n days_limit: 1825\n # This is presumably the maximum size of the transferred history sync blob, which may affect what the phone includes in the blob.\n size_mb_limit: 512\n # This is presumably the local storage quota, which may affect what the phone includes in the history sync blob.\n storage_quota_mb: 16384\n # Settings for media requests. If the media expired, then it will not be on the WA servers.\n # Media can always be requested by reacting with the ♻️ (recycle) emoji.\n # These settings determine if the media requests should be done automatically during or after backfill.\n media_requests:\n # Should the expired media be automatically requested from the server as part of the backfill process?\n auto_request_media: true\n # Whether to request the media immediately after the media message is backfilled (\"immediate\")\n # or at a specific time of the day (\"local_time\").\n request_method: immediate\n # If request_method is \"local_time\", what time should the requests be sent (in minutes after midnight)?\n request_local_time: 120\n # Maximum number of media request responses to handle in parallel per user.\n max_async_handle: 2\n\n{{ setfield . \"CommandPrefix\" \"!wa\" -}}\n{{ setfield . \"DatabaseFileName\" \"mautrix-whatsapp\" -}}\n{{ setfield . \"BridgeTypeName\" \"WhatsApp\" -}}\n{{ setfield . \"BridgeTypeIcon\" \"mxc://maunium.net/NeXNQarUbrlYBiPCpprYsRqr\" -}}\n{{ setfield . \"DefaultPickleKey\" \"maunium.net/go/mautrix-whatsapp\" -}}\n{{ template \"bridgev2.tpl.yaml\" . }}\n" +} as const + +export const generatedSupportedBridges = [ + "bluesky", + "bridgev2", + "discord", + "gmessages", + "googlechat", + "gvoice", + "heisenbridge", + "imessage", + "imessagego", + "linkedin", + "meta", + "signal", + "slack", + "telegram", + "twitter", + "whatsapp" +] as const + +export const generatedOfficialBridges: GeneratedOfficialBridge[] = [ + { + "typeName": "discord", + "names": [ + "discord" + ] + }, + { + "typeName": "meta", + "names": [ + "meta", + "instagram", + "facebook" + ] + }, + { + "typeName": "googlechat", + "names": [ + "googlechat", + "gchat" + ] + }, + { + "typeName": "imessagego", + "names": [ + "imessagego" + ] + }, + { + "typeName": "imessage", + "names": [ + "imessage" + ] + }, + { + "typeName": "linkedin", + "names": [ + "linkedin" + ] + }, + { + "typeName": "signal", + "names": [ + "signal" + ] + }, + { + "typeName": "slack", + "names": [ + "slack" + ] + }, + { + "typeName": "telegram", + "names": [ + "telegram" + ] + }, + { + "typeName": "twitter", + "names": [ + "twitter" + ] + }, + { + "typeName": "whatsapp", + "names": [ + "whatsapp" + ] + }, + { + "typeName": "heisenbridge", + "names": [ + "irc", + "heisenbridge" + ] + }, + { + "typeName": "gmessages", + "names": [ + "gmessages", + "googlemessages", + "rcs", + "sms" + ] + }, + { + "typeName": "gvoice", + "names": [ + "gvoice", + "googlevoice" + ] + }, + { + "typeName": "bluesky", + "names": [ + "bluesky", + "bsky" + ] + } +] + +export const generatedWebsocketBridges: Record = { + "discord": true, + "slack": true, + "whatsapp": true, + "gmessages": true, + "gvoice": true, + "heisenbridge": true, + "imessage": true, + "imessagego": true, + "signal": true, + "bridgev2": true, + "meta": true, + "twitter": true, + "bluesky": true, + "linkedin": true, + "telegram": true +} + +export const generatedBridgeIPSuffix: Record = { + "telegram": "17", + "whatsapp": "18", + "meta": "19", + "googlechat": "20", + "twitter": "27", + "signal": "28", + "discord": "34", + "slack": "35", + "gmessages": "36", + "imessagego": "37", + "gvoice": "38", + "bluesky": "40", + "linkedin": "41" +} + diff --git a/packages/cli/src/lib/bridges/go-template.ts b/packages/cli/src/lib/bridges/go-template.ts new file mode 100644 index 00000000..06e687b3 --- /dev/null +++ b/packages/cli/src/lib/bridges/go-template.ts @@ -0,0 +1,183 @@ +import type { BridgeCatalog } from './catalog.js' + +type Context = Record + +export function renderBridgeTemplate(catalog: BridgeCatalog, name: string, params: Context): string { + const template = catalog.templates[name] + if (template === undefined) throw new Error(`Unknown bridge template ${name}`) + return renderTemplate(catalog, name, template, params) +} + +function renderTemplate(catalog: BridgeCatalog, name: string, template: string, params: Context): string { + let out = '' + const tokens = tokenize(template) + const stack: Array<{ active: boolean; matched: boolean; parentActive: boolean }> = [] + const isActive = () => stack.every(item => item.active) + + for (const token of tokens) { + if (token.type === 'text') { + if (isActive()) out += token.value + continue + } + + const action = token.value.trim() + if (action.startsWith('if ')) { + const parentActive = isActive() + const condition = Boolean(evalExpr(action.slice(3).trim(), params)) + stack.push({ active: parentActive && condition, matched: condition, parentActive }) + } else if (action.startsWith('else if ')) { + const current = stack.at(-1) + if (!current) throw new Error(`Unexpected {{ else if }} in ${name}`) + const condition = !current.matched && Boolean(evalExpr(action.slice(8).trim(), params)) + current.active = current.parentActive && condition + current.matched = current.matched || condition + } else if (action === 'else') { + const current = stack.at(-1) + if (!current) throw new Error(`Unexpected {{ else }} in ${name}`) + current.active = current.parentActive && !current.matched + current.matched = true + } else if (action === 'end') { + if (!stack.pop()) throw new Error(`Unexpected {{ end }} in ${name}`) + } else if (isActive()) { + out += String(evalExpr(action, params, catalog) ?? '') + } + } + if (stack.length) throw new Error(`Unclosed {{ if }} block in ${name}`) + return out +} + +function tokenize(template: string): Array<{ type: 'action' | 'text'; value: string }> { + const tokens: Array<{ type: 'action' | 'text'; value: string }> = [] + let index = 0 + while (index < template.length) { + const start = template.indexOf('{{', index) + if (start === -1) { + tokens.push({ type: 'text', value: template.slice(index) }) + break + } + let text = template.slice(index, start) + const trimLeft = template[start + 2] === '-' + if (trimLeft) text = text.replace(/[ \t]*$/, '') + tokens.push({ type: 'text', value: text }) + const end = findActionEnd(template, start + 2) + if (end === -1) throw new Error('Unclosed template action') + const trimRight = template[end - 1] === '-' + const actionStart = start + (trimLeft ? 3 : 2) + const actionEnd = end - (trimRight ? 1 : 0) + tokens.push({ type: 'action', value: template.slice(actionStart, actionEnd) }) + index = end + 2 + if (trimRight) { + const match = template.slice(index).match(/^[ \t]*(?:\r?\n)?/) + index += match?.[0].length ?? 0 + } + } + return tokens +} + +function findActionEnd(template: string, start: number): number { + let quote: string | undefined + for (let i = start; i < template.length - 1; i++) { + const ch = template[i]! + if (quote) { + if (ch === quote) quote = undefined + } else if (ch === '"' || ch === '\'' || ch === '`') { + quote = ch + } else if (ch === '}' && template[i + 1] === '}') { + return i + } + } + return -1 +} + +function evalExpr(expr: string, ctx: Context, catalog?: BridgeCatalog): unknown { + const parts = splitArgs(expr) + if (!parts.length) return '' + const [head, ...rest] = parts + switch (head) { + case 'or': + for (const part of rest) { + const value = evalExpr(part!, ctx, catalog) + if (truthy(value)) return value + } + return '' + case 'replace': { + const [inputValue = '', search = '', replacement = ''] = rest.map(part => String(evalExpr(part!, ctx, catalog) ?? '')) + return inputValue.split(search).join(replacement) + } + case 'setfield': { + const [, field, valueExpr] = rest + if (!field || !valueExpr) return '' + ctx[stripQuotes(field)] = evalExpr(valueExpr, ctx, catalog) + return '' + } + case 'eq': + return rest.length >= 2 && evalExpr(rest[0]!, ctx, catalog) === evalExpr(rest[1]!, ctx, catalog) + case 'template': { + if (!catalog) throw new Error('template function requires a catalog') + const [templateExpr] = rest + const templateName = String(evalExpr(templateExpr!, ctx, catalog)) + return renderBridgeTemplate(catalog, templateName, ctx) + } + default: + if (parts.length > 1) throw new Error(`Unsupported template expression: ${expr}`) + return evalAtom(head!, ctx) + } +} + +function evalAtom(atom: string, ctx: Context): unknown { + atom = atom.trim() + if (atom === '.') return ctx + if (atom === 'true') return true + if (atom === 'false') return false + if (/^\d+$/.test(atom)) return Number(atom) + if (isQuoted(atom)) return stripQuotes(atom) + if (atom.startsWith('.')) return lookupPath(ctx, atom.slice(1).split('.').filter(Boolean)) + return atom +} + +function lookupPath(value: unknown, path: string[]): unknown { + let current = value + for (const part of path) { + if (!current || typeof current !== 'object') return '' + current = (current as Record)[part] + } + return current ?? '' +} + +function splitArgs(expr: string): string[] { + const out: string[] = [] + let current = '' + let quote: string | undefined + for (let i = 0; i < expr.length; i++) { + const ch = expr[i]! + if (quote) { + current += ch + if (ch === quote) quote = undefined + } else if (ch === '"' || ch === '\'' || ch === '`') { + quote = ch + current += ch + } else if (/\s/.test(ch)) { + if (current) { + out.push(current) + current = '' + } + } else { + current += ch + } + } + if (current) out.push(current) + return out +} + +function truthy(value: unknown): boolean { + return Boolean(value) +} + +function isQuoted(value: string): boolean { + return value.length >= 2 && ['"', '\'', '`'].includes(value[0]!) && value.at(-1) === value[0] +} + +function stripQuotes(value: string): string { + if (!isQuoted(value)) return value + return value.slice(1, -1) +} diff --git a/packages/cli/src/lib/bridges/manager.ts b/packages/cli/src/lib/bridges/manager.ts new file mode 100644 index 00000000..51907268 --- /dev/null +++ b/packages/cli/src/lib/bridges/manager.ts @@ -0,0 +1,761 @@ +import { createDecipheriv } from 'node:crypto' +import { createInterface } from 'node:readline/promises' +import { stdin as input, stdout as output } from 'node:process' +import { constants as fsConstants } from 'node:fs' +import { access, chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises' +import { createWriteStream } from 'node:fs' +import { homedir, platform } from 'node:os' +import { basename, dirname, join } from 'node:path' +import { spawn } from 'node:child_process' +import { once } from 'node:events' +import { pipeline } from 'node:stream/promises' +import { Readable } from 'node:stream' +import { clearTargetAuth, resolveTarget, saveTargetAuth, type Target } from '../targets.js' +import { authRequired } from '../errors.js' +import { isNoInput } from '../command.js' +import { isSupported, loadBridgeCatalog, templateName, type BridgeCatalog } from './catalog.js' +import { renderBridgeTemplate } from './go-template.js' + +export type BridgeEnv = { + accessToken: string + catalog: BridgeCatalog + domain: string + envName: string + target: Target +} + +export type BridgeTargetEnv = { + domain: string + envName: string + target: Target +} + +export type BridgeFlags = { + env?: string + 'base-url'?: string + target?: string +} + +export type WhoamiResponse = { + user: { + asmuxData?: { login_token?: string } + bridges?: Record + hungryserv?: WhoamiBridge + } + userInfo: { + isAdmin?: boolean + isFree?: boolean + username: string + channel?: string + createdAt?: string + email?: string + fullName?: string + bridgeClusterId?: string + supportRoomId?: string + } +} + +export type WhoamiBridge = { + version?: string + remoteState?: Record + bridgeState?: { + bridgeType?: string + isSelfHosted?: boolean + stateEvent?: string + } +} + +export type AppserviceRegistration = { + id: string + url?: string + as_token: string + hs_token: string + sender_localpart: string + namespaces?: { + users?: Array<{ exclusive: boolean; regex: string }> + aliases?: Array<{ exclusive: boolean; regex: string }> + rooms?: Array<{ exclusive: boolean; regex: string }> + } + protocols?: string[] + rate_limited?: boolean + receive_ephemeral?: boolean + 'de.sorunome.msc2409.push_ephemeral'?: boolean + 'org.matrix.msc3202'?: boolean + 'io.element.msc4190'?: boolean +} + +export type RegisterJSON = { + registration: AppserviceRegistration + homeserver_url: string + homeserver_domain: string + your_user_id: string +} + +export type GeneratedBridgeConfig = RegisterJSON & { + bridgeType: string + config?: string +} + +export async function prepareBridgeEnv(flags: BridgeFlags): Promise { + const targetEnv = await prepareBridgeTargetEnv(flags) + const { target } = targetEnv + const accessToken = process.env.BEEPER_ACCESS_TOKEN || target.auth?.accessToken + if (!accessToken) throw authRequired('beeper bridges requires the selected target to have a Matrix access token.') + if (!/^(syt|bat)_/.test(accessToken)) { + throw authRequired('beeper bridges requires a Matrix/Beeper access token (syt_ or bat_), not a Desktop API token.') + } + return { ...targetEnv, accessToken, catalog: await loadBridgeCatalog() } +} + +export async function prepareBridgeTargetEnv(flags: BridgeFlags): Promise { + const target = await resolveTarget({ target: flags.target, baseURL: flags['base-url'] }) + const envName = flags.env || process.env.BEEPER_BRIDGE_ENV || target.serverEnv || 'prod' + return { + domain: resolveDomain(envName), + envName, + target, + } +} + +export async function loginWithEmail(env: BridgeTargetEnv, email: string, codeProvider: (requestID: string) => Promise): Promise<{ accessToken: string; userID: string; whoami: WhoamiResponse | undefined }> { + const start = await beeperPublicAPI<{ request: string }>(env.domain, 'POST', '/user/login', {}) + await beeperPublicAPI(env.domain, 'POST', '/user/login/email', { + request: start.request, + email, + appType: 'bbctl', + onlyExistingAccounts: true, + }) + for (;;) { + const code = await codeProvider(start.request) + try { + const response = await beeperPublicAPI<{ token: string; whoami?: WhoamiResponse }>(env.domain, 'POST', '/user/login/response', { + request: start.request, + response: code, + appType: 'bbctl', + onlyExistingAccounts: true, + }) + const login = await matrixLogin(env.domain, { + type: 'org.matrix.login.jwt', + token: response.token, + initial_device_display_name: 'github.com/beeper/bridge-manager', + }) + await saveTargetAuth(env.target, { accessToken: login.access_token, source: 'manual', tokenType: 'Bearer' }) + return { accessToken: login.access_token, userID: login.user_id, whoami: response.whoami } + } catch (error) { + if (!String((error as Error).message).includes('invalid login code')) throw error + process.stderr.write(`${(error as Error).message}\n`) + } + } +} + +export async function loginWithPassword(env: BridgeTargetEnv, username: string, password: string): Promise<{ accessToken: string; userID: string }> { + const login = await matrixLogin(env.domain, { + type: 'm.login.password', + identifier: { type: 'm.id.user', user: username }, + password, + initial_device_display_name: 'github.com/beeper/bridge-manager', + }) + await saveTargetAuth(env.target, { accessToken: login.access_token, source: 'manual', tokenType: 'Bearer' }) + return { accessToken: login.access_token, userID: login.user_id } +} + +export async function logoutBridgeTarget(env: BridgeEnv, force: boolean): Promise { + try { + await matrixAPI(env.domain, env.accessToken, 'POST', '/_matrix/client/v3/logout', {}) + } catch (error) { + if (!force) throw new Error(`error logging out: ${(error as Error).message}`) + } + if (env.target.auth?.accessToken) { + await clearTargetAuth(env.target) + } else if (process.env.BEEPER_ACCESS_TOKEN) { + throw new Error('Logged out on the server, but BEEPER_ACCESS_TOKEN cannot be cleared from this process.') + } +} + +export async function whoami(env: BridgeEnv): Promise { + return beeperAPI(env, 'GET', '/whoami') as Promise +} + +export async function registerBridge( + env: BridgeEnv, + bridge: string, + options: { address?: string; bridgeType?: string; force?: boolean; get?: boolean; noState?: boolean }, +): Promise { + const info = await whoami(env) + const username = info.userInfo.username + const bridgeInfo = info.user.bridges?.[bridge] + if (bridgeInfo && !bridgeInfo.bridgeState?.isSelfHosted && !options.force) { + throw new Error(`Your ${bridge} bridge is not self-hosted.`) + } + + const req = options.address + ? { address: options.address, push: true, self_hosted: true } + : { push: false, self_hosted: true } + if (options.get && options.address) throw new Error("You can't use --get with --address") + + const registration = await hungryAPI( + env, + username, + options.get ? 'GET' : 'PUT', + `/_matrix/asmux/mxauth/appservice/${encodeURIComponent(username)}/${encodeURIComponent(bridge)}`, + options.get ? undefined : req, + ) + registration.namespaces = registration.namespaces ?? {} + if (registration.namespaces.users?.length) registration.namespaces.users = registration.namespaces.users.slice(0, 1) + + const state = (options.bridgeType && options.bridgeType !== 'heisenbridge') || ['androidsms', 'imessagecloud', 'imessage'].includes(bridge) + ? 'STARTING' + : 'RUNNING' + if (!options.noState) { + await postBridgeState(env, username, bridge, registration.as_token, { + stateEvent: state, + reason: 'SELF_HOST_REGISTERED', + isSelfHosted: true, + bridgeType: options.bridgeType || undefined, + }) + } + + return { + registration, + homeserver_url: hungryURL(env.domain, username), + homeserver_domain: 'beeper.local', + your_user_id: `@${username}:${env.domain}`, + } +} + +export async function deleteBridge(env: BridgeEnv, bridge: string): Promise { + await beeperAPI(env, 'DELETE', `/bridge/${encodeURIComponent(bridge)}`) +} + +export async function generateBridgeConfig( + env: BridgeEnv, + bridge: string, + options: { force?: boolean; noState?: boolean; params?: string[]; type?: string }, +): Promise { + validateBridgeName(bridge, Boolean(options.force)) + const info = await whoami(env) + const existingType = info.user.bridges?.[bridge]?.bridgeState?.bridgeType + const bridgeType = existingType || await guessOrAskBridgeType(env.catalog, bridge, options.type) + const extraParams = parseParams(options.params) + const cliParams = new Set(Object.keys(extraParams)) + await applyBridgeParamDefaults(bridge, bridgeType, extraParams) + const addedParams = Object.entries(extraParams).filter(([key]) => !cliParams.has(key)) + if (addedParams.length && !isNoInput()) { + process.stderr.write(`To run without specifying parameters interactively, add ${addedParams.map(([key, value]) => `--param '${key}=${value}'`).join(' ')} next time\n`) + } + + const reg = await registerBridge(env, bridge, { bridgeType, force: options.force, noState: options.noState }) + const websocket = Boolean(env.catalog.websocketBridges[bridgeType]) + let listenAddr = '' + let listenPort = 0 + if (!websocket) { + const proxy = getBridgeWebsocketProxyConfig(env.catalog, bridge, bridgeType) + listenAddr = proxy.listenAddr + listenPort = proxy.listenPort + reg.registration.url = proxy.url + } + + const databasePrefix = process.env.BEEPER_BRIDGE_DATABASE_DIR ? join(process.env.BEEPER_BRIDGE_DATABASE_DIR, `${bridge}-`) : '' + const params = { + HungryAddress: reg.homeserver_url, + BeeperDomain: env.domain, + Websocket: websocket, + ListenAddr: listenAddr, + ListenPort: listenPort, + AppserviceID: reg.registration.id, + ASToken: reg.registration.as_token, + HSToken: reg.registration.hs_token, + BridgeName: bridge, + Username: localpart(reg.your_user_id), + UserID: reg.your_user_id, + ProvisioningSecret: info.user.asmuxData?.login_token ?? '', + DatabasePrefix: databasePrefix, + Params: extraParams, + } + + return { + ...reg, + bridgeType, + config: renderBridgeTemplate(env.catalog, templateName(bridgeType), params), + } +} + +export function validateBridgeName(bridge: string, force = false): void { + if (!/^[a-z0-9-]{1,32}$/.test(bridge)) throw new Error('Invalid bridge name. Names must consist of 1-32 lowercase ASCII letters, digits and -.') + if (!bridge.startsWith('sh-')) { + if (!force) throw new Error('Self-hosted bridge names should start with sh-') + process.stderr.write('Self-hosted bridge names should start with sh-\n') + } +} + +export function validateBridgeID(bridge: string): void { + if (!/^[a-z0-9-]{1,32}$/.test(bridge)) throw new Error('Invalid bridge name') +} + +export async function outputFile(name: string, data: string, outputPath: string): Promise { + if (outputPath === '-') { + process.stderr.write(`${name} file:\n`) + process.stdout.write(`${data.trimEnd()}\n`) + return + } + await writeFile(outputPath, data, { mode: 0o600 }) + process.stderr.write(`Wrote ${name.toLowerCase()} file to ${outputPath}\n`) +} + +export async function writeRegistrationJSON(output: RegisterJSON): Promise { + process.stdout.write(`${JSON.stringify(output, null, 2)}\n`) +} + +export function registrationToYAML(registration: AppserviceRegistration): string { + return yamlValue({ + id: registration.id, + url: registration.url ?? '', + as_token: registration.as_token, + hs_token: registration.hs_token, + sender_localpart: registration.sender_localpart, + rate_limited: registration.rate_limited, + namespaces: registration.namespaces ?? {}, + protocols: registration.protocols, + 'de.sorunome.msc2409.push_ephemeral': registration['de.sorunome.msc2409.push_ephemeral'], + receive_ephemeral: registration.receive_ephemeral, + 'org.matrix.msc3202': registration['org.matrix.msc3202'], + 'io.element.msc4190': registration['io.element.msc4190'], + }) +} + +export function parseRegistration(data: string): AppserviceRegistration { + const trimmed = data.trim() + if (trimmed.startsWith('{')) return JSON.parse(trimmed) as AppserviceRegistration + const reg: Record = {} + const lines = data.split(/\r?\n/) + let section: string | undefined + let namespace: string | undefined + for (const line of lines) { + if (!line.trim() || line.trimStart().startsWith('#')) continue + const indent = line.match(/^ */)?.[0].length ?? 0 + const trimmedLine = line.trim() + if (indent === 0) { + const [key, ...rest] = trimmedLine.split(':') + section = key + namespace = undefined + if (rest.join(':').trim()) reg[key!] = parseScalar(rest.join(':').trim()) + else if (key === 'namespaces') reg.namespaces = {} + } else if (section === 'namespaces' && indent === 2) { + namespace = trimmedLine.slice(0, -1) + ;((reg.namespaces as Record)[namespace] = []) + } else if (section === 'namespaces' && namespace && trimmedLine.startsWith('- ')) { + const keyValue = trimmedLine.slice(2) + const [key, ...rest] = keyValue.split(':') + ;((reg.namespaces as Record)[namespace] ??= []).push({ [key!]: parseScalar(rest.join(':').trim()) }) + } else if (section === 'namespaces' && namespace && indent >= 6) { + const list = (reg.namespaces as Record>>)[namespace] + const item = list?.at(-1) + const [key, ...rest] = trimmedLine.split(':') + if (item && key) item[key] = parseScalar(rest.join(':').trim()) + } + } + return reg as AppserviceRegistration +} + +export function bridgeDataDir(envName: string): string { + const dataHome = process.env.BBCTL_DATA_HOME + || (platform() === 'win32' || platform() === 'darwin' ? userConfigDir() : process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share')) + return join(dataHome, 'bbctl', envName) +} + +export function getBridgeWebsocketProxyConfig(catalog: BridgeCatalog, bridgeName: string, bridgeType: string): { listenAddr: string; listenPort: number; url: string } { + const suffix = catalog.bridgeIPSuffix[bridgeType] || '1' + const listenAddr = platform() === 'darwin' ? '127.0.0.1' : `127.29.3.${suffix}` + const listenPort = 30_000 + (crc32(Buffer.from(bridgeName)) % 30_000) + return { listenAddr, listenPort, url: `http://${listenAddr}:${listenPort}` } +} + +export async function updateGoBridge(binaryPath: string, bridgeType: string, noUpdate: boolean): Promise { + await mkdir(dirname(binaryPath), { recursive: true, mode: 0o700 }) + let currentCommit = '' + if (await exists(binaryPath)) { + try { + const version = JSON.parse(await runCapture(binaryPath, ['--version-json'], dirname(binaryPath))) + currentCommit = version.Commit ?? version.commit ?? '' + } catch (error) { + process.stderr.write(`Failed to get current bridge version: ${(error as Error).message} - reinstalling\n`) + } + } + await downloadMautrixBridgeBinary(bridgeType, binaryPath, noUpdate, currentCommit) +} + +export async function compileGoBridge(buildDir: string, binaryPath: string, bridgeType: string, noUpdate: boolean): Promise { + await mkdir(dirname(buildDir), { recursive: true, mode: 0o700 }) + if (!await exists(buildDir)) { + const repo = bridgeType === 'imessagego' ? 'https://github.com/beeper/imessage.git' : `https://github.com/mautrix/${bridgeType}.git` + process.stderr.write(`Cloning ${repo} to ${buildDir}\n`) + await runCommand('git', ['clone', repo, buildDir], dirname(buildDir)) + } else { + if (await exists(binaryPath)) { + try { + await runCapture(binaryPath, ['--version-json'], buildDir) + if (noUpdate) { + process.stderr.write('Not updating bridge because --no-update was specified\n') + return + } + } catch (error) { + process.stderr.write(`Failed to get current bridge version: ${(error as Error).message} - reinstalling\n`) + } + } + process.stderr.write(`Pulling ${buildDir}\n`) + await runCommand('git', ['pull'], buildDir) + } + process.stderr.write('Compiling bridge with ./build.sh\n') + await runCommand('./build.sh', [], buildDir) +} + +export async function setupPythonVenv(bridgeDir: string, bridgeType: string, localDev: boolean): Promise { + let installPackage: string + let localRequirements = ['-r', 'requirements.txt'] + if (bridgeType === 'heisenbridge') installPackage = 'heisenbridge' + else if (bridgeType === 'googlechat') { + installPackage = 'mautrix-googlechat[all]' + localRequirements = [...localRequirements, '-r', 'optional-requirements.txt'] + } else { + throw new Error(`unknown python bridge type ${bridgeType}`) + } + const venvPath = join(bridgeDir, localDev ? '.venv' : 'venv') + const venvArgs = ['-m', 'venv', ...(process.env.SYSTEM_SITE_PACKAGES === 'true' ? ['--system-site-packages'] : []), venvPath] + process.stderr.write(`Creating Python virtualenv at ${venvPath}\n`) + await runCommand('python3', venvArgs, bridgeDir) + const packages = localDev ? localRequirements : [installPackage] + process.stderr.write(`Installing ${packages.join(' ')} into virtualenv\n`) + await runCommand(join(venvPath, 'bin', 'pip3'), ['install', '--upgrade', ...packages], bridgeDir) + return venvPath +} + +export async function runCommand(command: string, args: string[], cwd: string): Promise { + const child = spawn(command, args, { cwd, stdio: 'inherit' }) + const [code, signal] = await once(child, 'exit') as [number | null, NodeJS.Signals | null] + if (code !== 0) throw new Error(`${command} exited with ${signal ?? code}`) +} + +async function downloadMautrixBridgeBinary(bridge: string, binaryPath: string, noUpdate: boolean, currentCommit: string): Promise { + const repo = `mautrix/${bridge}` + const ref = bridge === 'imessage' ? 'master' : 'main' + const job = getJobFromBridge(bridge) + const fileName = basename(binaryPath) + process.stderr.write(`${currentCommit ? 'Checking for updates to' : 'Finding latest version of'} ${fileName} from mau.dev\n`) + const build = await getLastBuild('mau.dev', repo, ref, job) + if (build.commit === currentCommit) { + process.stderr.write(`${fileName} is up to date (${currentCommit.slice(0, 8)})\n`) + return + } + if (currentCommit && noUpdate) { + process.stderr.write(`${fileName} is out of date, latest commit is ${build.commit.slice(0, 8)}\n`) + return + } + const artifactURL = `https://mau.dev${build.jobURL}/artifacts/raw/${fileName}` + await downloadFile(artifactURL, binaryPath) + if (platform() === 'darwin' && needsLibolmDylib(bridge)) { + const libolmPath = join(dirname(binaryPath), 'libolm.3.dylib') + if (!await exists(libolmPath)) await downloadFile(`https://mau.dev${build.jobURL}/artifacts/raw/libolm.3.dylib`, libolmPath) + } + process.stderr.write(`Successfully installed ${fileName} commit ${build.commit.slice(0, 8)}\n`) +} + +async function getLastBuild(domain: string, repo: string, ref: string, job: string): Promise<{ commit: string; jobURL: string }> { + const query = `query($repo: ID!, $ref: String!, $job: String!) { + project(fullPath: $repo) { + pipelines(status: SUCCESS, ref: $ref, first: 1) { + nodes { sha job(name: $job) { webPath } } + } + } +}` + const response = await fetch(`https://${domain}/api/graphql`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'User-Agent': 'beeper-cli bridge-manager-ts' }, + body: JSON.stringify({ query, variables: { repo, ref, job } }), + }) + if (!response.ok) throw new Error(`GitLab GraphQL returned HTTP ${response.status}: ${await response.text()}`) + const json = await response.json() as { data?: { project?: { pipelines?: { nodes?: Array<{ sha: string; job?: { webPath?: string } }> } } } } + const node = json.data?.project?.pipelines?.nodes?.[0] + if (!node?.sha || !node.job?.webPath) throw new Error('did not get pipeline info in response') + return { commit: node.sha, jobURL: node.job.webPath } +} + +function getJobFromBridge(bridge: string): string { + const osAndArch = `${platform()}/${process.arch}` + if (osAndArch === 'linux/x64') return 'build amd64' + if (osAndArch === 'linux/arm64') return 'build arm64' + if (osAndArch === 'linux/arm') { + if (bridge === 'signal') throw new Error('mautrix-signal binaries for 32-bit arm are not built in the CI') + return 'build arm' + } + if (osAndArch === 'darwin/arm64') return bridge === 'imessage' ? 'build universal' : 'build macos arm64' + if (bridge === 'imessage') return 'build universal' + throw new Error(`binaries for ${osAndArch} are not built in the CI`) +} + +async function downloadFile(url: string, path: string): Promise { + await mkdir(dirname(path), { recursive: true, mode: 0o700 }) + const response = await fetch(url) + if (!response.ok || !response.body) throw new Error(`failed to download ${url}: HTTP ${response.status}`) + const tmp = join(dirname(path), `tmp-${basename(path)}-${Date.now()}`) + await pipeline(Readable.fromWeb(response.body as unknown as import('node:stream/web').ReadableStream), createWriteStream(tmp)) + await chmod(tmp, 0o755) + await rm(path, { force: true }) + await import('node:fs/promises').then(fs => fs.rename(tmp, path)) +} + +function needsLibolmDylib(bridge: string): boolean { + return ['imessage', 'whatsapp', 'discord', 'slack', 'gmessages', 'gvoice', 'signal', 'imessagego', 'meta', 'twitter', 'bluesky', 'linkedin', 'telegram'].includes(bridge) +} + +async function beeperAPI(env: BridgeEnv, method: string, path: string, body?: unknown): Promise { + return requestJSON(`https://api.${env.domain}${path}`, method, env.accessToken, body) +} + +async function beeperPublicAPI(domain: string, method: string, path: string, body?: unknown): Promise { + return requestJSON(`https://api.${domain}${path}`, method, 'BEEPER-PRIVATE-API-PLEASE-DONT-USE', body) as Promise +} + +async function hungryAPI(env: BridgeEnv, username: string, method: string, path: string, body?: unknown): Promise { + return requestJSON(`${hungryURL(env.domain, username)}${path}`, method, env.accessToken, body) as Promise +} + +async function postBridgeState(env: BridgeEnv, username: string, bridge: string, asToken: string, body: Record): Promise { + await requestJSON(`https://api.${env.domain}/bridgebox/${encodeURIComponent(username)}/bridge/${encodeURIComponent(bridge)}/bridge_state`, 'POST', asToken, body) +} + +async function matrixLogin(domain: string, body: Record): Promise<{ access_token: string; user_id: string }> { + return requestJSON(`https://matrix.${domain}/_matrix/client/v3/login`, 'POST', '', body) as Promise<{ access_token: string; user_id: string }> +} + +async function matrixAPI(domain: string, token: string, method: string, path: string, body?: unknown): Promise { + return requestJSON(`https://matrix.${domain}${path}`, method, token, body) +} + +async function requestJSON(url: string, method: string, token: string, body?: unknown): Promise { + const response = await fetch(url, { + method, + headers: { + ...(token ? { Authorization: `Bearer ${token}` } : {}), + 'Content-Type': 'application/json', + 'User-Agent': 'beeper-cli bridge-manager-ts', + }, + body: body === undefined ? undefined : JSON.stringify(body), + }) + if (!response.ok) { + const text = await response.text() + let message = text + try { + const parsed = JSON.parse(text) as { error?: string; errcode?: string } + message = parsed.error || parsed.errcode || text + } catch {} + throw new Error(`server returned error (HTTP ${response.status}): ${message}`) + } + if (response.status === 204) return undefined + return response.json() +} + +function resolveDomain(value: string): string { + const envs: Record = { + prod: 'beeper.com', + production: 'beeper.com', + staging: 'beeper-staging.com', + dev: 'beeper-dev.com', + local: 'beeper.localtest.me', + } + return envs[value] ?? value +} + +async function guessOrAskBridgeType(catalog: BridgeCatalog, bridge: string, bridgeType?: string): Promise { + if (!bridgeType) { + outer: + for (const item of catalog.officialBridges) { + for (const name of item.names) { + if (bridge.includes(name)) { + bridgeType = item.typeName + break outer + } + } + } + } + if (bridgeType && isSupported(catalog, bridgeType)) return bridgeType + process.stderr.write(`Unsupported bridge type ${bridgeType ?? ''}\n`) + if (isNoInput() || !process.stdin.isTTY) throw new Error('Pass --type with one of: ' + catalog.supportedBridges.join(', ')) + return promptSelect('Select bridge type:', catalog.supportedBridges) +} + +async function applyBridgeParamDefaults(bridgeName: string, bridgeType: string, params: Record): Promise { + if (bridgeType === 'telegram') { + params.api_id ??= '26417019' + params.api_hash ??= decryptTelegramAPIHash() + } else if (bridgeType === 'meta') { + let metaPlatform = params.meta_platform + if (!metaPlatform) { + if (bridgeName.includes('facebook-tor') || bridgeName.includes('facebooktor')) metaPlatform = 'facebook-tor' + else if (bridgeName.includes('facebook')) metaPlatform = 'facebook' + else if (bridgeName.includes('messenger')) metaPlatform = 'messenger' + else if (bridgeName.includes('instagram')) metaPlatform = 'instagram' + else metaPlatform = '' + params.meta_platform = metaPlatform + } + if (metaPlatform && !['instagram', 'facebook', 'facebook-tor', 'messenger', 'messenger-lite'].includes(metaPlatform)) { + throw new Error('Invalid Meta platform specified') + } + if (metaPlatform === 'facebook-tor' && !params.proxy) params.proxy = await promptInput('Enter Tor proxy address', 'socks5://localhost:1080') + } else if (bridgeType === 'imessagego') { + if (!params.nac_token) params.nac_token = await promptInput('Enter iMessage registration code') + } else if (bridgeType === 'imessage') { + await applyIMessageParams(params) + } +} + +async function applyIMessageParams(params: Record): Promise { + if (platform() !== 'darwin' && !params.imessage_platform) params.imessage_platform = 'bluebubbles' + if (!params.imessage_platform) { + params.imessage_platform = await promptSelect('Select iMessage connector:', ['mac', 'mac-nosip', 'bluebubbles']) + } + if (params.imessage_platform === 'mac-nosip' && !params.barcelona_path) { + params.barcelona_path = await promptInput('Enter Barcelona executable path:', 'darwin-barcelona-mautrix') + } + if (params.imessage_platform === 'bluebubbles') { + if (!params.bluebubbles_url) params.bluebubbles_url = await promptInput('Enter BlueBubbles API address:') + if (!params.bluebubbles_password) params.bluebubbles_password = await promptInput('Enter BlueBubbles password:') + } +} + +function parseParams(values: string[] | undefined): Record { + const params: Record = {} + for (const item of values ?? []) { + const index = item.indexOf('=') + if (index <= 0) throw new Error(`Invalid param ${item}`) + params[item.slice(0, index).toLowerCase()] = item.slice(index + 1) + } + return params +} + +async function promptInput(message: string, defaultValue = ''): Promise { + if (isNoInput() || !process.stdin.isTTY) { + if (defaultValue) return defaultValue + throw new Error(`${message} is required. Pass it with --param.`) + } + const rl = createInterface({ input, output }) + try { + const suffix = defaultValue ? ` (${defaultValue})` : '' + return (await rl.question(`${message}${suffix}: `)).trim() || defaultValue + } finally { + rl.close() + } +} + +async function promptSelect(message: string, options: string[]): Promise { + const rl = createInterface({ input, output }) + try { + process.stdout.write(`${message}\n`) + options.forEach((option, index) => process.stdout.write(` ${index + 1}. ${option}\n`)) + for (;;) { + const answer = (await rl.question('Select: ')).trim() + const selected = Number.parseInt(answer, 10) + if (Number.isInteger(selected) && selected >= 1 && selected <= options.length) return options[selected - 1]! + if (options.includes(answer)) return answer + process.stdout.write('Choose one of the listed options.\n') + } + } finally { + rl.close() + } +} + +function decryptTelegramAPIHash(): string { + const key = Buffer.from('qDP2pQ1LogRjxUYrFUDjDw', 'base64url') + const data = Buffer.from('B9VMuZeZlFk0pkbLcfSDDQ', 'base64url') + const decipher = createDecipheriv('aes-128-ecb', key, null) + decipher.setAutoPadding(false) + return Buffer.concat([decipher.update(data), decipher.final()]).toString('hex') +} + +function localpart(userID: string): string { + return userID.startsWith('@') ? userID.slice(1).split(':')[0] ?? userID : userID +} + +export function hungryURL(domain: string, username: string): string { + return `https://matrix.${domain}/_hungryserv/${encodeURIComponent(username)}` +} + +function yamlValue(value: unknown, indent = 0): string { + if (Array.isArray(value)) { + return value.map(item => `${' '.repeat(indent)}- ${yamlInline(item, indent + 2)}`).join('') + } + if (!value || typeof value !== 'object') return `${yamlScalar(value)}\n` + let out = '' + for (const [key, child] of Object.entries(value as Record)) { + if (child === undefined) continue + if (child && typeof child === 'object') out += `${' '.repeat(indent)}${key}:\n${yamlValue(child, indent + 2)}` + else out += `${' '.repeat(indent)}${key}: ${yamlScalar(child)}\n` + } + return out +} + +function yamlInline(value: unknown, indent: number): string { + if (!value || typeof value !== 'object' || Array.isArray(value)) return yamlScalar(value) + '\n' + const entries = Object.entries(value as Record) + const [first, ...rest] = entries + if (!first) return '{}\n' + return `${first[0]}: ${yamlScalar(first[1])}\n${rest.map(([key, child]) => `${' '.repeat(indent)}${key}: ${yamlScalar(child)}\n`).join('')}` +} + +function yamlScalar(value: unknown): string { + if (typeof value === 'boolean') return value ? 'true' : 'false' + if (typeof value === 'number') return String(value) + if (value === null || value === undefined) return '' + const text = String(value) + if (!text || /[:\n{}[\],&*#?|\-<>=!%@`]/.test(text) || /^\s|\s$/.test(text)) return JSON.stringify(text) + return text +} + +function parseScalar(value: string): unknown { + if (value === 'true') return true + if (value === 'false') return false + if (/^-?\d+$/.test(value)) return Number(value) + if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) return value.slice(1, -1) + return value +} + +async function runCapture(command: string, args: string[], cwd: string): Promise { + const child = spawn(command, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] }) + let stdout = '' + let stderr = '' + child.stdout.on('data', chunk => stdout += String(chunk)) + child.stderr.on('data', chunk => stderr += String(chunk)) + const [code, signal] = await once(child, 'exit') as [number | null, NodeJS.Signals | null] + if (code !== 0) throw new Error(stderr.trim() || `${command} exited with ${signal ?? code}`) + return stdout +} + +async function exists(path: string): Promise { + try { + await access(path, fsConstants.F_OK) + return true + } catch { + return false + } +} + +function userConfigDir(): string { + if (platform() === 'darwin') return join(homedir(), 'Library', 'Application Support') + if (platform() === 'win32') return process.env.APPDATA || join(homedir(), 'AppData', 'Roaming') + return process.env.XDG_CONFIG_HOME || join(homedir(), '.config') +} + +const crcTable = new Uint32Array(256).map((_, index) => { + let c = index + for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1 + return c >>> 0 +}) + +function crc32(buffer: Buffer): number { + let crc = 0xffffffff + for (const byte of buffer) crc = crcTable[(crc ^ byte) & 0xff]! ^ (crc >>> 8) + return (crc ^ 0xffffffff) >>> 0 +} diff --git a/packages/cli/src/lib/bridges/websocket-proxy.ts b/packages/cli/src/lib/bridges/websocket-proxy.ts new file mode 100644 index 00000000..f676a0cd --- /dev/null +++ b/packages/cli/src/lib/bridges/websocket-proxy.ts @@ -0,0 +1,152 @@ +import { readFile } from 'node:fs/promises' +import { setTimeout as delay } from 'node:timers/promises' +import WebSocket from 'ws' +import { parseRegistration, type AppserviceRegistration } from './manager.js' + +type WebsocketMessage = { + command?: string + data?: unknown + id?: number + status?: string + txn_id?: string + [key: string]: unknown +} + +const defaultReconnectBackoff = 2_000 +const maxReconnectBackoff = 120_000 +const reconnectBackoffReset = 300_000 + +export async function proxyAppserviceWebsocket(options: { homeserverURL: string; registrationPath: string }): Promise { + const registration = parseRegistration(await readFile(options.registrationPath, 'utf8')) + if (!registration.url || registration.url === 'websocket') { + throw new Error('You must change the `url` field in the registration file to point at the local appservice HTTP server (e.g. `http://localhost:8080`)') + } + if (!registration.url.startsWith('http://') && !registration.url.startsWith('https://')) { + throw new Error('`url` field in registration must start with http:// or https://') + } + const controller = new AbortController() + process.once('SIGINT', () => controller.abort()) + process.once('SIGTERM', () => controller.abort()) + await runProxyLoop(controller.signal, options.homeserverURL, registration) +} + +export async function runProxyLoop(signal: AbortSignal, homeserverURL: string, registration: AppserviceRegistration): Promise { + let reconnectBackoff = defaultReconnectBackoff + let lastDisconnect = Date.now() + while (!signal.aborted) { + try { + await runSingleProxy(signal, homeserverURL, registration) + return + } catch (error) { + if (signal.aborted) return + if (String((error as Error).message).includes('conn_replaced')) return + process.stderr.write(`Error in appservice websocket: ${(error as Error).message}\n`) + } + const now = Date.now() + reconnectBackoff = lastDisconnect + reconnectBackoffReset < now ? defaultReconnectBackoff : Math.min(maxReconnectBackoff, reconnectBackoff * 2) + lastDisconnect = now + process.stderr.write(`Websocket disconnected, reconnecting in ${Math.round(reconnectBackoff / 1000)}s\n`) + await delay(reconnectBackoff, undefined, { signal }).catch(() => undefined) + } +} + +async function runSingleProxy(signal: AbortSignal, homeserverURL: string, registration: AppserviceRegistration): Promise { + const wsURL = new URL('/_matrix/client/unstable/fi.mau.as_sync', homeserverURL) + wsURL.protocol = wsURL.protocol === 'http:' ? 'ws:' : 'wss:' + const ws = new WebSocket(wsURL, { + headers: { + Authorization: `Bearer ${registration.as_token}`, + 'User-Agent': 'beeper-cli bridge-manager-ts', + 'X-Mautrix-Process-ID': String(process.pid), + 'X-Mautrix-Websocket-Version': '3', + }, + }) + signal.addEventListener('abort', () => ws.close()) + await new Promise((resolve, reject) => { + ws.once('open', () => { + send(ws, { command: 'bridge_status', data: { stateEvent: 'UNCONFIGURED' } }) + keepalive(signal, ws).catch(error => process.stderr.write(`Websocket ping returned error: ${(error as Error).message}\n`)) + resolve() + }) + ws.once('error', reject) + }) + await new Promise((resolve, reject) => { + ws.on('message', data => { + handleMessage(ws, registration, JSON.parse(String(data))).catch(error => { + process.stderr.write(`Failed to handle websocket message: ${(error as Error).message}\n`) + }) + }) + ws.once('close', (code, reason) => { + if (code === 4001 || String(reason).includes('conn_replaced')) reject(new Error('conn_replaced')) + else resolve() + }) + ws.once('error', reject) + }) +} + +async function handleMessage(ws: WebSocket, registration: AppserviceRegistration, msg: WebsocketMessage): Promise { + if (!msg.command || msg.command === 'transaction') { + const txnID = String(msg.txn_id ?? '') + const url = new URL(`/_matrix/app/v1/transactions/${encodeURIComponent(txnID)}`, registration.url) + const response = await fetch(url, { + method: 'PUT', + headers: { Authorization: `Bearer ${registration.hs_token}`, 'Content-Type': 'application/json' }, + body: JSON.stringify(msg), + }) + if (!response.ok) throw new Error(`transaction proxy returned HTTP ${response.status}: ${await response.text()}`) + sendResponse(ws, msg, true, { txn_id: txnID }) + } else if (msg.command === 'http_proxy') { + const req = msg.data as { body?: unknown; headers?: Record; path?: string; query?: string } + const url = new URL(req.path || '/', registration.url) + url.search = req.query || '' + const response = await fetch(url, { + method: 'PUT', + headers: req.headers as HeadersInit, + body: bodyFromProxyRequest(req.body), + }) + const bytes = Buffer.from(await response.arrayBuffer()) + const text = bytes.toString('utf8') + const body = isJSON(text) ? JSON.parse(text) : bytes.toString('base64url') + const headers: Record = {} + response.headers.forEach((value, key) => { + headers[key] = value + }) + sendResponse(ws, msg, true, { status: response.status, headers, body }) + } else if (msg.command === 'connect' || msg.command === 'response' || msg.command === 'error') { + return + } else { + sendResponse(ws, msg, false, { message: 'unknown request type' }) + } +} + +async function keepalive(signal: AbortSignal, ws: WebSocket): Promise { + while (!signal.aborted && ws.readyState === WebSocket.OPEN) { + await delay(180_000, undefined, { signal }).catch(() => undefined) + if (signal.aborted || ws.readyState !== WebSocket.OPEN) return + send(ws, { command: 'ping', data: { timestamp: Date.now() } }) + } +} + +function sendResponse(ws: WebSocket, msg: WebsocketMessage, ok: boolean, data: unknown): void { + if (!msg.id || msg.command === 'response' || msg.command === 'error') return + send(ws, { id: msg.id, command: ok ? 'response' : 'error', data }) +} + +function send(ws: WebSocket, msg: unknown): void { + if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg)) +} + +function bodyFromProxyRequest(body: unknown): BodyInit | undefined { + if (body === undefined || body === null) return undefined + if (typeof body === 'string') return body + return JSON.stringify(body) +} + +function isJSON(text: string): boolean { + try { + JSON.parse(text) + return true + } catch { + return false + } +} diff --git a/packages/cli/src/types/ws.d.ts b/packages/cli/src/types/ws.d.ts new file mode 100644 index 00000000..4d307d4d --- /dev/null +++ b/packages/cli/src/types/ws.d.ts @@ -0,0 +1,23 @@ +declare module 'ws' { + import type { IncomingHttpHeaders } from 'node:http' + + class WebSocket { + static OPEN: number + constructor(url: string | URL, options?: { headers?: Record }) + readyState: number + close(code?: number, reason?: string): void + kill?: () => void + addEventListener(event: 'open', listener: () => void): void + addEventListener(event: 'message', listener: (event: { data: Buffer | string }) => void): void + addEventListener(event: 'error', listener: (event: { error?: Error }) => void): void + addEventListener(event: 'close', listener: (event: { code: number; reason: string }) => void): void + once(event: 'open', listener: () => void): this + once(event: 'error', listener: (error: Error) => void): this + once(event: 'close', listener: (code: number, reason: Buffer) => void): this + on(event: 'message', listener: (data: Buffer | string) => void): this + send(data: string): void + } + + export { WebSocket } + export default WebSocket +} From 044405c52d0d7775ec7cad8899f1eb3632d777de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Wed, 3 Jun 2026 13:39:44 +0200 Subject: [PATCH 24/26] Simplify CLI setup and packaging --- .changeset/README.md | 7 - .changeset/config.json | 11 - .github/workflows/ci.yml | 36 +- .github/workflows/publish-release.yml | 17 +- .github/workflows/release-doctor.yml | 19 - README.md | 331 +- bun.lock | 1375 +------- docs/.gitignore | 16 - docs/astro.config.mjs | 100 - docs/bun.lock | 871 ----- docs/package.json | 19 - docs/public/favicon.svg | 4 - docs/src/assets/logo.svg | 4 - docs/src/content.config.ts | 7 - docs/src/content/docs/accounts.mdx | 64 - docs/src/content/docs/api.mdx | 47 - docs/src/content/docs/auth.mdx | 66 - docs/src/content/docs/chats.mdx | 71 - docs/src/content/docs/config.mdx | 55 - docs/src/content/docs/connect.mdx | 146 - docs/src/content/docs/contacts.mdx | 37 - docs/src/content/docs/exit-codes.mdx | 25 - docs/src/content/docs/export.mdx | 53 - docs/src/content/docs/index.mdx | 88 - docs/src/content/docs/install.mdx | 76 - docs/src/content/docs/media.mdx | 36 - docs/src/content/docs/messages.mdx | 64 - docs/src/content/docs/plugins.mdx | 45 - docs/src/content/docs/presence.mdx | 34 - docs/src/content/docs/quickstart.mdx | 89 - docs/src/content/docs/rpc.mdx | 58 - docs/src/content/docs/scripting.mdx | 85 - docs/src/content/docs/send.mdx | 62 - docs/src/content/docs/targets.mdx | 62 - docs/src/content/docs/update.mdx | 39 - docs/src/content/docs/watch.mdx | 66 - docs/src/styles/theme.css | 37 - docs/tsconfig.json | 5 - eslint.config.mjs | 35 - package.json | 24 +- packages/cli-plugin-cloudflare/LICENSE | 21 - packages/cli-plugin-cloudflare/README.md | 47 - packages/cli-plugin-cloudflare/package.json | 43 - .../src/commands/targets/tunnel.ts | 88 - packages/cli-plugin-cloudflare/src/index.ts | 1 - packages/cli-plugin-cloudflare/test/smoke.mjs | 25 - packages/cli-plugin-cloudflare/tsconfig.json | 17 - packages/cli/CHANGELOG.md | 125 - packages/cli/README.md | 3011 +---------------- packages/cli/SECURITY.md | 27 - packages/cli/beeper-setup-redesign-spec.md | 475 --- packages/cli/bin/binary-bootstrap.js | 47 - packages/cli/bin/check-release-environment | 33 - packages/cli/bin/cli.js | 11 +- packages/cli/bin/dev.js | 11 +- packages/cli/bin/logo.js | 97 - packages/cli/bin/run.js | 145 - packages/cli/links.txt | 89 - packages/cli/package.json | 123 +- packages/cli/release-please-config.json | 67 - packages/cli/safety-profiles/agent-safe.yaml | 23 + packages/cli/safety-profiles/full.yaml | 5 + packages/cli/safety-profiles/readonly.yaml | 19 + packages/cli/scripts/bootstrap | 25 - packages/cli/scripts/build | 8 - packages/cli/scripts/build-binaries.ts | 90 - .../cli/scripts/build-homebrew-archive.ts | 90 - packages/cli/scripts/check-api-copy.ts | 80 - packages/cli/scripts/check-manifest.ts | 82 - packages/cli/scripts/format | 8 - packages/cli/scripts/generate-command-map.ts | 71 - packages/cli/scripts/generate-readme.ts | 519 --- packages/cli/scripts/link | 8 - packages/cli/scripts/lint | 8 - packages/cli/scripts/mock | 52 - .../cli/scripts/publish-homebrew-formula.ts | 126 - packages/cli/scripts/publish-local-release.ts | 65 - packages/cli/scripts/read-signing-secrets.rb | 30 - packages/cli/scripts/run | 7 - packages/cli/scripts/sign-macos-binaries.ts | 275 -- .../cli/scripts/sync-bridge-manager-config.ts | 78 - packages/cli/scripts/test | 56 - packages/cli/scripts/unlink | 8 - packages/cli/src/cli/commands.ts | 1993 +++++++++++ packages/cli/src/cli/main.ts | 33 + packages/cli/src/cli/mcp.ts | 122 + packages/cli/src/cli/output.ts | 155 + packages/cli/src/cli/parse.ts | 183 + packages/cli/src/cli/policy.ts | 53 + packages/cli/src/cli/schema.ts | 102 + packages/cli/src/cli/setup.ts | 639 ++++ packages/cli/src/cli/types.ts | 49 + packages/cli/src/commands.generated.ts | 251 -- packages/cli/src/commands/_complete.ts | 101 - packages/cli/src/commands/accounts/add.ts | 221 -- packages/cli/src/commands/accounts/list.ts | 30 - packages/cli/src/commands/accounts/remove.ts | 27 - packages/cli/src/commands/accounts/show.ts | 19 - packages/cli/src/commands/accounts/use.ts | 35 - packages/cli/src/commands/api/get.ts | 27 - packages/cli/src/commands/api/post.ts | 39 - packages/cli/src/commands/api/request.ts | 34 - .../cli/src/commands/auth/email/response.ts | 34 - packages/cli/src/commands/auth/email/start.ts | 19 - packages/cli/src/commands/auth/logout.ts | 34 - packages/cli/src/commands/auth/status.ts | 23 - packages/cli/src/commands/autocomplete.ts | 12 - packages/cli/src/commands/bridges/config.ts | 50 - packages/cli/src/commands/bridges/delete.ts | 68 - packages/cli/src/commands/bridges/list.ts | 33 - .../src/commands/bridges/login-password.ts | 37 - packages/cli/src/commands/bridges/login.ts | 36 - packages/cli/src/commands/bridges/logout.ts | 20 - packages/cli/src/commands/bridges/proxy.ts | 23 - packages/cli/src/commands/bridges/register.ts | 40 - packages/cli/src/commands/bridges/run.ts | 179 - packages/cli/src/commands/bridges/show.ts | 37 - packages/cli/src/commands/bridges/whoami.ts | 78 - packages/cli/src/commands/chats/archive.ts | 22 - packages/cli/src/commands/chats/avatar.ts | 22 - .../cli/src/commands/chats/description.ts | 22 - packages/cli/src/commands/chats/disappear.ts | 31 - packages/cli/src/commands/chats/draft.ts | 41 - packages/cli/src/commands/chats/focus.ts | 28 - packages/cli/src/commands/chats/list.ts | 54 - packages/cli/src/commands/chats/mark-read.ts | 22 - .../cli/src/commands/chats/mark-unread.ts | 22 - packages/cli/src/commands/chats/mute.ts | 25 - .../cli/src/commands/chats/notify-anyway.ts | 22 - packages/cli/src/commands/chats/pin.ts | 22 - packages/cli/src/commands/chats/priority.ts | 28 - packages/cli/src/commands/chats/remind.ts | 27 - packages/cli/src/commands/chats/rename.ts | 25 - packages/cli/src/commands/chats/search.ts | 23 - packages/cli/src/commands/chats/show.ts | 16 - packages/cli/src/commands/chats/start.ts | 36 - packages/cli/src/commands/chats/unarchive.ts | 22 - packages/cli/src/commands/chats/unmute.ts | 22 - packages/cli/src/commands/chats/unpin.ts | 22 - packages/cli/src/commands/chats/unremind.ts | 23 - packages/cli/src/commands/completion.ts | 84 - packages/cli/src/commands/config/get.ts | 26 - packages/cli/src/commands/config/path.ts | 17 - packages/cli/src/commands/config/reset.ts | 19 - packages/cli/src/commands/config/set.ts | 29 - packages/cli/src/commands/contacts/list.ts | 55 - packages/cli/src/commands/contacts/search.ts | 50 - packages/cli/src/commands/contacts/show.ts | 26 - packages/cli/src/commands/docs.ts | 9 - packages/cli/src/commands/doctor.ts | 21 - packages/cli/src/commands/export.ts | 65 - packages/cli/src/commands/install/desktop.ts | 26 - packages/cli/src/commands/install/server.ts | 29 - packages/cli/src/commands/man.ts | 20 - packages/cli/src/commands/media/download.ts | 34 - packages/cli/src/commands/messages/context.ts | 24 - packages/cli/src/commands/messages/delete.ts | 27 - packages/cli/src/commands/messages/edit.ts | 26 - packages/cli/src/commands/messages/export.ts | 72 - packages/cli/src/commands/messages/list.ts | 49 - packages/cli/src/commands/messages/search.ts | 82 - packages/cli/src/commands/messages/show.ts | 21 - packages/cli/src/commands/plugins.ts | 16 - .../cli/src/commands/plugins/available.ts | 24 - packages/cli/src/commands/presence.ts | 43 - packages/cli/src/commands/resolve/account.ts | 47 - packages/cli/src/commands/resolve/bridge.ts | 50 - packages/cli/src/commands/resolve/chat.ts | 59 - packages/cli/src/commands/resolve/contact.ts | 63 - packages/cli/src/commands/resolve/target.ts | 52 - packages/cli/src/commands/rpc.ts | 54 - packages/cli/src/commands/schema.ts | 141 - packages/cli/src/commands/send/file.ts | 40 - packages/cli/src/commands/send/react.ts | 31 - packages/cli/src/commands/send/sticker.ts | 46 - packages/cli/src/commands/send/text.ts | 39 - packages/cli/src/commands/send/unreact.ts | 32 - packages/cli/src/commands/send/voice.ts | 50 - packages/cli/src/commands/setup.ts | 781 ----- packages/cli/src/commands/status.ts | 13 - .../cli/src/commands/targets/add/desktop.ts | 28 - .../cli/src/commands/targets/add/remote.ts | 28 - .../cli/src/commands/targets/add/server.ts | 28 - packages/cli/src/commands/targets/disable.ts | 23 - packages/cli/src/commands/targets/enable.ts | 23 - packages/cli/src/commands/targets/list.ts | 15 - packages/cli/src/commands/targets/logs.ts | 60 - packages/cli/src/commands/targets/remove.ts | 19 - packages/cli/src/commands/targets/restart.ts | 24 - packages/cli/src/commands/targets/show.ts | 15 - packages/cli/src/commands/targets/start.ts | 36 - packages/cli/src/commands/targets/status.ts | 18 - packages/cli/src/commands/targets/stop.ts | 23 - packages/cli/src/commands/targets/use.ts | 21 - packages/cli/src/commands/update.ts | 141 - packages/cli/src/commands/verify.ts | 19 - packages/cli/src/commands/verify/approve.ts | 23 - packages/cli/src/commands/verify/cancel.ts | 20 - packages/cli/src/commands/verify/list.ts | 11 - .../cli/src/commands/verify/qr-confirm.ts | 20 - packages/cli/src/commands/verify/qr-scan.ts | 21 - .../cli/src/commands/verify/recovery-key.ts | 20 - .../src/commands/verify/reset-recovery-key.ts | 32 - .../cli/src/commands/verify/sas-confirm.ts | 20 - packages/cli/src/commands/verify/sas.ts | 20 - packages/cli/src/commands/verify/show.ts | 10 - packages/cli/src/commands/verify/start.ts | 20 - packages/cli/src/commands/verify/status.ts | 10 - packages/cli/src/commands/version.ts | 14 - packages/cli/src/commands/watch.ts | 177 - packages/cli/src/lib/account-login.ts | 47 +- packages/cli/src/lib/api-values.ts | 11 + packages/cli/src/lib/app-api.ts | 51 +- packages/cli/src/lib/app-state.ts | 63 +- packages/cli/src/lib/argv.ts | 47 - packages/cli/src/lib/bridges/catalog.ts | 85 - packages/cli/src/lib/bridges/command.ts | 9 - packages/cli/src/lib/bridges/generated.ts | 178 - packages/cli/src/lib/bridges/go-template.ts | 183 - packages/cli/src/lib/bridges/manager.ts | 761 ----- .../cli/src/lib/bridges/websocket-proxy.ts | 152 - packages/cli/src/lib/client.ts | 22 - .../src/lib/cloudflare-tunnel.ts} | 143 +- packages/cli/src/lib/command-metadata.ts | 55 - packages/cli/src/lib/command.ts | 173 - packages/cli/src/lib/copy.ts | 66 - packages/cli/src/lib/desktop-auth.ts | 43 +- packages/cli/src/lib/did-you-mean.ts | 66 - packages/cli/src/lib/env.ts | 58 - packages/cli/src/lib/errors.ts | 33 +- .../src/lib/{export/index.ts => export.ts} | 42 +- packages/cli/src/lib/ink/components.tsx | 948 ------ packages/cli/src/lib/ink/format.ts | 121 - packages/cli/src/lib/ink/render.tsx | 338 -- packages/cli/src/lib/ink/spinner.tsx | 122 - packages/cli/src/lib/ink/theme.ts | 106 - packages/cli/src/lib/installations.ts | 85 +- packages/cli/src/lib/local-desktop.ts | 37 +- packages/cli/src/lib/manifest.ts | 612 ---- packages/cli/src/lib/oauth.ts | 39 +- packages/cli/src/lib/output.ts | 292 -- packages/cli/src/lib/paging.ts | 9 + packages/cli/src/lib/pkce.ts | 16 - packages/cli/src/lib/profiles.ts | 189 +- packages/cli/src/lib/prompts.ts | 35 + packages/cli/src/lib/recommended-plugins.ts | 15 - packages/cli/src/lib/resolve.ts | 184 +- packages/cli/src/lib/runner.ts | 39 - packages/cli/src/lib/send-message.ts | 74 - packages/cli/src/lib/server-env.ts | 2 +- packages/cli/src/lib/setup-login.ts | 51 +- packages/cli/src/lib/target-status.ts | 24 +- packages/cli/src/lib/targets.ts | 164 +- packages/cli/src/lib/update-banner.ts | 48 - packages/cli/src/lib/wait.ts | 24 - packages/cli/src/plugin-sdk.ts | 75 - packages/cli/src/types/qrcode.d.ts | 13 - packages/cli/src/types/ws.d.ts | 23 - packages/cli/test/account-login.test.ts | 14 +- packages/cli/test/cli-smoke.ts | 724 ++-- packages/cli/test/cloudflare-tunnel.test.ts | 23 + packages/cli/test/e2e-staging.ts | 345 +- packages/cli/test/e2e-staging/README.md | 68 +- packages/cli/test/errors.test.ts | 24 +- packages/cli/test/fixtures/fake-client.ts | 122 - .../cli/test/messages-list-filter.test.ts | 26 - .../test/messages-search-validation.test.ts | 7 +- packages/cli/test/plugin-sdk.test.ts | 44 - packages/cli/test/resolve.test.ts | 24 + packages/cli/test/watch-filter.test.ts | 31 - packages/cli/tsconfig.json | 2 +- packages/npm/.gitignore | 4 - packages/npm/package.json | 22 - packages/npm/scripts/build.ts | 35 - run.sh | 6 + scripts/publish-packages.ts | 140 - scripts/release.ts | 46 - 277 files changed, 4529 insertions(+), 22417 deletions(-) delete mode 100644 .changeset/README.md delete mode 100644 .changeset/config.json delete mode 100644 .github/workflows/release-doctor.yml delete mode 100644 docs/.gitignore delete mode 100644 docs/astro.config.mjs delete mode 100644 docs/bun.lock delete mode 100644 docs/package.json delete mode 100644 docs/public/favicon.svg delete mode 100644 docs/src/assets/logo.svg delete mode 100644 docs/src/content.config.ts delete mode 100644 docs/src/content/docs/accounts.mdx delete mode 100644 docs/src/content/docs/api.mdx delete mode 100644 docs/src/content/docs/auth.mdx delete mode 100644 docs/src/content/docs/chats.mdx delete mode 100644 docs/src/content/docs/config.mdx delete mode 100644 docs/src/content/docs/connect.mdx delete mode 100644 docs/src/content/docs/contacts.mdx delete mode 100644 docs/src/content/docs/exit-codes.mdx delete mode 100644 docs/src/content/docs/export.mdx delete mode 100644 docs/src/content/docs/index.mdx delete mode 100644 docs/src/content/docs/install.mdx delete mode 100644 docs/src/content/docs/media.mdx delete mode 100644 docs/src/content/docs/messages.mdx delete mode 100644 docs/src/content/docs/plugins.mdx delete mode 100644 docs/src/content/docs/presence.mdx delete mode 100644 docs/src/content/docs/quickstart.mdx delete mode 100644 docs/src/content/docs/rpc.mdx delete mode 100644 docs/src/content/docs/scripting.mdx delete mode 100644 docs/src/content/docs/send.mdx delete mode 100644 docs/src/content/docs/targets.mdx delete mode 100644 docs/src/content/docs/update.mdx delete mode 100644 docs/src/content/docs/watch.mdx delete mode 100644 docs/src/styles/theme.css delete mode 100644 docs/tsconfig.json delete mode 100644 eslint.config.mjs delete mode 100644 packages/cli-plugin-cloudflare/LICENSE delete mode 100644 packages/cli-plugin-cloudflare/README.md delete mode 100644 packages/cli-plugin-cloudflare/package.json delete mode 100644 packages/cli-plugin-cloudflare/src/commands/targets/tunnel.ts delete mode 100644 packages/cli-plugin-cloudflare/src/index.ts delete mode 100644 packages/cli-plugin-cloudflare/test/smoke.mjs delete mode 100644 packages/cli-plugin-cloudflare/tsconfig.json delete mode 100644 packages/cli/CHANGELOG.md delete mode 100644 packages/cli/SECURITY.md delete mode 100644 packages/cli/beeper-setup-redesign-spec.md delete mode 100644 packages/cli/bin/binary-bootstrap.js delete mode 100644 packages/cli/bin/check-release-environment mode change 100644 => 100755 packages/cli/bin/cli.js delete mode 100644 packages/cli/bin/logo.js delete mode 100755 packages/cli/bin/run.js delete mode 100644 packages/cli/links.txt delete mode 100644 packages/cli/release-please-config.json create mode 100644 packages/cli/safety-profiles/agent-safe.yaml create mode 100644 packages/cli/safety-profiles/full.yaml create mode 100644 packages/cli/safety-profiles/readonly.yaml delete mode 100755 packages/cli/scripts/bootstrap delete mode 100755 packages/cli/scripts/build delete mode 100644 packages/cli/scripts/build-binaries.ts delete mode 100644 packages/cli/scripts/build-homebrew-archive.ts delete mode 100644 packages/cli/scripts/check-api-copy.ts delete mode 100644 packages/cli/scripts/check-manifest.ts delete mode 100755 packages/cli/scripts/format delete mode 100644 packages/cli/scripts/generate-command-map.ts delete mode 100644 packages/cli/scripts/generate-readme.ts delete mode 100755 packages/cli/scripts/link delete mode 100755 packages/cli/scripts/lint delete mode 100755 packages/cli/scripts/mock delete mode 100644 packages/cli/scripts/publish-homebrew-formula.ts delete mode 100644 packages/cli/scripts/publish-local-release.ts delete mode 100644 packages/cli/scripts/read-signing-secrets.rb delete mode 100755 packages/cli/scripts/run delete mode 100644 packages/cli/scripts/sign-macos-binaries.ts delete mode 100644 packages/cli/scripts/sync-bridge-manager-config.ts delete mode 100755 packages/cli/scripts/test delete mode 100755 packages/cli/scripts/unlink create mode 100644 packages/cli/src/cli/commands.ts create mode 100644 packages/cli/src/cli/main.ts create mode 100644 packages/cli/src/cli/mcp.ts create mode 100644 packages/cli/src/cli/output.ts create mode 100644 packages/cli/src/cli/parse.ts create mode 100644 packages/cli/src/cli/policy.ts create mode 100644 packages/cli/src/cli/schema.ts create mode 100644 packages/cli/src/cli/setup.ts create mode 100644 packages/cli/src/cli/types.ts delete mode 100644 packages/cli/src/commands.generated.ts delete mode 100644 packages/cli/src/commands/_complete.ts delete mode 100644 packages/cli/src/commands/accounts/add.ts delete mode 100644 packages/cli/src/commands/accounts/list.ts delete mode 100644 packages/cli/src/commands/accounts/remove.ts delete mode 100644 packages/cli/src/commands/accounts/show.ts delete mode 100644 packages/cli/src/commands/accounts/use.ts delete mode 100644 packages/cli/src/commands/api/get.ts delete mode 100644 packages/cli/src/commands/api/post.ts delete mode 100644 packages/cli/src/commands/api/request.ts delete mode 100644 packages/cli/src/commands/auth/email/response.ts delete mode 100644 packages/cli/src/commands/auth/email/start.ts delete mode 100644 packages/cli/src/commands/auth/logout.ts delete mode 100644 packages/cli/src/commands/auth/status.ts delete mode 100644 packages/cli/src/commands/autocomplete.ts delete mode 100644 packages/cli/src/commands/bridges/config.ts delete mode 100644 packages/cli/src/commands/bridges/delete.ts delete mode 100644 packages/cli/src/commands/bridges/list.ts delete mode 100644 packages/cli/src/commands/bridges/login-password.ts delete mode 100644 packages/cli/src/commands/bridges/login.ts delete mode 100644 packages/cli/src/commands/bridges/logout.ts delete mode 100644 packages/cli/src/commands/bridges/proxy.ts delete mode 100644 packages/cli/src/commands/bridges/register.ts delete mode 100644 packages/cli/src/commands/bridges/run.ts delete mode 100644 packages/cli/src/commands/bridges/show.ts delete mode 100644 packages/cli/src/commands/bridges/whoami.ts delete mode 100644 packages/cli/src/commands/chats/archive.ts delete mode 100644 packages/cli/src/commands/chats/avatar.ts delete mode 100644 packages/cli/src/commands/chats/description.ts delete mode 100644 packages/cli/src/commands/chats/disappear.ts delete mode 100644 packages/cli/src/commands/chats/draft.ts delete mode 100644 packages/cli/src/commands/chats/focus.ts delete mode 100644 packages/cli/src/commands/chats/list.ts delete mode 100644 packages/cli/src/commands/chats/mark-read.ts delete mode 100644 packages/cli/src/commands/chats/mark-unread.ts delete mode 100644 packages/cli/src/commands/chats/mute.ts delete mode 100644 packages/cli/src/commands/chats/notify-anyway.ts delete mode 100644 packages/cli/src/commands/chats/pin.ts delete mode 100644 packages/cli/src/commands/chats/priority.ts delete mode 100644 packages/cli/src/commands/chats/remind.ts delete mode 100644 packages/cli/src/commands/chats/rename.ts delete mode 100644 packages/cli/src/commands/chats/search.ts delete mode 100644 packages/cli/src/commands/chats/show.ts delete mode 100644 packages/cli/src/commands/chats/start.ts delete mode 100644 packages/cli/src/commands/chats/unarchive.ts delete mode 100644 packages/cli/src/commands/chats/unmute.ts delete mode 100644 packages/cli/src/commands/chats/unpin.ts delete mode 100644 packages/cli/src/commands/chats/unremind.ts delete mode 100644 packages/cli/src/commands/completion.ts delete mode 100644 packages/cli/src/commands/config/get.ts delete mode 100644 packages/cli/src/commands/config/path.ts delete mode 100644 packages/cli/src/commands/config/reset.ts delete mode 100644 packages/cli/src/commands/config/set.ts delete mode 100644 packages/cli/src/commands/contacts/list.ts delete mode 100644 packages/cli/src/commands/contacts/search.ts delete mode 100644 packages/cli/src/commands/contacts/show.ts delete mode 100644 packages/cli/src/commands/docs.ts delete mode 100644 packages/cli/src/commands/doctor.ts delete mode 100644 packages/cli/src/commands/export.ts delete mode 100644 packages/cli/src/commands/install/desktop.ts delete mode 100644 packages/cli/src/commands/install/server.ts delete mode 100644 packages/cli/src/commands/man.ts delete mode 100644 packages/cli/src/commands/media/download.ts delete mode 100644 packages/cli/src/commands/messages/context.ts delete mode 100644 packages/cli/src/commands/messages/delete.ts delete mode 100644 packages/cli/src/commands/messages/edit.ts delete mode 100644 packages/cli/src/commands/messages/export.ts delete mode 100644 packages/cli/src/commands/messages/list.ts delete mode 100644 packages/cli/src/commands/messages/search.ts delete mode 100644 packages/cli/src/commands/messages/show.ts delete mode 100644 packages/cli/src/commands/plugins.ts delete mode 100644 packages/cli/src/commands/plugins/available.ts delete mode 100644 packages/cli/src/commands/presence.ts delete mode 100644 packages/cli/src/commands/resolve/account.ts delete mode 100644 packages/cli/src/commands/resolve/bridge.ts delete mode 100644 packages/cli/src/commands/resolve/chat.ts delete mode 100644 packages/cli/src/commands/resolve/contact.ts delete mode 100644 packages/cli/src/commands/resolve/target.ts delete mode 100644 packages/cli/src/commands/rpc.ts delete mode 100644 packages/cli/src/commands/schema.ts delete mode 100644 packages/cli/src/commands/send/file.ts delete mode 100644 packages/cli/src/commands/send/react.ts delete mode 100644 packages/cli/src/commands/send/sticker.ts delete mode 100644 packages/cli/src/commands/send/text.ts delete mode 100644 packages/cli/src/commands/send/unreact.ts delete mode 100644 packages/cli/src/commands/send/voice.ts delete mode 100644 packages/cli/src/commands/setup.ts delete mode 100644 packages/cli/src/commands/status.ts delete mode 100644 packages/cli/src/commands/targets/add/desktop.ts delete mode 100644 packages/cli/src/commands/targets/add/remote.ts delete mode 100644 packages/cli/src/commands/targets/add/server.ts delete mode 100644 packages/cli/src/commands/targets/disable.ts delete mode 100644 packages/cli/src/commands/targets/enable.ts delete mode 100644 packages/cli/src/commands/targets/list.ts delete mode 100644 packages/cli/src/commands/targets/logs.ts delete mode 100644 packages/cli/src/commands/targets/remove.ts delete mode 100644 packages/cli/src/commands/targets/restart.ts delete mode 100644 packages/cli/src/commands/targets/show.ts delete mode 100644 packages/cli/src/commands/targets/start.ts delete mode 100644 packages/cli/src/commands/targets/status.ts delete mode 100644 packages/cli/src/commands/targets/stop.ts delete mode 100644 packages/cli/src/commands/targets/use.ts delete mode 100644 packages/cli/src/commands/update.ts delete mode 100644 packages/cli/src/commands/verify.ts delete mode 100644 packages/cli/src/commands/verify/approve.ts delete mode 100644 packages/cli/src/commands/verify/cancel.ts delete mode 100644 packages/cli/src/commands/verify/list.ts delete mode 100644 packages/cli/src/commands/verify/qr-confirm.ts delete mode 100644 packages/cli/src/commands/verify/qr-scan.ts delete mode 100644 packages/cli/src/commands/verify/recovery-key.ts delete mode 100644 packages/cli/src/commands/verify/reset-recovery-key.ts delete mode 100644 packages/cli/src/commands/verify/sas-confirm.ts delete mode 100644 packages/cli/src/commands/verify/sas.ts delete mode 100644 packages/cli/src/commands/verify/show.ts delete mode 100644 packages/cli/src/commands/verify/start.ts delete mode 100644 packages/cli/src/commands/verify/status.ts delete mode 100644 packages/cli/src/commands/version.ts delete mode 100644 packages/cli/src/commands/watch.ts create mode 100644 packages/cli/src/lib/api-values.ts delete mode 100644 packages/cli/src/lib/argv.ts delete mode 100644 packages/cli/src/lib/bridges/catalog.ts delete mode 100644 packages/cli/src/lib/bridges/command.ts delete mode 100644 packages/cli/src/lib/bridges/generated.ts delete mode 100644 packages/cli/src/lib/bridges/go-template.ts delete mode 100644 packages/cli/src/lib/bridges/manager.ts delete mode 100644 packages/cli/src/lib/bridges/websocket-proxy.ts delete mode 100644 packages/cli/src/lib/client.ts rename packages/{cli-plugin-cloudflare/src/lib/cloudflared.ts => cli/src/lib/cloudflare-tunnel.ts} (69%) delete mode 100644 packages/cli/src/lib/command-metadata.ts delete mode 100644 packages/cli/src/lib/command.ts delete mode 100644 packages/cli/src/lib/copy.ts delete mode 100644 packages/cli/src/lib/did-you-mean.ts delete mode 100644 packages/cli/src/lib/env.ts rename packages/cli/src/lib/{export/index.ts => export.ts} (95%) delete mode 100644 packages/cli/src/lib/ink/components.tsx delete mode 100644 packages/cli/src/lib/ink/format.ts delete mode 100644 packages/cli/src/lib/ink/render.tsx delete mode 100644 packages/cli/src/lib/ink/spinner.tsx delete mode 100644 packages/cli/src/lib/ink/theme.ts delete mode 100644 packages/cli/src/lib/manifest.ts delete mode 100644 packages/cli/src/lib/output.ts create mode 100644 packages/cli/src/lib/paging.ts delete mode 100644 packages/cli/src/lib/pkce.ts create mode 100644 packages/cli/src/lib/prompts.ts delete mode 100644 packages/cli/src/lib/recommended-plugins.ts delete mode 100644 packages/cli/src/lib/runner.ts delete mode 100644 packages/cli/src/lib/send-message.ts delete mode 100644 packages/cli/src/lib/update-banner.ts delete mode 100644 packages/cli/src/lib/wait.ts delete mode 100644 packages/cli/src/plugin-sdk.ts delete mode 100644 packages/cli/src/types/qrcode.d.ts delete mode 100644 packages/cli/src/types/ws.d.ts create mode 100644 packages/cli/test/cloudflare-tunnel.test.ts delete mode 100644 packages/cli/test/fixtures/fake-client.ts delete mode 100644 packages/cli/test/messages-list-filter.test.ts delete mode 100644 packages/cli/test/plugin-sdk.test.ts create mode 100644 packages/cli/test/resolve.test.ts delete mode 100644 packages/cli/test/watch-filter.test.ts delete mode 100644 packages/npm/.gitignore delete mode 100644 packages/npm/package.json delete mode 100644 packages/npm/scripts/build.ts create mode 100755 run.sh delete mode 100644 scripts/publish-packages.ts delete mode 100644 scripts/release.ts diff --git a/.changeset/README.md b/.changeset/README.md deleted file mode 100644 index 489fb041..00000000 --- a/.changeset/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Changesets - -Use `pnpm changeset` in feature branches to describe user-facing package changes. - -When changes land on `main`, the release workflow opens or updates a release PR. -Merging that release PR publishes changed packages to npm and creates GitHub -Releases from the generated changelogs. diff --git a/.changeset/config.json b/.changeset/config.json deleted file mode 100644 index 3671d8f8..00000000 --- a/.changeset/config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "https://unpkg.com/@changesets/config@3.1.4/schema.json", - "changelog": ["@changesets/changelog-github", { "repo": "beeper/pickle" }], - "commit": false, - "fixed": [], - "linked": [], - "access": "public", - "baseBranch": "main", - "updateInternalDependencies": "patch", - "ignore": [] -} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3401acdb..b025ac9f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,18 +1,5 @@ name: CI -on: - push: - branches: - - "**" - - "!integrated/**" - - "!stl-preview-head/**" - - "!stl-preview-base/**" - - "!generated" - - "!codegen/**" - - "codegen/stl/**" - pull_request: - branches-ignore: - - "stl-preview-head/**" - - "stl-preview-base/**" +on: [push, pull_request] jobs: test: @@ -39,24 +26,3 @@ jobs: - name: Test run: bun run test - - # Binary/Homebrew packaging is intentionally disabled until binary releases - # are ready to ship. Re-enable this job with the package scripts already kept - # in packages/cli/package.json: - # - # package: - # name: package homebrew archive - # runs-on: macos-latest - # needs: test - # steps: - # - uses: actions/checkout@v6 - # - # - uses: oven-sh/setup-bun@v2 - # with: - # bun-version: 1.3.10 - # - # - name: Install dependencies - # run: bun install --frozen-lockfile - # - # - name: Package Homebrew archive - # run: bun run --filter beeper-cli pack:homebrew diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 6105b43e..fcc206a9 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -24,8 +24,21 @@ jobs: bun-version: 1.3.10 - name: Install dependencies run: bun install --frozen-lockfile - - name: Test - run: bun run test + - name: Check + run: bun run check + - name: Publish npm package + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + set -euo pipefail + version="${GITHUB_REF_NAME#v}" + echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > ~/.npmrc + if npm view "beeper-cli@${version}" version >/dev/null 2>&1; then + echo "beeper-cli@${version} is already published" + exit 0 + fi + npm version "${version}" --workspace beeper-cli --no-git-tag-version --allow-same-version + npm publish --workspace beeper-cli --access public - name: Publish GitHub release env: GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml deleted file mode 100644 index 095bb065..00000000 --- a/.github/workflows/release-doctor.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Release Doctor -on: - pull_request: - branches: - - main - workflow_dispatch: - -jobs: - release_doctor: - name: release doctor - runs-on: ubuntu-latest - if: github.repository == 'beeper/cli' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') - - steps: - - uses: actions/checkout@v6 - - - name: Check release environment - run: | - bash packages/cli/bin/check-release-environment diff --git a/README.md b/README.md index 385ff825..99e29095 100644 --- a/README.md +++ b/README.md @@ -1,332 +1,5 @@ -
- # beeper -**One CLI for all your chats.** Built for you and your agent — batteries included. - -[![npm](https://img.shields.io/npm/v/beeper-cli.svg?label=npm&color=6E56F8)](https://www.npmjs.com/package/beeper-cli) -[![license](https://img.shields.io/badge/license-MIT-6E56F8.svg)](https://github.com/beeper/cli/blob/main/packages/cli/LICENSE) -[![docs](https://img.shields.io/badge/docs-online-6E56F8.svg)](https://beeper.github.io/cli) -[![built with Bun](https://img.shields.io/badge/built%20with-Bun-6E56F8.svg)](https://bun.sh) - -
- -Talks to Beeper Desktop on this machine, to a Beeper Server you self-host, or -to either one running somewhere else. Send and receive across the chat -networks Beeper bridges, from one CLI shaped for scripts, agents, and humans -in a hurry. - -**Supported chat networks** (via Beeper's bridges): -WhatsApp · iMessage · Telegram · Discord · Signal · Instagram DMs · -Facebook Messenger · X (Twitter) DMs · LinkedIn · Slack · -Google Messages (RCS/SMS) · Google Chat · Matrix · IRC · Bluesky. -Run `beeper bridges list` for the live list on your target. - -📖 **[Read the docs](https://beeper.github.io/cli)** · command manual: `beeper man` · open docs: `beeper docs` - -## Features - -- **Connects to your Beeper.** Local Beeper Desktop on this machine (default), a Beeper Server you install and manage via the CLI, or a remote Beeper Desktop or Beeper Server authorized over OAuth/PKCE — or a bearer token in CI. -- **Setup that does the work.** `beeper setup` finds Beeper Desktop, offers to launch it, adopts the session. `--server --install` installs and starts a headless server in one step. `--oauth` opens the browser. `--remote URL` does the rest. -- **Every chat, every network.** List, search, start, archive, pin, mute, rename, focus. Read, edit, delete, react. Send text, files, stickers, voice, typing indicators. Download media. Export to JSON or Markdown. -- **Verification first-class.** SAS/QR device verification, recovery-key unlock, `status`/`doctor` to reach an encrypted-ready target — without leaving the shell. -- **Agent-shaped automation.** `--json` everywhere, NDJSON `--events`, `watch` with WebSocket + outbound HMAC-signed webhooks, `rpc` over stdin/stdout, `man --json` tool manifests, raw `api get`/`post`/`request` for Beeper Client API endpoints we haven't wrapped yet. -- **Safe by default.** `--read-only` rejects every mutating command. Writes stay explicit. Plugins extend the CLI without forking it. - -## Install - -### Homebrew (recommended) - -```sh -brew install beeper/tap/cli -``` - -The installed command is `beeper`. - -### npm - -```sh -npx beeper-cli --help -npm install -g beeper-cli -``` - -The package name is `beeper-cli`; the installed command is `beeper`. - -### Build from source - -This repo is a Bun workspace. From the repo root: - -```sh -bun install -bun --filter @beeper/cli run build -bun --filter @beeper/cli run dev -- --help -``` - -For local CLI development inside `packages/cli`: - -```sh -bun run dev -- --help -``` - -Regenerate this README after command, flag, or argument changes: - -```sh -bun run readme -``` - -## Quick start - -The happy path: Beeper Desktop is already on this machine. `beeper setup` finds -it, offers to launch it if it's not running, and adopts the session. - -```text -$ beeper setup -Looking for Beeper Desktop… found, not running. -Launch it now? [Y/n] y -▎ Launched Beeper Desktop - next Run `beeper setup` again once it finishes starting. - -$ beeper setup -Use this Desktop session for CLI access? [Y/n] y -▎ Connected desktop - accounts whatsapp, telegram, imessage - endpoint http://127.0.0.1:23373 - -$ beeper chats list --limit 3 - 10313 Family 3 unread - 8951 Alice · - 7204 Eng standup 12 unread - -$ beeper messages search "flight" - 8951 Alice · "your flight is at 6:40, gate B23" 2d ago - 10313 Family · "what flight are you on?" 1w ago - -$ beeper send text --to Family --message "on my way" -▎ Sent Family - message "on my way" - at 2026-05-18T14:02:11Z - -$ beeper export --out ./beeper-export -▎ Exported ./beeper-export - chats 214 messages 38,901 attachments 1,205 -``` - -Recipients accept a numeric local chat ID, a full Beeper/Matrix chat ID, an -iMessage chat ID, an exact title, or search text. Ambiguous matches prompt in a -TTY; pass `--pick N` in scripts. - -## Connecting a target - -A *target* is the Beeper endpoint `beeper` talks to — local Beeper Desktop, -local Beeper Server, or a remote Beeper Desktop or Beeper Server. Pick one of -four paths. - -### 1. Local Beeper Desktop (default, recommended) - -If Beeper Desktop is installed and signed in here, `beeper setup` discovers it -on `http://127.0.0.1:23373` and adopts the existing session. If it's installed -but not running, `setup` offers to launch it. If it's not installed at all, -`--install` does that in one step. - -```text -$ beeper setup --desktop --install -▎ Installed Beeper Desktop (stable) -▎ Launched Beeper Desktop - next Sign in to Beeper Desktop, then re-run `beeper setup`. - -$ beeper setup -▎ Connected desktop - accounts whatsapp, telegram -``` - -Variants: `beeper setup --local` to skip discovery and force the local path; -`beeper install desktop --channel nightly` for the nightly channel. - -### 2. Local Beeper Server (self-hosted, managed by the CLI) - -For a headless long-running setup on this machine, install and adopt a local -Beeper Server. The CLI manages the process — `targets start/stop/restart/logs/enable`. - -```text -$ beeper setup --server --install -▎ Installed Beeper Server (stable) -▎ Started server on http://127.0.0.1:23373 - auth Opening browser to authorize this server… -▎ Connected server - accounts (none) - next Run `beeper accounts add` to connect a network. - -$ beeper accounts add -? Which bridge? whatsapp - Scan this QR code with WhatsApp on your phone: - ▄▄▄▄▄▄▄ ▄ ▄ ▄▄▄▄▄▄▄ - █ ███ █ ▄█▄ █ ███ █ - █ ███ █ ▀█▀ █ ███ █ - ▀▀▀▀▀▀▀ ▀ ▀ ▀▀▀▀▀▀▀ -▎ Connected whatsapp · +1•••4242 -``` - -Variants: `beeper install server`, `beeper install server --server-env staging`. - -### 3. Remote Desktop or Server via OAuth (PKCE) - -For a Beeper Desktop or Server running on another machine, authorize the CLI -through a browser-based OAuth/PKCE flow. - -```text -$ beeper setup --remote https://desktop.example.com -▎ Authorizing https://desktop.example.com - flow OAuth (PKCE) — opening browser… -▎ Connected remote (desktop.example.com) - accounts whatsapp, telegram, signal -``` - -Variants: `beeper setup --oauth` (PKCE against the default Beeper auth); -`beeper targets add remote work https://desktop.example.com --default` to -register additional remotes. - -### 4. Bearer token (non-interactive / CI) - -For agents, CI, and scripts, hand the CLI a bearer token directly — no -browser, no interactive prompts. - -```sh -BEEPER_ACCESS_TOKEN=... beeper chats list --json -BEEPER_ACCESS_TOKEN=... BEEPER_DESKTOP_BASE_URL=https://desktop.example.com \ - beeper messages list --chat 10313 --json -``` - -Once connected, `beeper accounts add` walks each chat-network bridge through -its own login — QR, code, OAuth, cookie, whatever the bridge requires — so -WhatsApp, Telegram, Discord, iMessage, and the rest show up under `accounts list`. - -## Documentation - -Full documentation lives at **[beeper.github.io/cli](https://beeper.github.io/cli)** -(built from [`docs/`](docs/) with Astro Starlight — a fully static site). - -| Topic | Page | Commands | -| --- | --- | --- | -| **Setup + install** | [connect](https://beeper.github.io/cli/connect/) · [install](https://beeper.github.io/cli/install/) · [auth](https://beeper.github.io/cli/auth/) | `setup` · `install desktop` · `install server` · `verify` · `status` · `doctor` · `auth status` | -| **Targets** | [targets](https://beeper.github.io/cli/targets/) | `targets list` · `targets add desktop` · `targets add server` · `targets add remote` · `targets use` · `targets status` · `targets logs` | -| **Bridges + accounts** | [accounts](https://beeper.github.io/cli/accounts/) | `bridges list` · `bridges show` · `accounts list` · `accounts add` · `accounts show` · `accounts use` · `accounts remove` | -| **Chats** | [chats](https://beeper.github.io/cli/chats/) | `chats list` · `chats search` · `chats show` · `chats start` · `chats archive` · `chats pin` · `chats mute` · `chats priority` · `chats remind` · `chats rename` · `chats draft` · `chats focus` | -| **Messages** | [messages](https://beeper.github.io/cli/messages/) · [send](https://beeper.github.io/cli/send/) · [presence](https://beeper.github.io/cli/presence/) | `messages list` · `messages search` · `messages export` · `send text` · `send file` · `send sticker` · `send voice` · `send react` · `presence` | -| **Contacts + media** | [contacts](https://beeper.github.io/cli/contacts/) · [media](https://beeper.github.io/cli/media/) · [export](https://beeper.github.io/cli/export/) | `contacts list` · `contacts search` · `media download` · `export` | -| **Automation** | [scripting](https://beeper.github.io/cli/scripting/) · [watch](https://beeper.github.io/cli/watch/) · [rpc](https://beeper.github.io/cli/rpc/) · [api](https://beeper.github.io/cli/api/) | `watch` · `watch --webhook` · `rpc` · `man` · `api get` · `api post` · `api request` | -| **Maintenance** | [config](https://beeper.github.io/cli/config/) · [update](https://beeper.github.io/cli/update/) | `update` · `config` · `completion` · `docs` · `version` | - -Use `beeper docs` to open the CLI docs and `beeper man` to print the local -command manual. To work on the docs site locally: `cd docs && bun install && bun run dev`. - -## Configuration - -Default Beeper Client API target: `http://127.0.0.1:23373`. CLI configuration is -stored under your user config dir; print it with `beeper config path`. - -**Global flags:** `--base-url`, `--target`, `--json`, `--events`, -`--full`, `--timeout`, `--read-only`, `--debug`, `--yes`, `--quiet`. - -**Environment overrides:** - -| Variable | Effect | -| --- | --- | -| `BEEPER_ACCESS_TOKEN` | Bearer token for the selected target. Overrides stored OAuth login. | -| `BEEPER_DESKTOP_BASE_URL` | Beeper Client API base URL (Desktop or Server). Defaults to `http://127.0.0.1:23373`. | -| `BEEPER_READONLY` | `1`/`true`/`yes`/`on` enables read-only mode globally. | -| `BEEPER_CLI_CONFIG_DIR` | Override config directory for testing or isolated profiles. | - -## Exit codes - -| Code | Meaning | -| --- | --- | -| `0` | Success. | -| `1` | Generic runtime error. | -| `2` | Usage error (parsing, validation, missing required flag/arg, read-only refusal). | -| `3` | Auth required (no stored token; sign in or set `BEEPER_ACCESS_TOKEN`). | -| `4` | Target/account not ready (`doctor` reports this when readiness is not `ready`). | -| `5` | Selector matched nothing (unknown target, account, chat, contact). | -| `6` | Ambiguous selector (multiple matches; pass an exact ID or `--pick N`). | - -JSON output preserves the same envelope on failure: `{"success":false,"data":null,"error":"...","exitCode":N}` written to stderr. - -## Addressing - -- Chat arguments accept numeric local chat IDs, full Beeper/Matrix chat IDs, iMessage chat IDs, exact titles, or search text. -- For scripts on the same target/profile, prefer the numeric local chat ID shown by `beeper chats list`; use the full Beeper/Matrix chat ID when the selector must work across targets or profiles. -- Numeric local chat IDs come from the selected Desktop database. Treat them as local to that target/profile. -- Ambiguous chat matches return numbered choices; pass `--pick N` to select one. -- Account arguments accept account IDs, network names, bridge type/id, or account user identity. -- Account filters can expand a network name to multiple matching accounts. -- `contacts search` and `chats start` can search across all accounts when `--account` is omitted. -- `contacts list` accepts the same account selectors as other account-scoped commands. - -## Output and scripting - -Most commands support: - -- app-like text by default, optimized for scanning chats, messages, contacts, accounts, and media -- `--json` for `{"success":true,"data":...,"error":null}` output on stdout -- `--events` for NDJSON lifecycle events on stderr from long-running commands -- `--read-only` to reject commands that modify Beeper or local CLI state -- `--full` to disable truncation -- `--debug` for SDK debug logging -- `--target` or `--base-url` to point at a different target - -`man --json` prints a compact command manifest for tools and agents. -`rpc` runs newline-delimited JSON command RPC over stdin/stdout. - -## Raw API access - -Raw Beeper Client API calls live under `api`, so scripts can reach a new -endpoint before a workflow command exists: - -```sh -beeper api get /v1/info -beeper api post /v1/messages/{chatID}/send --body '{"text":"hello"}' -beeper api request DELETE /v1/chats/abc/messages/def/reactions --body '{"reactionKey":"👍"}' -``` - -## Plugins - -Beeper CLI supports optional oclif plugins. List recommended Beeper plugins: - -```sh -beeper plugins available -``` - -Install a published plugin: - -```sh -beeper plugins install @beeper/cli-plugin-cloudflare -``` - -For plugin development, import from `@beeper/cli/plugin-sdk` and expose oclif -commands from your package. Link a local plugin while working on it: - -```sh -beeper plugins link ./packages/cli-plugin-cloudflare -beeper targets tunnel --help -``` - -First-party optional plugins: - -| Package | Adds | -| --- | --- | -| `@beeper/cli-plugin-cloudflare` | `targets tunnel` for exposing a selected Beeper target through Cloudflare Tunnel. | - - -## Full command reference - -The complete `beeper` command summary and per-command reference (every flag, -arg, and example) lives in [`packages/cli/README.md`](packages/cli/README.md). -For terminal-side reference, `beeper man` prints the same manual locally and -`beeper man --json` emits a tool manifest for agents. - -## Inspiration - -- [wacli](https://wacli.sh/) — scriptable WhatsApp CLI whose command-line product shape we borrow from. - -## License +This repository contains the `beeper-cli` package. -MIT — see [`packages/cli/LICENSE`](packages/cli/LICENSE). +Use [packages/cli/README.md](packages/cli/README.md) for install, command, safety-profile, and development notes. diff --git a/bun.lock b/bun.lock index 63c3b491..9dc2bba0 100644 --- a/bun.lock +++ b/bun.lock @@ -3,1420 +3,111 @@ "configVersion": 1, "workspaces": { "": { - "name": "desktop-api-cli-monorepo", - "devDependencies": { - "@changesets/changelog-github": "^0.6.0", - "@changesets/cli": "^2.31.0", - "@types/bun": "^1.3.3", - "@types/node": "^20.0.0", - "eslint": "^9.39.4", - "eslint-config-oclif": "^6.0.165", - "eslint-config-prettier": "^10.1.8", - "tsdown": "^0.21.10", - "typescript": "^5.7.2", - }, + "name": "beeper-cli-monorepo", }, "packages/cli": { - "name": "@beeper/cli", - "version": "0.6.1", + "name": "beeper-cli", + "version": "0.6.2", "bin": { - "beeper": "bin/run.js", + "beeper": "bin/cli.js", }, "dependencies": { "@beeper/desktop-api": "github:beeper/desktop-api-js#next", - "@oclif/core": "^4.11.2", - "@oclif/plugin-autocomplete": "^3.2.49", - "@oclif/plugin-help": "^6.2.48", - "@oclif/plugin-not-found": "^3.2.85", - "@oclif/plugin-plugins": "^5.4.67", - "@oclif/plugin-warn-if-update-available": "^3.1.49", - "figures": "^6.1.0", - "ink": "^7.0.3", - "ink-spinner": "^5.0.0", "qrcode": "1.5.4", - "react": "^19.2.6", "ws": "^8.20.1", + "yaml": "^2.9.0", }, "devDependencies": { "@types/bun": "^1.3.3", "@types/node": "^20.0.0", - "@types/react": "^19.2.14", + "@types/qrcode": "^1.5.6", "@types/ws": "^8.18.1", "typescript": "^5.7.2", }, }, - "packages/cli-plugin-cloudflare": { - "name": "@beeper/cli-plugin-cloudflare", - "version": "0.6.0", - "dependencies": { - "@beeper/cli": "workspace:*", - "@oclif/core": "^4.11.2", - }, - "devDependencies": { - "@types/node": "^20.0.0", - "typescript": "^5.7.2", - }, - }, - "packages/npm": { - "name": "beeper-cli", - "version": "0.6.1", - "bin": { - "beeper": "./bin/beeper.js", - }, - }, }, "trustedDependencies": [ "@beeper/desktop-api", ], "packages": { - "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.3.0", "", { "dependencies": { "ansi-styles": "6.2.3", "is-fullwidth-code-point": "5.1.0" } }, "sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA=="], - - "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "7.28.5", "js-tokens": "4.0.0", "picocolors": "1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - - "@babel/generator": ["@babel/generator@8.0.0-rc.3", "", { "dependencies": { "@babel/parser": "8.0.0-rc.3", "@babel/types": "8.0.0-rc.3", "@jridgewell/gen-mapping": "0.3.13", "@jridgewell/trace-mapping": "0.3.31", "@types/jsesc": "2.5.1", "jsesc": "3.1.0" } }, "sha512-em37/13/nR320G4jab/nIIHZgc2Wz2y/D39lxnTyxB4/D/omPQncl/lSdlnJY1OhQcRGugTSIF2l/69o31C9dA=="], - - "@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.5", "", {}, "sha512-sN7R8rBvDurfaziNfDEIjIntlazmlkCDGO4SNl2RJ3wRCn+QxspLV7hzYAE8WWVd2joVuT8sUxeePdLp2idI1A=="], - - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - - "@babel/parser": ["@babel/parser@8.0.0-rc.3", "", { "dependencies": { "@babel/types": "8.0.0-rc.3" }, "bin": "./bin/babel-parser.js" }, "sha512-B20dvP3MfNc/XS5KKCHy/oyWl5IA6Cn9YjXRdDlCjNmUFrjvLXMNUfQq/QUy9fnG2gYkKKcrto2YaF9B32ToOQ=="], - - "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], - - "@babel/types": ["@babel/types@8.0.0-rc.3", "", { "dependencies": { "@babel/helper-string-parser": "8.0.0-rc.5", "@babel/helper-validator-identifier": "8.0.0-rc.3" } }, "sha512-mOm5ZrYmphGfqVWoH5YYMTITb3cDXsFgmvFlvkvWDMsR9X8RFnt7a0Wb6yNIdoFsiMO9WjYLq+U/FMtqIYAF8Q=="], - - "@beeper/cli": ["@beeper/cli@workspace:packages/cli"], - - "@beeper/cli-plugin-cloudflare": ["@beeper/cli-plugin-cloudflare@workspace:packages/cli-plugin-cloudflare"], - - "@beeper/desktop-api": ["@beeper/desktop-api@github:beeper/desktop-api-js#b9c1714", {}, "beeper-desktop-api-js-b9c1714", "sha512-Qlxz1R4ppJd6vPuzgKhJqEC2WpNPtdi6n9ONieX6S5mAC0wyWUjZP/SogeTcbDf8vQ2Sl+ET4lzRN4ZcKHkqww=="], - - "@changesets/apply-release-plan": ["@changesets/apply-release-plan@7.1.1", "", { "dependencies": { "@changesets/config": "3.1.4", "@changesets/get-version-range-type": "0.4.0", "@changesets/git": "3.0.4", "@changesets/should-skip-package": "0.1.2", "@changesets/types": "6.1.0", "@manypkg/get-packages": "1.1.3", "detect-indent": "6.1.0", "fs-extra": "7.0.1", "lodash.startcase": "4.4.0", "outdent": "0.5.0", "prettier": "2.8.8", "resolve-from": "5.0.0", "semver": "7.8.0" } }, "sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA=="], - - "@changesets/assemble-release-plan": ["@changesets/assemble-release-plan@6.0.10", "", { "dependencies": { "@changesets/errors": "0.2.0", "@changesets/get-dependents-graph": "2.1.4", "@changesets/should-skip-package": "0.1.2", "@changesets/types": "6.1.0", "@manypkg/get-packages": "1.1.3", "semver": "7.8.0" } }, "sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A=="], - - "@changesets/changelog-git": ["@changesets/changelog-git@0.2.1", "", { "dependencies": { "@changesets/types": "6.1.0" } }, "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q=="], - - "@changesets/changelog-github": ["@changesets/changelog-github@0.6.0", "", { "dependencies": { "@changesets/get-github-info": "0.8.0", "@changesets/types": "6.1.0", "dotenv": "8.6.0" } }, "sha512-wA2/y4hR/A1K411cCT75rz0d46Iezxp1WYRFoFJDIUpkQ6oDBAIUiU7BZkDCmYgz0NBl94X1lgcZO+mHoiHnFg=="], - - "@changesets/cli": ["@changesets/cli@2.31.0", "", { "dependencies": { "@changesets/apply-release-plan": "7.1.1", "@changesets/assemble-release-plan": "6.0.10", "@changesets/changelog-git": "0.2.1", "@changesets/config": "3.1.4", "@changesets/errors": "0.2.0", "@changesets/get-dependents-graph": "2.1.4", "@changesets/get-release-plan": "4.0.16", "@changesets/git": "3.0.4", "@changesets/logger": "0.1.1", "@changesets/pre": "2.0.2", "@changesets/read": "0.6.7", "@changesets/should-skip-package": "0.1.2", "@changesets/types": "6.1.0", "@changesets/write": "0.4.0", "@inquirer/external-editor": "1.0.3", "@manypkg/get-packages": "1.1.3", "ansi-colors": "4.1.3", "enquirer": "2.4.1", "fs-extra": "7.0.1", "mri": "1.2.0", "package-manager-detector": "0.2.11", "picocolors": "1.1.1", "resolve-from": "5.0.0", "semver": "7.8.0", "spawndamnit": "3.0.1", "term-size": "2.2.1" }, "bin": { "changeset": "bin.js" } }, "sha512-AhI4enNTgHu2IZr6K4WZyf0EPch4XVMn1yOMFmCD9gsfBGqMYaHXls5HyDv6/CL5axVQABz68eG30eCtbr2wFg=="], - - "@changesets/config": ["@changesets/config@3.1.4", "", { "dependencies": { "@changesets/errors": "0.2.0", "@changesets/get-dependents-graph": "2.1.4", "@changesets/logger": "0.1.1", "@changesets/should-skip-package": "0.1.2", "@changesets/types": "6.1.0", "@manypkg/get-packages": "1.1.3", "fs-extra": "7.0.1", "micromatch": "4.0.8" } }, "sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q=="], - - "@changesets/errors": ["@changesets/errors@0.2.0", "", { "dependencies": { "extendable-error": "0.1.7" } }, "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow=="], - - "@changesets/get-dependents-graph": ["@changesets/get-dependents-graph@2.1.4", "", { "dependencies": { "@changesets/types": "6.1.0", "@manypkg/get-packages": "1.1.3", "picocolors": "1.1.1", "semver": "7.8.0" } }, "sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg=="], - - "@changesets/get-github-info": ["@changesets/get-github-info@0.8.0", "", { "dependencies": { "dataloader": "1.4.0", "node-fetch": "2.7.0" } }, "sha512-cRnC+xdF0JIik7coko3iUP9qbnfi1iJQ3sAa6dE+Tx3+ET8bjFEm63PA4WEohgjYcmsOikPHWzPsMWWiZmntOQ=="], - - "@changesets/get-release-plan": ["@changesets/get-release-plan@4.0.16", "", { "dependencies": { "@changesets/assemble-release-plan": "6.0.10", "@changesets/config": "3.1.4", "@changesets/pre": "2.0.2", "@changesets/read": "0.6.7", "@changesets/types": "6.1.0", "@manypkg/get-packages": "1.1.3" } }, "sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g=="], - - "@changesets/get-version-range-type": ["@changesets/get-version-range-type@0.4.0", "", {}, "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ=="], - - "@changesets/git": ["@changesets/git@3.0.4", "", { "dependencies": { "@changesets/errors": "0.2.0", "@manypkg/get-packages": "1.1.3", "is-subdir": "1.2.0", "micromatch": "4.0.8", "spawndamnit": "3.0.1" } }, "sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw=="], - - "@changesets/logger": ["@changesets/logger@0.1.1", "", { "dependencies": { "picocolors": "1.1.1" } }, "sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg=="], - - "@changesets/parse": ["@changesets/parse@0.4.3", "", { "dependencies": { "@changesets/types": "6.1.0", "js-yaml": "4.1.1" } }, "sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A=="], - - "@changesets/pre": ["@changesets/pre@2.0.2", "", { "dependencies": { "@changesets/errors": "0.2.0", "@changesets/types": "6.1.0", "@manypkg/get-packages": "1.1.3", "fs-extra": "7.0.1" } }, "sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug=="], - - "@changesets/read": ["@changesets/read@0.6.7", "", { "dependencies": { "@changesets/git": "3.0.4", "@changesets/logger": "0.1.1", "@changesets/parse": "0.4.3", "@changesets/types": "6.1.0", "fs-extra": "7.0.1", "p-filter": "2.1.0", "picocolors": "1.1.1" } }, "sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA=="], - - "@changesets/should-skip-package": ["@changesets/should-skip-package@0.1.2", "", { "dependencies": { "@changesets/types": "6.1.0", "@manypkg/get-packages": "1.1.3" } }, "sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw=="], - - "@changesets/types": ["@changesets/types@6.1.0", "", {}, "sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA=="], - - "@changesets/write": ["@changesets/write@0.4.0", "", { "dependencies": { "@changesets/types": "6.1.0", "fs-extra": "7.0.1", "human-id": "4.1.3", "prettier": "2.8.8" } }, "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q=="], - - "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "2.8.1" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], - - "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], - - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], - - "@es-joy/jsdoccomment": ["@es-joy/jsdoccomment@0.50.2", "", { "dependencies": { "@types/estree": "1.0.9", "@typescript-eslint/types": "8.59.3", "comment-parser": "1.4.1", "esquery": "1.7.0", "jsdoc-type-pratt-parser": "4.1.0" } }, "sha512-YAdE/IJSpwbOTiaURNCKECdAwqrJuFiZhylmesBcIRawtYKnBR2wxPhoIewMg+Yu+QuYvHfJNReWpoxGBKOChA=="], - - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "3.4.3" }, "peerDependencies": { "eslint": "9.39.4" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], - - "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], - - "@eslint/compat": ["@eslint/compat@1.4.1", "", { "dependencies": { "@eslint/core": "0.17.0" }, "optionalDependencies": { "eslint": "9.39.4" } }, "sha512-cfO82V9zxxGBxcQDr1lfaYB7wykTa0b00mGa36FrJl7iTFd0Z2cHfEYuxcBRP/iNijCsWsEkA+jzT8hGYmv33w=="], - - "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "2.1.7", "debug": "4.4.3", "minimatch": "3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="], - - "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], - - "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], - - "@eslint/css": ["@eslint/css@0.10.0", "", { "dependencies": { "@eslint/core": "0.14.0", "@eslint/css-tree": "3.6.9", "@eslint/plugin-kit": "0.3.5" } }, "sha512-pHoYRWS08oeU0qVez1pZCcbqHzoJnM5VMtrxH2nWDJ0ukq9DkwWV1BTY+PWK+eWBbndN9W0O9WjJTyAHsDoPOg=="], - - "@eslint/css-tree": ["@eslint/css-tree@3.6.9", "", { "dependencies": { "mdn-data": "2.23.0", "source-map-js": "1.2.1" } }, "sha512-3D5/OHibNEGk+wKwNwMbz63NMf367EoR4mVNNpxddCHKEb2Nez7z62J2U6YjtErSsZDoY0CsccmoUpdEbkogNA=="], - - "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "6.15.0", "debug": "4.4.3", "espree": "10.4.0", "globals": "14.0.0", "ignore": "5.3.2", "import-fresh": "3.3.1", "js-yaml": "4.1.1", "minimatch": "3.1.5", "strip-json-comments": "3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="], - - "@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="], - - "@eslint/json": ["@eslint/json@0.13.2", "", { "dependencies": { "@eslint/core": "0.15.2", "@eslint/plugin-kit": "0.3.5", "@humanwhocodes/momoa": "3.3.10", "natural-compare": "1.4.0" } }, "sha512-yWLyRE18rHgHXhWigRpiyv1LDPkvWtC6oa7QHXW7YdP6gosJoq7BiLZW2yCs9U7zN7X4U3ZeOJjepA10XAOIMw=="], - - "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], - - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "0.17.0", "levn": "0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], - - "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], - - "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "0.19.2", "@humanfs/types": "0.15.0", "@humanwhocodes/retry": "0.4.3" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="], - - "@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="], - - "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], - - "@humanwhocodes/momoa": ["@humanwhocodes/momoa@3.3.10", "", {}, "sha512-KWiFQpSAqEIyrTXko3hFNLeQvSK8zXlJQzhhxsyVn58WFRYXST99b3Nqnu+ttOtjds2Pl2grUHGpe2NzhPynuQ=="], - - "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - - "@inquirer/ansi": ["@inquirer/ansi@1.0.2", "", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="], - - "@inquirer/checkbox": ["@inquirer/checkbox@4.3.2", "", { "dependencies": { "@inquirer/ansi": "1.0.2", "@inquirer/core": "10.3.2", "@inquirer/figures": "1.0.15", "@inquirer/type": "3.0.10", "yoctocolors-cjs": "2.1.3" }, "optionalDependencies": { "@types/node": "20.19.41" } }, "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA=="], - - "@inquirer/confirm": ["@inquirer/confirm@5.1.21", "", { "dependencies": { "@inquirer/core": "10.3.2", "@inquirer/type": "3.0.10" }, "optionalDependencies": { "@types/node": "20.19.41" } }, "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ=="], - - "@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "1.0.2", "@inquirer/figures": "1.0.15", "@inquirer/type": "3.0.10", "cli-width": "4.1.0", "mute-stream": "2.0.0", "signal-exit": "4.1.0", "wrap-ansi": "6.2.0", "yoctocolors-cjs": "2.1.3" }, "optionalDependencies": { "@types/node": "20.19.41" } }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="], - - "@inquirer/editor": ["@inquirer/editor@4.2.23", "", { "dependencies": { "@inquirer/core": "10.3.2", "@inquirer/external-editor": "1.0.3", "@inquirer/type": "3.0.10" }, "optionalDependencies": { "@types/node": "20.19.41" } }, "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ=="], - - "@inquirer/expand": ["@inquirer/expand@4.0.23", "", { "dependencies": { "@inquirer/core": "10.3.2", "@inquirer/type": "3.0.10", "yoctocolors-cjs": "2.1.3" }, "optionalDependencies": { "@types/node": "20.19.41" } }, "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew=="], - - "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "2.1.1", "iconv-lite": "0.7.2" }, "optionalDependencies": { "@types/node": "20.19.41" } }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="], - - "@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="], - - "@inquirer/input": ["@inquirer/input@4.3.1", "", { "dependencies": { "@inquirer/core": "10.3.2", "@inquirer/type": "3.0.10" }, "optionalDependencies": { "@types/node": "20.19.41" } }, "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g=="], - - "@inquirer/number": ["@inquirer/number@3.0.23", "", { "dependencies": { "@inquirer/core": "10.3.2", "@inquirer/type": "3.0.10" }, "optionalDependencies": { "@types/node": "20.19.41" } }, "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg=="], - - "@inquirer/password": ["@inquirer/password@4.0.23", "", { "dependencies": { "@inquirer/ansi": "1.0.2", "@inquirer/core": "10.3.2", "@inquirer/type": "3.0.10" }, "optionalDependencies": { "@types/node": "20.19.41" } }, "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA=="], - - "@inquirer/prompts": ["@inquirer/prompts@7.10.1", "", { "dependencies": { "@inquirer/checkbox": "4.3.2", "@inquirer/confirm": "5.1.21", "@inquirer/editor": "4.2.23", "@inquirer/expand": "4.0.23", "@inquirer/input": "4.3.1", "@inquirer/number": "3.0.23", "@inquirer/password": "4.0.23", "@inquirer/rawlist": "4.1.11", "@inquirer/search": "3.2.2", "@inquirer/select": "4.4.2" }, "optionalDependencies": { "@types/node": "20.19.41" } }, "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg=="], - - "@inquirer/rawlist": ["@inquirer/rawlist@4.1.11", "", { "dependencies": { "@inquirer/core": "10.3.2", "@inquirer/type": "3.0.10", "yoctocolors-cjs": "2.1.3" }, "optionalDependencies": { "@types/node": "20.19.41" } }, "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw=="], - - "@inquirer/search": ["@inquirer/search@3.2.2", "", { "dependencies": { "@inquirer/core": "10.3.2", "@inquirer/figures": "1.0.15", "@inquirer/type": "3.0.10", "yoctocolors-cjs": "2.1.3" }, "optionalDependencies": { "@types/node": "20.19.41" } }, "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA=="], - - "@inquirer/select": ["@inquirer/select@4.4.2", "", { "dependencies": { "@inquirer/ansi": "1.0.2", "@inquirer/core": "10.3.2", "@inquirer/figures": "1.0.15", "@inquirer/type": "3.0.10", "yoctocolors-cjs": "2.1.3" }, "optionalDependencies": { "@types/node": "20.19.41" } }, "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w=="], - - "@inquirer/type": ["@inquirer/type@3.0.10", "", { "optionalDependencies": { "@types/node": "20.19.41" } }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="], - - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "1.5.5", "@jridgewell/trace-mapping": "0.3.31" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "3.1.2", "@jridgewell/sourcemap-codec": "1.5.5" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - - "@manypkg/find-root": ["@manypkg/find-root@1.1.0", "", { "dependencies": { "@babel/runtime": "7.29.2", "@types/node": "12.20.55", "find-up": "4.1.0", "fs-extra": "8.1.0" } }, "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA=="], - - "@manypkg/get-packages": ["@manypkg/get-packages@1.1.3", "", { "dependencies": { "@babel/runtime": "7.29.2", "@changesets/types": "4.1.0", "@manypkg/find-root": "1.1.0", "fs-extra": "8.1.0", "globby": "11.1.0", "read-yaml-file": "1.1.0" } }, "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A=="], - - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "0.10.2" }, "peerDependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], - - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "1.2.0" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], - - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], - - "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "1.20.1" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - - "@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="], - - "@oclif/core": ["@oclif/core@4.11.3", "", { "dependencies": { "ansi-escapes": "4.3.2", "ansis": "3.17.0", "clean-stack": "3.0.1", "cli-spinners": "2.9.2", "debug": "4.4.3", "ejs": "3.1.10", "get-package-type": "0.1.0", "indent-string": "4.0.0", "is-wsl": "2.2.0", "lilconfig": "3.1.3", "minimatch": "10.2.5", "semver": "7.8.0", "string-width": "4.2.3", "supports-color": "8.1.1", "tinyglobby": "0.2.16", "widest-line": "3.1.0", "wordwrap": "1.0.0", "wrap-ansi": "7.0.0" } }, "sha512-gQCSYAtUhJilGKaSaZhqejH9X1dDu+jWQjLmtGOgN/XcKaAEPPSeT2mu1UvlvtPox1/NNRdlBcUa8KRKo2HnJQ=="], - - "@oclif/plugin-autocomplete": ["@oclif/plugin-autocomplete@3.2.49", "", { "dependencies": { "@oclif/core": "4.11.3", "ansis": "3.17.0", "debug": "4.4.3", "ejs": "3.1.10" } }, "sha512-+rrAZ468bW/B9uVrn6sEnFYepy3M1N/BWht8mHzhFIFCIduPSoE+8MweROxZLOGBZrXGWt0iavuPQmy0eaXRfQ=="], - - "@oclif/plugin-help": ["@oclif/plugin-help@6.2.48", "", { "dependencies": { "@oclif/core": "4.11.3" } }, "sha512-nvGLBtUZUWrHfoAEDRsRZUHKVwptyZ6F+MErdVRLQBo3dja0GCZH8DE33dA7mBux2KOmbxGqop15gyud9HZYhQ=="], - - "@oclif/plugin-not-found": ["@oclif/plugin-not-found@3.2.85", "", { "dependencies": { "@inquirer/prompts": "7.10.1", "@oclif/core": "4.11.3", "ansis": "3.17.0", "fast-levenshtein": "3.0.0" } }, "sha512-Si18rRKWknlvQ5anmFbQz9oKBae5/l/Npreuf05xdoNWfOV1J97Z7cpzqBlHbldmxCIiDRgmDKuCBBi4XN6ACA=="], - - "@oclif/plugin-plugins": ["@oclif/plugin-plugins@5.4.67", "", { "dependencies": { "@oclif/core": "4.11.3", "ansis": "3.17.0", "debug": "4.4.3", "npm": "11.14.1", "npm-package-arg": "11.0.3", "npm-run-path": "5.3.0", "object-treeify": "4.0.1", "semver": "7.8.0", "validate-npm-package-name": "5.0.1", "which": "4.0.0", "yarn": "1.22.22" } }, "sha512-hNSNSo3kGxWsU7aRICN82bmpgPkQmjr+SAMrFnlH3v9UchoIG9bBoj5DSSCsoDAShIU118h8xRBOhRyEzq4+Qg=="], - - "@oclif/plugin-warn-if-update-available": ["@oclif/plugin-warn-if-update-available@3.1.65", "", { "dependencies": { "@oclif/core": "4.11.3", "ansis": "3.17.0", "debug": "4.4.3", "http-call": "5.3.0", "lodash": "4.18.1", "registry-auth-token": "5.1.1" } }, "sha512-HcSJc8SeCVUBHwc063xDL0LcpdjcamAISlisSX14VDDYQayMantvtVNOo9PmciwYpXRXfAykeH1z066YkA9JvQ=="], - - "@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], - - "@pnpm/config.env-replace": ["@pnpm/config.env-replace@1.1.0", "", {}, "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w=="], - - "@pnpm/network.ca-file": ["@pnpm/network.ca-file@1.0.2", "", { "dependencies": { "graceful-fs": "4.2.10" } }, "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA=="], - - "@pnpm/npm-conf": ["@pnpm/npm-conf@3.0.2", "", { "dependencies": { "@pnpm/config.env-replace": "1.1.0", "@pnpm/network.ca-file": "1.0.2", "config-chain": "1.1.13" } }, "sha512-h104Kh26rR8tm+a3Qkc5S4VLYint3FE48as7+/5oCEcKR2idC/pF1G6AhIXKI+eHPJa/3J9i5z0Al47IeGHPkA=="], - - "@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="], - - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.17", "", { "os": "android", "cpu": "arm64" }, "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ=="], - - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw=="], - - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw=="], - - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.17", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw=="], - - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm" }, "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ=="], - - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q=="], - - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg=="], - - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA=="], - - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "s390x" }, "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA=="], - - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA=="], - - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw=="], - - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.17", "", { "os": "none", "cpu": "arm64" }, "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA=="], - - "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.17", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "1.1.4" }, "cpu": "none" }, "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA=="], - - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA=="], - - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "x64" }, "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg=="], - - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.17", "", {}, "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg=="], - - "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], - - "@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@3.1.0", "", { "dependencies": { "@typescript-eslint/utils": "8.59.3", "eslint-visitor-keys": "4.2.1", "espree": "10.4.0", "estraverse": "5.3.0", "picomatch": "4.0.4" }, "peerDependencies": { "eslint": "9.39.4" } }, "sha512-pA6VOrOqk0+S8toJYhQGv2MWpQQR0QpeUo9AhNkC49Y26nxBQ/nH1rta9bUU1rPw2fJ1zZEMV5oCX5AazT7J2g=="], - - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + "@beeper/desktop-api": ["@beeper/desktop-api@github:beeper/desktop-api-js#1d94580", {}, "beeper-desktop-api-js-1d94580", "sha512-HVItzImUS2nsk45TXPQEAIhqWU0kK+Og72Wg0mvJbjzy9BqO9f+cQWIcf73B4TCoxb/MEIYB6p4K0nrYBHS4bQ=="], "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], - "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], - - "@types/jsesc": ["@types/jsesc@2.5.1", "", {}, "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw=="], - - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - - "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], - "@types/node": ["@types/node@20.19.41", "", { "dependencies": { "undici-types": "6.21.0" } }, "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ=="], - "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], - - "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "3.2.3" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "20.19.41" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.3", "", { "dependencies": { "@eslint-community/regexpp": "4.12.2", "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/type-utils": "8.59.3", "@typescript-eslint/utils": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3", "ignore": "7.0.5", "natural-compare": "1.4.0", "ts-api-utils": "2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "8.59.3", "eslint": "9.39.4", "typescript": "5.9.3" } }, "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw=="], - - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.3", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3", "debug": "4.4.3" }, "peerDependencies": { "eslint": "9.39.4", "typescript": "5.9.3" } }, "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg=="], - - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.3", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "8.59.3", "@typescript-eslint/types": "8.59.3", "debug": "4.4.3" }, "peerDependencies": { "typescript": "5.9.3" } }, "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng=="], - - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.3", "", { "dependencies": { "@typescript-eslint/types": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3" } }, "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA=="], - - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.3", "", { "peerDependencies": { "typescript": "5.9.3" } }, "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw=="], - - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.3", "", { "dependencies": { "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3", "@typescript-eslint/utils": "8.59.3", "debug": "4.4.3", "ts-api-utils": "2.5.0" }, "peerDependencies": { "eslint": "9.39.4", "typescript": "5.9.3" } }, "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ=="], - - "@typescript-eslint/types": ["@typescript-eslint/types@8.59.3", "", {}, "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg=="], - - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.3", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.3", "@typescript-eslint/tsconfig-utils": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3", "debug": "4.4.3", "minimatch": "10.2.5", "semver": "7.8.0", "tinyglobby": "0.2.16", "ts-api-utils": "2.5.0" }, "peerDependencies": { "typescript": "5.9.3" } }, "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg=="], - - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.3", "", { "dependencies": { "@eslint-community/eslint-utils": "4.9.1", "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3" }, "peerDependencies": { "eslint": "9.39.4", "typescript": "5.9.3" } }, "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg=="], - - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.3", "", { "dependencies": { "@typescript-eslint/types": "8.59.3", "eslint-visitor-keys": "5.0.1" } }, "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg=="], - - "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="], - - "@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.11.1", "", { "os": "android", "cpu": "arm64" }, "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g=="], - - "@unrs/resolver-binding-darwin-arm64": ["@unrs/resolver-binding-darwin-arm64@1.11.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g=="], - - "@unrs/resolver-binding-darwin-x64": ["@unrs/resolver-binding-darwin-x64@1.11.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ=="], - - "@unrs/resolver-binding-freebsd-x64": ["@unrs/resolver-binding-freebsd-x64@1.11.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw=="], - - "@unrs/resolver-binding-linux-arm-gnueabihf": ["@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw=="], - - "@unrs/resolver-binding-linux-arm-musleabihf": ["@unrs/resolver-binding-linux-arm-musleabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw=="], - - "@unrs/resolver-binding-linux-arm64-gnu": ["@unrs/resolver-binding-linux-arm64-gnu@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ=="], - - "@unrs/resolver-binding-linux-arm64-musl": ["@unrs/resolver-binding-linux-arm64-musl@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w=="], - - "@unrs/resolver-binding-linux-ppc64-gnu": ["@unrs/resolver-binding-linux-ppc64-gnu@1.11.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA=="], - - "@unrs/resolver-binding-linux-riscv64-gnu": ["@unrs/resolver-binding-linux-riscv64-gnu@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ=="], - - "@unrs/resolver-binding-linux-riscv64-musl": ["@unrs/resolver-binding-linux-riscv64-musl@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew=="], - - "@unrs/resolver-binding-linux-s390x-gnu": ["@unrs/resolver-binding-linux-s390x-gnu@1.11.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg=="], - - "@unrs/resolver-binding-linux-x64-gnu": ["@unrs/resolver-binding-linux-x64-gnu@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w=="], - - "@unrs/resolver-binding-linux-x64-musl": ["@unrs/resolver-binding-linux-x64-musl@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA=="], - - "@unrs/resolver-binding-wasm32-wasi": ["@unrs/resolver-binding-wasm32-wasi@1.11.1", "", { "dependencies": { "@napi-rs/wasm-runtime": "0.2.12" }, "cpu": "none" }, "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ=="], - - "@unrs/resolver-binding-win32-arm64-msvc": ["@unrs/resolver-binding-win32-arm64-msvc@1.11.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw=="], - - "@unrs/resolver-binding-win32-ia32-msvc": ["@unrs/resolver-binding-win32-ia32-msvc@1.11.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ=="], - - "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], - - "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], - - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "8.16.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - - "ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "3.1.3", "fast-json-stable-stringify": "2.1.0", "json-schema-traverse": "0.4.1", "uri-js": "4.4.1" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], - - "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], - - "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - - "ansis": ["ansis@4.3.0", "", {}, "sha512-44mvgtPvohuU/70DdY5Oz2AIrLJ9k6/5x4KmoSvPwO+5Moijo0+N9D0fKbbYZQWP1hNm5CpOf+E01jhxG/r8xg=="], - - "are-docs-informative": ["are-docs-informative@0.0.2", "", {}, "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig=="], - - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "1.0.4", "is-array-buffer": "3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], - - "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "1.0.9", "call-bound": "1.0.4", "define-properties": "1.2.1", "es-abstract": "1.24.2", "es-object-atoms": "1.1.1", "get-intrinsic": "1.3.0", "is-string": "1.1.1", "math-intrinsics": "1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], - - "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], - - "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "1.0.9", "call-bound": "1.0.4", "define-properties": "1.2.1", "es-abstract": "1.24.2", "es-errors": "1.3.0", "es-object-atoms": "1.1.1", "es-shim-unscopables": "1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="], - - "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "1.0.9", "define-properties": "1.2.1", "es-abstract": "1.24.2", "es-shim-unscopables": "1.1.0" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], - - "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "1.0.9", "define-properties": "1.2.1", "es-abstract": "1.24.2", "es-shim-unscopables": "1.1.0" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], - - "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "1.0.2", "call-bind": "1.0.9", "define-properties": "1.2.1", "es-abstract": "1.24.2", "es-errors": "1.3.0", "get-intrinsic": "1.3.0", "is-array-buffer": "3.0.5" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], - - "ast-kit": ["ast-kit@3.0.0-beta.1", "", { "dependencies": { "@babel/parser": "8.0.0-rc.3", "estree-walker": "3.0.3", "pathe": "2.0.3" } }, "sha512-trmleAnZ2PxN/loHWVhhx1qeOHSRXq4TDsBBxq3GqeJitfk3+jTQ+v/C1km/KYq9M7wKqCewMh+/NAvVH7m+bw=="], + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], - - "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], - - "auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="], - - "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "1.1.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.30", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-xjOFN16Ha1+Rz4nFYKqHU/LSB+gx/Vi3yQLX7r7sAW+Wa+8hhF2h4pvqTrTMc8+WcDBEunnUurr46Jvv0jk3Vg=="], - - "beeper-cli": ["beeper-cli@workspace:packages/npm"], - - "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "1.0.2" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="], - - "birpc": ["birpc@4.0.0", "", {}, "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw=="], - - "brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "1.0.2", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], - - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - - "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "2.10.30", "caniuse-lite": "1.0.30001793", "electron-to-chromium": "1.5.357", "node-releases": "2.0.44", "update-browserslist-db": "1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], - - "builtin-modules": ["builtin-modules@3.3.0", "", {}, "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw=="], - - "builtins": ["builtins@5.1.0", "", { "dependencies": { "semver": "7.8.0" } }, "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg=="], + "beeper-cli": ["beeper-cli@workspace:packages/cli"], "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], - "cac": ["cac@7.0.0", "", {}, "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ=="], - - "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "1.0.2", "es-define-property": "1.0.1", "get-intrinsic": "1.3.0", "set-function-length": "1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], - - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "1.3.0", "function-bind": "1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "1.0.2", "get-intrinsic": "1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - - "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], - "caniuse-lite": ["caniuse-lite@1.0.30001793", "", {}, "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA=="], - - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "4.3.0", "supports-color": "7.2.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], - - "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], - - "clean-regexp": ["clean-regexp@1.0.0", "", { "dependencies": { "escape-string-regexp": "1.0.5" } }, "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw=="], - - "clean-stack": ["clean-stack@3.0.1", "", { "dependencies": { "escape-string-regexp": "4.0.0" } }, "sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg=="], - - "cli-boxes": ["cli-boxes@4.0.1", "", {}, "sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw=="], - - "cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="], - - "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], - - "cli-truncate": ["cli-truncate@6.0.0", "", { "dependencies": { "slice-ansi": "9.0.0", "string-width": "8.2.1" } }, "sha512-3+YKIUFsohD9MIoOFPFBldjAlnfCmCDcqe6aYGFqlDTRKg80p4wg35L+j83QQ63iOlKRccEkbn8IuM++HsgEjA=="], - - "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], - "cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], - "code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "comment-parser": ["comment-parser@1.4.1", "", {}, "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg=="], - - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - - "config-chain": ["config-chain@1.1.13", "", { "dependencies": { "ini": "1.3.8", "proto-list": "1.2.4" } }, "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ=="], - - "confusing-browser-globals": ["confusing-browser-globals@1.0.11", "", {}, "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA=="], - - "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - - "convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="], - - "core-js-compat": ["core-js-compat@3.49.0", "", { "dependencies": { "browserslist": "4.28.2" } }, "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "3.1.1", "shebang-command": "2.0.0", "which": "2.0.2" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - - "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - - "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "1.0.4", "es-errors": "1.3.0", "is-data-view": "1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], - - "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "1.0.4", "es-errors": "1.3.0", "is-data-view": "1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], - - "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "1.0.4", "es-errors": "1.3.0", "is-data-view": "1.0.2" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], - - "dataloader": ["dataloader@1.4.0", "", {}, "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw=="], - - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "2.1.3" }, "optionalDependencies": { "supports-color": "8.1.1" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], - - "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "1.0.1", "es-errors": "1.3.0", "gopd": "1.2.0" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], - - "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "1.1.4", "has-property-descriptors": "1.0.2", "object-keys": "1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], - - "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], - - "detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="], - "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], - "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], - - "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "2.0.3" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], - - "dotenv": ["dotenv@8.6.0", "", {}, "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g=="], - - "dts-resolver": ["dts-resolver@2.1.3", "", {}, "sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw=="], - - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "1.0.2", "es-errors": "1.3.0", "gopd": "1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - - "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "10.9.4" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], - - "electron-to-chromium": ["electron-to-chromium@1.5.357", "", {}, "sha512-NHlTIQDK8fmVwHwuIzmXYEJ1Ewq3D9wDNc0cWXxDGysP6Pb21giwGNkxiTifyKy/4SoPuN5l6GLP1W9Sv7zB2g=="], - "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "empathic": ["empathic@2.0.1", "", {}, "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q=="], + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "5.0.0", "path-exists": "4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], - "enhanced-resolve": ["enhanced-resolve@5.21.3", "", { "dependencies": { "graceful-fs": "4.2.11", "tapable": "2.3.3" } }, "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q=="], - - "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "4.1.3", "strip-ansi": "6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], - - "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], - - "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], - - "es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "1.0.2", "arraybuffer.prototype.slice": "1.0.4", "available-typed-arrays": "1.0.7", "call-bind": "1.0.9", "call-bound": "1.0.4", "data-view-buffer": "1.0.2", "data-view-byte-length": "1.0.2", "data-view-byte-offset": "1.0.1", "es-define-property": "1.0.1", "es-errors": "1.3.0", "es-object-atoms": "1.1.1", "es-set-tostringtag": "2.1.0", "es-to-primitive": "1.3.0", "function.prototype.name": "1.1.8", "get-intrinsic": "1.3.0", "get-proto": "1.0.1", "get-symbol-description": "1.1.0", "globalthis": "1.0.4", "gopd": "1.2.0", "has-property-descriptors": "1.0.2", "has-proto": "1.2.0", "has-symbols": "1.1.0", "hasown": "2.0.3", "internal-slot": "1.1.0", "is-array-buffer": "3.0.5", "is-callable": "1.2.7", "is-data-view": "1.0.2", "is-negative-zero": "2.0.3", "is-regex": "1.2.1", "is-set": "2.0.3", "is-shared-array-buffer": "1.0.4", "is-string": "1.1.1", "is-typed-array": "1.1.15", "is-weakref": "1.1.1", "math-intrinsics": "1.1.0", "object-inspect": "1.13.4", "object-keys": "1.1.1", "object.assign": "4.1.7", "own-keys": "1.0.1", "regexp.prototype.flags": "1.5.4", "safe-array-concat": "1.1.4", "safe-push-apply": "1.0.0", "safe-regex-test": "1.1.0", "set-proto": "1.0.0", "stop-iteration-iterator": "1.1.0", "string.prototype.trim": "1.2.10", "string.prototype.trimend": "1.0.9", "string.prototype.trimstart": "1.0.8", "typed-array-buffer": "1.0.3", "typed-array-byte-length": "1.0.3", "typed-array-byte-offset": "1.0.4", "typed-array-length": "1.0.7", "unbox-primitive": "1.1.0", "which-typed-array": "1.1.20" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="], - - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "1.3.0", "get-intrinsic": "1.3.0", "has-tostringtag": "1.0.2", "hasown": "2.0.3" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "2.0.3" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], + "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], - "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "1.2.7", "is-date-object": "1.1.0", "is-symbol": "1.1.1" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "2.2.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], - "es-toolkit": ["es-toolkit@1.46.1", "", {}, "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ=="], + "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "2.3.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - "eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "4.9.1", "@eslint-community/regexpp": "4.12.2", "@eslint/config-array": "0.21.2", "@eslint/config-helpers": "0.4.2", "@eslint/core": "0.17.0", "@eslint/eslintrc": "3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "0.4.1", "@humanfs/node": "0.16.8", "@humanwhocodes/module-importer": "1.0.1", "@humanwhocodes/retry": "0.4.3", "@types/estree": "1.0.9", "ajv": "6.15.0", "chalk": "4.1.2", "cross-spawn": "7.0.6", "debug": "4.4.3", "escape-string-regexp": "4.0.0", "eslint-scope": "8.4.0", "eslint-visitor-keys": "4.2.1", "espree": "10.4.0", "esquery": "1.7.0", "esutils": "2.0.3", "fast-deep-equal": "3.1.3", "file-entry-cache": "8.0.0", "find-up": "5.0.0", "glob-parent": "6.0.2", "ignore": "5.3.2", "imurmurhash": "0.1.4", "is-glob": "4.0.3", "json-stable-stringify-without-jsonify": "1.0.1", "lodash.merge": "4.6.2", "minimatch": "3.1.5", "natural-compare": "1.4.0", "optionator": "0.9.4" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], + "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], - "eslint-compat-utils": ["eslint-compat-utils@0.5.1", "", { "dependencies": { "semver": "7.8.0" }, "peerDependencies": { "eslint": "9.39.4" } }, "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q=="], + "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], - "eslint-config-oclif": ["eslint-config-oclif@6.0.165", "", { "dependencies": { "@eslint/compat": "1.4.1", "@eslint/eslintrc": "3.3.5", "@eslint/js": "9.39.4", "@stylistic/eslint-plugin": "3.1.0", "@typescript-eslint/eslint-plugin": "8.59.3", "@typescript-eslint/parser": "8.59.3", "eslint-config-oclif": "5.2.2", "eslint-config-xo": "0.49.0", "eslint-config-xo-space": "0.35.0", "eslint-import-resolver-typescript": "3.10.1", "eslint-plugin-import": "2.32.0", "eslint-plugin-jsdoc": "50.8.0", "eslint-plugin-mocha": "10.5.0", "eslint-plugin-n": "17.24.0", "eslint-plugin-perfectionist": "4.15.1", "eslint-plugin-unicorn": "56.0.1", "typescript-eslint": "8.59.3" } }, "sha512-kbzxHAXEHKTY2X4UVVu4cPjjxP2YsVEsgYaXJDakpBEoAUEUSnYCKOOoxrIHl1egDM3q07kOZnBPkwYQ+nR4Og=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": "9.39.4" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="], + "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], - "eslint-config-xo": ["eslint-config-xo@0.49.0", "", { "dependencies": { "@eslint/css": "0.10.0", "@eslint/json": "0.13.2", "@stylistic/eslint-plugin": "5.10.0", "confusing-browser-globals": "1.0.11", "globals": "16.5.0" }, "peerDependencies": { "eslint": "9.39.4" } }, "sha512-hGtD689+fdJxggx1QbEjWfgGOsTasmYqtfk3Rsxru9QyKg2iOhXO2fvR9C7ck8AGw+n2wy6FsA8/MBIzznt5/Q=="], + "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], - "eslint-config-xo-space": ["eslint-config-xo-space@0.35.0", "", { "dependencies": { "eslint-config-xo": "0.44.0" }, "peerDependencies": { "eslint": "9.39.4" } }, "sha512-+79iVcoLi3PvGcjqYDpSPzbLfqYpNcMlhsCBRsnmDoHAn4npJG6YxmHpelQKpXM7v/EeZTUKb4e1xotWlei8KA=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "8.0.0", "is-fullwidth-code-point": "3.0.0", "strip-ansi": "6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.10", "", { "dependencies": { "debug": "3.2.7", "is-core-module": "2.16.2", "resolve": "2.0.0-next.7" } }, "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ=="], + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@3.10.1", "", { "dependencies": { "@nolyfill/is-core-module": "1.0.39", "debug": "4.4.3", "get-tsconfig": "4.14.0", "is-bun-module": "2.0.0", "stable-hash": "0.0.5", "tinyglobby": "0.2.16", "unrs-resolver": "1.11.1" }, "optionalDependencies": { "eslint-plugin-import": "2.32.0" }, "peerDependencies": { "eslint": "9.39.4" } }, "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "3.2.7" }, "optionalDependencies": { "@typescript-eslint/parser": "8.59.3", "eslint": "9.39.4", "eslint-import-resolver-node": "0.3.10", "eslint-import-resolver-typescript": "3.10.1" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="], + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "eslint-plugin-es": ["eslint-plugin-es@4.1.0", "", { "dependencies": { "eslint-utils": "2.1.0", "regexpp": "3.2.0" }, "peerDependencies": { "eslint": "9.39.4" } }, "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ=="], + "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], - "eslint-plugin-es-x": ["eslint-plugin-es-x@7.8.0", "", { "dependencies": { "@eslint-community/eslint-utils": "4.9.1", "@eslint-community/regexpp": "4.12.2", "eslint-compat-utils": "0.5.1" }, "peerDependencies": { "eslint": "9.39.4" } }, "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ=="], + "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "4.3.0", "string-width": "4.2.3", "strip-ansi": "6.0.1" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], - "eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "1.1.0", "array-includes": "3.1.9", "array.prototype.findlastindex": "1.2.6", "array.prototype.flat": "1.3.3", "array.prototype.flatmap": "1.3.3", "debug": "3.2.7", "doctrine": "2.1.0", "eslint-import-resolver-node": "0.3.10", "eslint-module-utils": "2.12.1", "hasown": "2.0.3", "is-core-module": "2.16.2", "is-glob": "4.0.3", "minimatch": "3.1.5", "object.fromentries": "2.0.8", "object.groupby": "1.0.3", "object.values": "1.2.1", "semver": "6.3.1", "string.prototype.trimend": "1.0.9", "tsconfig-paths": "3.15.0" }, "optionalDependencies": { "@typescript-eslint/parser": "8.59.3" }, "peerDependencies": { "eslint": "9.39.4" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="], + "ws": ["ws@8.20.1", "", {}, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="], - "eslint-plugin-jsdoc": ["eslint-plugin-jsdoc@50.8.0", "", { "dependencies": { "@es-joy/jsdoccomment": "0.50.2", "are-docs-informative": "0.0.2", "comment-parser": "1.4.1", "debug": "4.4.3", "escape-string-regexp": "4.0.0", "espree": "10.4.0", "esquery": "1.7.0", "parse-imports-exports": "0.2.4", "semver": "7.8.0", "spdx-expression-parse": "4.0.0" }, "peerDependencies": { "eslint": "9.39.4" } }, "sha512-UyGb5755LMFWPrZTEqqvTJ3urLz1iqj+bYOHFNag+sw3NvaMWP9K2z+uIn37XfNALmQLQyrBlJ5mkiVPL7ADEg=="], + "y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], - "eslint-plugin-mocha": ["eslint-plugin-mocha@10.5.0", "", { "dependencies": { "eslint-utils": "3.0.0", "globals": "13.24.0", "rambda": "7.5.0" }, "peerDependencies": { "eslint": "9.39.4" } }, "sha512-F2ALmQVPT1GoP27O1JTZGrV9Pqg8k79OeIuvw63UxMtQKREZtmkK1NFgkZQ2TW7L2JSSFKHFPTtHu5z8R9QNRw=="], + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], - "eslint-plugin-n": ["eslint-plugin-n@17.24.0", "", { "dependencies": { "@eslint-community/eslint-utils": "4.9.1", "enhanced-resolve": "5.21.3", "eslint-plugin-es-x": "7.8.0", "get-tsconfig": "4.14.0", "globals": "15.15.0", "globrex": "0.1.2", "ignore": "5.3.2", "semver": "7.8.0", "ts-declaration-location": "1.0.7" }, "peerDependencies": { "eslint": "9.39.4" } }, "sha512-/gC7/KAYmfNnPNOb3eu8vw+TdVnV0zhdQwexsw6FLXbhzroVj20vRn2qL8lDWDGnAQ2J8DhdfvXxX9EoxvERvw=="], - - "eslint-plugin-perfectionist": ["eslint-plugin-perfectionist@4.15.1", "", { "dependencies": { "@typescript-eslint/types": "8.59.3", "@typescript-eslint/utils": "8.59.3", "natural-orderby": "5.0.0" }, "peerDependencies": { "eslint": "9.39.4" } }, "sha512-MHF0cBoOG0XyBf7G0EAFCuJJu4I18wy0zAoT1OHfx2o6EOx1EFTIzr2HGeuZa1kDcusoX0xJ9V7oZmaeFd773Q=="], - - "eslint-plugin-unicorn": ["eslint-plugin-unicorn@56.0.1", "", { "dependencies": { "@babel/helper-validator-identifier": "7.28.5", "@eslint-community/eslint-utils": "4.9.1", "ci-info": "4.4.0", "clean-regexp": "1.0.0", "core-js-compat": "3.49.0", "esquery": "1.7.0", "globals": "15.15.0", "indent-string": "4.0.0", "is-builtin-module": "3.2.1", "jsesc": "3.1.0", "pluralize": "8.0.0", "read-pkg-up": "7.0.1", "regexp-tree": "0.1.27", "regjsparser": "0.10.0", "semver": "7.8.0", "strip-indent": "3.0.0" }, "peerDependencies": { "eslint": "9.39.4" } }, "sha512-FwVV0Uwf8XPfVnKSGpMg7NtlZh0G0gBarCaFcMUOoqPxXryxdYxTRRv4kH6B9TFCVIrjRXG+emcxIk2ayZilog=="], - - "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "4.3.0", "estraverse": "5.3.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], - - "eslint-utils": ["eslint-utils@3.0.0", "", { "dependencies": { "eslint-visitor-keys": "2.1.0" }, "peerDependencies": { "eslint": "9.39.4" } }, "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA=="], - - "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], - - "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "8.16.0", "acorn-jsx": "5.3.2", "eslint-visitor-keys": "4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], - - "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], - - "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "5.3.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], - - "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "5.3.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], - - "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "1.0.9" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - - "extendable-error": ["extendable-error@0.1.7", "", {}, "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg=="], - - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "@nodelib/fs.walk": "1.2.8", "glob-parent": "5.1.2", "merge2": "1.4.1", "micromatch": "4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - - "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - - "fast-levenshtein": ["fast-levenshtein@3.0.0", "", { "dependencies": { "fastest-levenshtein": "1.0.16" } }, "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ=="], - - "fastest-levenshtein": ["fastest-levenshtein@1.0.16", "", {}, "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg=="], - - "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "1.1.0" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], - - "fdir": ["fdir@6.5.0", "", { "optionalDependencies": { "picomatch": "4.0.4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - - "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "2.1.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], - - "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "4.0.1" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], - - "filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "5.1.9" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="], - - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - - "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "6.0.0", "path-exists": "4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - - "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "3.4.2", "keyv": "4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], - - "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], - - "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], - - "fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "4.2.11", "jsonfile": "4.0.0", "universalify": "0.1.2" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "1.0.9", "call-bound": "1.0.4", "define-properties": "1.2.1", "functions-have-names": "1.2.3", "hasown": "2.0.3", "is-callable": "1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], - - "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], - - "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], - - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - - "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "1.0.2", "es-define-property": "1.0.1", "es-errors": "1.3.0", "es-object-atoms": "1.1.1", "function-bind": "1.1.2", "get-proto": "1.0.1", "gopd": "1.2.0", "has-symbols": "1.1.0", "hasown": "2.0.3", "math-intrinsics": "1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-package-type": ["get-package-type@0.1.0", "", {}, "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "1.0.1", "es-object-atoms": "1.1.1" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - - "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "1.0.4", "es-errors": "1.3.0", "get-intrinsic": "1.3.0" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], - - "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], - - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - - "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], - - "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "1.2.1", "gopd": "1.2.0" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], - - "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "2.1.0", "dir-glob": "3.0.1", "fast-glob": "3.3.3", "ignore": "5.3.2", "merge2": "1.4.1", "slash": "3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], - - "globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="], - - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - - "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], - - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - - "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "1.0.1" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], - - "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "1.0.1" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], - - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "1.1.0" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - - "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], - - "hookable": ["hookable@6.1.1", "", {}, "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ=="], - - "hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "10.4.3" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], - - "http-call": ["http-call@5.3.0", "", { "dependencies": { "content-type": "1.0.5", "debug": "4.4.3", "is-retry-allowed": "1.2.0", "is-stream": "2.0.1", "parse-json": "4.0.0", "tunnel-agent": "0.6.0" } }, "sha512-ahwimsC23ICE4kPl9xTBjKB4inbRaeLyZeRunC/1Jy/Z6X8tv22MEAjK+KBOMSVLaqXPTTmd8638waVIKLGx2w=="], - - "human-id": ["human-id@4.1.3", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q=="], - - "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": "2.1.2" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], - - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "1.0.1", "resolve-from": "4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], - - "import-without-cache": ["import-without-cache@0.3.3", "", {}, "sha512-bDxwDdF04gm550DfZHgffvlX+9kUlcz32UD0AeBTmVPFiWkrexF2XVmiuFFbDhiFuP8fQkrkvI2KdSNPYWAXkQ=="], - - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - - "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], - - "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - - "ink": ["ink@7.0.3", "", { "dependencies": { "@alcalzone/ansi-tokenize": "0.3.0", "ansi-escapes": "7.3.0", "ansi-styles": "6.2.3", "auto-bind": "5.0.1", "chalk": "5.6.2", "cli-boxes": "4.0.1", "cli-cursor": "4.0.0", "cli-truncate": "6.0.0", "code-excerpt": "4.0.0", "es-toolkit": "1.46.1", "indent-string": "5.0.0", "is-in-ci": "2.0.0", "patch-console": "2.0.0", "react-reconciler": "0.33.0", "scheduler": "0.27.0", "signal-exit": "3.0.7", "slice-ansi": "9.0.0", "stack-utils": "2.0.6", "string-width": "8.2.1", "terminal-size": "4.0.1", "type-fest": "5.6.0", "widest-line": "6.0.0", "wrap-ansi": "10.0.0", "ws": "8.20.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@types/react": "19.2.14" }, "peerDependencies": { "react": "19.2.6" } }, "sha512-5kxHkIj9+RuqCU3zyvP4qvYWNOSHP2TW/SHayHGHOmk87KwfVcZwvJGemi9ch+ci2gXUqerK/Eh2DGEDt5q45g=="], - - "ink-spinner": ["ink-spinner@5.0.0", "", { "dependencies": { "cli-spinners": "2.9.2" }, "peerDependencies": { "ink": "7.0.3", "react": "19.2.6" } }, "sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA=="], - - "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "1.3.0", "hasown": "2.0.3", "side-channel": "1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], - - "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "1.0.9", "call-bound": "1.0.4", "get-intrinsic": "1.3.0" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], - - "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], - - "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "1.0.0", "call-bound": "1.0.4", "get-proto": "1.0.1", "has-tostringtag": "1.0.2", "safe-regex-test": "1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], - - "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "1.1.0" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], - - "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "1.0.4", "has-tostringtag": "1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], - - "is-builtin-module": ["is-builtin-module@3.2.1", "", { "dependencies": { "builtin-modules": "3.3.0" } }, "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A=="], - - "is-bun-module": ["is-bun-module@2.0.0", "", { "dependencies": { "semver": "7.8.0" } }, "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ=="], - - "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], - - "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], - - "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "1.0.4", "get-intrinsic": "1.3.0", "is-typed-array": "1.1.15" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], - - "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "1.0.4", "has-tostringtag": "1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], - - "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], - - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "1.0.4" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], - - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "1.0.4", "generator-function": "2.0.1", "get-proto": "1.0.1", "has-tostringtag": "1.0.2", "safe-regex-test": "1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - - "is-in-ci": ["is-in-ci@2.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w=="], - - "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], - - "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], - - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - - "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "1.0.4", "has-tostringtag": "1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], - - "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "1.0.4", "gopd": "1.2.0", "has-tostringtag": "1.0.2", "hasown": "2.0.3" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], - - "is-retry-allowed": ["is-retry-allowed@1.2.0", "", {}, "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg=="], - - "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], - - "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "1.0.4" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], - - "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], - - "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "1.0.4", "has-tostringtag": "1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], - - "is-subdir": ["is-subdir@1.2.0", "", { "dependencies": { "better-path-resolve": "1.0.0" } }, "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw=="], - - "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "1.0.4", "has-symbols": "1.1.0", "safe-regex-test": "1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], - - "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "1.1.20" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], - - "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], - - "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], - - "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "1.0.4" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], - - "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "1.0.4", "get-intrinsic": "1.3.0" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], - - "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], - - "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "2.2.1" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - - "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], - - "isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], - - "jake": ["jake@10.9.4", "", { "dependencies": { "async": "3.2.6", "filelist": "1.0.6", "picocolors": "1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="], - - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "jsdoc-type-pratt-parser": ["jsdoc-type-pratt-parser@4.1.0", "", {}, "sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg=="], - - "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], - - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], - - "json-parse-better-errors": ["json-parse-better-errors@1.0.2", "", {}, "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="], - - "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], - - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - - "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], - - "json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "1.2.8" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], - - "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "4.2.11" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], - - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - - "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "1.2.1", "type-check": "0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], - - "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], - - "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], - - "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - - "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], - - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - - "lodash.startcase": ["lodash.startcase@4.4.0", "", {}, "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg=="], - - "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "mdn-data": ["mdn-data@2.23.0", "", {}, "sha512-786vq1+4079JSeu2XdcDjrhi/Ry7BWtjDl9WtGPWLiIHb2T66GvIVflZTBoSNZ5JqTtJGYEVMuFA/lbQlMOyDQ=="], - - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "3.0.3", "picomatch": "2.3.2" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - - "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - - "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], - - "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "1.1.14" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - - "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], - - "napi-postinstall": ["napi-postinstall@0.3.4", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="], - - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - - "natural-orderby": ["natural-orderby@5.0.0", "", {}, "sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg=="], - - "node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "1.3.3", "es-errors": "1.3.0", "object.entries": "1.1.9", "semver": "6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="], - - "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "5.0.0" } }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - - "node-releases": ["node-releases@2.0.44", "", {}, "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ=="], - - "normalize-package-data": ["normalize-package-data@2.5.0", "", { "dependencies": { "hosted-git-info": "2.8.9", "resolve": "1.22.12", "semver": "5.7.2", "validate-npm-package-license": "3.0.4" } }, "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA=="], - - "npm": ["npm@11.14.1", "", { "bin": { "npm": "bin/npm-cli.js", "npx": "bin/npx-cli.js" } }, "sha512-aopNZ0eEl6LbxoFcrXLmTEPzNBNxfiQnVgR9RmJBqzm+5h5pFoOmRljpRJbsXxocBeSl7GLcx3MoDf2UlEOjZw=="], - - "npm-package-arg": ["npm-package-arg@11.0.3", "", { "dependencies": { "hosted-git-info": "7.0.2", "proc-log": "4.2.0", "semver": "7.8.0", "validate-npm-package-name": "5.0.1" } }, "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw=="], - - "npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], - - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - - "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], - - "object-treeify": ["object-treeify@4.0.1", "", {}, "sha512-Y6tg5rHfsefSkfKujv2SwHulInROy/rCL5F4w0QOWxut8AnxYxf0YmNhTh95Zfyxpsudo66uqkux0ACFnyMSgQ=="], - - "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "1.0.9", "call-bound": "1.0.4", "define-properties": "1.2.1", "es-object-atoms": "1.1.1", "has-symbols": "1.1.0", "object-keys": "1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], - - "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "1.0.9", "call-bound": "1.0.4", "define-properties": "1.2.1", "es-object-atoms": "1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="], - - "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "1.0.9", "define-properties": "1.2.1", "es-abstract": "1.24.2", "es-object-atoms": "1.1.1" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], - - "object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "1.0.9", "define-properties": "1.2.1", "es-abstract": "1.24.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="], - - "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "1.0.9", "call-bound": "1.0.4", "define-properties": "1.2.1", "es-object-atoms": "1.1.1" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], - - "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], - - "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], - - "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "0.1.4", "fast-levenshtein": "2.0.6", "levn": "0.4.1", "prelude-ls": "1.2.1", "type-check": "0.4.0", "word-wrap": "1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], - - "outdent": ["outdent@0.5.0", "", {}, "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q=="], - - "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "1.3.0", "object-keys": "1.1.1", "safe-push-apply": "1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], - - "p-filter": ["p-filter@2.1.0", "", { "dependencies": { "p-map": "2.1.0" } }, "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw=="], - - "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], - - "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "3.1.0" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - - "p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="], - - "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], - - "package-manager-detector": ["package-manager-detector@0.2.11", "", { "dependencies": { "quansync": "0.2.11" } }, "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ=="], - - "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "3.1.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], - - "parse-imports-exports": ["parse-imports-exports@0.2.4", "", { "dependencies": { "parse-statements": "1.0.11" } }, "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ=="], - - "parse-json": ["parse-json@4.0.0", "", { "dependencies": { "error-ex": "1.3.4", "json-parse-better-errors": "1.0.2" } }, "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw=="], - - "parse-statements": ["parse-statements@1.0.11", "", {}, "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA=="], - - "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="], - - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - - "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], - - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], - - "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], - - "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="], - - "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], - - "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - - "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - - "prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], - - "proc-log": ["proc-log@4.2.0", "", {}, "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA=="], - - "proto-list": ["proto-list@1.2.4", "", {}, "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="], - - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - - "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], - - "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], - - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - - "rambda": ["rambda@7.5.0", "", {}, "sha512-y/M9weqWAH4iopRd7EHDEQQvpFPHj1AA3oHozE9tfITHUtTR7Z9PSlIRRG2l1GuW7sefC1cXFfIcF+cgnShdBA=="], - - "react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="], - - "react-reconciler": ["react-reconciler@0.33.0", "", { "dependencies": { "scheduler": "0.27.0" }, "peerDependencies": { "react": "19.2.6" } }, "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA=="], - - "read-pkg": ["read-pkg@5.2.0", "", { "dependencies": { "@types/normalize-package-data": "2.4.4", "normalize-package-data": "2.5.0", "parse-json": "5.2.0", "type-fest": "0.6.0" } }, "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg=="], - - "read-pkg-up": ["read-pkg-up@7.0.1", "", { "dependencies": { "find-up": "4.1.0", "read-pkg": "5.2.0", "type-fest": "0.8.1" } }, "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg=="], - - "read-yaml-file": ["read-yaml-file@1.1.0", "", { "dependencies": { "graceful-fs": "4.2.11", "js-yaml": "3.14.2", "pify": "4.0.1", "strip-bom": "3.0.0" } }, "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA=="], - - "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "1.0.9", "define-properties": "1.2.1", "es-abstract": "1.24.2", "es-errors": "1.3.0", "es-object-atoms": "1.1.1", "get-intrinsic": "1.3.0", "get-proto": "1.0.1", "which-builtin-type": "1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], - - "regexp-tree": ["regexp-tree@0.1.27", "", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="], - - "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "1.0.9", "define-properties": "1.2.1", "es-errors": "1.3.0", "get-proto": "1.0.1", "gopd": "1.2.0", "set-function-name": "2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], - - "regexpp": ["regexpp@3.2.0", "", {}, "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg=="], - - "registry-auth-token": ["registry-auth-token@5.1.1", "", { "dependencies": { "@pnpm/npm-conf": "3.0.2" } }, "sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q=="], - - "regjsparser": ["regjsparser@0.10.0", "", { "dependencies": { "jsesc": "0.5.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA=="], - - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - - "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], - - "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "1.3.0", "is-core-module": "2.16.2", "path-parse": "1.0.7", "supports-preserve-symlinks-flag": "1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], - - "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], - - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - - "restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "5.1.2", "signal-exit": "3.0.7" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="], - - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - - "rolldown": ["rolldown@1.0.0-rc.17", "", { "dependencies": { "@oxc-project/types": "0.127.0", "@rolldown/pluginutils": "1.0.0-rc.17" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-x64": "1.0.0-rc.17", "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA=="], - - "rolldown-plugin-dts": ["rolldown-plugin-dts@0.23.2", "", { "dependencies": { "@babel/generator": "8.0.0-rc.3", "@babel/helper-validator-identifier": "8.0.0-rc.3", "@babel/parser": "8.0.0-rc.3", "@babel/types": "8.0.0-rc.3", "ast-kit": "3.0.0-beta.1", "birpc": "4.0.0", "dts-resolver": "2.1.3", "get-tsconfig": "4.14.0", "obug": "2.1.1", "picomatch": "4.0.4" }, "optionalDependencies": { "typescript": "5.9.3" }, "peerDependencies": { "rolldown": "1.0.0-rc.17" } }, "sha512-PbSqLawLgZBGcOGT3yqWBGn4cX+wh2nt5FuBGdcMHyOhoukmjbhYAl8NT9sE4U38Cm9tqLOIQeOrvzeayM0DLQ=="], - - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "1.2.3" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - - "safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "1.0.9", "call-bound": "1.0.4", "get-intrinsic": "1.3.0", "has-symbols": "1.1.0", "isarray": "2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="], - - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "1.3.0", "isarray": "2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], - - "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "1.0.4", "es-errors": "1.3.0", "is-regex": "1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - - "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - - "semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], - - "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], - - "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "1.1.4", "es-errors": "1.3.0", "function-bind": "1.1.2", "get-intrinsic": "1.3.0", "gopd": "1.2.0", "has-property-descriptors": "1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], - - "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "1.1.4", "es-errors": "1.3.0", "functions-have-names": "1.2.3", "has-property-descriptors": "1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], - - "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "1.0.1", "es-errors": "1.3.0", "es-object-atoms": "1.1.1" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], - - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - - "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "1.3.0", "object-inspect": "1.13.4", "side-channel-list": "1.0.1", "side-channel-map": "1.0.1", "side-channel-weakmap": "1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - - "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "1.3.0", "object-inspect": "1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], - - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "1.0.4", "es-errors": "1.3.0", "get-intrinsic": "1.3.0", "object-inspect": "1.13.4" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], - - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "1.0.4", "es-errors": "1.3.0", "get-intrinsic": "1.3.0", "object-inspect": "1.13.4", "side-channel-map": "1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - - "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - - "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - - "slice-ansi": ["slice-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "6.2.3", "is-fullwidth-code-point": "5.1.0" } }, "sha512-SO/3iYL5S3W57LLEniscOGPZgOqZUPCx6d3dB+52B80yJ0XstzsC/eV8gnA4tM3MHDrKz+OCFSLNjswdSC+/bA=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "spawndamnit": ["spawndamnit@3.0.1", "", { "dependencies": { "cross-spawn": "7.0.6", "signal-exit": "4.1.0" } }, "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg=="], - - "spdx-correct": ["spdx-correct@3.2.0", "", { "dependencies": { "spdx-expression-parse": "3.0.1", "spdx-license-ids": "3.0.23" } }, "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA=="], - - "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="], - - "spdx-expression-parse": ["spdx-expression-parse@4.0.0", "", { "dependencies": { "spdx-exceptions": "2.5.0", "spdx-license-ids": "3.0.23" } }, "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ=="], - - "spdx-license-ids": ["spdx-license-ids@3.0.23", "", {}, "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw=="], - - "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], - - "stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="], - - "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], - - "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "1.3.0", "internal-slot": "1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], - - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "8.0.0", "is-fullwidth-code-point": "3.0.0", "strip-ansi": "6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "1.0.9", "call-bound": "1.0.4", "define-data-property": "1.1.4", "define-properties": "1.2.1", "es-abstract": "1.24.2", "es-object-atoms": "1.1.1", "has-property-descriptors": "1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], - - "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "1.0.9", "call-bound": "1.0.4", "define-properties": "1.2.1", "es-object-atoms": "1.1.1" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], - - "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "1.0.9", "define-properties": "1.2.1", "es-object-atoms": "1.1.1" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], - - "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], - - "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "1.0.1" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], - - "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - - "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - - "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], - - "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], - - "term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="], - - "terminal-size": ["terminal-size@4.0.1", "", {}, "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ=="], - - "tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="], - - "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "6.5.0", "picomatch": "4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], - - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - - "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], - - "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], - - "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": "5.9.3" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], - - "ts-declaration-location": ["ts-declaration-location@1.0.7", "", { "dependencies": { "picomatch": "4.0.4" }, "peerDependencies": { "typescript": "5.9.3" } }, "sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA=="], - - "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "0.0.29", "json5": "1.0.2", "minimist": "1.2.8", "strip-bom": "3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], - - "tsdown": ["tsdown@0.21.10", "", { "dependencies": { "ansis": "4.3.0", "cac": "7.0.0", "defu": "6.1.7", "empathic": "2.0.1", "hookable": "6.1.1", "import-without-cache": "0.3.3", "obug": "2.1.1", "picomatch": "4.0.4", "rolldown": "1.0.0-rc.17", "rolldown-plugin-dts": "0.23.2", "semver": "7.8.0", "tinyexec": "1.1.2", "tinyglobby": "0.2.16", "tree-kill": "1.2.2", "unconfig-core": "7.5.0", "unrun": "0.2.39" }, "optionalDependencies": { "typescript": "5.9.3" }, "bin": { "tsdown": "dist/run.mjs" } }, "sha512-3wk73yBhZe/wX7REqSdivNQ84TDs1mJ+IlnzrrEREP70xlJ/AEIzqaI04l/TzMKVIdkTdC3CPaADn2Lk/0SkdA=="], - - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], - - "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - - "type-fest": ["type-fest@5.6.0", "", { "dependencies": { "tagged-tag": "1.0.0" } }, "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA=="], - - "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "1.0.4", "es-errors": "1.3.0", "is-typed-array": "1.1.15" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], - - "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "1.0.9", "for-each": "0.3.5", "gopd": "1.2.0", "has-proto": "1.2.0", "is-typed-array": "1.1.15" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], - - "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "1.0.7", "call-bind": "1.0.9", "for-each": "0.3.5", "gopd": "1.2.0", "has-proto": "1.2.0", "is-typed-array": "1.1.15", "reflect.getprototypeof": "1.0.10" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], - - "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "1.0.9", "for-each": "0.3.5", "gopd": "1.2.0", "is-typed-array": "1.1.15", "possible-typed-array-names": "1.1.0", "reflect.getprototypeof": "1.0.10" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "typescript-eslint": ["typescript-eslint@8.59.3", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.3", "@typescript-eslint/parser": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3", "@typescript-eslint/utils": "8.59.3" }, "peerDependencies": { "eslint": "9.39.4", "typescript": "5.9.3" } }, "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg=="], - - "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "1.0.4", "has-bigints": "1.1.0", "has-symbols": "1.1.0", "which-boxed-primitive": "1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], - - "unconfig-core": ["unconfig-core@7.5.0", "", { "dependencies": { "@quansync/fs": "1.0.0", "quansync": "1.0.0" } }, "sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w=="], - - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], - - "unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "0.3.4" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="], - - "unrun": ["unrun@0.2.39", "", { "dependencies": { "rolldown": "1.0.0-rc.17" }, "bin": { "unrun": "dist/cli.mjs" } }, "sha512-h9FxYVpztY/wwq+bauLOh6Y3CWu2IVeRLq5lxzneBiIU9Tn86OGp9xiQrGhnYspAmg5dzdY0Cc8+Y70kuTARCg=="], - - "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "3.2.0", "picocolors": "1.1.1" }, "peerDependencies": { "browserslist": "4.28.2" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], - - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "2.3.1" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - - "validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "3.2.0", "spdx-expression-parse": "3.0.1" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="], - - "validate-npm-package-name": ["validate-npm-package-name@5.0.1", "", {}, "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ=="], - - "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - - "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "0.0.3", "webidl-conversions": "3.0.1" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - - "which": ["which@4.0.0", "", { "dependencies": { "isexe": "3.1.5" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], - - "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "1.1.0", "is-boolean-object": "1.2.2", "is-number-object": "1.1.1", "is-string": "1.1.1", "is-symbol": "1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], - - "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "1.0.4", "function.prototype.name": "1.1.8", "has-tostringtag": "1.0.2", "is-async-function": "2.1.1", "is-date-object": "1.1.0", "is-finalizationregistry": "1.1.1", "is-generator-function": "1.1.2", "is-regex": "1.2.1", "is-weakref": "1.1.1", "isarray": "2.0.5", "which-boxed-primitive": "1.1.1", "which-collection": "1.0.2", "which-typed-array": "1.1.20" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], - - "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "2.0.3", "is-set": "2.0.3", "is-weakmap": "2.0.2", "is-weakset": "2.0.4" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], - - "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], - - "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "1.0.7", "call-bind": "1.0.9", "call-bound": "1.0.4", "for-each": "0.3.5", "get-proto": "1.0.1", "gopd": "1.2.0", "has-tostringtag": "1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], - - "widest-line": ["widest-line@3.1.0", "", { "dependencies": { "string-width": "4.2.3" } }, "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg=="], - - "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - - "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], - - "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "4.3.0", "string-width": "4.2.3", "strip-ansi": "6.0.1" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "ws": ["ws@8.20.1", "", {}, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="], - - "y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], - - "yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], + "yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], "yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], - - "yarn": ["yarn@1.22.22", "", { "bin": { "yarn": "bin/yarn.js", "yarnpkg": "bin/yarn.js" } }, "sha512-prL3kGtyG7o9Z9Sv8IPfBNrWTDmXB4Qbes8A9rEzt6wkJV8mUvoirjU0Mp3GGAU06Y0XQyA3/2/RQFVuK7MTfg=="], - - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - - "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], - - "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], - - "@alcalzone/ansi-tokenize/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "1.6.0" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], - - "@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@8.0.0-rc.3", "", {}, "sha512-8AWCJ2VJJyDFlGBep5GpaaQ9AAaE/FjAcrqI7jyssYhtL7WGV0DOKpJsQqM037xDbpRLHXsY8TwU7zDma7coOw=="], - - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - - "@eslint/css/@eslint/core": ["@eslint/core@0.14.0", "", { "dependencies": { "@types/json-schema": "7.0.15" } }, "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg=="], - - "@eslint/css/@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.5", "", { "dependencies": { "@eslint/core": "0.15.2", "levn": "0.4.1" } }, "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w=="], - - "@eslint/json/@eslint/core": ["@eslint/core@0.15.2", "", { "dependencies": { "@types/json-schema": "7.0.15" } }, "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg=="], - - "@eslint/json/@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.5", "", { "dependencies": { "@eslint/core": "0.15.2", "levn": "0.4.1" } }, "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w=="], - - "@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - - "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "4.3.0", "string-width": "4.2.3", "strip-ansi": "6.0.1" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], - - "@manypkg/find-root/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], - - "@manypkg/find-root/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "5.0.0", "path-exists": "4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], - - "@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "4.2.11", "jsonfile": "4.0.0", "universalify": "0.1.2" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], - - "@manypkg/get-packages/@changesets/types": ["@changesets/types@4.1.0", "", {}, "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw=="], - - "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "4.2.11", "jsonfile": "4.0.0", "universalify": "0.1.2" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], - - "@oclif/core/ansis": ["ansis@3.17.0", "", {}, "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg=="], - - "@oclif/core/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "5.0.6" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - - "@oclif/plugin-autocomplete/ansis": ["ansis@3.17.0", "", {}, "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg=="], - - "@oclif/plugin-not-found/ansis": ["ansis@3.17.0", "", {}, "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg=="], - - "@oclif/plugin-plugins/ansis": ["ansis@3.17.0", "", {}, "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg=="], - - "@oclif/plugin-warn-if-update-available/ansis": ["ansis@3.17.0", "", {}, "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg=="], - - "@pnpm/network.ca-file/graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="], - - "@quansync/fs/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="], - - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "5.0.6" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - - "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], - - "@unrs/resolver-binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@tybys/wasm-util": "0.10.2" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], - - "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], - - "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "clean-regexp/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], - - "cli-truncate/string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "1.6.0", "strip-ansi": "7.2.0" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], - - "cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "4.3.0", "string-width": "4.2.3", "strip-ansi": "6.0.1" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], - - "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - - "eslint-config-oclif/eslint-config-oclif": ["eslint-config-oclif@5.2.2", "", { "dependencies": { "eslint-config-xo-space": "0.35.0", "eslint-plugin-mocha": "10.5.0", "eslint-plugin-n": "15.7.0", "eslint-plugin-unicorn": "48.0.1" } }, "sha512-NNTyyolSmKJicgxtoWZ/hoy2Rw56WIoWCFxgnBkXqDgi9qPKMwZs2Nx2b6SHLJvCiWWhZhWr5V46CFPo3PSPag=="], - - "eslint-config-xo/@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.10.0", "", { "dependencies": { "@eslint-community/eslint-utils": "4.9.1", "@typescript-eslint/types": "8.59.3", "eslint-visitor-keys": "4.2.1", "espree": "10.4.0", "estraverse": "5.3.0", "picomatch": "4.0.4" }, "peerDependencies": { "eslint": "9.39.4" } }, "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ=="], - - "eslint-config-xo/globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], - - "eslint-config-xo-space/eslint-config-xo": ["eslint-config-xo@0.44.0", "", { "dependencies": { "confusing-browser-globals": "1.0.11" }, "peerDependencies": { "eslint": "9.39.4" } }, "sha512-YG4gdaor0mJJi8UBeRJqDPO42MedTWYMaUyucF5bhm2pi/HS98JIxfFQmTLuyj6hGpQlAazNfyVnn7JuDn+Sew=="], - - "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - - "eslint-import-resolver-node/resolve": ["resolve@2.0.0-next.7", "", { "dependencies": { "es-errors": "1.3.0", "is-core-module": "2.16.2", "node-exports-info": "1.6.0", "object-keys": "1.1.1", "path-parse": "1.0.7", "supports-preserve-symlinks-flag": "1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ=="], - - "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - - "eslint-plugin-es/eslint-utils": ["eslint-utils@2.1.0", "", { "dependencies": { "eslint-visitor-keys": "1.3.0" } }, "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg=="], - - "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - - "eslint-plugin-import/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "eslint-plugin-mocha/globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="], - - "eslint-plugin-n/globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="], - - "eslint-plugin-unicorn/globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="], - - "eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@2.1.0", "", {}, "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw=="], - - "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "4.0.3" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "2.1.0" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], - - "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - - "ink/ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "1.1.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], - - "ink/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - - "ink/indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], - - "ink/string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "1.6.0", "strip-ansi": "7.2.0" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], - - "ink/widest-line": ["widest-line@6.0.0", "", { "dependencies": { "string-width": "8.2.1" } }, "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA=="], - - "ink/wrap-ansi": ["wrap-ansi@10.0.0", "", { "dependencies": { "ansi-styles": "6.2.3", "string-width": "8.2.1", "strip-ansi": "7.2.0" } }, "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ=="], - - "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - - "node-exports-info/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "normalize-package-data/hosted-git-info": ["hosted-git-info@2.8.9", "", {}, "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="], - - "normalize-package-data/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], - - "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], - - "optionator/fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - - "read-pkg/parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "7.29.0", "error-ex": "1.3.4", "json-parse-even-better-errors": "2.3.1", "lines-and-columns": "1.2.4" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], - - "read-pkg/type-fest": ["type-fest@0.6.0", "", {}, "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg=="], - - "read-pkg-up/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "5.0.0", "path-exists": "4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], - - "read-pkg-up/type-fest": ["type-fest@0.8.1", "", {}, "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA=="], - - "read-yaml-file/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "1.0.10", "esprima": "4.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], - - "regjsparser/jsesc": ["jsesc@0.5.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA=="], - - "rolldown-plugin-dts/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@8.0.0-rc.3", "", {}, "sha512-8AWCJ2VJJyDFlGBep5GpaaQ9AAaE/FjAcrqI7jyssYhtL7WGV0DOKpJsQqM037xDbpRLHXsY8TwU7zDma7coOw=="], - - "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "1.6.0" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], - - "spawndamnit/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - - "spdx-correct/spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "2.5.0", "spdx-license-ids": "3.0.23" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], - - "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], - - "unconfig-core/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="], - - "validate-npm-package-license/spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "2.5.0", "spdx-license-ids": "3.0.23" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], - - "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "5.0.0", "path-exists": "4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], - - "@eslint/css/@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.15.2", "", { "dependencies": { "@types/json-schema": "7.0.15" } }, "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg=="], - - "@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "@manypkg/find-root/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], - - "@oclif/core/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "4.0.4" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], - - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "4.0.4" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], - - "cli-truncate/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], - - "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "eslint-config-oclif/eslint-config-oclif/eslint-plugin-n": ["eslint-plugin-n@15.7.0", "", { "dependencies": { "builtins": "5.1.0", "eslint-plugin-es": "4.1.0", "eslint-utils": "3.0.0", "ignore": "5.3.2", "is-core-module": "2.16.2", "minimatch": "3.1.5", "resolve": "1.22.12", "semver": "7.8.0" }, "peerDependencies": { "eslint": "9.39.4" } }, "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q=="], - - "eslint-config-oclif/eslint-config-oclif/eslint-plugin-unicorn": ["eslint-plugin-unicorn@48.0.1", "", { "dependencies": { "@babel/helper-validator-identifier": "7.28.5", "@eslint-community/eslint-utils": "4.9.1", "ci-info": "3.9.0", "clean-regexp": "1.0.0", "esquery": "1.7.0", "indent-string": "4.0.0", "is-builtin-module": "3.2.1", "jsesc": "3.1.0", "lodash": "4.18.1", "pluralize": "8.0.0", "read-pkg-up": "7.0.1", "regexp-tree": "0.1.27", "regjsparser": "0.10.0", "semver": "7.8.0", "strip-indent": "3.0.0" }, "peerDependencies": { "eslint": "9.39.4" } }, "sha512-FW+4r20myG/DqFcCSzoumaddKBicIPeFnTrifon2mWIzlfyvzwyqZjqVP7m4Cqr/ZYisS2aiLghkUWaPg6vtCw=="], - - "eslint-plugin-es/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@1.3.0", "", {}, "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ=="], - - "eslint-plugin-mocha/globals/type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], - - "filelist/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "1.0.2" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], - - "ink/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], - - "ink/wrap-ansi/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], - - "read-pkg-up/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], - - "read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "1.0.3" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - - "yargs/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], - - "@manypkg/find-root/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "2.3.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - - "@oclif/core/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - - "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - - "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - - "eslint-config-oclif/eslint-config-oclif/eslint-plugin-unicorn/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], - - "ink/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - - "ink/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - - "read-pkg-up/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "2.3.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - - "yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "2.3.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - - "@manypkg/find-root/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "2.2.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], - - "read-pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "2.2.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], - - "yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "2.2.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], } } diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index 294d3b6a..00000000 --- a/docs/.gitignore +++ /dev/null @@ -1,16 +0,0 @@ -# build output -dist/ -# generated types -.astro/ -# dependencies -node_modules/ -# environment -.env -.env.production -# logs -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -# macOS -.DS_Store diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs deleted file mode 100644 index 313d8f39..00000000 --- a/docs/astro.config.mjs +++ /dev/null @@ -1,100 +0,0 @@ -// @ts-check -import { defineConfig } from 'astro/config'; -import starlight from '@astrojs/starlight'; - -// Fully static export. `output: 'static'` is Astro's default; it is set -// explicitly here so the intent is obvious and `astro build` always emits a -// self-contained `./dist` you can drop on any static host or CDN. -// -// When you pick a home for the docs, set `site` to the canonical origin (used -// for sitemap + canonical URLs) and, if serving from a sub-path, set `base`. -export default defineConfig({ - site: 'https://beeper.github.io', - base: '/cli/', - output: 'static', - trailingSlash: 'always', - integrations: [ - starlight({ - title: 'Beeper CLI', - description: - 'One CLI for all your chats — WhatsApp, iMessage, Telegram, Signal, Discord and more, shaped for scripts, agents, and humans in a hurry.', - tagline: 'One CLI for all your chats. Built for you and your agent.', - logo: { - src: './src/assets/logo.svg', - replacesTitle: false, - }, - social: [ - { - icon: 'github', - label: 'GitHub', - href: 'https://github.com/beeper/cli', - }, - ], - editLink: { - baseUrl: - 'https://github.com/beeper/cli/edit/main/docs/', - }, - customCss: ['./src/styles/theme.css'], - // Starlight ships full-text search (Pagefind) and dark mode by default. - sidebar: [ - { - label: 'Start here', - items: [ - { label: 'Overview', link: '/' }, - { label: 'Install', link: '/install/' }, - { label: 'Connect a target', link: '/connect/' }, - { label: 'Quick start', link: '/quickstart/' }, - ], - }, - { - label: 'Targets & accounts', - items: [ - { label: 'Targets', link: '/targets/' }, - { label: 'Bridges & accounts', link: '/accounts/' }, - { label: 'Auth & verification', link: '/auth/' }, - ], - }, - { - label: 'Messaging', - items: [ - { label: 'Chats', link: '/chats/' }, - { label: 'Messages', link: '/messages/' }, - { label: 'Sending', link: '/send/' }, - { label: 'Contacts', link: '/contacts/' }, - { label: 'Media', link: '/media/' }, - { label: 'Export', link: '/export/' }, - { label: 'Presence', link: '/presence/' }, - ], - }, - { - label: 'Automation & agents', - items: [ - { label: 'Output & scripting', link: '/scripting/' }, - { label: 'Watch (live events)', link: '/watch/' }, - { label: 'RPC', link: '/rpc/' }, - { label: 'Raw API access', link: '/api/' }, - { label: 'Exit codes', link: '/exit-codes/' }, - ], - }, - { - label: 'Reference', - items: [ - { label: 'Configuration', link: '/config/' }, - { label: 'Plugins', link: '/plugins/' }, - { label: 'Updating', link: '/update/' }, - { - label: 'Full command reference', - link: 'https://github.com/beeper/cli/blob/main/packages/cli/README.md', - attrs: { target: '_blank' }, - }, - { - label: 'Desktop API reference', - link: 'https://developers.beeper.com/desktop-api-reference', - attrs: { target: '_blank' }, - }, - ], - }, - ], - }), - ], -}); diff --git a/docs/bun.lock b/docs/bun.lock deleted file mode 100644 index 5aa85d3f..00000000 --- a/docs/bun.lock +++ /dev/null @@ -1,871 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "@beeper/cli-docs", - "dependencies": { - "@astrojs/starlight": "^0.39.2", - "astro": "^6.4.2", - "sharp": "^0.34.5", - }, - }, - }, - "packages": { - "@astrojs/compiler": ["@astrojs/compiler@4.0.0", "", {}, "sha512-eouss7G8ygdZqHuke033VMcVw5HTZUu+PXd/h06DGDUg/jt5btPYPqh66ENWw/mU78rBrf/oeC4oqoBwMtDMNA=="], - - "@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.0", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.1.1", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-Ry2R3VPeIN4uPCSA4xQc+e+vsJXkalKpEbDc07hV+a/o5Bs2N/s/uDcPJH/05L19DKh9tAy7e6JM3YZ6Cxfezw=="], - - "@astrojs/markdown-remark": ["@astrojs/markdown-remark@7.2.0", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.0", "@astrojs/prism": "4.0.2", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-+YxmVQu1Bd+MFfSzjq1rOJvD9+nIOJzz5YIIhdIH01RrxRkKbyKoEgyIqP3yv51MhzMDgd79QaPv+kCVPT8vHw=="], - - "@astrojs/mdx": ["@astrojs/mdx@5.0.6", "", { "dependencies": { "@astrojs/markdown-remark": "7.1.2", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.16.0", "es-module-lexer": "^2.0.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^6.0.0" } }, "sha512-4dKe0ZMmqujofPNDHahzClkwinn9f8jHPcaXcgdGvPAlboD2mjzkUCofli2cBnxYAkdfhC6d50gBJ8i/cH8gHw=="], - - "@astrojs/prism": ["@astrojs/prism@4.0.2", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA=="], - - "@astrojs/sitemap": ["@astrojs/sitemap@3.7.3", "", { "dependencies": { "sitemap": "^9.0.0", "stream-replace-string": "^2.0.0", "zod": "^4.3.6" } }, "sha512-f8euLVsyeAmAkSm/1M2Kb8sL8byQmfgbvBNaHFItCheTj/IpiJYSEWVcqDHZ/yEHxiS7+w87mQkzwZaPHmk5GA=="], - - "@astrojs/starlight": ["@astrojs/starlight@0.39.2", "", { "dependencies": { "@astrojs/markdown-remark": "^7.1.1", "@astrojs/mdx": "^5.0.4", "@astrojs/sitemap": "^3.7.2", "@pagefind/default-ui": "^1.3.0", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", "@types/mdast": "^4.0.4", "astro-expressive-code": "^0.42.0", "bcp-47": "^2.1.0", "hast-util-from-html": "^2.0.3", "hast-util-select": "^6.0.4", "hast-util-to-string": "^3.0.1", "hastscript": "^9.0.1", "i18next": "^26.0.7", "js-yaml": "^4.1.1", "klona": "^2.0.6", "magic-string": "^0.30.21", "mdast-util-directive": "^3.1.0", "mdast-util-to-markdown": "^2.1.2", "mdast-util-to-string": "^4.0.0", "pagefind": "^1.3.0", "rehype": "^13.0.2", "rehype-format": "^5.0.1", "remark-directive": "^4.0.0", "ultrahtml": "^1.6.0", "unified": "^11.0.5", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^6.0.0" } }, "sha512-vlw+bwnjtf5buCTUtLU7JfV6D3knslxqnspr6LKs6hfRuFZiyr5hT44F7GyDqR9FKANUqFxnIzWM81F1k/kOUA=="], - - "@astrojs/telemetry": ["@astrojs/telemetry@3.3.2", "", { "dependencies": { "ci-info": "^4.4.0", "dset": "^3.1.4", "is-docker": "^4.0.0", "is-wsl": "^3.1.1", "which-pm-runs": "^1.1.0" } }, "sha512-j8DNruA8ors99Al39RYZPJK4DC1bKkoNm93mAMuBhY9TCNC4R8n1q7ovFnJ5qhGh5Lsh7pa1gpQVpYpsJPeTHQ=="], - - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - - "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - - "@capsizecss/unpack": ["@capsizecss/unpack@4.0.0", "", { "dependencies": { "fontkitten": "^1.0.0" } }, "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA=="], - - "@clack/core": ["@clack/core@1.4.0", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-7Wctjq6f7c1CPz8sPpkwUnz8yRgVANkpNupb81q432FjcJg4l+Sw7XANdNSdWfAKq0IHI0JTcUeK5dxs/HrGPw=="], - - "@clack/prompts": ["@clack/prompts@1.5.0", "", { "dependencies": { "@clack/core": "1.4.0", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-wKh+wTjmrUoUdkZg8KpJO5X+p9PWV+KE9mePseq9UYWkukgTKsGS47RRL2HstwVcvDQH+PenrPJWII8+MfiiyA=="], - - "@ctrl/tinycolor": ["@ctrl/tinycolor@4.2.0", "", {}, "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A=="], - - "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], - - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], - - "@expressive-code/core": ["@expressive-code/core@0.42.0", "", { "dependencies": { "@ctrl/tinycolor": "^4.0.4", "hast-util-select": "^6.0.2", "hast-util-to-html": "^9.0.1", "hast-util-to-text": "^4.0.1", "hastscript": "^9.0.0", "postcss": "^8.4.38", "postcss-nested": "^6.0.1", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1" } }, "sha512-MN11+9nfmaC7sYu2BZJXAXqwkBRt8t1xTSqP+Ti1NfTEskgl6xUnzDxoaiQkg0BMzpglA0pys4dpDKquP/cyIw=="], - - "@expressive-code/plugin-frames": ["@expressive-code/plugin-frames@0.42.0", "", { "dependencies": { "@expressive-code/core": "^0.42.0" } }, "sha512-XtkPm+941Uta7Y+81Acv+OA/20F1NJmJhCX6UYGKpqEIGqplNh3PTOhcURp6tcruhlzJcWcvpWy6Oigz3SrjqA=="], - - "@expressive-code/plugin-shiki": ["@expressive-code/plugin-shiki@0.42.0", "", { "dependencies": { "@expressive-code/core": "^0.42.0", "shiki": "^4.0.2" } }, "sha512-PMKey/kLmewttAHQezL+Y5Fx3vVssfDi3+FJOYQQS2mXP3tQspFELtKKAfsXfmSXdToZYgwoO69HJndqfE+09g=="], - - "@expressive-code/plugin-text-markers": ["@expressive-code/plugin-text-markers@0.42.0", "", { "dependencies": { "@expressive-code/core": "^0.42.0" } }, "sha512-l59lUx8fq1v5g6SpmbDjiU0+7IdfbiWnAyRmtTVSpfhyq+nZMN4UcmYyu2b9Mynhzt7Gr+O+cXyEPDNb2AVWVQ=="], - - "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], - - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], - - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], - - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], - - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], - - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], - - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], - - "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], - - "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], - - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], - - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], - - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], - - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], - - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], - - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], - - "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], - - "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], - - "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], - - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], - - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], - - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], - - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], - - "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], - - "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], - - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - - "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], - - "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], - - "@pagefind/darwin-arm64": ["@pagefind/darwin-arm64@1.5.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ=="], - - "@pagefind/darwin-x64": ["@pagefind/darwin-x64@1.5.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw=="], - - "@pagefind/default-ui": ["@pagefind/default-ui@1.5.2", "", {}, "sha512-pm1LMnQg8N2B3n2TnjKlhaFihpz6zTiA4HiGQ6/slKO/+8K9CAU5kcjdSSPgpuk1PMuuN4hxLipUIifnrkl3Sg=="], - - "@pagefind/freebsd-x64": ["@pagefind/freebsd-x64@1.5.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA=="], - - "@pagefind/linux-arm64": ["@pagefind/linux-arm64@1.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw=="], - - "@pagefind/linux-x64": ["@pagefind/linux-x64@1.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA=="], - - "@pagefind/windows-arm64": ["@pagefind/windows-arm64@1.5.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g=="], - - "@pagefind/windows-x64": ["@pagefind/windows-x64@1.5.2", "", { "os": "win32", "cpu": "x64" }, "sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg=="], - - "@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="], - - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.4", "", { "os": "android", "cpu": "arm" }, "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ=="], - - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.4", "", { "os": "android", "cpu": "arm64" }, "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw=="], - - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.4", "", { "os": "linux", "cpu": "arm" }, "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.4", "", { "os": "linux", "cpu": "arm" }, "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A=="], - - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ=="], - - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw=="], - - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg=="], - - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg=="], - - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA=="], - - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.4", "", { "os": "none", "cpu": "arm64" }, "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA=="], - - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw=="], - - "@shikijs/core": ["@shikijs/core@4.1.0", "", { "dependencies": { "@shikijs/primitive": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ=="], - - "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-YquhawCUgaBfhsS72e2Y/dI59gCBNPHu3fEO/tvLaXrTssxZrY5ddjtNLTwndrMgPo8b3IscE+xoICDzpTmlFQ=="], - - "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-axLpjVs45YBvvINa+dJF+NPW+KtFkNXsFr4SDw2BMj9GdeMnGxVB9PQb2xXlJYovslt/nz6giedAyOANkfc7hg=="], - - "@shikijs/langs": ["@shikijs/langs@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0" } }, "sha512-nwOMruEkbgdZfQ/b8CgpNBVOpvG1k0N5tbmgiFeqsan401+x3ILqlzZJowSla4Agmq4hG2Uf2wh5jLTEhR8VSg=="], - - "@shikijs/primitive": ["@shikijs/primitive@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zx2/2Uwj2q9X3KSyYREEhXO23xBw5WUhP4orK2lE4r+t9JGITmEe0JH+wPmJhqHpOT2bRRs6lAL945+LDvOAGw=="], - - "@shikijs/themes": ["@shikijs/themes@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0" } }, "sha512-emCcTnUM7yO2wltYbaxm+yLvcCI4+h8XBKc4KmJ7EZUXoSGjcCHifkI//R4OFit9ewpg7H2/9tjOuXrT2v/Knw=="], - - "@shikijs/types": ["@shikijs/types@4.1.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA=="], - - "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], - - "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - - "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], - - "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], - - "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], - - "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], - - "@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], - - "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - - "@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="], - - "@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="], - - "@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="], - - "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - - "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="], - - "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], - - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - - "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], - - "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], - - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], - - "array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="], - - "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], - - "astro": ["astro@6.4.2", "", { "dependencies": { "@astrojs/compiler": "^4.0.0", "@astrojs/internal-helpers": "0.10.0", "@astrojs/markdown-remark": "7.2.0", "@astrojs/telemetry": "3.3.2", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^1.1.1", "devalue": "^5.6.3", "diff": "^8.0.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.27.3", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.4", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.1.1", "jsonc-parser": "^3.3.1", "magic-string": "^0.30.21", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", "rehype": "^13.0.2", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "svgo": "^4.0.1", "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "ultrahtml": "^1.6.0", "unifont": "~0.7.4", "unist-util-visit": "^5.1.0", "unstorage": "^1.17.5", "vfile": "^6.0.3", "vite": "^7.3.2", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.3.6" }, "optionalDependencies": { "sharp": "^0.34.0" }, "bin": { "astro": "./bin/astro.mjs" } }, "sha512-8H89CH2dKL5SCU99OCqdU9BGjmPkSJqaPurywj5XMo7eMFGUFD3vsNhdEKnEh4mK4LgGje3/QDTTSIIGst0G0Q=="], - - "astro-expressive-code": ["astro-expressive-code@0.42.0", "", { "dependencies": { "rehype-expressive-code": "^0.42.0" }, "peerDependencies": { "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta" } }, "sha512-aiTePi2Cn0mJPYWZSzP1GcxCinX9mNtJyCCshVVPSg1yRwM7ADvFJOx0FnS440M9t65hp8JH//dc2qr22Bm4ag=="], - - "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], - - "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], - - "bcp-47": ["bcp-47@2.1.0", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w=="], - - "bcp-47-match": ["bcp-47-match@2.0.3", "", {}, "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ=="], - - "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], - - "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], - - "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], - - "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], - - "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], - - "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], - - "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], - - "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], - - "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - - "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], - - "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], - - "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], - - "common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="], - - "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], - - "cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="], - - "crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], - - "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], - - "css-selector-parser": ["css-selector-parser@3.3.0", "", {}, "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g=="], - - "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], - - "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], - - "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], - - "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="], - - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], - - "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], - - "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], - - "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], - - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - - "devalue": ["devalue@5.8.1", "", {}, "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw=="], - - "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], - - "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], - - "direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="], - - "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], - - "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], - - "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], - - "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], - - "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], - - "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - - "es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="], - - "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], - - "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], - - "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], - - "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], - - "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], - - "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="], - - "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], - - "estree-util-scope": ["estree-util-scope@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="], - - "estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="], - - "estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="], - - "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], - - "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], - - "expressive-code": ["expressive-code@0.42.0", "", { "dependencies": { "@expressive-code/core": "^0.42.0", "@expressive-code/plugin-frames": "^0.42.0", "@expressive-code/plugin-shiki": "^0.42.0", "@expressive-code/plugin-text-markers": "^0.42.0" } }, "sha512-V5DtJLEKuj4wf9O6IRtPtRObkMVy2ggR+S0MdjrTw6m58krZnDioyhW1si3Y04c5YPeooP4nd85Yq9NwEVHS4g=="], - - "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], - - "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], - - "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], - - "fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="], - - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - - "flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="], - - "fontace": ["fontace@0.4.1", "", { "dependencies": { "fontkitten": "^1.0.2" } }, "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw=="], - - "fontkitten": ["fontkitten@1.0.3", "", { "dependencies": { "tiny-inflate": "^1.0.3" } }, "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "get-tsconfig": ["get-tsconfig@5.0.0-beta.4", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ=="], - - "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], - - "h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="], - - "hast-util-embedded": ["hast-util-embedded@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-is-element": "^3.0.0" } }, "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA=="], - - "hast-util-format": ["hast-util-format@1.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-minify-whitespace": "^1.0.0", "hast-util-phrasing": "^3.0.0", "hast-util-whitespace": "^3.0.0", "html-whitespace-sensitive-tag-names": "^3.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA=="], - - "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="], - - "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], - - "hast-util-has-property": ["hast-util-has-property@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA=="], - - "hast-util-is-body-ok-link": ["hast-util-is-body-ok-link@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ=="], - - "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="], - - "hast-util-minify-whitespace": ["hast-util-minify-whitespace@1.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-is-element": "^3.0.0", "hast-util-whitespace": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw=="], - - "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], - - "hast-util-phrasing": ["hast-util-phrasing@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-has-property": "^3.0.0", "hast-util-is-body-ok-link": "^3.0.0", "hast-util-is-element": "^3.0.0" } }, "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ=="], - - "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], - - "hast-util-select": ["hast-util-select@6.0.4", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "bcp-47-match": "^2.0.0", "comma-separated-tokens": "^2.0.0", "css-selector-parser": "^3.0.0", "devlop": "^1.0.0", "direction": "^2.0.0", "hast-util-has-property": "^3.0.0", "hast-util-to-string": "^3.0.0", "hast-util-whitespace": "^3.0.0", "nth-check": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw=="], - - "hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="], - - "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], - - "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], - - "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], - - "hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="], - - "hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="], - - "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], - - "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], - - "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], - - "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], - - "html-whitespace-sensitive-tag-names": ["html-whitespace-sensitive-tag-names@3.0.1", "", {}, "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA=="], - - "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], - - "i18next": ["i18next@26.3.0", "", { "peerDependencies": { "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-gHSgGpUXVmuqE2El1W61DmxeyeTlFfZgdJRWMo9jScAn5pu7TuTuiccb1zh3E2J9hEBVGJ23+96x0ieBhfuIHA=="], - - "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], - - "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], - - "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], - - "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], - - "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], - - "is-docker": ["is-docker@4.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA=="], - - "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], - - "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], - - "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - - "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], - - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], - - "klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="], - - "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], - - "lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="], - - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - - "magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="], - - "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], - - "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], - - "mdast-util-definitions": ["mdast-util-definitions@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="], - - "mdast-util-directive": ["mdast-util-directive@3.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q=="], - - "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], - - "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], - - "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], - - "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], - - "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], - - "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], - - "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], - - "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], - - "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="], - - "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], - - "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], - - "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], - - "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], - - "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], - - "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], - - "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], - - "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], - - "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], - - "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], - - "micromark-extension-directive": ["micromark-extension-directive@4.0.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "parse-entities": "^4.0.0" } }, "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg=="], - - "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], - - "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], - - "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], - - "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], - - "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], - - "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], - - "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], - - "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="], - - "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="], - - "micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="], - - "micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="], - - "micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="], - - "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], - - "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], - - "micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="], - - "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], - - "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], - - "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], - - "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], - - "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], - - "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], - - "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], - - "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], - - "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], - - "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], - - "micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="], - - "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], - - "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], - - "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], - - "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], - - "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], - - "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], - - "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], - - "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], - - "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="], - - "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], - - "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], - - "node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="], - - "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], - - "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], - - "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], - - "ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], - - "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], - - "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="], - - "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], - - "p-limit": ["p-limit@7.3.0", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw=="], - - "p-queue": ["p-queue@9.3.0", "", { "dependencies": { "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" } }, "sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang=="], - - "p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="], - - "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], - - "pagefind": ["pagefind@1.5.2", "", { "optionalDependencies": { "@pagefind/darwin-arm64": "1.5.2", "@pagefind/darwin-x64": "1.5.2", "@pagefind/freebsd-x64": "1.5.2", "@pagefind/linux-arm64": "1.5.2", "@pagefind/linux-x64": "1.5.2", "@pagefind/windows-arm64": "1.5.2", "@pagefind/windows-x64": "1.5.2" }, "bin": { "pagefind": "lib/runner/bin.cjs" } }, "sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q=="], - - "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], - - "parse-latin": ["parse-latin@7.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="], - - "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], - - "piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], - - "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], - - "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], - - "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], - - "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], - - "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], - - "radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="], - - "readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], - - "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], - - "recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], - - "recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="], - - "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], - - "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], - - "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], - - "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], - - "rehype": ["rehype@13.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", "unified": "^11.0.0" } }, "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A=="], - - "rehype-expressive-code": ["rehype-expressive-code@0.42.0", "", { "dependencies": { "expressive-code": "^0.42.0" } }, "sha512-8rp/1YMEVVSYbtz+bFBx+uSx3vA4i4T8RwRm5Q/IWbucQnnQqQ0hDqtmKOr8tv+59Cik6cu5aH3WPo0I7csuTA=="], - - "rehype-format": ["rehype-format@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-format": "^1.0.0" } }, "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ=="], - - "rehype-parse": ["rehype-parse@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-html": "^2.0.0", "unified": "^11.0.0" } }, "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag=="], - - "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], - - "rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="], - - "rehype-stringify": ["rehype-stringify@10.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-html": "^9.0.0", "unified": "^11.0.0" } }, "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA=="], - - "remark-directive": ["remark-directive@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-directive": "^3.0.0", "micromark-extension-directive": "^4.0.0", "unified": "^11.0.0" } }, "sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA=="], - - "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], - - "remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="], - - "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], - - "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], - - "remark-smartypants": ["remark-smartypants@3.0.2", "", { "dependencies": { "retext": "^9.0.0", "retext-smartypants": "^6.0.0", "unified": "^11.0.4", "unist-util-visit": "^5.0.0" } }, "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA=="], - - "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], - - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - - "retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="], - - "retext-latin": ["retext-latin@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="], - - "retext-smartypants": ["retext-smartypants@6.2.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ=="], - - "retext-stringify": ["retext-stringify@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" } }, "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA=="], - - "rollup": ["rollup@4.60.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.4", "@rollup/rollup-android-arm64": "4.60.4", "@rollup/rollup-darwin-arm64": "4.60.4", "@rollup/rollup-darwin-x64": "4.60.4", "@rollup/rollup-freebsd-arm64": "4.60.4", "@rollup/rollup-freebsd-x64": "4.60.4", "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", "@rollup/rollup-linux-arm-musleabihf": "4.60.4", "@rollup/rollup-linux-arm64-gnu": "4.60.4", "@rollup/rollup-linux-arm64-musl": "4.60.4", "@rollup/rollup-linux-loong64-gnu": "4.60.4", "@rollup/rollup-linux-loong64-musl": "4.60.4", "@rollup/rollup-linux-ppc64-gnu": "4.60.4", "@rollup/rollup-linux-ppc64-musl": "4.60.4", "@rollup/rollup-linux-riscv64-gnu": "4.60.4", "@rollup/rollup-linux-riscv64-musl": "4.60.4", "@rollup/rollup-linux-s390x-gnu": "4.60.4", "@rollup/rollup-linux-x64-gnu": "4.60.4", "@rollup/rollup-linux-x64-musl": "4.60.4", "@rollup/rollup-openbsd-x64": "4.60.4", "@rollup/rollup-openharmony-arm64": "4.60.4", "@rollup/rollup-win32-arm64-msvc": "4.60.4", "@rollup/rollup-win32-ia32-msvc": "4.60.4", "@rollup/rollup-win32-x64-gnu": "4.60.4", "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g=="], - - "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], - - "semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - - "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], - - "shiki": ["shiki@4.1.0", "", { "dependencies": { "@shikijs/core": "4.1.0", "@shikijs/engine-javascript": "4.1.0", "@shikijs/engine-oniguruma": "4.1.0", "@shikijs/langs": "4.1.0", "@shikijs/themes": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-l/ABZPUR5v70jI10EzqfMS/I96vjSGv2y0ihUV+WYFzv0EfvW4s54m0Lg8wCrrL+2IkwBzFTuxkZjPf8b2NX9Q=="], - - "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], - - "sitemap": ["sitemap@9.0.1", "", { "dependencies": { "@types/node": "^24.9.2", "@types/sax": "^1.2.1", "arg": "^5.0.0", "sax": "^1.4.1" }, "bin": { "sitemap": "dist/esm/cli.js" } }, "sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ=="], - - "smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="], - - "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], - - "stream-replace-string": ["stream-replace-string@2.0.0", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="], - - "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], - - "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], - - "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], - - "svgo": ["svgo@4.0.1", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w=="], - - "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], - - "tinyclip": ["tinyclip@0.1.13", "", {}, "sha512-8OqlXQ35euK9+e7L68u8UwcODxkHoIkjbGsgXuARKNyQ5G6xt8nw1YPeMbxMLgCPFkToU+UEK5j05t2t8edKpQ=="], - - "tinyexec": ["tinyexec@1.2.3", "", {}, "sha512-g62dB+w1/OEFnPvmX0yd/HnetYITOL+1nJW7kitOycOeAvmbWC/nu0fwmmQ/kupNojqExzyC/T++pST/jRJ2mQ=="], - - "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], - - "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], - - "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], - - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], - - "ultrahtml": ["ultrahtml@1.6.0", "", {}, "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw=="], - - "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], - - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], - - "unifont": ["unifont@0.7.4", "", { "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg=="], - - "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], - - "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], - - "unist-util-modify-children": ["unist-util-modify-children@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "array-iterate": "^2.0.0" } }, "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw=="], - - "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], - - "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="], - - "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], - - "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], - - "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], - - "unist-util-visit-children": ["unist-util-visit-children@3.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA=="], - - "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], - - "unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="], - - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - - "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], - - "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], - - "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], - - "vite": ["vite@7.3.3", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA=="], - - "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], - - "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], - - "which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="], - - "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], - - "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], - - "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], - - "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - - "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - - "@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@7.1.2", "", { "dependencies": { "@astrojs/internal-helpers": "0.9.1", "@astrojs/prism": "4.0.2", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "js-yaml": "^4.1.1", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "retext-smartypants": "^6.2.0", "shiki": "^4.0.0", "smol-toml": "^1.6.0", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-caXZ4Dc2St2dW8luEg22GlP0gupLdztCTQE4EzZOxW1pqWXz9mbeJEuHUkgDYcKWW8tjIHkydYDhWLVoxJ327Q=="], - - "@mdx-js/mdx/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - - "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - - "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], - - "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - - "estree-util-build-jsx/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - - "is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], - - "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - - "@astrojs/mdx/@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.9.1", "", { "dependencies": { "picomatch": "^4.0.4" } }, "sha512-1pWuARqYom/TzuU3+0ZugsTrKlUydWKuULmDqSMTuonY+9IRDUEGKX/8PXQ1nBxRq3w85uGtd9q9SXfqEldMIQ=="], - - "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], - } -} diff --git a/docs/package.json b/docs/package.json deleted file mode 100644 index 6a69220a..00000000 --- a/docs/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@beeper/cli-docs", - "private": true, - "type": "module", - "version": "0.0.0", - "description": "Documentation site for the Beeper CLI (Astro Starlight, fully static export).", - "scripts": { - "dev": "astro dev", - "start": "astro dev", - "build": "astro build", - "preview": "astro preview", - "check": "astro check" - }, - "dependencies": { - "@astrojs/starlight": "^0.39.2", - "astro": "^6.4.2", - "sharp": "^0.34.5" - } -} diff --git a/docs/public/favicon.svg b/docs/public/favicon.svg deleted file mode 100644 index 1648cdfc..00000000 --- a/docs/public/favicon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/docs/src/assets/logo.svg b/docs/src/assets/logo.svg deleted file mode 100644 index baf84afc..00000000 --- a/docs/src/assets/logo.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/docs/src/content.config.ts b/docs/src/content.config.ts deleted file mode 100644 index 6a7b7a02..00000000 --- a/docs/src/content.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineCollection } from 'astro:content'; -import { docsLoader } from '@astrojs/starlight/loaders'; -import { docsSchema } from '@astrojs/starlight/schema'; - -export const collections = { - docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), -}; diff --git a/docs/src/content/docs/accounts.mdx b/docs/src/content/docs/accounts.mdx deleted file mode 100644 index 7dae136a..00000000 --- a/docs/src/content/docs/accounts.mdx +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: Bridges & accounts -description: List or add chat-network accounts (WhatsApp, Discord, iMessage, …), choose a default account for --account-filtered commands, or remove one. -sidebar: - label: Bridges & accounts ---- - -import { Aside } from '@astrojs/starlight/components'; - -A **bridge** is the connector used to add or reconnect a chat account. An -**account** is a signed-in instance of a network on your target. - - - -## Commands - -```sh -beeper bridges list -beeper bridges show - -beeper accounts list [--account SELECTOR]... [--ids] -beeper accounts add [bridge] [--flow ID] [--login-id ID] [--cookie name=value]... [--field id=value]... [--webview] [--non-interactive] [--no-guided] -beeper accounts show -beeper accounts use # "" clears defaultAccount -beeper accounts remove -``` - -## Account selectors - -An **account selector** matches by account ID, network name, bridge type/id, or -user identity (display name, username, email, phone). A network name can expand -to multiple matching accounts. - -## Notes - -- `bridges list` is the scriptable catalog; `accounts add` is the guided account - connection flow. `accounts add` without a bridge opens the connection chooser. -- `accounts use NAME` persists `defaultAccount` in CLI config. Subsequent - account-scoped commands fall back to that default when `--account` is omitted. - `accounts use ""` clears it. -- `accounts list --json` annotates the default account with `default: true`. -- For non-interactive sign-in, pass `--flow`, `--field`, and `--cookie`, and add - `--non-interactive` to fail instead of prompting. -- For cookie-based sign-in, `--webview` can use Bun.WebView with Chrome to - collect cookie fields before falling back to prompts. Chrome remote debugging - must be enabled for a visible interactive tab; otherwise Bun may spawn a - headless browser. - -## Examples - -```sh -beeper accounts list --json -beeper bridges list -beeper accounts add local-whatsapp -beeper accounts add discord --non-interactive --cookie sessionid=… -beeper accounts add discord --webview --webview-backend chrome -beeper accounts use whatsapp-main -beeper accounts use "" -beeper accounts show whatsapp-main --json -beeper accounts remove whatsapp-main -``` diff --git a/docs/src/content/docs/api.mdx b/docs/src/content/docs/api.mdx deleted file mode 100644 index 5e85ede1..00000000 --- a/docs/src/content/docs/api.mdx +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: Raw API access -description: Call raw Desktop API endpoints the CLI doesn't yet wrap with a workflow command. -sidebar: - label: Raw API access ---- - -import { Aside } from '@astrojs/starlight/components'; - - - -## Commands - -```sh -beeper api get [--no-auth] -beeper api post [--body JSON] [--no-auth] -beeper api request [--body JSON] [--no-auth] -``` - -## Notes - -- `` is a Desktop API path, e.g. `/v1/info` or `/v1/chats/{chatID}/read`. -- `--no-auth` calls a public path without the bearer token. -- `--body` is sent as `application/json`; default is `{}` for `post`. -- `api request` lets you hit `GET | POST | PUT | PATCH | DELETE`; the others are - convenience shortcuts. -- `--read-only` blocks `api post` / `api put` / `api patch` / `api delete` / - `api request `. - -## Examples - -```sh -beeper api get /v1/info -beeper api get /v1/chats --json -beeper api post /v1/chats/abc/read --body '{"messageID":"x"}' -beeper api request PATCH /v1/chats/abc --body '{"isPinned":true}' -beeper api request DELETE /v1/chats/abc/messages/def/reactions --body '{"reactionKey":"👍"}' -``` - -## See also - -The full Desktop API surface is documented at the -[Beeper Desktop API reference](https://developers.beeper.com/desktop-api-reference). diff --git a/docs/src/content/docs/auth.mdx b/docs/src/content/docs/auth.mdx deleted file mode 100644 index b32a6583..00000000 --- a/docs/src/content/docs/auth.mdx +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: Auth & verification -description: Check sign-in status, clear stored tokens, or drive an end-to-end device-verification flow for encrypted messages. -sidebar: - label: Auth & verification ---- - -import { Aside } from '@astrojs/starlight/components'; - -`auth` commands inspect and manage CLI-side authentication state and -encryption-readiness. The selected target's stored OAuth token lives in the -target file under `~/.beeper/targets/`; `BEEPER_ACCESS_TOKEN` overrides it. - - - -## Commands - -```sh -beeper auth status -beeper auth logout -beeper auth verify [--user @id] # interactive happy-path -beeper auth verify start [--user @id] # individual steps -beeper auth verify status -beeper auth verify list | show -beeper auth verify approve [--id active] [--code …] -beeper auth verify sas -beeper auth verify sas-confirm -beeper auth verify qr-scan --payload -beeper auth verify qr-confirm -beeper auth verify recovery-key [--code KEY] -beeper auth verify reset-recovery-key -beeper auth verify cancel -``` - -## Notes - -- `auth status` reports the token source (env vs. target file) and metadata; it - does not call the network. -- `auth logout` revokes the token at the Desktop OAuth endpoint and clears the - local copy. -- `auth verify` (no subcommand) walks the most common SAS/emoji verification - flow interactively. -- For agents, drive the explicit subcommands (`start` → `sas` → `sas-confirm`) - and use `--json` to inspect state. -- `verify status` returns the encryption-readiness state (`ready`, - `needs-verification`, `verification-in-progress`). -- `recovery-key` and `reset-recovery-key` apply to the encrypted-messages key, - not to Beeper account login. - -## Examples - -```sh -beeper auth status --json -beeper auth verify -beeper auth verify recovery-key --code ABCD-EFGH-IJKL-MNOP -beeper auth verify reset-recovery-key -beeper auth logout -``` - - diff --git a/docs/src/content/docs/chats.mdx b/docs/src/content/docs/chats.mdx deleted file mode 100644 index 7e0fc70d..00000000 --- a/docs/src/content/docs/chats.mdx +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: Chats -description: List, search, inspect, and change chat state — archive, pin, mute, mark-read, priority, rename, draft, focus, disappearing timer, reminders. ---- - -import { Aside } from '@astrojs/starlight/components'; - - - -## Commands - -```sh -beeper chats list [--account SEL]... [--archived] [--pinned] [--muted] [--unread] [--low-priority] [--limit N] [--ids] -beeper chats search [--account SEL]... [--limit N] [--ids] -beeper chats show --chat SEL [--max-participants N] [--pick N] -beeper chats start [--account SEL] [--title TEXT] -beeper chats archive | unarchive --chat SEL [--pick N] -beeper chats pin | unpin --chat SEL [--pick N] -beeper chats mute | unmute --chat SEL [--pick N] -beeper chats mark-read | mark-unread --chat SEL [--message MSG_ID] [--pick N] -beeper chats priority --chat SEL --level inbox|low [--pick N] -beeper chats notify-anyway --chat SEL [--pick N] -beeper chats rename --chat SEL --title NEW [--pick N] -beeper chats description --chat SEL [--description TEXT | --clear] [--pick N] -beeper chats avatar --chat SEL [--file PATH | --clear] [--pick N] -beeper chats draft --chat SEL [--text TEXT [--file PATH [--filename N] [--mime TYPE]] | --clear] [--pick N] -beeper chats disappear --chat SEL --seconds N [--pick N] -beeper chats remind --chat SEL --when ISO [--dismiss-on-message] [--pick N] -beeper chats unremind --chat SEL [--pick N] -beeper chats focus --chat SEL [--message MSG_ID] [--draft TEXT] [--attachment PATH] [--pick N] -``` - -## Selecting a chat - -All `--chat` flags accept a **Beeper chat ID, a local chat ID, the exact title, -or search text**. Ambiguous matches return numbered choices; pass `--pick N` to -select one in scripts. - -## Notes - -- `chats list` filters compose: e.g. `--unread --no-muted --pinned` returns only - pinned, unread, non-muted chats. -- `chats mute` is currently boolean — the Desktop API does not yet expose a mute - duration. -- `chats focus` opens Beeper Desktop on the selected chat (and optionally scrolls - to a message or prefills the composer). -- `chats disappear --seconds 0` turns disappearing messages off. -- Labels are not yet supported by the Desktop API; there is no `chats label` - command in this CLI. - -## Examples - -```sh -beeper chats list --pinned --limit 50 -beeper chats list --unread --no-muted --json -beeper chats search Family -beeper chats start +15551234567 -beeper chats archive --chat "Family" -beeper chats mute --chat "Marketing" -beeper chats priority --chat "Family" --level inbox -beeper chats rename --chat "Family" --title "Family ❤" -beeper chats draft --chat "Family" --text "on my way" -beeper chats draft --chat "Family" --clear -beeper chats disappear --chat "Friends" --seconds 86400 -beeper chats remind --chat "Family" --when 2026-06-01T09:00:00Z -beeper chats focus --chat "Family" -``` diff --git a/docs/src/content/docs/config.mdx b/docs/src/content/docs/config.mdx deleted file mode 100644 index b763a9a3..00000000 --- a/docs/src/content/docs/config.mdx +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: Configuration -description: Inspect, change, or reset the CLI's local configuration, and the environment variables that override it. ---- - -import { Aside } from '@astrojs/starlight/components'; - -CLI configuration is stored under your user config dir — `~/.beeper/config.json`, -or wherever `BEEPER_CLI_CONFIG_DIR` points. Print the path with `beeper config -path`. The default Beeper Client API target is `http://127.0.0.1:23373`. - - - -## Commands - -```sh -beeper config path -beeper config get [defaultTarget | defaultAccount | baseURL | auth] -beeper config set -beeper config reset -``` - -## Notes - -- `config path` prints the JSON config path (suitable for `cat` or `cd - $(dirname …)`). -- `config get` without a key prints the full config; passing a key prints just - that field. `auth.accessToken` is always redacted. -- `config set ""` clears the field. Only `defaultTarget` and - `defaultAccount` are settable here; other fields are written by commands like - `targets use` and `auth verify`. -- `config reset` deletes the config file. - -## Environment overrides - -| Variable | Effect | -| --- | --- | -| `BEEPER_ACCESS_TOKEN` | Bearer token for the selected target. Overrides stored OAuth login. | -| `BEEPER_DESKTOP_BASE_URL` | Beeper Client API base URL (Desktop or Server). Defaults to `http://127.0.0.1:23373`. | -| `BEEPER_TARGET` | Selects a configured target by name for a single shell. | -| `BEEPER_READONLY` | `1`/`true`/`yes`/`on` enables read-only mode globally. | -| `BEEPER_CLI_CONFIG_DIR` | Override config directory for testing or isolated profiles. | - -## Examples - -```sh -beeper config path -beeper config get --json -beeper config get defaultTarget -beeper config set defaultTarget work -beeper config set defaultAccount "" -beeper config reset -``` diff --git a/docs/src/content/docs/connect.mdx b/docs/src/content/docs/connect.mdx deleted file mode 100644 index b072a530..00000000 --- a/docs/src/content/docs/connect.mdx +++ /dev/null @@ -1,146 +0,0 @@ ---- -title: Connect a target -description: A target is the Beeper endpoint the CLI talks to. Connect local Desktop, a self-hosted Server, a remote target over OAuth, or a bearer token in CI. -sidebar: - label: Connect a target - order: 2 ---- - -import { Tabs, TabItem, Aside, Steps } from '@astrojs/starlight/components'; - -A **target** is the Beeper endpoint `beeper` talks to — local Beeper Desktop, -local Beeper Server, or a remote Beeper Desktop or Beeper Server. Pick one of -four paths. `beeper setup` orchestrates all of them. - -The selected target is persisted in `~/.beeper/config.json` (override the whole -config directory with `BEEPER_CLI_CONFIG_DIR`). See [Targets](/targets/) to -manage more than one. - - - - **Default, recommended.** If Beeper Desktop is installed and signed in here, - `beeper setup` discovers it on `http://127.0.0.1:23373` and adopts the - existing session. If it's installed but not running, `setup` offers to launch - it. If it isn't installed at all, `--install` does that in one step. - - ```text - $ beeper setup --desktop --install - ▎ Installed Beeper Desktop (stable) - ▎ Launched Beeper Desktop - next Sign in to Beeper Desktop, then re-run `beeper setup`. - - $ beeper setup - ▎ Connected desktop - accounts whatsapp, telegram - ``` - - Variants: `beeper setup --local` skips discovery and forces the local path; - `beeper install desktop --channel nightly` uses the nightly channel. - - - - For a headless, long-running setup on this machine, install and adopt a - local Beeper Server. The CLI manages the process — `targets - start/stop/restart/logs/enable`. - - ```text - $ beeper setup --server --install - ▎ Installed Beeper Server (stable) - ▎ Started server on http://127.0.0.1:23373 - auth Opening browser to authorize this server… - ▎ Connected server - accounts (none) - next Run `beeper accounts add` to connect a network. - ``` - - Then connect a network — `beeper accounts add` walks each bridge through its - own login (QR, code, OAuth, cookie): - - ```text - $ beeper accounts add - ? Which bridge? whatsapp - Scan this QR code with WhatsApp on your phone: - ▄▄▄▄▄▄▄ ▄ ▄ ▄▄▄▄▄▄▄ - █ ███ █ ▄█▄ █ ███ █ - █ ███ █ ▀█▀ █ ███ █ - ▀▀▀▀▀▀▀ ▀ ▀ ▀▀▀▀▀▀▀ - ▎ Connected whatsapp · +1•••4242 - ``` - - Variants: `beeper install server`, `beeper install server --server-env staging`. - - - - For a Beeper Desktop or Server running on another machine, authorize the CLI - through a browser-based OAuth/PKCE flow. - - ```text - $ beeper setup --remote https://desktop.example.com - ▎ Authorizing https://desktop.example.com - flow OAuth (PKCE) — opening browser… - ▎ Connected remote (desktop.example.com) - accounts whatsapp, telegram, signal - ``` - - Variants: `beeper setup --oauth` (PKCE against the default Beeper auth); - `beeper targets add remote work https://desktop.example.com --default` - registers additional remotes. - - - - For agents, CI, and scripts, hand the CLI a bearer token directly — no - browser, no interactive prompts. - - ```sh - BEEPER_ACCESS_TOKEN=... beeper chats list --json - BEEPER_ACCESS_TOKEN=... BEEPER_DESKTOP_BASE_URL=https://desktop.example.com \ - beeper messages list --chat 10313 --json - ``` - - `BEEPER_ACCESS_TOKEN` overrides any stored OAuth login for the selected - target. See [Configuration](/config/) for every environment override. - - - -## The happy path - -If Beeper Desktop is already on this machine, there's nothing to choose: - - - -1. Run `beeper setup`. It finds Desktop, offers to launch it if needed, and - adopts the session. - - ```text - $ beeper setup - Looking for Beeper Desktop… found, not running. - Launch it now? [Y/n] y - ▎ Launched Beeper Desktop - - $ beeper setup - Use this Desktop session for CLI access? [Y/n] y - ▎ Connected desktop - accounts whatsapp, telegram, imessage - endpoint http://127.0.0.1:23373 - ``` - -2. Confirm you're ready with `beeper status` (or `beeper doctor` for full - setup/auth/encryption diagnostics). - -3. You're connected. Head to the [Quick start](/quickstart/). - - - - - -## Encrypted messages - -Reaching some networks requires device verification for end-to-end encrypted -messages. `beeper status` / `beeper doctor` tell you whether the target is -encryption-ready; [Auth & verification](/auth/) covers the SAS/QR and -recovery-key flows. diff --git a/docs/src/content/docs/contacts.mdx b/docs/src/content/docs/contacts.mdx deleted file mode 100644 index 1aba9d6b..00000000 --- a/docs/src/content/docs/contacts.mdx +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: Contacts -description: Look up contacts across one or more accounts. ---- - -import { Aside } from '@astrojs/starlight/components'; - - - -## Commands - -```sh -beeper contacts list [--account SEL]... [--query TEXT] [--limit N] [--ids] -beeper contacts search [--account SEL]... -beeper contacts show [--account SEL]... -``` - -## Notes - -- `contacts list` reads merged account contacts; without `--account` it iterates - all accounts. -- `contacts search` runs the network search where available and returns merged - results across accounts; omitting `--account` searches every account. -- `contacts show` accepts a user ID, display name, or phone/handle and finds it - on the first matching account. - -## Examples - -```sh -beeper contacts list --account whatsapp --query alice -beeper contacts list --json -beeper contacts search "alice" -beeper contacts show "Alice" --account whatsapp -beeper contacts show +15551234567 -``` diff --git a/docs/src/content/docs/exit-codes.mdx b/docs/src/content/docs/exit-codes.mdx deleted file mode 100644 index f9a1cce3..00000000 --- a/docs/src/content/docs/exit-codes.mdx +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: Exit codes -description: The deterministic exit codes the Beeper CLI returns, so scripts and agents can branch on failure reasons. ---- - -Every command returns a deterministic exit code so scripts and agents can branch -on the failure reason without parsing text. - -| Code | Meaning | -| --- | --- | -| `0` | Success. | -| `1` | Generic runtime error. | -| `2` | Usage error (parsing, validation, missing required flag/arg, read-only refusal). | -| `3` | Auth required (no stored token; sign in or set `BEEPER_ACCESS_TOKEN`). | -| `4` | Target/account not ready (`doctor` reports this when readiness is not `ready`). | -| `5` | Selector matched nothing (unknown target, account, chat, contact). | -| `6` | Ambiguous selector (multiple matches; pass an exact ID or `--pick N`). | - -JSON output preserves the same envelope on failure, written to stderr: - -```json -{"success":false,"data":null,"error":"…","exitCode":N} -``` - -See [Output & scripting](/scripting/) for the full envelope and global flags. diff --git a/docs/src/content/docs/export.mdx b/docs/src/content/docs/export.mdx deleted file mode 100644 index 1c603f10..00000000 --- a/docs/src/content/docs/export.mdx +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: Export -description: Make a heavy, multi-chat, attachment-including export of Beeper data to disk. Resumable. ---- - -import { Aside } from '@astrojs/starlight/components'; - - - -## Command - -```sh -beeper export - [-o, --out DIR] - [--account SEL]... - [--chat SEL]... - [--limit-chats N] - [--limit-messages N] - [--max-participants N] - [--no-attachments] - [--force] - [--quiet] - [--pick N] -``` - -## On-disk layout - -The export directory contains `accounts.json`, `chats.json`, `manifest.json`, -plus one directory per chat with `chat.json`, `messages.json`, -`messages.markdown`, `messages.html`, attachments, and per-chat checkpoint state. - -## Notes - -- Default `--out` directory is `beeper-export`. -- Exports are **resumable**. Re-running picks up where the last run left off - unless `--force` is set. -- `--max-participants` (default 500) bounds the participant list stored in each - `chat.json`. -- `--no-attachments` skips downloading media; metadata is still recorded. -- `--limit-chats` / `--limit-messages` are intended for sanity-checking large - exports. - -## Examples - -```sh -beeper export --out ./beeper-export -beeper export --chat "Family" --out ./family -beeper export --account whatsapp --no-attachments --quiet -beeper export --force --out ./beeper-export -``` diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx deleted file mode 100644 index 07cc2d8b..00000000 --- a/docs/src/content/docs/index.mdx +++ /dev/null @@ -1,88 +0,0 @@ ---- -title: Beeper CLI -description: One CLI for all your chats — WhatsApp, iMessage, Telegram, Signal, Discord and more. Built for scripts, agents, and humans in a hurry. -template: splash -hero: - tagline: One CLI for all your chats. Built for you and your agent — batteries included. - image: - file: ../../assets/logo.svg - actions: - - text: Quick start - link: /quickstart/ - icon: right-arrow - variant: primary - - text: Install - link: /install/ - icon: download - variant: secondary - - text: View on GitHub - link: https://github.com/beeper/desktop-api-cli - icon: external - variant: minimal ---- - -import { Card, CardGrid, LinkCard } from '@astrojs/starlight/components'; - -`beeper` talks to **Beeper Desktop** on this machine, to a **Beeper Server** you -self-host, or to either one running somewhere else. Send and receive across -every chat network Beeper bridges — from one CLI shaped for scripts, agents, and -humans in a hurry. - -```sh -brew install beeper/tap/cli -beeper setup -beeper send text --to Family --message "on my way" -``` - -## What it does - - - - Local Beeper Desktop (default), a self-hosted Beeper Server you manage from - the CLI, or a remote target over OAuth/PKCE — or a bearer token in CI. - - - List, search, start, archive, pin, mute, rename, focus. Read, edit, delete, - react. Send text, files, stickers, voice, and typing indicators. - - - `--json` everywhere, NDJSON `--events`, a `watch` stream with HMAC-signed - webhooks, `rpc` over stdin/stdout, and `man --json` tool manifests. - - - `--read-only` rejects every mutating command. Writes stay explicit. Plugins - extend the CLI without forking it. - - - -## Supported chat networks - -Everything Beeper's bridges reach — run `beeper bridges list` for the live list -on your target. - -
    -
  • WhatsApp
  • -
  • iMessage
  • -
  • Telegram
  • -
  • Discord
  • -
  • Signal
  • -
  • Instagram DMs
  • -
  • Facebook Messenger
  • -
  • X (Twitter) DMs
  • -
  • LinkedIn
  • -
  • Slack
  • -
  • Google Messages (RCS/SMS)
  • -
  • Google Chat
  • -
  • Matrix
  • -
  • IRC
  • -
  • Bluesky
  • -
- -## Start here - - - - - - - diff --git a/docs/src/content/docs/install.mdx b/docs/src/content/docs/install.mdx deleted file mode 100644 index 4798574f..00000000 --- a/docs/src/content/docs/install.mdx +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: Install -description: Install the Beeper CLI via Homebrew, npm, or from source. The installed command is `beeper`. -sidebar: - order: 1 ---- - -import { Tabs, TabItem, Aside } from '@astrojs/starlight/components'; - -The package name is `beeper-cli`; the installed command is `beeper`. - - - - Recommended on macOS and Linux. Installs a signed standalone binary. - - ```sh - brew install beeper/tap/cli - ``` - - Upgrade later with `brew upgrade beeper/tap/cli`, or run `beeper update --cli` - to print the right command for your install method. - - - Run it once without installing: - - ```sh - npx beeper-cli --help - ``` - - Or install it globally: - - ```sh - npm install -g beeper-cli - ``` - - The npm package is a thin launcher that downloads, verifies, and runs the - matching standalone binary for your platform. - - - This repo is a [Bun](https://bun.sh) workspace. From the repo root: - - ```sh - bun install - bun --filter @beeper/cli run build - bun --filter @beeper/cli run dev -- --help - ``` - - For local CLI development inside `packages/cli`: - - ```sh - bun run dev -- --help - ``` - - - -## Verify the install - -```sh -beeper version -beeper --help -``` - -The CLI checks for updates in the background and prints a one-line notice when a -newer release is available. It never upgrades itself — see [Updating](/update/). - - - -## Next step - -You have the CLI, but it isn't talking to anything yet. Point it at a Beeper -target — see [Connect a target](/connect/). diff --git a/docs/src/content/docs/media.mdx b/docs/src/content/docs/media.mdx deleted file mode 100644 index 79f77a3b..00000000 --- a/docs/src/content/docs/media.mdx +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Media -description: Download a media file attached to a message. ---- - -import { Aside } from '@astrojs/starlight/components'; - - - -## Commands - -```sh -beeper media download [-o, --out DIR | -] -``` - -## Notes - -- `` accepts `mxc://` and `localmxc://` URLs (typically taken from a message - payload). -- `--out` defaults to `.` (current directory); the file is named from the URL - path. -- `--out -` streams the binary to stdout for piping. - -## Examples - -```sh -beeper media download mxc://beeper.com/abc --out ./downloads -beeper media download mxc://beeper.com/abc -o - > photo.jpg -``` - -## See also - -For bulk media, [`export`](/export/) downloads every chat's attachments into a -resumable on-disk archive. diff --git a/docs/src/content/docs/messages.mdx b/docs/src/content/docs/messages.mdx deleted file mode 100644 index 1d258268..00000000 --- a/docs/src/content/docs/messages.mdx +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: Messages -description: List, search, show, contextualize, edit, delete, react to, or export messages from chats. ---- - -import { Aside } from '@astrojs/starlight/components'; - - - -## Commands - -```sh -beeper messages list --chat SEL [--before-cursor MSG_ID | --after-cursor MSG_ID] [--sender me|others|] [--asc] [--limit N] [--ids] [--pick N] -beeper messages search [query] [--account SEL]... [--chat SEL]... [--chat-type group|single] [--sender me|others|] [--media TYPE]... [--after ISO] [--before ISO] [--include-muted | --no-include-muted] [--exclude-low-priority | --no-exclude-low-priority] [--limit N] [--ids] -beeper messages show --chat SEL --id MSG_ID [--pick N] -beeper messages context --chat SEL --id MSG_ID [--before N] [--after N] [--pick N] -beeper messages edit --chat SEL --id MSG_ID --message TEXT [--pick N] -beeper messages delete --chat SEL --id MSG_ID [--for-everyone] [--pick N] -beeper messages react --chat SEL --id MSG_ID --reaction KEY [--pick N] # hidden; prefer `send react` -beeper messages unreact --chat SEL --id MSG_ID --reaction KEY [--pick N] # hidden; prefer `send unreact` -beeper messages export --chat SEL [--before-cursor MSG_ID | --after-cursor MSG_ID] [--after ISO] [--before ISO] [--limit N] [--output PATH | -o -] [--asc] [--pick N] -``` - -## Pagination & filtering - -- `--before-cursor` / `--after-cursor` paginate by message ID (the SDK's cursor - model). -- `--before` / `--after` in `messages search` and `messages export` filter by - ISO timestamp. -- `messages list --sender` filters client-side: `me` (your own messages), - `others`, or an exact user ID. -- `messages list --asc` reverses the default newest-first order. - -## Notes - -- `messages search` rejects an empty query *and* no filter flags with exit code - `2` (usage error). -- `messages export` writes one chat to JSON. Use top-level [`export`](/export/) - for a full export with transcripts, attachments, and multiple chats. - `messages export --output -` writes JSON to stdout for piping. -- `messages delete --for-everyone` requires the network to support it; otherwise - it falls back to delete-for-you. -- `messages edit` only succeeds on your own text messages with no attachments. -- `messages react` / `unreact` are hidden in `--help` in favor of - [`send react`](/send/) / `send unreact`. - -## Examples - -```sh -beeper messages list --chat 10313 --limit 50 -beeper messages list --chat 10313 --sender me --asc -beeper messages list --chat 8951 --before-cursor "$LAST_ID" --limit 200 -beeper messages search "invoice" -beeper messages search --chat 10313 --sender me --media image --after 2026-01-01 -beeper messages show --chat 10313 --id ABC123 -beeper messages context --chat 10313 --id ABC123 --before 5 --after 5 -beeper messages edit --chat 10313 --id ABC123 --message "fixed typo" -beeper messages delete --chat 10313 --id ABC123 --for-everyone -beeper messages export --chat 10313 --output family.json -beeper messages export --chat 8951 --after 2026-01-01T00:00:00Z -o - -``` diff --git a/docs/src/content/docs/plugins.mdx b/docs/src/content/docs/plugins.mdx deleted file mode 100644 index 4fb7916b..00000000 --- a/docs/src/content/docs/plugins.mdx +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: Plugins -description: Extend the Beeper CLI with optional oclif plugins without forking it, and build your own with the plugin SDK. ---- - -import { Aside } from '@astrojs/starlight/components'; - -Beeper CLI supports optional [oclif](https://oclif.io) plugins, so you can extend -the CLI without forking it. - -## Using plugins - -List recommended Beeper plugins: - -```sh -beeper plugins available -``` - -Install a published plugin: - -```sh -beeper plugins install @beeper/cli-plugin-cloudflare -``` - -## First-party plugins - -| Package | Adds | -| --- | --- | -| `@beeper/cli-plugin-cloudflare` | `targets tunnel` — expose a selected Beeper target through Cloudflare Tunnel. | - -## Building a plugin - -Import from `@beeper/cli/plugin-sdk` and expose oclif commands from your package. -Link a local plugin while working on it: - -```sh -beeper plugins link ./packages/cli-plugin-cloudflare -beeper targets tunnel --help -``` - - diff --git a/docs/src/content/docs/presence.mdx b/docs/src/content/docs/presence.mdx deleted file mode 100644 index 5f199844..00000000 --- a/docs/src/content/docs/presence.mdx +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Presence -description: Send typing/paused indicators into a chat from a script or agent. ---- - -import { Aside } from '@astrojs/starlight/components'; - - - -## Commands - -```sh -beeper presence --chat SEL [--state typing|paused] [--duration SECONDS] [--pick N] -``` - -## Notes - -- Requires server-side support; networks without typing notifications return an - error. -- `--state` defaults to `typing`. -- `--duration N` (only valid with `--state typing`) sends `typing`, sleeps N - seconds, then sends `paused`. -- The selected chat must be addressable via the usual selector rules (ID, local - ID, title, or search text). - -## Examples - -```sh -beeper presence --chat "Family" -beeper presence --chat "Family" --state paused -beeper presence --chat "Family" --duration 5 -``` diff --git a/docs/src/content/docs/quickstart.mdx b/docs/src/content/docs/quickstart.mdx deleted file mode 100644 index 81790cd9..00000000 --- a/docs/src/content/docs/quickstart.mdx +++ /dev/null @@ -1,89 +0,0 @@ ---- -title: Quick start -description: From zero to your first sent message in about a minute, on the happy path where Beeper Desktop is already on this machine. -sidebar: - order: 3 ---- - -import { Steps, Aside, LinkCard } from '@astrojs/starlight/components'; - -The happy path: Beeper Desktop is already on this machine. `beeper setup` finds -it, offers to launch it if it isn't running, and adopts the session. - - - -1. **Install** the CLI: - - ```sh - brew install beeper/tap/cli - ``` - -2. **Connect** to your local Desktop: - - ```text - $ beeper setup - Use this Desktop session for CLI access? [Y/n] y - ▎ Connected desktop - accounts whatsapp, telegram, imessage - endpoint http://127.0.0.1:23373 - ``` - -3. **List** your chats: - - ```text - $ beeper chats list --limit 3 - 10313 Family 3 unread - 8951 Alice · - 7204 Eng standup 12 unread - ``` - -4. **Search** across every network at once: - - ```text - $ beeper messages search "flight" - 8951 Alice · "your flight is at 6:40, gate B23" 2d ago - 10313 Family · "what flight are you on?" 1w ago - ``` - -5. **Send** a message: - - ```text - $ beeper send text --to Family --message "on my way" - ▎ Sent Family - message "on my way" - at 2026-05-18T14:02:11Z - ``` - -6. **Export** everything to disk when you want a backup: - - ```text - $ beeper export --out ./beeper-export - ▎ Exported ./beeper-export - chats 214 messages 38,901 attachments 1,205 - ``` - - - -## Addressing chats - -Recipients (`--to`, `--chat`) accept a numeric local chat ID, a full -Beeper/Matrix chat ID, an iMessage chat ID, an exact title, or search text. -Ambiguous matches prompt in a TTY; pass `--pick N` in scripts. - -```sh -beeper send text --to 10313 --message "by local id" -beeper send text --to "Family" --message "by exact title" -beeper send text --to "@alice:beeper.com" --message "by full chat id" -``` - - - -## Where to next - - - - diff --git a/docs/src/content/docs/rpc.mdx b/docs/src/content/docs/rpc.mdx deleted file mode 100644 index 518b95b8..00000000 --- a/docs/src/content/docs/rpc.mdx +++ /dev/null @@ -1,58 +0,0 @@ ---- -title: RPC -description: Drive many CLI commands from a long-lived process over newline-delimited JSON on stdin/stdout — no new process per command. ---- - -import { Aside } from '@astrojs/starlight/components'; - - - -## Command - -```sh -beeper rpc -``` - -Reads newline-delimited JSON requests on stdin and writes one response line per -request on stdout. - -## Request shape - -```json -{"id":1,"command":"chats list --json"} -{"id":2,"args":["send","text","--to","Family","--message","ack","--json"]} -``` - -Each request must include one of: - -- `command` — a single string parsed with shell-like quoting; -- `args` — an explicit `argv` array; -- `argv` — alias for `args`. - -`id` is echoed back in the response (string, number, or null). - -## Response shape - -```json -{"id":1,"ok":true,"code":0,"signal":null,"stdout":"…","stderr":""} -{"id":2,"ok":false,"error":"…"} -``` - -`ok` mirrors `code === 0`. `stdout`/`stderr` capture the child command's output. - -## Notes - -- Nesting `rpc` or `shell` is rejected to avoid recursion. -- `--json` on inner commands produces the standard envelope inside `stdout`. -- Exit codes use the same table as direct CLI invocation; see [Exit - codes](/exit-codes/). - -## Examples - -```sh -printf '{"id":1,"command":"auth status --json"}\n' | beeper rpc -printf '{"id":1,"args":["chats","list","--json"]}\n{"id":2,"args":["status","--json"]}\n' | beeper rpc -``` diff --git a/docs/src/content/docs/scripting.mdx b/docs/src/content/docs/scripting.mdx deleted file mode 100644 index 1ece1386..00000000 --- a/docs/src/content/docs/scripting.mdx +++ /dev/null @@ -1,85 +0,0 @@ ---- -title: Output & scripting -description: JSON output, NDJSON events, global flags, addressing rules, and the conventions that make the Beeper CLI safe to drive from scripts and agents. -sidebar: - label: Output & scripting - order: 1 ---- - -import { Aside } from '@astrojs/starlight/components'; - -The CLI is designed to be driven by humans *and* programs. Every command prints -human-friendly text by default and switches to a stable machine envelope on -`--json`. - -## Output modes - -Most commands support: - -- **app-like text by default**, optimized for scanning chats, messages, contacts, - accounts, and media; -- **`--json`** for a `{"success":true,"data":...,"error":null}` envelope on stdout; -- **`--events`** for NDJSON lifecycle events on stderr from long-running commands; -- **`--read-only`** to reject commands that modify Beeper or local CLI state; -- **`--full`** to disable truncation; -- **`--debug`** for SDK debug logging; -- **`--target`** or **`--base-url`** to point at a different target. - -The JSON envelope is stable across success and failure: - -```json -{"success":true,"data":{ /* … */ },"error":null} -{"success":false,"data":null,"error":"…","exitCode":3} -``` - -On failure the envelope is written to **stderr** and the process exits with the -matching [exit code](/exit-codes/). - -## Global flags - -`--base-url` · `--target` · `--json` · `--events` · `--full` · `--timeout` · -`--read-only` · `--debug` · `--yes` · `--quiet` - - - -## Addressing - -- Chat arguments accept numeric local chat IDs, full Beeper/Matrix chat IDs, - iMessage chat IDs, exact titles, or search text. -- For scripts on the same target/profile, prefer the **numeric local chat ID** - shown by `beeper chats list`; use the **full Beeper/Matrix chat ID** when the - selector must work across targets or profiles. -- Numeric local chat IDs come from the selected Desktop database — treat them as - local to that target/profile. -- Ambiguous chat matches return numbered choices; pass `--pick N` to select one. -- Account arguments accept account IDs, network names, bridge type/id, or - account user identity. A network name can expand to multiple matching accounts. -- `contacts search` and `chats start` search across all accounts when - `--account` is omitted; `contacts list` accepts the same account selectors as - other account-scoped commands. - -## For agents - -- `man --json` prints a compact command manifest for tools and agents. -- `rpc` runs newline-delimited JSON command RPC over stdin/stdout — see [RPC](/rpc/). -- `watch` streams live events and can forward them to an HMAC-signed webhook — - see [Watch](/watch/). -- Raw, un-wrapped endpoints are reachable under [`api`](/api/). - -## Example: a JSON pipeline - -```sh -# Unread chat titles, newest first, as a plain list: -beeper chats list --unread --json \ - | jq -r '.data[].title' - -# Send to every pinned chat: -beeper chats list --pinned --json \ - | jq -r '.data[].id' \ - | while read -r id; do - beeper send text --to "$id" --message "heads up" --json - done -``` diff --git a/docs/src/content/docs/send.mdx b/docs/src/content/docs/send.mdx deleted file mode 100644 index b57b8cf5..00000000 --- a/docs/src/content/docs/send.mdx +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: Sending -description: Send text, files, reactions, stickers, or voice notes from scripts or interactive use. -sidebar: - label: Sending ---- - -import { Aside } from '@astrojs/starlight/components'; - - - -## Commands - -```sh -beeper send text --to SEL --message TEXT [--reply-to MSG_ID] [--mention USER]... [--no-preview] [--wait] [--wait-timeout MS] [--pick N] -beeper send file --to SEL --file PATH [--caption TEXT] [--filename NAME] [--mime TYPE] [--reply-to MSG_ID] [--wait] [--wait-timeout MS] [--pick N] -beeper send sticker --to SEL --file PATH [--filename NAME] [--mime TYPE] [--reply-to MSG_ID] [--wait] [--wait-timeout MS] [--pick N] -beeper send voice --to SEL --file PATH [--duration SECONDS] [--filename NAME] [--mime TYPE] [--reply-to MSG_ID] [--wait] [--wait-timeout MS] [--pick N] -beeper send react --to SEL --id MSG_ID --reaction KEY [--transaction TX_ID] [--pick N] -beeper send unreact --to SEL --id MSG_ID --reaction KEY [--pick N] -``` - -## Addressing - -`--to` accepts a chat ID, local chat ID, exact title, or search text. Prefer -numeric local chat IDs from `beeper chats list` when scripting against the same -target/profile; use full Beeper/Matrix chat IDs for selectors that need to work -across targets or profiles. - -## Confirming delivery - -Send commands return when Desktop **accepts** the send request. Use `--wait` when -you need to know whether the message left the pending state or failed — it blocks -until the message leaves pending (or fails). Default poll cap: `--wait-timeout -30000` ms. - -## Notes - -- `--reply-to` quotes an existing message ID. -- `send text --mention ` adds a Matrix mention; repeat for multiple - users. `send text --no-preview` disables automatic link previews. -- `send sticker` defaults `--mime` to `image/webp`; stickers should be 512×512. -- `send voice` defaults `--mime` to `audio/ogg`; pass `--duration` to override - the detected length. -- `send file` accepts any file up to 500 MB. MIME type is detected from the - upload if `--mime` is omitted. - -## Examples - -```sh -beeper send text --to 10313 --message "on my way" -beeper send text --to 8951 --message "ack" --reply-to ABC123 -beeper send text --to "@alice:beeper.com" --message "hi @alice" --mention @alice:beeper.com --no-preview -beeper send file --to 10313 --file ./photo.jpg --caption "from today" -beeper send sticker --to 10313 --file ./hi.webp -beeper send voice --to 8951 --file ./note.ogg --duration 12 -beeper send react --to 10313 --id ABC123 --reaction "+1" -beeper send unreact --to 10313 --id ABC123 --reaction "+1" -``` diff --git a/docs/src/content/docs/targets.mdx b/docs/src/content/docs/targets.mdx deleted file mode 100644 index 85cd276e..00000000 --- a/docs/src/content/docs/targets.mdx +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: Targets -description: Manage local Desktop, managed Server, and remote Beeper targets — add, switch, start/stop a managed runtime, or remove a target. ---- - -import { Aside } from '@astrojs/starlight/components'; - -A **target** is a runnable or reachable Beeper endpoint profile: local Desktop, -local Server, or a remote Desktop/Server. The CLI tracks an optional default; -commands use it unless `--target ` overrides. - - - -## Commands - -```sh -beeper targets list -beeper targets add desktop [name] [--port N] [--server-env prod|staging] [--default] -beeper targets add server [name] [--port N] [--server-env prod|staging] [--default] -beeper targets add remote [--default] -beeper targets use -beeper targets show [name] -beeper targets status [name] -beeper targets start | stop | restart [name] -beeper targets logs [name] -beeper targets enable | disable [name] # start at login -beeper targets remove -``` - -## Notes - -- `list` prints all configured targets; the default one has `default: true`. -- `show` defaults to the currently-selected target if no name is given. -- `status` checks endpoint and process reachability. For setup/auth/encryption - diagnostics use `beeper doctor`. -- `start` / `stop` / `restart` only apply to managed targets (`type: - desktop|server`); they error for `remote`. -- `enable` / `disable` registers or unregisters the launchd or systemd unit that - starts the managed target at login. -- Removing the active default clears the `defaultTarget` config field. -- `BEEPER_TARGET=` overrides the default for a single shell. - -## Examples - -```sh -beeper targets list --json -beeper targets add desktop work --default -beeper targets add server prod --server-env prod --default -beeper targets add remote office https://desktop.office.example.com --default -beeper targets use work -beeper targets logs work | less -beeper targets restart work -``` - -## See also - -- [Connect a target](/connect/) — first-time setup for each target type. -- [Auth & verification](/auth/) — sign-in state and encryption readiness. -- [Configuration](/config/) — where the selected target is stored. diff --git a/docs/src/content/docs/update.mdx b/docs/src/content/docs/update.mdx deleted file mode 100644 index 5a6a827d..00000000 --- a/docs/src/content/docs/update.mdx +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: Updating -description: Check for new versions of the CLI, the CLI-managed Desktop install, or the CLI-managed Server install — and choose whether to install. -sidebar: - label: Updating ---- - -import { Aside } from '@astrojs/starlight/components'; - - - -## Command - -```sh -beeper update [--check] [--cli] [--desktop] [--server] -``` - -## Notes - -- With no kind flag, checks all three (CLI, Desktop, Server) that apply. -- `--check` prints what's available without installing. -- The CLI itself is **never auto-upgraded**; `--cli` prints the right command for - your install method (Homebrew, npm-global, or in-repo git build). -- `--desktop` reports on the CLI-owned Desktop install; updating Desktop itself - happens inside the Desktop app. -- `--server` updates the CLI-managed Server install in place, then restarts any - running managed Server targets. - -## Examples - -```sh -beeper update --check -beeper update --cli -beeper update --desktop --json -beeper update --server -``` diff --git a/docs/src/content/docs/watch.mdx b/docs/src/content/docs/watch.mdx deleted file mode 100644 index 79e15916..00000000 --- a/docs/src/content/docs/watch.mdx +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: Watch (live events) -description: Subscribe to live Desktop API events — new/updated/deleted chats and messages — and optionally forward them to a webhook. -sidebar: - label: Watch (live events) ---- - -import { Aside } from '@astrojs/starlight/components'; - - - -## Commands - -```sh -beeper watch - [-c, --chat CHAT_ID]... - [--include-type EVENT_TYPE]... - [--exclude-type EVENT_TYPE]... - [--webhook URL [--webhook-secret SECRET] [--webhook-queue N]] - [--json] -``` - -## Notes - -- Subscribes to the Desktop API WebSocket at the path returned by `/v1/info` - (defaults to `/v1/ws`). -- Without `--chat`, subscribes to all chats. -- Event types come from the Desktop API: `chat.upserted`, `chat.deleted`, - `message.upserted`, `message.deleted`. -- `--include-type` and `--exclude-type` are mutually exclusive. -- `--webhook URL` forwards every event as a POST body (best-effort, - fire-and-forget). -- `--webhook-secret SECRET` signs the body with HMAC-SHA256 and sets - `X-Beeper-Signature: sha256=`. -- `--webhook-queue` (default 64) caps pending deliveries; excess events are - dropped with a stderr warning. -- `--quiet` suppresses the human-mode status line; `--json` prints raw events - line-delimited. - -## Examples - -```sh -beeper watch -beeper watch --chat '!abc:beeper.com' --json -beeper watch --include-type message.upserted --include-type message.deleted -beeper watch --webhook https://example.com/hook --webhook-secret "$BEEPER_WEBHOOK_SECRET" -``` - -## Verifying webhook signatures - -Each delivery is signed with `X-Beeper-Signature: sha256=` over the raw -request body. Recompute the HMAC with your shared secret and compare: - -```js -import { createHmac, timingSafeEqual } from 'node:crypto'; - -function verify(rawBody, header, secret) { - const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex'); - const a = Buffer.from(header); - const b = Buffer.from(expected); - return a.length === b.length && timingSafeEqual(a, b); -} -``` diff --git a/docs/src/styles/theme.css b/docs/src/styles/theme.css deleted file mode 100644 index 83769cc8..00000000 --- a/docs/src/styles/theme.css +++ /dev/null @@ -1,37 +0,0 @@ -/* Beeper CLI docs — brand accent on top of Starlight defaults. */ -:root { - /* Purple accent ramp (light) */ - --sl-color-accent-low: #e0d9ff; - --sl-color-accent: #6e56f8; - --sl-color-accent-high: #3f2db8; -} - -:root[data-theme='dark'] { - /* Purple accent ramp (dark) */ - --sl-color-accent-low: #2a2160; - --sl-color-accent: #8b78ff; - --sl-color-accent-high: #d6cdff; -} - -/* Roomier hero on the landing page. */ -.hero > .stack { - gap: clamp(1rem, 5vw, 2rem); -} - -/* Tighten the supported-networks list rendered on the overview page. */ -.networks { - display: flex; - flex-wrap: wrap; - gap: 0.4rem; - margin-block: 1rem; - padding: 0; - list-style: none; -} - -.networks li { - border: 1px solid var(--sl-color-gray-5); - border-radius: 999px; - padding: 0.15rem 0.7rem; - font-size: var(--sl-text-sm); - white-space: nowrap; -} diff --git a/docs/tsconfig.json b/docs/tsconfig.json deleted file mode 100644 index 8bf91d3b..00000000 --- a/docs/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "astro/tsconfigs/strict", - "include": [".astro/types.d.ts", "**/*"], - "exclude": ["dist"] -} diff --git a/eslint.config.mjs b/eslint.config.mjs deleted file mode 100644 index 948f565b..00000000 --- a/eslint.config.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import oclif from 'eslint-config-oclif' -import prettier from 'eslint-config-prettier' - -export default [ - { - ignores: [ - '**/dist/**', - '**/node_modules/**', - '.packs/**', - '.upstream/**', - 'packages/cli/README.md', - ], - }, - ...oclif, - prettier, - { - languageOptions: { - globals: { - Bun: 'readonly', - }, - }, - rules: { - 'import/no-unresolved': 'off', - 'n/no-unsupported-features/node-builtins': 'off', - 'no-await-in-loop': 'off', - 'perfectionist/sort-classes': 'off', - 'perfectionist/sort-imports': 'off', - 'perfectionist/sort-named-imports': 'off', - 'perfectionist/sort-object-types': 'off', - 'perfectionist/sort-objects': 'off', - 'perfectionist/sort-union-types': 'off', - 'unicorn/prefer-string-replace-all': 'off', - }, - }, -] diff --git a/package.json b/package.json index cbd382a8..1dbda169 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@beeper/cli", + "name": "beeper-cli-monorepo", "private": true, "type": "module", "packageManager": "bun@1.3.10", @@ -11,26 +11,10 @@ ], "scripts": { "build": "bun run --workspaces --sequential build", - "check": "bun run typecheck && bun run test && bun run build && bun run pack:packages", + "check": "bun run test && bun run pack:packages", "clean": "bun run --workspaces clean", - "changeset": "changeset", - "lint": "eslint eslint.config.mjs packages/cli-plugin-cloudflare/src packages/cli-plugin-cloudflare/test", - "pack:packages": "mkdir -p .packs && (cd packages/cli && bun pm pack --destination ../../.packs) && (cd packages/cli-plugin-cloudflare && bun pm pack --destination ../../.packs)", - "publish:packages": "bun scripts/publish-packages.ts", - "release": "bun scripts/release.ts", + "pack:packages": "mkdir -p .packs && npm pack --workspace beeper-cli --pack-destination .packs", "test": "bun run --workspaces --sequential test", - "typecheck": "bun run --filter @beeper/cli build && bun run --workspaces --sequential typecheck", - "version-packages": "changeset version" - }, - "devDependencies": { - "@changesets/changelog-github": "^0.6.0", - "@changesets/cli": "^2.31.0", - "@types/bun": "^1.3.3", - "@types/node": "^20.0.0", - "eslint": "^9.39.4", - "eslint-config-oclif": "^6.0.165", - "eslint-config-prettier": "^10.1.8", - "tsdown": "^0.21.10", - "typescript": "^5.7.2" + "typecheck": "bun run --filter beeper-cli build && bun run --workspaces --sequential typecheck" } } diff --git a/packages/cli-plugin-cloudflare/LICENSE b/packages/cli-plugin-cloudflare/LICENSE deleted file mode 100644 index 742aa938..00000000 --- a/packages/cli-plugin-cloudflare/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Beeper - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/cli-plugin-cloudflare/README.md b/packages/cli-plugin-cloudflare/README.md deleted file mode 100644 index e675ecde..00000000 --- a/packages/cli-plugin-cloudflare/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# @beeper/cli-plugin-cloudflare - -Cloudflare Tunnel commands for Beeper CLI. - -```sh -beeper plugins install @beeper/cli-plugin-cloudflare -beeper targets tunnel -``` - -`targets tunnel` exposes the selected Beeper Desktop or Server API target through -a Cloudflare quick tunnel and keeps the tunnel running until interrupted. - -```sh -beeper targets tunnel desktop -beeper targets tunnel --target server -beeper targets tunnel --base-url http://127.0.0.1:23373 -beeper targets tunnel --url-only -``` - -The command uses `cloudflared`. If it is already installed, pass its path: - -```sh -BEEPER_CLOUDFLARED_PATH=/opt/homebrew/bin/cloudflared beeper targets tunnel -``` - -Or let the plugin download the pinned `cloudflared` binary: - -```sh -beeper targets tunnel --install -``` - -Environment overrides: - -| Variable | Effect | -| --- | --- | -| `BEEPER_CLOUDFLARED_PATH` | Use this `cloudflared` binary path. | -| `BEEPER_IGNORE_CLOUDFLARED` | Skip install/version checks and try to run the configured binary path directly. | -| `BEEPER_CLOUDFLARED_DOMAIN` | Override the domain used when parsing the public URL from `cloudflared` output. Defaults to `trycloudflare.com`. | - -Tunneling makes the Desktop API reachable from the public internet. Only run it -for targets and networks you intend to expose, and stop it with `Ctrl-C` when you -are done. - -`targets tunnel` uses Cloudflare quick tunnels. Quick tunnels return temporary -public URLs. For a stable hostname on your own domain, configure a named -Cloudflare Tunnel and public hostname in Cloudflare, then route it to your Beeper -target outside this quick-tunnel command. diff --git a/packages/cli-plugin-cloudflare/package.json b/packages/cli-plugin-cloudflare/package.json deleted file mode 100644 index c032a758..00000000 --- a/packages/cli-plugin-cloudflare/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@beeper/cli-plugin-cloudflare", - "version": "0.6.0", - "description": "Cloudflare Tunnel commands for Beeper CLI", - "license": "MIT", - "type": "module", - "exports": "./dist/index.js", - "files": [ - "dist", - "README.md", - "LICENSE" - ], - "scripts": { - "build": "bun run --filter @beeper/cli build && bun run clean && tsc -p tsconfig.json", - "clean": "rm -rf dist", - "test": "bun run build && bun test/smoke.mjs", - "typecheck": "tsc -p tsconfig.json --noEmit" - }, - "oclif": { - "commands": { - "strategy": "pattern", - "target": "./dist/commands", - "globPatterns": [ - "**/!(*.d).{js,ts,tsx}" - ] - }, - "flexibleTaxonomy": true, - "topicSeparator": " ", - "topics": { - "tunnel": { - "description": "Expose Beeper targets through Cloudflare Tunnel" - } - } - }, - "dependencies": { - "@oclif/core": "^4.11.2", - "@beeper/cli": "workspace:*" - }, - "devDependencies": { - "@types/node": "^20.0.0", - "typescript": "^5.7.2" - } -} diff --git a/packages/cli-plugin-cloudflare/src/commands/targets/tunnel.ts b/packages/cli-plugin-cloudflare/src/commands/targets/tunnel.ts deleted file mode 100644 index dbac2136..00000000 --- a/packages/cli-plugin-cloudflare/src/commands/targets/tunnel.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { Args, Flags, BeeperCommand, ensureWritable, printData, printSuccess, resolveTarget, writeEvent } from '@beeper/cli/plugin-sdk' -import { cloudflaredPath, startCloudflareTunnel } from '../../lib/cloudflared.js' - -export default class TargetsTunnel extends BeeperCommand { - static override summary = 'Expose a Beeper target through Cloudflare Tunnel' - static override description = 'Starts a Cloudflare quick tunnel for the selected Beeper Desktop or Server API target. The command stays in the foreground until interrupted.' - static override args = { - name: Args.string({ required: false, description: 'Target name. Defaults to the selected target.' }), - } - static override flags = { - install: Flags.boolean({ default: false, description: 'Download the pinned cloudflared binary if it is missing or outdated' }), - 'cloudflared-path': Flags.string({ description: 'Path to a cloudflared binary. Also configurable with BEEPER_CLOUDFLARED_PATH.' }), - retries: Flags.integer({ default: 5, description: 'Number of startup retries before giving up' }), - 'url-only': Flags.boolean({ default: false, description: 'Print only the public tunnel URL' }), - } - - async run(): Promise { - const { args, flags } = await this.parse(TargetsTunnel) - if (flags.install) ensureWritable(flags) - const target = await resolveTarget({ target: args.name ?? flags.target, baseURL: flags['base-url'] }) - const localURL = normalizeLocalURL(target.baseURL) - const started = await startCloudflareTunnel({ - cloudflaredPath: flags['cloudflared-path'], - debug: flags.debug, - install: flags.install, - retries: flags.retries, - timeoutMs: parseTimeout(flags.timeout) ?? 40_000, - url: localURL, - }) - - if (flags.events) writeEvent('tunnel.connected', { target: target.id, localURL, url: started.url }) - - if (flags['url-only']) { - process.stdout.write(`${started.url}\n`) - } else if (flags.json) { - await printData({ target: target.id, localURL, url: started.url, cloudflaredPath: cloudflaredPath(flags['cloudflared-path']) }, 'json') - } else { - await printSuccess({ - message: `Cloudflare Tunnel connected for ${target.id}`, - detail: `${started.url} -> ${localURL}`, - data: { target: target.id, localURL, url: started.url }, - }, 'human') - process.stderr.write('Press Ctrl-C to stop the tunnel.\n') - } - - const exit = await waitForExit(started) - if (exit.reason === 'process' && exit.code !== 0) { - throw new Error(`cloudflared exited after the tunnel connected${exit.code === null ? '' : ` with code ${exit.code}`}.\n${started.tryMessage}`) - } - } -} - -function normalizeLocalURL(value: string): string { - const url = new URL(value) - url.pathname = url.pathname === '/' ? '/' : url.pathname - url.search = '' - url.hash = '' - return url.toString().replace(/\/$/, '') -} - -function parseTimeout(value?: string): number | undefined { - if (!value) return undefined - const match = value.trim().match(/^(\d+(?:\.\d+)?)(ms|s|m)?$/i) - if (!match) throw new Error(`Invalid --timeout value "${value}". Use values like 500ms, 30s, or 2m.`) - const amount = Number(match[1]) - const unit = (match[2] ?? 'ms').toLowerCase() - if (unit === 'ms') return amount - if (unit === 's') return amount * 1000 - if (unit === 'm') return amount * 60_000 - return amount -} - -async function waitForExit(started: Awaited>): Promise<{ code: number | null; reason: 'process' | 'signal' }> { - return new Promise(resolve => { - const finish = () => { - started.stop() - resolve({ code: 0, reason: 'signal' }) - } - - process.once('SIGINT', finish) - process.once('SIGTERM', finish) - started.done.then(({ code }) => { - process.off('SIGINT', finish) - process.off('SIGTERM', finish) - resolve({ code, reason: 'process' }) - }) - }) -} diff --git a/packages/cli-plugin-cloudflare/src/index.ts b/packages/cli-plugin-cloudflare/src/index.ts deleted file mode 100644 index 63fcd66d..00000000 --- a/packages/cli-plugin-cloudflare/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export const name = '@beeper/cli-plugin-cloudflare' diff --git a/packages/cli-plugin-cloudflare/test/smoke.mjs b/packages/cli-plugin-cloudflare/test/smoke.mjs deleted file mode 100644 index de1dc4bd..00000000 --- a/packages/cli-plugin-cloudflare/test/smoke.mjs +++ /dev/null @@ -1,25 +0,0 @@ -import assert from 'node:assert/strict' -import { Config } from '@oclif/core/config' -import { cloudflaredDomain, findKnownError, findTunnelURL, versionIsGreaterThan, whatToTry } from '../dist/lib/cloudflared.js' - -const config = await Config.load({ root: new URL('..', import.meta.url).pathname }) -const commandIDs = [...config.commands].map(command => command.id) - -assert.deepEqual(commandIDs, ['targets:tunnel']) -assert.equal(versionIsGreaterThan('2024.8.2', '2024.8.1'), true) -assert.equal(versionIsGreaterThan('2024.8.2', '2024.8.2'), false) -assert.equal(versionIsGreaterThan('2024.8.2', '2024.9.0'), false) -assert.equal(findTunnelURL('INF https://example.trycloudflare.com ready'), 'https://example.trycloudflare.com') -assert.equal(findTunnelURL('INF https://example.example.com ready', 'example.com'), 'https://example.example.com') -assert.equal(findTunnelURL('INF https://example.example.com ready'), undefined) -assert.match(findKnownError('2024-01-01 ERR Failed to serve quic connection connIndex=1'), /Could not start Cloudflare Tunnel/) -assert.match(whatToTry(), /BEEPER_CLOUDFLARED_PATH/) - -const previousDomain = process.env.BEEPER_CLOUDFLARED_DOMAIN -process.env.BEEPER_CLOUDFLARED_DOMAIN = 'beeper.test' -assert.equal(cloudflaredDomain(), 'beeper.test') -if (previousDomain === undefined) { - delete process.env.BEEPER_CLOUDFLARED_DOMAIN -} else { - process.env.BEEPER_CLOUDFLARED_DOMAIN = previousDomain -} diff --git a/packages/cli-plugin-cloudflare/tsconfig.json b/packages/cli-plugin-cloudflare/tsconfig.json deleted file mode 100644 index 7ea087bd..00000000 --- a/packages/cli-plugin-cloudflare/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "exactOptionalPropertyTypes": false, - "isolatedModules": false, - "module": "NodeNext", - "moduleResolution": "NodeNext", - "outDir": "./dist", - "baseUrl": ".", - "paths": { - "@beeper/cli/plugin-sdk": ["../cli/dist/plugin-sdk.d.ts"] - }, - "rootDir": "./src" - }, - "include": ["src/**/*.ts"], - "exclude": ["dist", "node_modules"] -} diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md deleted file mode 100644 index a8fa2acd..00000000 --- a/packages/cli/CHANGELOG.md +++ /dev/null @@ -1,125 +0,0 @@ -# Changelog - -## 0.3.0 (2026-03-24) - -Full Changelog: [v0.2.0...v0.3.0](https://github.com/beeper/desktop-api-cli/compare/v0.2.0...v0.3.0) - -### Features - -* **api:** manual updates ([0770d3a](https://github.com/beeper/desktop-api-cli/commit/0770d3ad509df79341bb1067de910a4ececdc07e)) - - -### Bug Fixes - -* avoid reading from stdin unless request body is form encoded or json ([040b555](https://github.com/beeper/desktop-api-cli/commit/040b5552613ef61130225e1f6cf81ab213d3a4a9)) -* better support passing client args in any position ([9100cf6](https://github.com/beeper/desktop-api-cli/commit/9100cf6e1ee5b4e7c7bcb262636fe3f458224cd4)) -* cli no longer hangs when stdin is attached to a pipe with empty input ([4842b1e](https://github.com/beeper/desktop-api-cli/commit/4842b1eb8edf8b1bef423780f23908e10f317631)) -* fix for test cases with newlines in YAML and better error reporting ([c997492](https://github.com/beeper/desktop-api-cli/commit/c9974925c5d60aa534b8d619e51594cab0d622f0)) -* improve linking behavior when developing on a branch not in the Go SDK ([c5964a1](https://github.com/beeper/desktop-api-cli/commit/c5964a17c6203b0de179d6c73c62665c8c33e364)) -* improved workflow for developing on branches ([e7e8488](https://github.com/beeper/desktop-api-cli/commit/e7e84887ed26fb2de032a1960ea881fb15c7b3c4)) -* no longer require an API key when building on production repos ([f38af96](https://github.com/beeper/desktop-api-cli/commit/f38af96666a55ffd4014c4f6190f5807df8b740e)) -* only set client options when the corresponding CLI flag or env var is explicitly set ([923b0eb](https://github.com/beeper/desktop-api-cli/commit/923b0ebdeeafe5aa17f416651d82ee20c643fd16)) - - -### Chores - -* **internal:** codegen related update ([2a0195e](https://github.com/beeper/desktop-api-cli/commit/2a0195ef893339e931434cc30db149f45a0479aa)) -* **internal:** tweak CI branches ([e727f07](https://github.com/beeper/desktop-api-cli/commit/e727f07b8f5f6792c0e67fd6a048cfdd91ab3ba3)) -* **internal:** update gitignore ([4b48c83](https://github.com/beeper/desktop-api-cli/commit/4b48c838b1e7afe2cf6545b0de8a4f3146e4e1dd)) -* **tests:** bump steady to v0.19.4 ([f4cc892](https://github.com/beeper/desktop-api-cli/commit/f4cc892fc179e321e0b75e381f68805fd732774e)) -* **tests:** bump steady to v0.19.5 ([097fdc8](https://github.com/beeper/desktop-api-cli/commit/097fdc85a1790b8687763f7e187645bbcba22f93)) -* **tests:** bump steady to v0.19.6 ([4af2dce](https://github.com/beeper/desktop-api-cli/commit/4af2dce0afd3741d445d75a85bf4523a9297dcbe)) - - -### Refactors - -* **tests:** switch from prism to steady ([e67e839](https://github.com/beeper/desktop-api-cli/commit/e67e839be04bd2da13cc0a941047a8552f95a32b)) - -## 0.2.0 (2026-03-06) - -Full Changelog: [v0.1.1...v0.2.0](https://github.com/beeper/desktop-api-cli/compare/v0.1.1...v0.2.0) - -### Features - -* add `--max-items` flag for paginated/streaming endpoints ([d2cd184](https://github.com/beeper/desktop-api-cli/commit/d2cd184ffe8bd8bef28411f35c23c9e0bbed958f)) -* add support for file downloads from binary response endpoints ([2e0b0a7](https://github.com/beeper/desktop-api-cli/commit/2e0b0a7080adef772b1c2b9f33d957f267d354ad)) -* improved documentation and flags for client options ([46e772c](https://github.com/beeper/desktop-api-cli/commit/46e772c72af1ea406d17ddaa7aeaf58e8b917a16)) -* support passing required body params through pipes ([4aa3f99](https://github.com/beeper/desktop-api-cli/commit/4aa3f993788d31566b0035a34134b4745ccd6643)) - - -### Bug Fixes - -* add missing client parameter flags to test cases ([83e3537](https://github.com/beeper/desktop-api-cli/commit/83e35370ae87614bba0c4deacdafcbd6614ed4ee)) -* add missing example parameters for test cases ([86b6743](https://github.com/beeper/desktop-api-cli/commit/86b6743be9efd4809bfdfbf02da29089121e680c)) -* avoid printing usage errors twice ([62fbdae](https://github.com/beeper/desktop-api-cli/commit/62fbdaedc3cf1724fc9102eae7dcb87e148aabb7)) -* fix for encoding arrays with `any` type items ([148ce2c](https://github.com/beeper/desktop-api-cli/commit/148ce2c1a8fe9b284fb29a4397c8904a0e366824)) -* more gracefully handle empty stdin input ([d758018](https://github.com/beeper/desktop-api-cli/commit/d75801861b11dfd1cb5568e2b5da3eb0176b7c21)) - - -### Chores - -* **ci:** skip uploading artifacts on stainless-internal branches ([858bf53](https://github.com/beeper/desktop-api-cli/commit/858bf533863314ae579e4bd8a3fc25ff1b0490d4)) -* **internal:** codegen related update ([2fcd536](https://github.com/beeper/desktop-api-cli/commit/2fcd53660f6195309b786d5b50aaeb22595a5404)) -* **test:** do not count install time for mock server timeout ([c89d312](https://github.com/beeper/desktop-api-cli/commit/c89d31244a0735738964709d9c15961c2f9a2a71)) -* zip READMEs as part of build artifact ([d1a1267](https://github.com/beeper/desktop-api-cli/commit/d1a12679c7c0366a8a50972010874bc9e9a6d643)) - -## 0.1.1 (2026-02-25) - -Full Changelog: [v0.1.0...v0.1.1](https://github.com/beeper/desktop-api-cli/compare/v0.1.0...v0.1.1) - -### Bug Fixes - -* pin formatting for headers to always use repeat/dot formats ([97ea814](https://github.com/beeper/desktop-api-cli/commit/97ea81439b3abccbcdaeb1fdd393e44e6a07aa6e)) - - -### Chores - -* update readme with better instructions for installing with homebrew ([b248f13](https://github.com/beeper/desktop-api-cli/commit/b248f13f6bf455a224a85fa97af2c17122c53101)) - -## 0.1.0 (2026-02-24) - -Full Changelog: [v0.0.1...v0.1.0](https://github.com/beeper/desktop-api-cli/compare/v0.0.1...v0.1.0) - -### ⚠ BREAKING CHANGES - -* add support for passing files as parameters - -### Features - -* add readme documentation for passing files as arguments ([f7b1b4a](https://github.com/beeper/desktop-api-cli/commit/f7b1b4af1c7220c9cd21afc58aba32508504073b)) -* add support for passing files as parameters ([49ca642](https://github.com/beeper/desktop-api-cli/commit/49ca642691b546494d700c2f782aa8ae88d9767e)) -* **api:** add cli ([c57f02a](https://github.com/beeper/desktop-api-cli/commit/c57f02af602f2def16c59c1ba1db4059ff2b0fd5)) -* **api:** add reactions ([b16c08a](https://github.com/beeper/desktop-api-cli/commit/b16c08ae98ca116514ce0a8977142db49196c5d9)) -* **api:** add upload asset and edit message endpoints ([da2ca66](https://github.com/beeper/desktop-api-cli/commit/da2ca66a4910e80ffd919fd8105b026497b9a0ea)) -* **api:** api update ([56afbbc](https://github.com/beeper/desktop-api-cli/commit/56afbbc6d75f019edac752e6100caefe333de434)) -* **api:** api update ([9f69525](https://github.com/beeper/desktop-api-cli/commit/9f69525f394b74266a664c50899f760525844cd8)) -* **api:** manual updates ([0c8a0ee](https://github.com/beeper/desktop-api-cli/commit/0c8a0ee510531e30ce5ed8748af4b56c19cf2433)) -* **api:** manual updates ([b3fb2a0](https://github.com/beeper/desktop-api-cli/commit/b3fb2a0cf62fff27da2f2d1ca8062bf3b3ede582)) -* **api:** manual updates ([b66f2b5](https://github.com/beeper/desktop-api-cli/commit/b66f2b5c68eb90c5644faf0c1f2fc67a94f100cb)) -* **client:** provide file completions when using file embed syntax ([bdf34ce](https://github.com/beeper/desktop-api-cli/commit/bdf34cecc8cdbd2e9d19dade4616970bfd43ae6a)) -* **cli:** improve shell completions for namespaced commands and flags ([eded84a](https://github.com/beeper/desktop-api-cli/commit/eded84a5cc05bb700f5d0c50add30ec257738aa0)) -* improved support for passing files for `any`-typed arguments ([8c8fa87](https://github.com/beeper/desktop-api-cli/commit/8c8fa8743cbfd67e8ddbf165a06c151d319e2612)) - - -### Bug Fixes - -* fix for file uploads to octet stream and form encoding endpoints ([f26b475](https://github.com/beeper/desktop-api-cli/commit/f26b475dce7f9eb0cc9fa5a20c26667e1c32fc1a)) -* fix for nullable arguments ([5f10511](https://github.com/beeper/desktop-api-cli/commit/5f105117110982a972554fb9ab720b354829bae4)) -* fix for when terminal width is not available ([eba0a3f](https://github.com/beeper/desktop-api-cli/commit/eba0a3f905f7eb7bc3cd9a8f571713e5bdff1f87)) -* fix mock tests with inner fields that have underscores ([7c4554a](https://github.com/beeper/desktop-api-cli/commit/7c4554a35871394eeed6927ee401ce7cc6fe99b8)) -* preserve filename in content-disposition for file uploads ([c230cef](https://github.com/beeper/desktop-api-cli/commit/c230cefdf540e6d49e102cbb9cb9010625be04c6)) -* prevent tests from hanging on streaming/paginated endpoints ([fcf4608](https://github.com/beeper/desktop-api-cli/commit/fcf4608ef721212651ce3839bd4885d0b86744cf)) -* restore support for void endpoints ([de2984b](https://github.com/beeper/desktop-api-cli/commit/de2984b4cec53693f0b5b684cdc498c410211a82)) -* use RawJSON for iterated values instead of re-marshalling ([06bc1c7](https://github.com/beeper/desktop-api-cli/commit/06bc1c7a0ba890d76e2c210476ad1d7586cd069a)) - - -### Chores - -* add build step to ci ([f2bddcf](https://github.com/beeper/desktop-api-cli/commit/f2bddcf00a9a3faacf1a1a8293f3f46b7befe187)) -* configure new SDK language ([6db7b30](https://github.com/beeper/desktop-api-cli/commit/6db7b300c46fd6331b4bada5759f5e31ed5a0b56)) -* configure new SDK language ([388b391](https://github.com/beeper/desktop-api-cli/commit/388b3910792deb197365fc9e5fbf266260845d9e)) -* **internal:** codegen related update ([1cfe60b](https://github.com/beeper/desktop-api-cli/commit/1cfe60b670c95b1e9840d9768b8b5936512919aa)) -* **internal:** codegen related update ([8e91787](https://github.com/beeper/desktop-api-cli/commit/8e91787eccfac7cc9455444fca72045a312f95a0)) -* **internal:** codegen related update ([46e5aef](https://github.com/beeper/desktop-api-cli/commit/46e5aefac484dd40ad069ea5500dd2c885abf611)) -* **internal:** codegen related update ([6987a4c](https://github.com/beeper/desktop-api-cli/commit/6987a4c5870caa9323a8be80124069fc5f28d45a)) -* update documentation in readme ([5633fad](https://github.com/beeper/desktop-api-cli/commit/5633fad79b0a5db1d66b83a58d1d0e8fe7cf3f1d)) diff --git a/packages/cli/README.md b/packages/cli/README.md index 2d1e574d..8e498beb 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,2999 +1,110 @@ -
- # beeper -**One CLI for all your chats.** Built for you and your agent — batteries included. - -[![npm](https://img.shields.io/npm/v/beeper-cli.svg?label=npm&color=6E56F8)](https://www.npmjs.com/package/beeper-cli) -[![license](https://img.shields.io/badge/license-MIT-6E56F8.svg)](https://github.com/beeper/cli/blob/main/packages/cli/LICENSE) -[![docs](https://img.shields.io/badge/docs-online-6E56F8.svg)](https://beeper.github.io/cli) -[![built with Bun](https://img.shields.io/badge/built%20with-Bun-6E56F8.svg)](https://bun.sh) - -
- -Talks to Beeper Desktop on this machine, to a Beeper Server you self-host, or -to either one running somewhere else. Send and receive across the chat -networks Beeper bridges, from one CLI shaped for scripts, agents, and humans -in a hurry. - -**Supported chat networks** (via Beeper's bridges): -WhatsApp · iMessage · Telegram · Discord · Signal · Instagram DMs · -Facebook Messenger · X (Twitter) DMs · LinkedIn · Slack · -Google Messages (RCS/SMS) · Google Chat · Matrix · IRC · Bluesky. -Run `beeper bridges list` for the live list on your target. - -📖 **[Read the docs](https://beeper.github.io/cli)** · command manual: `beeper man` · open docs: `beeper docs` - -## Features - -- **Connects to your Beeper.** Local Beeper Desktop on this machine (default), a Beeper Server you install and manage via the CLI, or a remote Beeper Desktop or Beeper Server authorized over OAuth/PKCE — or a bearer token in CI. -- **Setup that does the work.** `beeper setup` finds Beeper Desktop, offers to launch it, adopts the session. `--server --install` installs and starts a headless server in one step. `--oauth` opens the browser. `--remote URL` does the rest. -- **Every chat, every network.** List, search, start, archive, pin, mute, rename, focus. Read, edit, delete, react. Send text, files, stickers, voice, typing indicators. Download media. Export to JSON or Markdown. -- **Verification first-class.** SAS/QR device verification, recovery-key unlock, `status`/`doctor` to reach an encrypted-ready target — without leaving the shell. -- **Agent-shaped automation.** `--json` everywhere, NDJSON `--events`, `watch` with WebSocket + outbound HMAC-signed webhooks, `rpc` over stdin/stdout, `man --json` tool manifests, raw `api get`/`post`/`request` for Beeper Client API endpoints we haven't wrapped yet. -- **Safe by default.** `--read-only` rejects every mutating command. Writes stay explicit. Plugins extend the CLI without forking it. +Beeper CLI for Beeper Desktop and Beeper Server. It talks to a selected target, +keeps setup interactive, and exposes a scriptable command surface for humans, +shell scripts, and agents. ## Install -### Homebrew (recommended) - -```sh -brew install beeper/tap/cli -``` - -The installed command is `beeper`. - -### npm - ```sh -npx beeper-cli --help npm install -g beeper-cli +beeper --help ``` -The package name is `beeper-cli`; the installed command is `beeper`. - -### Build from source - -This repo is a Bun workspace. From the repo root: +For source builds: ```sh bun install -bun --filter @beeper/cli run build -bun --filter @beeper/cli run dev -- --help -``` - -For local CLI development inside `packages/cli`: - -```sh -bun run dev -- --help -``` - -Regenerate this README after command, flag, or argument changes: - -```sh -bun run readme -``` - -## Quick start - -The happy path: Beeper Desktop is already on this machine. `beeper setup` finds -it, offers to launch it if it's not running, and adopts the session. - -```text -$ beeper setup -Looking for Beeper Desktop… found, not running. -Launch it now? [Y/n] y -▎ Launched Beeper Desktop - next Run `beeper setup` again once it finishes starting. - -$ beeper setup -Use this Desktop session for CLI access? [Y/n] y -▎ Connected desktop - accounts whatsapp, telegram, imessage - endpoint http://127.0.0.1:23373 - -$ beeper chats list --limit 3 - 10313 Family 3 unread - 8951 Alice · - 7204 Eng standup 12 unread - -$ beeper messages search "flight" - 8951 Alice · "your flight is at 6:40, gate B23" 2d ago - 10313 Family · "what flight are you on?" 1w ago - -$ beeper send text --to Family --message "on my way" -▎ Sent Family - message "on my way" - at 2026-05-18T14:02:11Z - -$ beeper export --out ./beeper-export -▎ Exported ./beeper-export - chats 214 messages 38,901 attachments 1,205 -``` - -Recipients accept a numeric local chat ID, a full Beeper/Matrix chat ID, an -iMessage chat ID, an exact title, or search text. Ambiguous matches prompt in a -TTY; pass `--pick N` in scripts. - -## Connecting a target - -A *target* is the Beeper endpoint `beeper` talks to — local Beeper Desktop, -local Beeper Server, or a remote Beeper Desktop or Beeper Server. Pick one of -four paths. - -### 1. Local Beeper Desktop (default, recommended) - -If Beeper Desktop is installed and signed in here, `beeper setup` discovers it -on `http://127.0.0.1:23373` and adopts the existing session. If it's installed -but not running, `setup` offers to launch it. If it's not installed at all, -`--install` does that in one step. - -```text -$ beeper setup --desktop --install -▎ Installed Beeper Desktop (stable) -▎ Launched Beeper Desktop - next Sign in to Beeper Desktop, then re-run `beeper setup`. - -$ beeper setup -▎ Connected desktop - accounts whatsapp, telegram -``` - -Variants: `beeper setup --local` to skip discovery and force the local path; -`beeper install desktop --channel nightly` for the nightly channel. - -### 2. Local Beeper Server (self-hosted, managed by the CLI) - -For a headless long-running setup on this machine, install and adopt a local -Beeper Server. The CLI manages the process — `targets start/stop/restart/logs/enable`. - -```text -$ beeper setup --server --install -▎ Installed Beeper Server (stable) -▎ Started server on http://127.0.0.1:23373 - auth Opening browser to authorize this server… -▎ Connected server - accounts (none) - next Run `beeper accounts add` to connect a network. - -$ beeper accounts add -? Which bridge? whatsapp - Scan this QR code with WhatsApp on your phone: - ▄▄▄▄▄▄▄ ▄ ▄ ▄▄▄▄▄▄▄ - █ ███ █ ▄█▄ █ ███ █ - █ ███ █ ▀█▀ █ ███ █ - ▀▀▀▀▀▀▀ ▀ ▀ ▀▀▀▀▀▀▀ -▎ Connected whatsapp · +1•••4242 -``` - -Variants: `beeper install server`, `beeper install server --server-env staging`. - -### 3. Remote Desktop or Server via OAuth (PKCE) - -For a Beeper Desktop or Server running on another machine, authorize the CLI -through a browser-based OAuth/PKCE flow. - -```text -$ beeper setup --remote https://desktop.example.com -▎ Authorizing https://desktop.example.com - flow OAuth (PKCE) — opening browser… -▎ Connected remote (desktop.example.com) - accounts whatsapp, telegram, signal -``` - -Variants: `beeper setup --oauth` (PKCE against the default Beeper auth); -`beeper targets add remote work https://desktop.example.com --default` to -register additional remotes. - -### 4. Bearer token (non-interactive / CI) - -For agents, CI, and scripts, hand the CLI a bearer token directly — no -browser, no interactive prompts. - -```sh -BEEPER_ACCESS_TOKEN=... beeper chats list --json -BEEPER_ACCESS_TOKEN=... BEEPER_DESKTOP_BASE_URL=https://desktop.example.com \ - beeper messages list --chat 10313 --json -``` - -Once connected, `beeper accounts add` walks each chat-network bridge through -its own login — QR, code, OAuth, cookie, whatever the bridge requires — so -WhatsApp, Telegram, Discord, iMessage, and the rest show up under `accounts list`. - -## Documentation - -Full documentation lives at **[beeper.github.io/cli](https://beeper.github.io/cli)** -(built from [`docs/`](docs/) with Astro Starlight — a fully static site). - -| Topic | Page | Commands | -| --- | --- | --- | -| **Setup + install** | [connect](https://beeper.github.io/cli/connect/) · [install](https://beeper.github.io/cli/install/) · [auth](https://beeper.github.io/cli/auth/) | `setup` · `install desktop` · `install server` · `verify` · `status` · `doctor` · `auth status` | -| **Targets** | [targets](https://beeper.github.io/cli/targets/) | `targets list` · `targets add desktop` · `targets add server` · `targets add remote` · `targets use` · `targets status` · `targets logs` | -| **Bridges + accounts** | [accounts](https://beeper.github.io/cli/accounts/) | `bridges list` · `bridges show` · `accounts list` · `accounts add` · `accounts show` · `accounts use` · `accounts remove` | -| **Chats** | [chats](https://beeper.github.io/cli/chats/) | `chats list` · `chats search` · `chats show` · `chats start` · `chats archive` · `chats pin` · `chats mute` · `chats priority` · `chats remind` · `chats rename` · `chats draft` · `chats focus` | -| **Messages** | [messages](https://beeper.github.io/cli/messages/) · [send](https://beeper.github.io/cli/send/) · [presence](https://beeper.github.io/cli/presence/) | `messages list` · `messages search` · `messages export` · `send text` · `send file` · `send sticker` · `send voice` · `send react` · `presence` | -| **Contacts + media** | [contacts](https://beeper.github.io/cli/contacts/) · [media](https://beeper.github.io/cli/media/) · [export](https://beeper.github.io/cli/export/) | `contacts list` · `contacts search` · `media download` · `export` | -| **Automation** | [scripting](https://beeper.github.io/cli/scripting/) · [watch](https://beeper.github.io/cli/watch/) · [rpc](https://beeper.github.io/cli/rpc/) · [api](https://beeper.github.io/cli/api/) | `watch` · `watch --webhook` · `rpc` · `man` · `api get` · `api post` · `api request` | -| **Maintenance** | [config](https://beeper.github.io/cli/config/) · [update](https://beeper.github.io/cli/update/) | `update` · `config` · `completion` · `docs` · `version` | - -Use `beeper docs` to open the CLI docs and `beeper man` to print the local -command manual. To work on the docs site locally: `cd docs && bun install && bun run dev`. - -## Configuration - -Default Beeper Client API target: `http://127.0.0.1:23373`. CLI configuration is -stored under your user config dir; print it with `beeper config path`. - -**Global flags:** `--base-url`, `--target`, `--json`, `--events`, -`--full`, `--timeout`, `--read-only`, `--debug`, `--yes`, `--quiet`. - -**Environment overrides:** - -| Variable | Effect | -| --- | --- | -| `BEEPER_ACCESS_TOKEN` | Bearer token for the selected target. Overrides stored OAuth login. | -| `BEEPER_DESKTOP_BASE_URL` | Beeper Client API base URL (Desktop or Server). Defaults to `http://127.0.0.1:23373`. | -| `BEEPER_READONLY` | `1`/`true`/`yes`/`on` enables read-only mode globally. | -| `BEEPER_CLI_CONFIG_DIR` | Override config directory for testing or isolated profiles. | - -## Exit codes - -| Code | Meaning | -| --- | --- | -| `0` | Success. | -| `1` | Generic runtime error. | -| `2` | Usage error (parsing, validation, missing required flag/arg, read-only refusal). | -| `3` | Auth required (no stored token; sign in or set `BEEPER_ACCESS_TOKEN`). | -| `4` | Target/account not ready (`doctor` reports this when readiness is not `ready`). | -| `5` | Selector matched nothing (unknown target, account, chat, contact). | -| `6` | Ambiguous selector (multiple matches; pass an exact ID or `--pick N`). | - -JSON output preserves the same envelope on failure: `{"success":false,"data":null,"error":"...","exitCode":N}` written to stderr. - -## Addressing - -- Chat arguments accept numeric local chat IDs, full Beeper/Matrix chat IDs, iMessage chat IDs, exact titles, or search text. -- For scripts on the same target/profile, prefer the numeric local chat ID shown by `beeper chats list`; use the full Beeper/Matrix chat ID when the selector must work across targets or profiles. -- Numeric local chat IDs come from the selected Desktop database. Treat them as local to that target/profile. -- Ambiguous chat matches return numbered choices; pass `--pick N` to select one. -- Account arguments accept account IDs, network names, bridge type/id, or account user identity. -- Account filters can expand a network name to multiple matching accounts. -- `contacts search` and `chats start` can search across all accounts when `--account` is omitted. -- `contacts list` accepts the same account selectors as other account-scoped commands. - -## Output and scripting - -Most commands support: - -- app-like text by default, optimized for scanning chats, messages, contacts, accounts, and media -- `--json` for `{"success":true,"data":...,"error":null}` output on stdout -- `--events` for NDJSON lifecycle events on stderr from long-running commands -- `--read-only` to reject commands that modify Beeper or local CLI state -- `--full` to disable truncation -- `--debug` for SDK debug logging -- `--target` or `--base-url` to point at a different target - -`man --json` prints a compact command manifest for tools and agents. -`rpc` runs newline-delimited JSON command RPC over stdin/stdout. - -## Raw API access - -Raw Beeper Client API calls live under `api`, so scripts can reach a new -endpoint before a workflow command exists: - -```sh -beeper api get /v1/info -beeper api post /v1/messages/{chatID}/send --body '{"text":"hello"}' -beeper api request DELETE /v1/chats/abc/messages/def/reactions --body '{"reactionKey":"👍"}' -``` - -## Plugins - -Beeper CLI supports optional oclif plugins. List recommended Beeper plugins: - -```sh -beeper plugins available -``` - -Install a published plugin: - -```sh -beeper plugins install @beeper/cli-plugin-cloudflare -``` - -For plugin development, import from `@beeper/cli/plugin-sdk` and expose oclif -commands from your package. Link a local plugin while working on it: - -```sh -beeper plugins link ./packages/cli-plugin-cloudflare -beeper targets tunnel --help -``` - -First-party optional plugins: - -| Package | Adds | -| --- | --- | -| `@beeper/cli-plugin-cloudflare` | `targets tunnel` for exposing a selected Beeper target through Cloudflare Tunnel. | - - -## Command Summary - -| Command | Summary | -| --- | --- | -| `setup` | Make the selected target ready for messaging | -| `install desktop` | Install Beeper Desktop locally | -| `install server` | Install Beeper Server locally | -| `targets list` | List configured Beeper targets | -| `bridges list` | List bridges that can connect chat accounts | -| `bridges show` | Show bridge details, login flows, and connected accounts | -| `targets add desktop` | Add a managed Beeper Desktop target | -| `targets add server` | Add a managed Beeper Server target | -| `targets add remote` | Add a remote Beeper Desktop or Server target | -| `targets use` | Set the default target | -| `targets show` | Show target details | -| `targets status` | Check endpoint and process reachability for a target | -| `targets start` | Start a local Server target or open Beeper Desktop | -| `targets stop` | Stop a local Beeper Server target | -| `targets restart` | Restart a local Beeper Server target | -| `targets logs` | Print logs for a local Beeper Desktop or Server install | -| `targets enable` | Enable a local Beeper Server target at login | -| `targets disable` | Disable a local Beeper Server target at login | -| `targets remove` | Remove a target | -| `targets tunnel` | Expose a local Desktop API over a public Cloudflare tunnel | -| `auth status` | Show stored auth for the selected target | -| `auth logout` | Clear stored authentication | -| `auth email start` | Start email sign-in for a target | -| `auth email response` | Finish email sign-in with a verification code | -| `verify` | Finish setup verification or verify another device | -| `verify status` | Show encryption and device-verification readiness | -| `verify approve` | Approve a pending device verification request | -| `verify recovery-key` | Unlock encrypted messages with a recovery key | -| `verify reset-recovery-key` | Create a new encrypted-messages recovery key | -| `verify cancel` | Cancel an in-progress device verification | -| `verify list` | List active verification work | -| `verify start` | Start a device verification request | -| `verify show` | Show the current active verification request | -| `verify sas` | Start emoji verification | -| `verify sas-confirm` | Confirm matching emoji verification | -| `verify qr-scan` | Submit a scanned QR-code verification payload | -| `verify qr-confirm` | Confirm that the other device scanned your QR code | -| `accounts list` | List connected accounts | -| `accounts add` | Connect a chat account by bridge | -| `accounts show` | Show account details | -| `accounts remove` | Remove an account | -| `accounts use` | Select a default account for account-scoped commands | -| `chats list` | List chats | -| `chats search` | Search chats | -| `chats show` | Show chat details | -| `chats start` | Start a chat | -| `chats archive` | Archive a chat | -| `chats unarchive` | Unarchive a chat | -| `chats pin` | Pin a chat | -| `chats unpin` | Unpin a chat | -| `chats mute` | Mute a chat | -| `chats unmute` | Unmute a chat | -| `chats mark-read` | Mark a chat as read | -| `chats mark-unread` | Mark a chat as unread | -| `chats priority` | Move a chat to the Inbox or Low Priority | -| `chats notify-anyway` | Send an iMessage Notify Anyway alert | -| `chats rename` | Rename a chat | -| `chats description` | Set a chat description | -| `chats avatar` | Set a chat avatar | -| `chats draft` | Set or clear a chat draft | -| `chats disappear` | Set disappearing-message expiry | -| `chats remind` | Set a chat reminder | -| `chats unremind` | Clear a chat reminder | -| `chats focus` | Focus Beeper Desktop on a chat | -| `messages list` | List chat messages | -| `messages search` | Search messages across chats | -| `messages show` | Show one message | -| `messages context` | Show message context | -| `messages edit` | Edit a message | -| `messages delete` | Delete a message | -| `messages export` | Export one chat to JSON | -| `send text` | Send a text message | -| `send file` | Send a file | -| `send react` | Send a reaction to a message | -| `send sticker` | Send a sticker | -| `send unreact` | Remove a reaction from a message | -| `send voice` | Send a voice note | -| `presence` | Send a typing (or paused) indicator to a chat | -| `contacts list` | List contacts | -| `contacts search` | Search contacts | -| `contacts show` | Show contact details | -| `resolve chat` | Resolve a chat selector to concrete chat candidates | -| `resolve account` | Resolve an account selector | -| `resolve contact` | Resolve a contact selector | -| `resolve target` | Resolve a target selector | -| `resolve bridge` | Resolve a bridge selector | -| `media download` | Download message media | -| `export` | Export accounts, chats, messages, Markdown transcripts, and attachments | -| `watch` | Stream Desktop API WebSocket events | -| `rpc` | Run newline-delimited JSON command RPC over stdin/stdout | -| `man` | Print the command manual | -| `schema` | Print machine-readable command/flag schema | -| `doctor` | Probe the target live and report diagnostics | -| `status` | Show selected target and setup readiness | -| `docs` | Open Beeper CLI docs | -| `version` | Print CLI version | -| `completion` | Print shell completion setup | -| `plugins` | Manage Beeper CLI plugins | -| `plugins available` | List recommended optional Beeper CLI plugins | -| `update` | Check and install Beeper updates | -| `config get` | Print CLI configuration | -| `config set` | Set a CLI configuration value | -| `config path` | Print the CLI config path | -| `config reset` | Reset CLI configuration | -| `api get` | Call a raw Desktop API GET path | -| `api post` | Call a raw Desktop API POST path with a JSON body | -| `api request` | Call a raw Desktop API path with any supported HTTP method | - -## Command Reference - -### `beeper setup` -Make the selected target ready for messaging - -```sh -beeper setup +bun run check +bun --filter beeper-cli run dev -- --help ``` -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--channel=` | option | Install release channel Default: stable | -| `--desktop` | boolean | Set up a local Beeper Desktop target | -| `--email=` | option | Sign in with an email address | -| `--install` | boolean | Allow installing missing managed runtime | -| `--local` | boolean | Use the local Beeper Desktop session on this device | -| `--oauth` | boolean | Authorize the target with browser OAuth/PKCE | -| `--remote=` | option | Connect to a remote Beeper Desktop or Server URL | -| `--server` | boolean | Set up a local Beeper Server target | -| `--server-env=` | option | Server environment: local, dev, staging, or prod Default: prod | -| `--username=` | option | Username to use if setup creates a new account | - -Examples: +## Quick Start ```sh beeper setup -beeper setup --local -beeper setup --oauth -beeper setup --remote https://desktop.example.com -beeper setup --desktop --install -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper install desktop` -Install Beeper Desktop locally - -```sh -beeper install desktop -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--channel=` | option | Desktop release channel Default: stable | - -Examples: - -```sh -beeper install desktop -beeper install desktop --channel nightly -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper install server` -Install Beeper Server locally - -```sh -beeper install server -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--channel=` | option | Server release channel Default: stable | -| `--server-env=` | option | Server environment: local, dev, staging, or prod Default: prod | - -Examples: - -```sh -beeper install server -beeper install server --server-env staging -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper targets list` -List configured Beeper targets - -```sh beeper targets list +beeper status +beeper chats list --limit 10 +beeper messages search "flight" +beeper send text --to "Family" --message "on my way" ``` -Examples: - -```sh -beeper targets list -beeper targets list --json -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper bridges list` -List bridges that can connect chat accounts - -```sh -beeper bridges list -``` - -`bridges list` is the scriptable bridge catalog. Use `accounts add` without an argument for the guided account connection flow. - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--available` | boolean | Only bridges available to add (--no-available to exclude) | -| `--provider=` | option | Limit to bridge provider | - -Examples: - -```sh -beeper bridges list -beeper bridges list --provider local --json -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper bridges show` -Show bridge details, login flows, and connected accounts - -```sh -beeper bridges show -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `bridge` | yes | Bridge ID, display name, network, or type | - -Examples: - -```sh -beeper bridges show local-whatsapp -beeper bridges show telegram -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper targets add desktop` -Add a managed Beeper Desktop target - -```sh -beeper targets add desktop [name] -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `name` | no | Target name (default: "desktop") | - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--default` | boolean | Set this target as the default after creation | -| `--port=` | option | TCP port the managed Desktop will expose its API on | -| `--server-env=` | option | Server environment: local, dev, staging, or prod Default: prod | - -Examples: - -```sh -beeper targets add desktop work --default -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper targets add server` -Add a managed Beeper Server target - -```sh -beeper targets add server [name] -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `name` | no | Target name (default: "server") | - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--default` | boolean | Set this target as the default after creation | -| `--port=` | option | TCP port the managed Server will expose its API on | -| `--server-env=` | option | Server environment: local, dev, staging, or prod Default: prod | - -Examples: - -```sh -beeper targets add server prod --server-env prod --default -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper targets add remote` -Add a remote Beeper Desktop or Server target - -```sh -beeper targets add remote -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `name` | yes | Local name for the target | -| `url` | yes | Base URL of the remote Desktop or Server API | - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--default` | boolean | Set this target as the default after creation | - -Examples: - -```sh -beeper targets add remote work https://desktop.example.com --default -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper targets use` -Set the default target - -```sh -beeper targets use -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `name` | yes | Target name | - -Examples: - -```sh -beeper targets use work -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper targets show` -Show target details - -```sh -beeper targets show [name] -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `name` | no | Target name. Defaults to the selected target. | - -Examples: - -```sh -beeper targets show -beeper targets show work -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper targets status` -Check endpoint and process reachability for a target - -```sh -beeper targets status [name] -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `name` | no | Target name. Defaults to the selected target. | - -Examples: - -```sh -beeper targets status -beeper targets status work --json -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper targets start` -Start a local Server target or open Beeper Desktop - -```sh -beeper targets start [name] -``` - -Arguments: +`beeper setup` preserves the interactive setup flow: it discovers local Beeper +Desktop, can install or launch Desktop, can install and start Beeper Server, +and can adopt remote targets. Use `--no-input` when running non-interactively. -| Name | Required | Description | -| --- | --- | --- | -| `name` | no | Target name. Defaults to the selected target. | +## Command Reference -Examples: +The live command registry is the source of truth. Use: ```sh -beeper targets start work +beeper --help +beeper schema --json +beeper --help ``` -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. +Current command groups: -### `beeper targets stop` -Stop a local Beeper Server target +- `setup`, `status`, `version`, `schema` +- `use account`, `use target`, `remove account`, `remove target` +- `auth email start`, `auth email response`, `auth logout` +- `targets add`, `targets list`, `targets runtime start`, `targets runtime stop`, `targets runtime restart`, `targets logs`, `targets tunnel` +- `install desktop`, `install server` +- `accounts add`, `accounts list` +- `chats list`, `chats show`, `chats start`, `chats archive`, `chats pin`, `chats mute`, `chats read`, `chats rename`, `chats description`, `chats avatar`, `chats priority`, `chats draft`, `chats remind`, `chats disappear`, `chats focus`, `chats notify-anyway` +- `messages list`, `messages search`, `messages context`, `messages edit`, `messages delete` +- `send text`, `send file`, `send sticker`, `send voice`, `send react`, `send presence` +- `contacts list` +- `media download`, `export`, `watch` +- `api request`, `mcp` +- `resolve account`, `resolve bridge`, `resolve chat`, `resolve contact`, `resolve target` -```sh -beeper targets stop [name] -``` +## Global Flags -Arguments: +- Output: `--json`, `--plain`, `--events`, `--debug` +- Targeting: `--target` +- Safety: `--dry-run`, `--safety-profile`, `--wrap-untrusted` +- Interaction: `--no-input`, `--force` -| Name | Required | Description | -| --- | --- | --- | -| `name` | no | Target name. Defaults to the selected target. | +## Targets -Examples: +A target is the endpoint the CLI talks to. It can be local Desktop, local +Server, or a remote Desktop/Server. ```sh -beeper targets stop work +beeper setup --desktop +beeper setup --server --install +beeper targets add work https://desktop.example.com --default +beeper targets tunnel work --url-only ``` -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper targets restart` -Restart a local Beeper Server target - -```sh -beeper targets restart [name] -``` +## Safety Profiles -Arguments: +Safety profiles live in `packages/cli/safety-profiles/`: -| Name | Required | Description | -| --- | --- | --- | -| `name` | no | Target name. Defaults to the selected target. | +- `readonly.yaml`: blocks mutating commands. +- `agent-safe.yaml`: allows common read and messaging workflows, blocks high-risk operations. +- `full.yaml`: leaves command policy unrestricted. -Examples: +Use them with: ```sh -beeper targets restart work +beeper --safety-profile packages/cli/safety-profiles/readonly.yaml chats list ``` -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper targets logs` -Print logs for a local Beeper Desktop or Server install +## Development ```sh -beeper targets logs [name] +bun --filter beeper-cli run typecheck +bun --filter beeper-cli run test +bun --filter beeper-cli run build +bun run check ``` -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `name` | no | Target name. Defaults to the selected target. | - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--all` | boolean | Print all matching log files instead of only recent files | -| `--files=` | option | Desktop log files to print, newest first Default: 5 | -| `--lines=` | option | Lines to print from each log file Default: 200 | - -Examples: - -```sh -beeper targets logs work -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper targets enable` -Enable a local Beeper Server target at login - -```sh -beeper targets enable [name] -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `name` | no | Target name. Defaults to the selected target. | - -Examples: - -```sh -beeper targets enable work -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper targets disable` -Disable a local Beeper Server target at login - -```sh -beeper targets disable [name] -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `name` | no | Target name. Defaults to the selected target. | - -Examples: - -```sh -beeper targets disable work -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper targets remove` -Remove a target - -```sh -beeper targets remove -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `name` | yes | Target name | - -Examples: - -```sh -beeper targets remove work -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper targets tunnel` -Expose a local Desktop API over a public Cloudflare tunnel - -```sh -beeper targets tunnel -``` - -Examples: - -```sh -beeper targets tunnel -beeper targets tunnel --target work --read-only -beeper targets tunnel --as work-laptop --port 23373 -``` - -### `beeper auth status` -Show stored auth for the selected target - -```sh -beeper auth status -``` - -Examples: - -```sh -beeper auth status -beeper auth status --json -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper auth logout` -Clear stored authentication - -```sh -beeper auth logout -``` - -Examples: - -```sh -beeper auth logout -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper auth email start` -Start email sign-in for a target - -```sh -beeper auth email start -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--email=` | option | Email address to sign in with Required. | - -Examples: - -```sh -beeper auth email start --email you@example.com --target work --json -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper auth email response` -Finish email sign-in with a verification code - -```sh -beeper auth email response -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--code=` | option | Email verification code Required. | -| `--setup-request-id=` | option | Setup request ID from auth email start Required. | -| `--username=` | option | Username to use if setup creates a new account | - -Examples: - -```sh -beeper auth email response --setup-request-id --code --target work --json -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper verify` -Finish setup verification or verify another device - -```sh -beeper verify -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--user=` | option | User ID to verify against (defaults to your own account) | - -Examples: - -```sh -beeper verify -beeper verify --user @alice:beeper.com -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper verify status` -Show encryption and device-verification readiness - -```sh -beeper verify status -``` - -Examples: - -```sh -beeper verify status --json -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper verify approve` -Approve a pending device verification request - -```sh -beeper verify approve -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--id=` | option | Verification request ID. Defaults to the active request. | - -Examples: - -```sh -beeper verify approve --id active -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper verify recovery-key` -Unlock encrypted messages with a recovery key - -```sh -beeper verify recovery-key -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--key=` | option | Recovery key string Required. | - -Examples: - -```sh -beeper verify recovery-key --key ABCD-EFGH-IJKL -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper verify reset-recovery-key` -Create a new encrypted-messages recovery key - -```sh -beeper verify reset-recovery-key -``` - -Examples: - -```sh -beeper verify reset-recovery-key -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper verify cancel` -Cancel an in-progress device verification - -```sh -beeper verify cancel -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--id=` | option | Verification request ID. Defaults to the active request. | - -Examples: - -```sh -beeper verify cancel -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper verify list` -List active verification work - -```sh -beeper verify list -``` - -Examples: - -```sh -beeper verify list -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper verify start` -Start a device verification request - -```sh -beeper verify start -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--user=` | option | User ID to verify with (defaults to your own account) | - -Examples: - -```sh -beeper verify start --user @alice:beeper.com -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper verify show` -Show the current active verification request - -```sh -beeper verify show -``` - -Examples: - -```sh -beeper verify show --json -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper verify sas` -Start emoji verification - -```sh -beeper verify sas -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--id=` | option | Verification request ID. Defaults to the active request. | - -Examples: - -```sh -beeper verify sas -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper verify sas-confirm` -Confirm matching emoji verification - -```sh -beeper verify sas-confirm -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--id=` | option | Verification request ID. Defaults to the active request. | - -Examples: - -```sh -beeper verify sas-confirm -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper verify qr-scan` -Submit a scanned QR-code verification payload - -```sh -beeper verify qr-scan -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--id=` | option | Verification request ID. Defaults to the active request. | -| `--payload=` | option | Raw QR-code data scanned from the other device Required. | - -Examples: - -```sh -beeper verify qr-scan --payload "..." -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper verify qr-confirm` -Confirm that the other device scanned your QR code - -```sh -beeper verify qr-confirm -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--id=` | option | Verification request ID. Defaults to the active request. | - -Examples: - -```sh -beeper verify qr-confirm -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper accounts list` -List connected accounts - -```sh -beeper accounts list -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--account=...` | option | Filter by account selector | -| `--ids` | boolean | Print only account IDs | - -Examples: - -```sh -beeper accounts list -beeper accounts list --account whatsapp --json -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper accounts add` -Connect a chat account by bridge - -```sh -beeper accounts add [bridge] -``` - -`accounts add` without an argument opens the guided bridge chooser. Pass a bridge ID when you already know which chat network connector to use. - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `bridge` | no | Bridge ID, network, or type to connect. Omit to list available bridges. | - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--cookie=...` | option | Cookie value for non-interactive login, in name=value form. Repeat for multiple cookies. | -| `--field=...` | option | Field value for non-interactive login, in id=value form. Repeat for multiple fields. | -| `--flow=` | option | Login flow ID. If omitted, Desktop chooses the default flow. | -| `--guided` | boolean | Prompt through login steps until completion | -| `--login-id=` | option | Existing login ID to re-login as | -| `--non-interactive` | boolean | Do not prompt; require --flow, --field, and --cookie values when needed. | -| `--webview` | boolean | Use Bun.WebView to collect cookie login fields when a cookie step is returned. | -| `--webview-backend=` | option | Bun.WebView backend for cookie login steps. Default: chrome | -| `--webview-timeout=` | option | Seconds to wait for Bun.WebView cookie collection. Default: 120 | - -Examples: - -```sh -beeper accounts add -beeper accounts add local-whatsapp -beeper accounts add discord --non-interactive --cookie sessiontoken=... -beeper accounts add discord --webview --webview-backend chrome -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper accounts show` -Show account details - -```sh -beeper accounts show -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `account` | yes | Account selector (ID, network, bridge, or user identity) | - -Examples: - -```sh -beeper accounts show whatsapp-main -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper accounts remove` -Remove an account - -```sh -beeper accounts remove -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `account` | yes | Account selector (ID, network, bridge, or user identity) | - -Examples: - -```sh -beeper accounts remove whatsapp-main -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper accounts use` -Select a default account for account-scoped commands - -```sh -beeper accounts use -``` - -Persists the choice in CLI config. Account-scoped commands that take --account fall back to this default when --account is omitted. Use `beeper accounts use ""` (or `beeper config set defaultAccount ""`) to clear. - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `account` | yes | Account selector (ID, network, bridge, user identity), or "" to clear. | - -Examples: - -```sh -beeper accounts use whatsapp-main -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper chats list` -List chats - -```sh -beeper chats list -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--account=...` | option | Limit to Account ID, network, bridge, or account user | -| `--archived` | boolean | Only archived chats (--no-archived to exclude) | -| `--ids` | boolean | Print preferred chat selectors, using numeric local chat IDs when available | -| `--limit=` | option | Maximum chats to print Default: 20 | -| `--low-priority` | boolean | Only Low Priority chats (--no-low-priority to exclude) | -| `--muted` | boolean | Only muted chats (--no-muted to exclude) | -| `--pinned` | boolean | Only pinned chats (--no-pinned to exclude) | -| `--unread` | boolean | Only chats with unread messages (--no-unread to exclude) | - -Examples: - -```sh -beeper chats list -beeper chats list --pinned --limit 50 -beeper chats list --unread --no-muted --format json -beeper ls --format ids -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper chats search` -Search chats - -```sh -beeper chats search -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `query` | yes | Search query (title, participant, or network) | - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--account=...` | option | Limit to Account ID, network, bridge, or account user | -| `--ids` | boolean | Print preferred chat selectors, using numeric local chat IDs when available | -| `--limit=` | option | Maximum chats to print Default: 20 | - -Examples: - -```sh -beeper chats search Family -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper chats show` -Show chat details - -```sh -beeper chats show -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--max-participants=` | option | Limit number of participants returned in chat details | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | - -Examples: - -```sh -beeper chats show --chat 10313 -beeper chats show --chat '!plUOsWkvMmJmJPVAjS:beeper.com' -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper chats start` -Start a chat - -```sh -beeper chats start -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `user` | yes | User ID, phone number, email, or display name | - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--account=` | option | Account selector. Defaults to the single available account or the matrix account. | -| `--title=` | option | Optional initial title for a new group chat | - -Examples: - -```sh -beeper chats start +15551234567 -beeper chats start @alice:beeper.com --title "Alice" -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper chats archive` -Archive a chat - -```sh -beeper chats archive -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | - -Examples: - -```sh -beeper chats archive --chat 10313 -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper chats unarchive` -Unarchive a chat - -```sh -beeper chats unarchive -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | - -Examples: - -```sh -beeper chats unarchive --chat 10313 -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper chats pin` -Pin a chat - -```sh -beeper chats pin -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | - -Examples: - -```sh -beeper chats pin --chat 10313 -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper chats unpin` -Unpin a chat - -```sh -beeper chats unpin -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | - -Examples: - -```sh -beeper chats unpin --chat 10313 -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper chats mute` -Mute a chat - -```sh -beeper chats mute -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | - -Examples: - -```sh -beeper chats mute --chat 10313 -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper chats unmute` -Unmute a chat - -```sh -beeper chats unmute -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | - -Examples: - -```sh -beeper chats unmute --chat 10313 -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper chats mark-read` -Mark a chat as read - -```sh -beeper chats mark-read -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--message=` | option | Mark read at (or unread starting from) this message ID | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | - -Examples: - -```sh -beeper chats mark-read --chat 10313 -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper chats mark-unread` -Mark a chat as unread - -```sh -beeper chats mark-unread -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--message=` | option | Mark read at (or unread starting from) this message ID | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | - -Examples: - -```sh -beeper chats mark-unread --chat 10313 -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper chats priority` -Move a chat to the Inbox or Low Priority - -```sh -beeper chats priority -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--level=` | option | Destination: inbox (default mailbox) or low (Low Priority) Required. | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | - -Examples: - -```sh -beeper chats priority --chat 10313 --level inbox -beeper chats priority --chat '!plUOsWkvMmJmJPVAjS:beeper.com' --level low -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper chats notify-anyway` -Send an iMessage Notify Anyway alert - -```sh -beeper chats notify-anyway -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | - -Examples: - -```sh -beeper chats notify-anyway --chat 10313 -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper chats rename` -Rename a chat - -```sh -beeper chats rename -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | -| `--title=` | option | New chat title Required. | - -Examples: - -```sh -beeper chats rename --chat 10313 --title "Family" -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper chats description` -Set a chat description - -```sh -beeper chats description -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--clear` | boolean | Clear the existing description instead of setting one | -| `--description=` | option | New chat description | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | - -Examples: - -```sh -beeper chats description --chat 10313 --description "Engineering chat" -beeper chats description --chat 10313 --clear -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper chats avatar` -Set a chat avatar - -```sh -beeper chats avatar -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--clear` | boolean | Clear the existing avatar instead of setting a new one | -| `--file=` | option | Image file to upload as the new avatar | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | - -Examples: - -```sh -beeper chats avatar --chat 10313 --file ./team.png -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper chats draft` -Set or clear a chat draft - -```sh -beeper chats draft -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--clear` | boolean | Clear the existing draft instead of setting one | -| `--file=` | option | Attachment file to upload with the draft | -| `--filename=` | option | Override the displayed filename of the attachment | -| `--mime=` | option | Override MIME type detection for the attachment | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | -| `--text=` | option | Draft text. Omit and pass --clear to remove the draft. | - -Examples: - -```sh -beeper chats draft --chat 10313 --text "on my way" -beeper chats draft --chat 10313 --clear -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper chats disappear` -Set disappearing-message expiry - -```sh -beeper chats disappear -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | -| `--seconds=` | option | Timer in seconds, or "off" to disable Required. | - -Examples: - -```sh -beeper chats disappear --chat 10313 --seconds 86400 -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper chats remind` -Set a chat reminder - -```sh -beeper chats remind -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--dismiss-on-message` | boolean | Dismiss the reminder automatically when a new message arrives | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | -| `--when=` | option | ISO timestamp when the reminder should trigger Required. | - -Examples: - -```sh -beeper chats remind --chat 10313 --when 2026-06-01T09:00:00Z -beeper chats remind --chat 10313 --when 2026-06-01T09:00:00Z --dismiss-on-message -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper chats unremind` -Clear a chat reminder - -```sh -beeper chats unremind -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | - -Examples: - -```sh -beeper chats unremind --chat 10313 -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper chats focus` -Focus Beeper Desktop on a chat - -```sh -beeper chats focus -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--attachment=` | option | Prefill the chat composer with this attachment file path | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--draft=` | option | Prefill the chat composer with this draft text | -| `--message=` | option | Scroll Desktop to this message ID after focusing | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | - -Examples: - -```sh -beeper chats focus --chat 10313 -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper messages list` -List chat messages - -```sh -beeper messages list -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--after-cursor=` | option | Paginate messages newer than this message ID | -| `--asc` | boolean | Order oldest first (default: newest first) | -| `--before-cursor=` | option | Paginate messages older than this message ID | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--ids` | boolean | Print only message IDs | -| `--limit=` | option | Maximum messages to print Default: 50 | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | -| `--sender=` | option | Filter by sender: me, others, or a specific user ID (client-side) | - -Examples: - -```sh -beeper messages list --chat 10313 --limit 50 -beeper messages list --chat 10313 --before-cursor "" --limit 100 -beeper messages list --chat 10313 --sender me --asc -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper messages search` -Search messages across chats - -```sh -beeper messages search [query] -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `query` | no | Search text (literal word match) | - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--account=...` | option | Limit to an account selector. Repeat for multiple. | -| `--after=` | option | Only messages at or after this ISO timestamp | -| `--before=` | option | Only messages at or before this ISO timestamp | -| `--chat=...` | option | Limit to a chat selector. Repeat for multiple. | -| `--chat-type=` | option | Only group chats or direct messages | -| `--exclude-low-priority` | boolean | Exclude low-priority chats | -| `--ids` | boolean | Print only message IDs | -| `--include-muted` | boolean | Include muted chats | -| `--limit=` | option | Maximum results Default: 50 | -| `--media=...` | option | Filter by media type. Repeat for multiple. | -| `--sender=` | option | me, others, or a user ID | - -Examples: - -```sh -beeper messages search invoice -beeper search invoice --format jsonl --select id,chatID,text -beeper messages search --chat 10313 --sender me --media image -beeper messages search "flight" --after 2026-01-01 --before 2026-02-01 -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper messages show` -Show one message - -```sh -beeper messages show -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--id=` | option | Message ID, pendingMessageID, or Matrix event ID Required. | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | - -Examples: - -```sh -beeper messages show --chat 10313 --id -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper messages context` -Show message context - -```sh -beeper messages context -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--after=` | option | Number of messages to include after the target Default: 10 | -| `--before=` | option | Number of messages to include before the target Default: 10 | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--id=` | option | Target message ID to center the window on Required. | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | - -Examples: - -```sh -beeper messages context --chat 10313 --id --before 5 --after 5 -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper messages edit` -Edit a message - -```sh -beeper messages edit -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--id=` | option | Message ID to edit (must be one of your own messages with no attachments) Required. | -| `--message=` | option | New message text Required. | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | - -Examples: - -```sh -beeper messages edit --chat 10313 --id --message "fixed" -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper messages delete` -Delete a message - -```sh -beeper messages delete -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--for-everyone` | boolean | Delete for everyone when the network supports it (otherwise deletes only for you) | -| `--id=` | option | Message ID to delete (final message ID; pending IDs are rejected) Required. | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | - -Examples: - -```sh -beeper messages delete --chat 10313 --id --for-everyone -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper messages export` -Export one chat to JSON - -```sh -beeper messages export -``` - -Lightweight per-chat JSON export. For a full export with transcripts, attachments, and multiple chats, use `beeper export`. - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--after=` | option | Only messages at or after this ISO timestamp (client-side filter) | -| `--after-cursor=` | option | Paginate messages newer than this message ID | -| `--asc` | boolean | Order oldest first (default: newest first) | -| `--before=` | option | Only messages at or before this ISO timestamp (client-side filter) | -| `--before-cursor=` | option | Paginate messages older than this message ID | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--limit=` | option | Maximum messages to export | -| `-o, --output=` | option | Output path; - writes JSON to stdout Default: - | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | - -Examples: - -```sh -beeper messages export --chat 10313 --output chat.json -beeper messages export --chat 8951 --after 2026-01-01T00:00:00Z --output - -beeper messages export --chat '!plUOsWkvMmJmJPVAjS:beeper.com' --before-cursor "" --limit 500 -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper send text` -Send a text message - -```sh -beeper send text -``` - -Returns when Desktop accepts the send request. Pass `--wait` to wait until the message leaves the pending state or fails. - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--mention=...` | option | User ID to @-mention (repeatable) | -| `--message=` | option | Message text to send Required. | -| `--no-preview` | boolean | Disable automatic link preview for URLs in the message | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | -| `--reply-to=` | option | Send as a reply to this message ID | -| `--to=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--wait` | boolean | Wait for the message to leave the pending state (or fail) before returning | -| `--wait-timeout=` | option | Maximum wait time in ms when --wait is set Default: 30000 | - -Examples: - -```sh -beeper send --to 10313 --message "on my way" --dry-run --format json -beeper send text --to 10313 --message "on my way" -beeper send text --to 8951 --message "hi" -beeper send text --to "Family" --message "hi" --pick 1 -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper send file` -Send a file - -```sh -beeper send file -``` - -Returns when Desktop accepts the send request. Pass `--wait` to wait until the message leaves the pending state or fails. - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--caption=` | option | Optional caption to send alongside the file | -| `--file=` | option | Local file path to upload (max 500 MB) Required. | -| `--filename=` | option | Override the displayed filename | -| `--mime=` | option | Override MIME type detection | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | -| `--reply-to=` | option | Send as a reply to this message ID | -| `--to=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--wait` | boolean | Wait for the message to leave the pending state (or fail) before returning | -| `--wait-timeout=` | option | Maximum wait time in ms when --wait is set Default: 30000 | - -Examples: - -```sh -beeper send file --to 8951 --file ./photo.jpg --caption "from today" -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper send react` -Send a reaction to a message - -```sh -beeper send react -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--id=` | option | Message ID to react to Required. | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | -| `--reaction=` | option | Reaction key (emoji, shortcode, or custom emoji key) Required. | -| `--to=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--transaction=` | option | Optional transaction ID for deduplication | - -Examples: - -```sh -beeper send react --to 10313 --id --reaction "+1" -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper send sticker` -Send a sticker - -```sh -beeper send sticker -``` - -Uploads the file and sends as a sticker message. Defaults --mime to image/webp. - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--file=` | option | Sticker file (typically 512x512 WebP) Required. | -| `--filename=` | option | Override the displayed filename | -| `--mime=` | option | MIME type for the sticker (default: image/webp) Default: image/webp | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | -| `--reply-to=` | option | Send as a reply to this message ID | -| `--to=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--wait` | boolean | Wait for the message to leave the pending state (or fail) before returning | -| `--wait-timeout=` | option | Maximum wait time in ms when --wait is set Default: 30000 | - -Examples: - -```sh -beeper send sticker --to 10313 --file ./hi.webp -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper send unreact` -Remove a reaction from a message - -```sh -beeper send unreact -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--id=` | option | Message ID whose reaction to remove Required. | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | -| `--reaction=` | option | Reaction key to remove (emoji, shortcode, or custom emoji key) Required. | -| `--to=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--transaction=` | option | Optional transaction ID for deduplication | - -Examples: - -```sh -beeper send unreact --to 10313 --id --reaction "+1" -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper send voice` -Send a voice note - -```sh -beeper send voice -``` - -Uploads the audio file and sends as a voice note. Defaults --mime to audio/ogg. - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--duration=` | option | Voice note duration in seconds (overrides upload-detected duration) | -| `--file=` | option | Voice note audio file (OGG/Opus recommended) Required. | -| `--filename=` | option | Override the displayed filename | -| `--mime=` | option | MIME type for the voice note (default: audio/ogg) Default: audio/ogg | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | -| `--reply-to=` | option | Send as a reply to this message ID | -| `--to=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--wait` | boolean | Wait for the message to leave the pending state (or fail) before returning | -| `--wait-timeout=` | option | Maximum wait time in ms when --wait is set Default: 30000 | - -Examples: - -```sh -beeper send voice --to 10313 --file ./note.ogg -beeper send voice --to 10313 --file ./note.ogg --duration 12 -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper presence` -Send a typing (or paused) indicator to a chat - -```sh -beeper presence -``` - -Requires server-side support. Networks without typing notifications return an error. - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--chat=` | option | Chat selector (ID, local ID, title, or search text) Required. | -| `--duration=` | option | When --state is typing, send paused automatically after this many seconds | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | -| `--state=` | option | Indicator to send Default: typing | - -Examples: - -```sh -beeper presence --chat 10313 -beeper presence --chat 10313 --state paused -beeper presence --chat 10313 --duration 5 -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper contacts list` -List contacts - -```sh -beeper contacts list -``` - -List merged contacts for a specific account with cursor-based pagination. - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--account=...` | option | Limit to Account ID, network, bridge, or account user | -| `--ids` | boolean | Print only contact user IDs | -| `--limit=` | option | Maximum contacts to print Default: 50 | -| `--query=` | option | Optional blended contact lookup query | - -Examples: - -```sh -beeper contacts list --account whatsapp --query alice -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper contacts search` -Search contacts - -```sh -beeper contacts search -``` - -Search contacts on a specific account using merged account contacts, network search, and exact identifier lookup. - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `query` | yes | Contact search query | - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--account=...` | option | Account ID, network, bridge, or account user. Omit to search every account. | - -Examples: - -```sh -beeper contacts search alice -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper contacts show` -Show contact details - -```sh -beeper contacts show -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `id` | yes | Contact user ID, display name, or phone/handle | - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--account=...` | option | Limit to account ID, network, bridge, or account user | - -Examples: - -```sh -beeper contacts show "Alice" --account whatsapp -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper resolve chat` -Resolve a chat selector to concrete chat candidates - -```sh -beeper resolve chat -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `selector` | yes | Chat ID, local ID, exact title, or search text | - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--account=...` | option | Limit to account selector. Repeat for multiple. | -| `--limit=` | option | Maximum candidates to return Default: 10 | -| `--pick=` | option | Select the Nth candidate (1-indexed) | - -Examples: - -```sh -beeper resolve chat Family --format json -beeper resolve chat Family --pick 1 --results-only -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper resolve account` -Resolve an account selector - -```sh -beeper resolve account -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `selector` | yes | Account ID, network, bridge, or account user selector | - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--pick=` | option | Select the Nth candidate (1-indexed) | - -Examples: - -```sh -beeper resolve account whatsapp --format json -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper resolve contact` -Resolve a contact selector - -```sh -beeper resolve contact -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `selector` | yes | Contact name, username, phone, email, or ID | - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--account=...` | option | Limit to account selector. Repeat for multiple. | -| `--limit=` | option | Maximum candidates to return per account Default: 10 | -| `--pick=` | option | Select the Nth candidate (1-indexed) | - -Examples: - -```sh -beeper resolve contact Alice --account whatsapp --format json -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper resolve target` -Resolve a target selector - -```sh -beeper resolve target -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `selector` | yes | Target name, ID, type, or base URL | - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--pick=` | option | Select the Nth candidate (1-indexed) | - -Examples: - -```sh -beeper resolve target desktop --format json -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper resolve bridge` -Resolve a bridge selector - -```sh -beeper resolve bridge -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `selector` | yes | Bridge ID, type, provider, or display name | - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--pick=` | option | Select the Nth candidate (1-indexed) | - -Examples: - -```sh -beeper resolve bridge whatsapp --format json -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper media download` -Download message media - -```sh -beeper media download -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `url` | yes | mxc:// or localmxc:// URL | - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `-o, --out=` | option | Output directory; pass - to stream the file to stdout Default: . | - -Examples: - -```sh -beeper media download mxc://beeper.com/abc --out ./downloads -beeper media download mxc://beeper.com/abc -o - > photo.jpg -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper export` -Export accounts, chats, messages, Markdown transcripts, and attachments - -```sh -beeper export -``` - -Creates a resumable Beeper Desktop export using the official Desktop API SDK. The export directory contains accounts.json, chats.json, manifest.json, and one directory per chat with chat.json, messages.json, messages.markdown, messages.html, downloaded attachments, and checkpoint state for interrupted runs. - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--account=...` | option | Limit to an account selector. Repeat to include more accounts. | -| `--chat=...` | option | Limit to a chat selector. Repeat to include more chats. | -| `--limit-chats=` | option | Maximum chats to export. Intended for testing large exports. | -| `--limit-messages=` | option | Maximum messages per chat. Intended for testing large exports. | -| `--max-participants=` | option | Maximum participants to include in each chat.json. Default: 500 | -| `--no-attachments` | boolean | Skip downloading message attachments. | -| `-o, --out=` | option | Export directory. Default: beeper-export | -| `--pick=` | option | Pick the Nth result when the selector is ambiguous (1-indexed) | - -Examples: - -```sh -beeper export --out ./beeper-export -beeper export --chat 10313 --out ./chat -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper watch` -Stream Desktop API WebSocket events - -```sh -beeper watch -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `-c, --chat=...` | option | Chat ID to subscribe to. Defaults to all chats. | -| `--exclude-type=...` | option | Drop events of these types. Repeat for multiple. | -| `--include-type=...` | option | Only forward events of these types. Repeat for multiple. | -| `--webhook=` | option | Forward each event to this URL as a POST request (best-effort, fire-and-forget) | -| `--webhook-queue=` | option | Maximum pending webhook deliveries before dropping events Default: 64 | -| `--webhook-secret=` | option | HMAC-SHA256 secret. Signs payloads with X-Beeper-Signature: sha256= | - -Examples: - -```sh -beeper watch -beeper watch --chat 10313 --json -beeper watch --include-type message.upserted --include-type message.deleted -beeper watch --webhook https://example.com/hook --webhook-secret "$BEEPER_WEBHOOK_SECRET" -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper rpc` -Run newline-delimited JSON command RPC over stdin/stdout - -```sh -beeper rpc -``` - -Reads JSON lines like {"id":1,"command":"send text --to 10313 --message hello"} or {"id":1,"args":["status","--json"]}. - -Examples: - -```sh -printf '{"id":1,"command":"chats list --json"}\n' | beeper rpc -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper man` -Print the command manual - -```sh -beeper man -``` - -Examples: - -```sh -beeper man -beeper man --format json -beeper man --format ids -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper schema` -Print machine-readable command/flag schema - -```sh -beeper schema [command] -``` - -Agent-first schema for commands, flags, args, examples, mutation metadata, selectors, output shapes, and related commands. - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `command` | no | Optional command path, such as "messages search" | - -Examples: - -```sh -beeper schema -beeper schema send --results-only -beeper schema --select commands.path,commands.flags.name --results-only -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper doctor` -Probe the target live and report diagnostics - -```sh -beeper doctor -``` - -Active reachability check plus readiness diagnostics. Exits non-zero when the target is not ready. For a cheap snapshot use `beeper status`. - -Examples: - -```sh -beeper doctor -beeper doctor --json -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper status` -Show selected target and setup readiness - -```sh -beeper status -``` - -Read-only readiness snapshot for the selected target. For active reachability checks and diagnostics, run `beeper doctor`. - -Examples: - -```sh -beeper status -beeper status --json -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper docs` -Open Beeper CLI docs - -```sh -beeper docs -``` - -Examples: - -```sh -beeper docs -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper version` -Print CLI version - -```sh -beeper version -``` - -Examples: - -```sh -beeper version -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper completion` -Print shell completion setup - -```sh -beeper completion [shell] -``` - -Print static shell completion setup for bash, zsh, fish, or PowerShell. Pass `--semantic` to print a small supplementary snippet that adds live suggestions for `--chat`, `--to`, `--account`, and `--target` by calling back into `beeper _complete`. Source it after the static completion setup. - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `shell` | no | Shell to set up (bash, zsh, fish, or powershell) | - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `-r, --refresh-cache` | boolean | Refresh the autocomplete cache before printing setup | -| `--semantic` | boolean | Print a semantic-completion snippet (chats/accounts/targets) for bash or zsh | - -Examples: - -```sh -beeper completion -``` - -### `beeper plugins` -Manage Beeper CLI plugins - -```sh -beeper plugins -``` - -List recommended Beeper CLI plugins, or use oclif plugin commands to install, link, update, and remove plugins. - -Examples: - -```sh -beeper plugins -beeper plugins install @beeper/cli-plugin-cloudflare -``` - -### `beeper plugins available` -List recommended optional Beeper CLI plugins - -```sh -beeper plugins available -``` - -Examples: - -```sh -beeper plugins available -beeper plugins available --json -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper update` -Check and install Beeper updates - -```sh -beeper update -``` - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--check` | boolean | Only check for updates; do not install | -| `--cli` | boolean | Check the Beeper CLI package | -| `--desktop` | boolean | Check the CLI-owned Desktop install | -| `--server` | boolean | Check the CLI-owned Server install | - -Examples: - -```sh -beeper update --check -beeper update --cli -beeper update --server -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper config get` -Print CLI configuration - -```sh -beeper config get [key] -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `key` | no | Optional config key to print | - -Examples: - -```sh -beeper config get -beeper config get defaultTarget -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper config set` -Set a CLI configuration value - -```sh -beeper config set -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `key` | yes | Config key to set | -| `value` | yes | Config value (pass "" to clear) | - -Examples: - -```sh -beeper config set defaultTarget work -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper config path` -Print the CLI config path - -```sh -beeper config path -``` - -Examples: - -```sh -beeper config path -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper config reset` -Reset CLI configuration - -```sh -beeper config reset -``` - -Examples: - -```sh -beeper config reset -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper api get` -Call a raw Desktop API GET path - -```sh -beeper api get -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `path` | yes | API path, for example /v1/info | - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--no-auth` | boolean | Call a public API path without a bearer token | - -Examples: - -```sh -beeper api get /v1/info -beeper api get /v1/chats --json -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper api post` -Call a raw Desktop API POST path with a JSON body - -```sh -beeper api post -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `path` | yes | API path, for example /v1/messages/{chatID}/send | - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--body=` | option | JSON request body Default: {} | -| `--no-auth` | boolean | Call a public API path without a bearer token | - -Examples: - -```sh -beeper api post /v1/chats/abc/read --body '{"messageID":"x"}' -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -### `beeper api request` -Call a raw Desktop API path with any supported HTTP method - -```sh -beeper api request -``` - -Arguments: - -| Name | Required | Description | -| --- | --- | --- | -| `method` | yes | HTTP method | -| `path` | yes | API path, for example /v1/info | - -Flags: - -| Flag | Type | Description | -| --- | --- | --- | -| `--body=` | option | JSON request body | -| `--no-auth` | boolean | Call a public API path without a bearer token | - -Examples: - -```sh -beeper api request DELETE /v1/chats/abc/messages/def/reactions --body '{"reactionKey":"👍"}' -``` - -Global flags: `--base-url`, `--target`, `--debug`, `--dry-run`, `--events`, `--force`, `--format`, `--full`, `--json`, `--no-input`, `--quiet`, `--read-only`, `--results-only`, `--select`, `--timeout`, `--yes`. - -## Publishing - -Beeper CLI releases ship signed macOS Bun binaries inside versioned archives and -a thin npm package that downloads, verifies, extracts, and runs the matching -GitHub Release archive. - -For now, publishing runs from a local macOS machine: - -```sh -bun run release 0.6.1 -``` - -The local release command: - -- builds standalone Bun binaries -- signs and notarizes macOS binaries when local signing credentials are available -- uploads versioned macOS and Linux archives to the GitHub release -- publishes `beeper-cli` to npm as a thin binary launcher package -- updates `beeper/homebrew-tap` with the pinned archive SHA - -Required local credentials: - -- GitHub CLI authenticated with release and tap access -- npm auth for publishing `beeper-cli` -- local Developer ID signing identity, or Fastlane match access via a - `MOBILE_SECRETS_FILE` path exported in your shell -- `HOMEBREW_TAP_GITHUB_TOKEN` for updating the tap - -## Inspiration - -- [wacli](https://wacli.sh/) — scriptable WhatsApp CLI whose command-line product shape we borrow from. +The package entrypoint is `packages/cli/bin/cli.js`; local development uses +`packages/cli/bin/dev.js`. ## License -MIT — see [`packages/cli/LICENSE`](packages/cli/LICENSE). +MIT. See `LICENSE`. diff --git a/packages/cli/SECURITY.md b/packages/cli/SECURITY.md deleted file mode 100644 index 60d38a66..00000000 --- a/packages/cli/SECURITY.md +++ /dev/null @@ -1,27 +0,0 @@ -# Security Policy - -## Reporting Security Issues - -This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. - -To report a security issue, please contact the Stainless team at security@stainless.com. - -## Responsible Disclosure - -We appreciate the efforts of security researchers and individuals who help us maintain the security of -SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible -disclosure practices by allowing us a reasonable amount of time to investigate and address the issue -before making any information public. - -## Reporting Non-SDK Related Security Issues - -If you encounter security issues that are not directly related to SDKs but pertain to the services -or products provided by Beeper Desktop, please follow the respective company's security reporting guidelines. - -### Beeper Desktop Terms and Policies - -Please contact security@beeper.com for any questions or concerns regarding the security of our services. - ---- - -Thank you for helping us keep the SDKs and systems they interact with secure. diff --git a/packages/cli/beeper-setup-redesign-spec.md b/packages/cli/beeper-setup-redesign-spec.md deleted file mode 100644 index e3767d5a..00000000 --- a/packages/cli/beeper-setup-redesign-spec.md +++ /dev/null @@ -1,475 +0,0 @@ -# Beeper CLI Setup Redesign Plan - -## Goal - -`beeper setup` should make Beeper CLI usable with the least possible explanation. - -For most Desktop users, the happy path should be: - -```sh -beeper setup -``` - -Then: - -```txt -Found Beeper Desktop on this device. - -Signed in as: you@beeper.com -Connected accounts: iMessage, WhatsApp, Signal - -Use this Desktop session for CLI access? [Y/n] -``` - -Pressing Enter should leave the CLI ready. - -## Critique Of The Draft - -The intern proposal has the right product instinct, but it over-expands the public surface. - -Keep: - -- Desktop-first setup. -- Local Desktop session as the recommended path. -- OAuth/PKCE as the limited-permission path. -- Remote Desktop/Server setup. -- Install Desktop/Server from setup when explicitly confirmed. -- Concrete prompts that show the detected account and connected accounts. -- Every interactive choice having a command or flag equivalent. - -Reject or defer: - -- A new public `connections` model. The CLI already has targets; use them. -- `beeper auth list/use/desktop/remote/manual` in the first pass. That recreates target management under another noun. -- Public `beeper setup desktop|server|remote|manual` subcommands for v1. They are redundant if flags and existing target/install commands exist. -- Keychain as a blocker. Store like the current CLI first, add keychain later. -- Email-code setup flags as the main Desktop experience. Normal users receive codes asynchronously, and installed Desktop may not expose the setup-login routes. Desktop login should be driven by Desktop itself unless the Server/headless API explicitly supports the flow. - -## Product Model - -Use one object: **target**. - -A target is a runnable or reachable Beeper endpoint/profile: - -- Built-in local Desktop target: `desktop` -- Managed Desktop profile: `targets create desktop ` -- Managed Server profile: `targets create server ` -- Remote Desktop or Server: `targets add remote ` -- One-off URL: `--base-url` - -Auth is target-scoped metadata, not a separate public object. - -`setup` is the guided orchestrator that creates, selects, starts, installs, and authenticates targets by calling the same primitives users can run directly. - -## Public Command Shape - -Keep the public command tree narrow: - -```sh -beeper setup -beeper setup --local -beeper setup --oauth -beeper setup --remote URL -beeper setup --server -beeper setup --desktop -beeper setup --install -beeper setup --yes - -beeper targets ... -beeper install desktop -beeper install server -beeper auth status -beeper auth logout -beeper verify ... -beeper doctor -``` - -Do not add these in the first pass: - -```sh -beeper setup desktop -beeper setup server -beeper setup remote -beeper auth list -beeper auth use -beeper auth desktop -beeper auth remote -beeper auth manual -``` - -If we later need script-focused auth commands, add them only after targets/setup prove insufficient. - -## Setup Modes - -### `beeper setup` - -Interactive, magical default. - -Detection order: - -1. Existing configured default target. -2. Detected local Beeper Desktop. -3. Running local Desktop API. -4. Desktop login/session state. -5. Local Desktop DB/cache/session token availability. -6. Connected accounts via Desktop API when reachable. -7. Managed Server install/config. -8. Remote URL from env/config. - -Behavior: - -- If a ready target already exists, show it and offer to keep, repair, switch, or add another target. -- If local Desktop is signed in and local session auth is readable, recommend direct Desktop connection. -- If local Desktop is installed but logged out, launch Desktop and ask the user to sign in there. -- If Desktop is missing, offer Desktop install only on supported GUI platforms. -- If Server is requested or Desktop is unsuitable, offer Server install/setup. -- If remote URL is supplied or selected, use PKCE against that target. - -### `beeper setup --local` - -Explicit local Desktop DB/cache/session path. - -Use when: - -- Desktop is installed locally. -- The user wants the fastest trusted-device path. -- QA wants to test the direct local Desktop flow without menu interaction. - -This should: - -1. Detect Desktop app/profile. -2. Read local session/token from Desktop state. -3. Materialize or update target `desktop`. -4. Store target auth with `source: "desktop-db"` or `source: "desktop-cache"`. -5. Fetch readiness and connected accounts. - -### `beeper setup --oauth` - -Explicit browser-authorized path for the resolved target. - -Use when: - -- The user wants limited/revocable permissions. -- The target is remote. -- Local Desktop DB/cache auth is unavailable or declined. - -This should use the existing PKCE implementation and store auth with: - -```ts -source: "desktop-oauth" | "remote-oauth" -``` - -### `beeper setup --remote URL` - -Shortcut for: - -1. Create or update a remote target for `URL`. -2. Use OAuth/PKCE. -3. Optionally make it default after confirmation. - -Equivalent primitive path: - -```sh -beeper targets add remote -beeper setup -t --oauth -beeper targets use -``` - -### `beeper setup --server` - -Guided Server setup. - -If no local server binary exists: - -- Prompt to install. -- Require `--install --yes` for non-interactive download/install. -- Use staging only when requested by target/server env flags in tests. - -Equivalent primitive path: - -```sh -beeper install server -beeper targets create server -beeper targets start -beeper setup -t --oauth -``` - -### `beeper setup --desktop --install` - -Guided Desktop install/setup. - -Equivalent primitive path: - -```sh -beeper install desktop -beeper targets create desktop -beeper targets start -beeper setup -t --local -``` - -## Target Schema - -Extend the current target model; do not create a separate connection schema. - -```ts -type AuthSource = - | "desktop-db" - | "desktop-cache" - | "desktop-oauth" - | "remote-oauth" - | "manual"; - -type StoredAuth = { - accessToken: string; - clientID?: string; - expiresAt?: string; - scope?: string; - tokenType: "Bearer"; - source?: AuthSource; -}; -``` - -Target records should contain stable endpoint/profile/runtime metadata. - -Volatile facts should go in cache: - -- Desktop app version. -- Reachability. -- Current signed-in user. -- Connected account summary. -- Readiness state. - -## Built-In Desktop Target - -Stop creating `personal`. - -Use `desktop` as the built-in local Desktop target when a selector is needed. - -User-facing UI should not lead with the ID: - -```txt -Current connection: - Beeper Desktop v4.2.842 on this device - connected directly -``` - -For named targets, use target language: - -```txt -Default target: - work-server - Remote Beeper Server - https://beeper.example.com -``` - -`beeper targets list` may show: - -```txt -Built-in: - desktop Beeper Desktop on this device connected directly - -Targets: - work-server remote server https://beeper.example.com - local-server managed server http://127.0.0.1:23374 -``` - -Removing `desktop` means forget CLI state only. Do not uninstall Desktop, delete Desktop data, or revoke anything remote unless a separate explicit command asks for it. - -## UX Copy - -Avoid scary or implementation-first language in the main path. - -Avoid: - -- full access -- unrestricted -- extract token -- scrape database -- read DB - -Use: - -- Use your existing Desktop session. -- Connect directly to Beeper Desktop. -- Authorize with browser. -- Limited permissions. -- Connected accounts. - -Advanced/debug output can be explicit: - -```txt -Auth source: desktop-db -Requests: Desktop API at http://127.0.0.1:23373 -``` - -## Readiness And Repair - -`setup`, `status`, `doctor`, and `verify` must share the same readiness evaluator. - -States: - -```txt -no-target -target-unreachable -needs-login -login-in-progress -initializing -needs-cross-signing-setup -needs-verification -verification-in-progress -needs-recovery-key -needs-secrets -needs-first-sync -ready -error -``` - -Setup should never dead-end on these states. It should show the next repair action: - -- `target-unreachable`: start target, install runtime, or fix URL. -- `needs-login`: Desktop users sign in in Desktop; Server/headless may use supported setup API if available. -- `needs-verification`: run or continue `verify`. -- `needs-recovery-key` / `needs-secrets`: run recovery-key flow. -- `needs-first-sync`: wait with events, and resume on rerun. - -Ctrl+C while waiting should not cancel remote verification or sync. Print: - -```txt -Run `beeper setup` to continue. -``` - -## Non-Interactive Contract - -No prompts when: - -- `--json` -- non-TTY -- `--yes` - -If blocked on a human action, return JSON error on stderr: - -```json -{"success":false,"data":null,"error":"Desktop sign-in required"} -``` - -Include current state and available actions in `data` when possible. - -Downloads/installs require: - -```sh ---install --yes -``` - -## Implementation Plan - -1. Revert public email/code setup flags from the main UX. - - Do not make OTP login the normal Desktop setup path. - - Keep any Server/headless setup API helper internal until verified against live Server. - -2. Replace `personal` with built-in `desktop`. - - Materialize `desktop` when detected or selected. - - Update `resolveTarget`, `targets list`, `targets remove`, setup docs, and smoke tests. - -3. Add auth source metadata. - - Extend `StoredAuth.source`. - - Populate for local Desktop, Desktop OAuth, remote OAuth, and manual token paths. - -4. Implement local Desktop direct auth. - - Inspect Desktop app/profile/session state. - - Read Matrix access token from local Desktop state/cache using structured storage access, not ad hoc text parsing. - - Cache target auth and verify with Desktop API or Matrix-backed API calls. - - Show connected accounts before confirmation when possible. - -5. Keep OAuth as setup mode, not auth namespace sprawl. - - Add `setup --oauth`. - - Reuse existing PKCE implementation. - - Support `setup -t --oauth` and `setup --remote URL`. - -6. Make setup orchestrate installs. - - Use existing `install desktop` and `install server`. - - Never download in non-interactive mode without `--install --yes`. - - Preserve `--server-env staging` for tests. - -7. Use primitives for direct testing. - - `targets create/start/status/logs` - - `install desktop/server` - - `setup --local` - - `setup --oauth` - - `setup --remote` - - `verify ...` - -8. Keep `auth` narrow. - - `auth status` - - `auth logout` - - Add revoke/list/use only if target commands cannot cover the need. - -9. Regenerate docs from metadata. - - README and man output should describe `setup` as guided orchestration. - - Advanced examples should show primitive equivalents. - -10. Update E2E staging scripts. - - Test primitives directly first. - - Test `beeper setup` as a shortcut over those primitives. - - Fail fast on required phase failures. - - Continue to isolate config, ports, and profiles from the default Desktop instance. - -## Test Plan - -### Local Unit/Smoke - -- Command tree still matches the nuclear redesign. -- No new public `auth login/list/use` unless deliberately added. -- `setup --json` returns stable envelopes. -- `setup --read-only` refuses writes. -- `setup --install --yes` is required for downloads. -- `targets list` shows built-in Desktop distinctly from named targets. -- `targets remove desktop` forgets CLI state only. - -### Local Desktop - -- Running signed-in Desktop: - - `setup --local` detects user and connected accounts. - - Auth source is `desktop-db` or `desktop-cache`. - - `status`, `accounts list`, `chats list` work through the configured target. - -- Installed but logged-out Desktop: - - `setup` launches or points to Desktop sign-in. - - Rerunning `setup` resumes after sign-in. - -- No Desktop: - - `setup` offers install only where supported. - - Non-interactive mode returns actionable JSON instead of prompting. - -### OAuth / Remote - -- `setup --oauth` uses PKCE for the resolved target. -- `setup --remote URL` creates/updates a remote target and authenticates. -- Remote targets reject local runtime commands with clear errors. - -### Server - -- `install server --server-env staging` installs the staging binary. -- `targets create server/start/status/logs/stop` work. -- `setup -t --oauth` works when Server supports PKCE. -- Server/headless email-code setup is tested only through verified supported routes, not assumed from Desktop. - -### Multi-Target Staging - -- Create three isolated targets: - - one managed Desktop profile - - two managed Server profiles -- Provide staging account emails and verification codes through environment variables only in the scripts that target verified setup APIs. -- Start all targets on non-default ports. -- Authenticate each target through the appropriate setup mode. -- Run device-to-device verification between two signed-in targets. -- Send messages between targets. -- Cleanup stops managed Server targets and records any Desktop target that must be quit manually. - -## Acceptance Criteria - -- `beeper setup` feels like a product wizard, not an API debugger. -- The default Desktop path succeeds without asking users to understand tokens, ports, profiles, or OAuth. -- Every wizard choice has a direct command or flag equivalent. -- The public model remains one target system, not targets plus connections. -- E2E scripts test direct primitives and then setup shortcuts. -- The default Desktop instance is never modified during staging tests unless explicitly requested. diff --git a/packages/cli/bin/binary-bootstrap.js b/packages/cli/bin/binary-bootstrap.js deleted file mode 100644 index 72cff777..00000000 --- a/packages/cli/bin/binary-bootstrap.js +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bun -import payloadArchive from '../dist/binary-payload.tar.gz' with { type: 'file' } -import { createHash } from 'node:crypto' -import { existsSync } from 'node:fs' -import { mkdir, readFile } from 'node:fs/promises' -import { homedir, tmpdir } from 'node:os' -import { dirname, join } from 'node:path' - -void (async () => { - const archive = await readFile(payloadArchive) - const payloadHash = createHash('sha256').update(archive).digest('hex').slice(0, 16) - const cacheRoot = process.env.BEEPER_CLI_BINARY_CACHE_DIR || join(homedir(), '.cache', 'beeper-cli', 'binary') - const payloadRoot = join(cacheRoot, payloadHash) - const entrypoint = join(payloadRoot, 'bin', 'cli.js') - - if (!existsSync(entrypoint)) { - const tempArchive = join(tmpdir(), `beeper-cli-${payloadHash}.tar.gz`) - await mkdir(dirname(tempArchive), { recursive: true }) - await mkdir(payloadRoot, { recursive: true }) - await Bun.write(tempArchive, archive) - await run('tar', ['-xzf', tempArchive, '-C', payloadRoot]) - } - - const child = Bun.spawn([process.execPath, entrypoint, ...process.argv.slice(2)], { - env: { - ...process.env, - BUN_BE_BUN: '1', - }, - stdin: 'inherit', - stdout: 'inherit', - stderr: 'inherit', - }) - - const code = await child.exited - if (child.signalCode) process.kill(process.pid, child.signalCode) - process.exit(code ?? 1) -})() - -async function run(command, args) { - const child = Bun.spawn([command, ...args], { - stdin: 'inherit', - stdout: 'inherit', - stderr: 'inherit', - }) - const code = await child.exited - if (code !== 0) throw new Error(`${command} ${args.join(' ')} exited with ${code}`) -} diff --git a/packages/cli/bin/check-release-environment b/packages/cli/bin/check-release-environment deleted file mode 100644 index 5c60e10e..00000000 --- a/packages/cli/bin/check-release-environment +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" -cd "${root}" - -errors=() - -for path in package.json bun.lock scripts/release.ts packages/npm/package.json packages/npm/scripts/build.ts packages/cli/scripts/build-homebrew-archive.ts packages/cli/scripts/publish-homebrew-formula.ts packages/cli/scripts/publish-local-release.ts packages/cli/scripts/sign-macos-binaries.ts .github/workflows/publish-release.yml; do - if [[ ! -f "${path}" ]]; then - errors+=("Missing required release file: ${path}") - fi -done - -for command in bun git gh npm tar; do - if ! command -v "${command}" >/dev/null 2>&1; then - errors+=("Missing required release command: ${command}") - fi -done - -lenErrors=${#errors[@]} - -if [[ lenErrors -gt 0 ]]; then - echo -e "Found the following errors in the release environment:\n" - - for error in "${errors[@]}"; do - echo -e "- $error\n" - done - - exit 1 -fi - -echo "The environment is ready to push releases!" diff --git a/packages/cli/bin/cli.js b/packages/cli/bin/cli.js old mode 100644 new mode 100755 index d05ebe6e..7ecabef0 --- a/packages/cli/bin/cli.js +++ b/packages/cli/bin/cli.js @@ -1,11 +1,4 @@ #!/usr/bin/env bun -import { execute } from '@oclif/core' -import { renderStartupLogo } from './logo.js' +import { runCli } from '../dist/cli/main.js' -void (async () => { - if (process.argv.slice(2).length === 0 && process.env.BEEPER_NO_LOGO !== '1') { - process.stdout.write(`${renderStartupLogo()}\n\n`) - } - - await execute({ dir: import.meta.url }) -})() +await runCli() diff --git a/packages/cli/bin/dev.js b/packages/cli/bin/dev.js index a8376fa5..1b8512eb 100755 --- a/packages/cli/bin/dev.js +++ b/packages/cli/bin/dev.js @@ -1,11 +1,4 @@ #!/usr/bin/env bun -import { execute } from '@oclif/core' -import { renderStartupLogo } from './logo.js' +import { runCli } from '../src/cli/main.ts' -void (async () => { - if (process.argv.slice(2).length === 0 && process.env.BEEPER_NO_LOGO !== '1') { - process.stdout.write(`${renderStartupLogo()}\n\n`) - } - - await execute({ development: true, dir: import.meta.url }) -})() +await runCli() diff --git a/packages/cli/bin/logo.js b/packages/cli/bin/logo.js deleted file mode 100644 index 392bb50f..00000000 --- a/packages/cli/bin/logo.js +++ /dev/null @@ -1,97 +0,0 @@ -const iconSource = String.raw` - @@@@@@@@@@@@@@@@@@@@@@@@@@ - @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@ @@@@@@@ - @@@@@@ @@@@@@ - @@@@@ @@@@@ -@@@@@ @@@@@ -@@@@@ @@@@@ -@@@@@ @@@@@ -@@@@@ @@@@@ -@@@@@ @@@@ - @@@@@ @@@@@ - @@@@@@ @@@@@@ - @@@@@@@ @@@@@@@ - @@@@@@@@@@@ @@@@@@@ - @@@@@@@@@@@@@@ @@@@@@@ - @@@@@@@@@@@@@@ @@@@@@@@ - @@@@@@@@@@@@@@ @@@@@@@@@@@@@ - @@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@ - @@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@ - @@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@ - @@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@ - @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ - @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ - @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ - @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ - @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ - @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ - @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ - @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -` - -const wordmarkSource = String.raw` -@@@@@@@ @@@@@@@@ @@@@@@@@ @@@@@@@ @@@@@@@@ @@@@@@@ -@@ @@ @@ @@ @@ @@ @@ @@ @@ -@@@@@@@ @@@@@@ @@@@@@ @@@@@@@ @@@@@@ @@@@@@@ -@@ @@ @@ @@ @@ @@ @@ @@ -@@ @@ @@ @@ @@ @@ @@ @@ -@@@@@@@ @@@@@@@@ @@@@@@@@ @@ @@@@@@@@ @@ @@ -` - -const normalize = source => { - const lines = source.trim().split('\n') - const width = Math.max(...lines.map(line => line.length)) - return lines.map(line => line.padEnd(width, ' ')) -} - -const scale = (source, width, height) => { - const lines = normalize(source) - const sourceHeight = lines.length - const sourceWidth = lines[0]?.length ?? 0 - const result = [] - - for (let y = 0; y < height; y += 1) { - const sourceY = Math.min(sourceHeight - 1, Math.floor(((y + 0.5) / height) * sourceHeight)) - let line = '' - - for (let x = 0; x < width; x += 1) { - const sourceX = Math.min(sourceWidth - 1, Math.floor(((x + 0.5) / width) * sourceWidth)) - line += lines[sourceY]?.[sourceX] === '@' ? '@' : ' ' - } - - result.push(line) - } - - return result -} - -const combine = (icon, wordmark, gap) => { - const iconWidth = icon[0]?.length ?? 0 - const height = Math.max(icon.length, wordmark.length) - const wordTop = Math.max(0, Math.floor((height - wordmark.length) / 2)) - const result = [] - - for (let y = 0; y < height; y += 1) { - const iconLine = icon[y] ?? ' '.repeat(iconWidth) - const wordLine = wordmark[y - wordTop] ?? '' - result.push(`${iconLine}${' '.repeat(gap)}${wordLine}`.trimEnd()) - } - - return result -} - -export function renderStartupLogo(columns = process.stdout.columns ?? 80) { - const maxWidth = Math.max(36, columns - 2) - const gap = maxWidth < 60 ? 2 : 4 - const iconWidth = Math.min(20, Math.max(14, Math.floor(maxWidth * 0.25))) - const iconHeight = Math.max(8, Math.round(iconWidth * 0.55)) - const wordWidth = Math.max(20, maxWidth - iconWidth - gap) - const wordHeight = 6 - - const icon = scale(iconSource, iconWidth, iconHeight) - const wordmark = scale(wordmarkSource, wordWidth, wordHeight) - - return combine(icon, wordmark, gap).join('\n') -} diff --git a/packages/cli/bin/run.js b/packages/cli/bin/run.js deleted file mode 100755 index de26bc2c..00000000 --- a/packages/cli/bin/run.js +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env node -import { createHash } from 'node:crypto' -import { createWriteStream, existsSync } from 'node:fs' -import { chmod, mkdir, readFile, rename, rm } from 'node:fs/promises' -import { get } from 'node:https' -import { homedir, tmpdir } from 'node:os' -import { basename, dirname, join } from 'node:path' -import { pipeline } from 'node:stream/promises' -import { fileURLToPath } from 'node:url' -import { spawn } from 'node:child_process' - -const packageRoot = dirname(dirname(fileURLToPath(import.meta.url))) -const pkg = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8')) -const version = pkg.version -const platform = normalizePlatform(process.platform) -const arch = normalizeArch(process.arch) -const releaseTag = process.env.BEEPER_CLI_RELEASE_TAG || `v${version}` -const releaseRepository = process.env.GITHUB_REPOSITORY || 'beeper/cli' -const releaseBaseURL = (process.env.BEEPER_CLI_RELEASE_BASE_URL || `https://github.com/${releaseRepository}/releases/download/${releaseTag}`).replace(/\/$/, '') -const cacheRoot = process.env.BEEPER_CLI_BINARY_CACHE_DIR || join(homedir(), '.cache', 'beeper-cli') -const cacheDir = join(cacheRoot, version, `${platform}-${arch}`) -const cachedExecutable = join(cacheDir, platform === 'windows' ? 'beeper.exe' : 'beeper') - -try { - const executable = await ensureExecutable() - const child = spawn(executable, process.argv.slice(2), { - env: process.env, - stdio: 'inherit', - }) - child.once('exit', (code, signal) => { - if (signal) process.kill(process.pid, signal) - process.exit(code ?? 1) - }) - child.once('error', error => { - console.error(`beeper-cli: failed to start downloaded binary: ${error.message}`) - process.exit(1) - }) -} catch (error) { - console.error(`beeper-cli: ${error instanceof Error ? error.message : String(error)}`) - process.exit(1) -} - -async function ensureExecutable() { - if (existsSync(cachedExecutable)) return cachedExecutable - - const artifact = await fetchArtifact() - const tmpDir = join(tmpdir(), `beeper-cli-${version}-${process.pid}-${Date.now()}`) - const tmpPath = join(tmpDir, artifact.file) - const url = `${releaseBaseURL}/${artifact.file}` - console.error(`beeper-cli: downloading ${url}`) - await rm(tmpDir, { recursive: true, force: true }) - await mkdir(tmpDir, { recursive: true }) - await download(url, tmpPath) - - const actualHash = await sha256(tmpPath) - if (actualHash !== artifact.sha256) { - await rm(tmpDir, { recursive: true, force: true }) - throw new Error(`downloaded archive checksum mismatch for ${artifact.file}`) - } - - await extract(tmpPath, tmpDir) - const extractedExecutable = join(tmpDir, 'bin', platform === 'windows' ? 'beeper.exe' : 'beeper') - if (platform !== 'windows') await chmod(extractedExecutable, 0o755) - await rm(cacheDir, { recursive: true, force: true }) - await mkdir(cacheDir, { recursive: true }) - await rename(extractedExecutable, cachedExecutable) - await rm(tmpDir, { recursive: true, force: true }) - return cachedExecutable -} - -function normalizePlatform(value) { - if (value === 'darwin') return 'darwin' - if (value === 'linux') return 'linux' - if (value === 'win32') return 'windows' - throw new Error(`unsupported platform: ${value}`) -} - -function normalizeArch(value) { - if (value === 'x64') return 'x64' - if (value === 'arm64') return 'arm64' - throw new Error(`unsupported architecture: ${value}`) -} - -async function fetchArtifact() { - const manifestURL = `${releaseBaseURL}/binaries.json` - const manifestPath = join(tmpdir(), `beeper-cli-binaries-${version}-${process.pid}-${Date.now()}.json`) - try { - await download(manifestURL, manifestPath, { quiet: true }) - const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) - const artifact = manifest.artifacts?.find(artifact => artifact.platform === `${platform}-${arch}`) - if (!artifact) throw new Error(`no release archive found for ${platform}-${arch}`) - return artifact - } finally { - await rm(manifestPath, { force: true }) - } -} - -async function download(url, destination, options = {}, redirectCount = 0) { - if (redirectCount > 10) throw new Error(`too many redirects while downloading ${basename(url)}`) - await mkdir(dirname(destination), { recursive: true }) - await new Promise((resolve, reject) => { - const request = get(url, response => { - if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) { - response.resume() - download(new URL(response.headers.location, url).toString(), destination, options, redirectCount + 1).then(resolve, reject) - return - } - if (response.statusCode !== 200) { - response.resume() - reject(new Error(`download failed for ${basename(url)}: HTTP ${response.statusCode}`)) - return - } - pipeline(response, createWriteStream(destination)).then(resolve, reject) - }) - request.once('error', reject) - request.setTimeout(120_000, () => request.destroy(new Error(`download timed out: ${url}`))) - }) -} - -async function sha256(path) { - return createHash('sha256').update(await readFile(path)).digest('hex') -} - -async function extract(archivePath, destination) { - if (archivePath.endsWith('.zip')) { - await run('/usr/bin/ditto', ['-x', '-k', archivePath, destination]) - return - } - if (archivePath.endsWith('.tar.gz')) { - await run('tar', ['-xzf', archivePath, '-C', destination]) - return - } - throw new Error(`unsupported release archive: ${basename(archivePath)}`) -} - -async function run(command, args) { - await new Promise((resolve, reject) => { - const child = spawn(command, args, { stdio: 'ignore' }) - child.once('error', reject) - child.once('exit', code => { - if (code === 0) resolve() - else reject(new Error(`${command} ${args.join(' ')} exited with ${code}`)) - }) - }) -} diff --git a/packages/cli/links.txt b/packages/cli/links.txt deleted file mode 100644 index b9d08d50..00000000 --- a/packages/cli/links.txt +++ /dev/null @@ -1,89 +0,0 @@ -- Beeper Desktop - - Beeper Server desktop variant - - Channels: - - ```text - prod = stable - nightly = nightly - beta = beta - ``` - - Bundle IDs - - ```text - Beeper Desktop prod: com.automattic.beeper.desktop - Beeper Desktop nightly: com.automattic.beeper.desktop.nightly - - Beeper Server prod: com.automattic.beeper.server - Beeper Server nightly: com.automattic.beeper.server.nightly - ``` - - JSON feed API - - ### Beeper Desktop - - ```text - https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.desktop&platform=darwin&channel=stable&arch=x64 - https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.desktop&platform=darwin&channel=stable&arch=arm64 - https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.desktop&platform=win32&channel=stable&arch=x64 - https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.desktop&platform=win32&channel=stable&arch=arm64 - https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.desktop&platform=linux&channel=stable&arch=x64 - https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.desktop&platform=linux&channel=stable&arch=arm64 - - https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.desktop.nightly&platform=darwin&channel=nightly&arch=x64 - https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.desktop.nightly&platform=darwin&channel=nightly&arch=arm64 - https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.desktop.nightly&platform=win32&channel=nightly&arch=x64 - https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.desktop.nightly&platform=win32&channel=nightly&arch=arm64 - https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.desktop.nightly&platform=linux&channel=nightly&arch=x64 - https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.desktop.nightly&platform=linux&channel=nightly&arch=arm64 - ``` - - ### Beeper Server - - ```text - https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=darwin&channel=stable&arch=x64 - https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=darwin&channel=stable&arch=arm64 - https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=win32&channel=stable&arch=x64 - https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=win32&channel=stable&arch=arm64 - https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=linux&channel=stable&arch=x64 - https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=linux&channel=stable&arch=arm64 - - https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server.nightly&platform=darwin&channel=nightly&arch=x64 - https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server.nightly&platform=darwin&channel=nightly&arch=arm64 - https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server.nightly&platform=win32&channel=nightly&arch=x64 - https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server.nightly&platform=win32&channel=nightly&arch=arm64 - https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server.nightly&platform=linux&channel=nightly&arch=x64 - https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server.nightly&platform=linux&channel=nightly&arch=arm64 - ``` - - YAML feed API - - Use same bundle IDs/channels/arches with these endpoints: - - ```text - macOS: https://api.beeper.com/desktop/update-feed/latest-mac.yml?bundleID=&channel=&arch= - Windows: https://api.beeper.com/desktop/update-feed/latest.yml?bundleID=&channel=&arch= - Linux x64: https://api.beeper.com/desktop/update-feed/latest-linux.yml?bundleID=&channel=&arch=x64 - Linux arm64: https://api.beeper.com/desktop/update-feed/latest-linux-arm64.yml?bundleID=&channel=&arch=arm64 - ``` - - Download redirect API - - Pattern: - - ```text - https://api.beeper.com/desktop/download//// - ``` - - Examples: - - ```text - https://api.beeper.com/desktop/download/macos/arm64/stable/com.automattic.beeper.desktop - https://api.beeper.com/desktop/download/macos/arm64/nightly/com.automattic.beeper.desktop.nightly - - https://api.beeper.com/desktop/download/macos/arm64/stable/com.automattic.beeper.server - https://api.beeper.com/desktop/download/macos/arm64/nightly/com.automattic.beeper.server.nightly - ``` - - Public API endpoints above are the supported feed and download link patterns. Do not publish underlying CDN feed URLs. diff --git a/packages/cli/package.json b/packages/cli/package.json index bf9a7345..ed70cddb 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,145 +1,40 @@ { - "name": "@beeper/cli", + "name": "beeper-cli", "version": "0.6.2", "description": "Beeper CLI", "license": "MIT", "type": "module", "bin": { - "beeper": "bin/run.js" + "beeper": "bin/cli.js" }, "exports": { - "./plugin-sdk": { - "types": "./dist/plugin-sdk.d.ts", - "import": "./dist/plugin-sdk.js" - }, "./package.json": "./package.json" }, "files": [ - "bin", + "bin/cli.js", "dist", + "safety-profiles", "README.md", "LICENSE" ], "scripts": { - "build": "bun run clean && bun scripts/prepare-desktop-api.ts && bun scripts/generate-command-map.ts && tsc -p tsconfig.json", - "binary": "bun run build && bun scripts/build-binaries.ts", - "bridges:sync-config": "bun scripts/sync-bridge-manager-config.ts", - "release:local": "bun run build && bun scripts/build-binaries.ts && BEEPER_CLI_REQUIRE_MACOS_SIGNING=1 bun scripts/sign-macos-binaries.ts && bun scripts/build-homebrew-archive.ts && (cd ../npm && bun run build) && bun scripts/publish-local-release.ts", - "check:api-copy": "bun run build && bun scripts/check-api-copy.ts", - "check:readme": "bun run build && bun scripts/generate-readme.ts --check", + "build": "bun run clean && bun scripts/prepare-desktop-api.ts && tsc -p tsconfig.json", "clean": "rm -rf dist", "dev": "bun ./bin/dev.js", - "dev:shim": "node ./bin/run.js", "e2e:staging": "bun run build && bun test/e2e-staging.ts", - "pack:homebrew": "bun run binary && bun scripts/sign-macos-binaries.ts && bun scripts/build-homebrew-archive.ts", - "readme": "bun run build && bun scripts/generate-readme.ts", - "test": "bun run build && bun scripts/generate-readme.ts --check && bun scripts/check-api-copy.ts && bun scripts/check-manifest.ts && bun ./test/cli-smoke.ts && bun test", + "test": "bun run build && bun ./test/cli-smoke.ts && bun test", "typecheck": "tsc -p tsconfig.json --noEmit" }, - "oclif": { - "additionalHelpFlags": [ - "-h" - ], - "commands": { - "strategy": "explicit", - "target": "./dist/commands.generated.js", - "identifier": "commands" - }, - "bin": "beeper", - "dirname": "beeper", - "flexibleTaxonomy": true, - "helpOptions": { - "maxWidth": 100, - "hideAliasesFromRoot": true, - "hideCommandSummaryInDescription": true - }, - "scope": "beeper", - "pluginPrefix": "plugin", - "jitPlugins": { - "@beeper/cli-plugin-cloudflare": "^0.6.0" - }, - "plugins": [ - "@oclif/plugin-autocomplete", - "@oclif/plugin-help", - "@oclif/plugin-not-found", - "@oclif/plugin-plugins", - "@oclif/plugin-warn-if-update-available" - ], - "warn-if-update-available": { - "timeoutInDays": 1, - "message": "<%= chalk.dim('beeper-cli') %> <%= chalk.cyan(config.version) %> → <%= chalk.green(latest) %>. Run <%= chalk.bold('beeper update') %> to upgrade.", - "registry": "https://registry.npmjs.org", - "frequency": 6, - "frequencyUnit": "hours" - }, - "topicSeparator": " ", - "topics": { - "api": { - "description": "Call raw Desktop API endpoints" - }, - "accounts": { - "description": "Manage connected chat-network accounts" - }, - "auth": { - "description": "Inspect and clear stored authentication" - }, - "bridges": { - "description": "List bridges that can connect or reconnect chat accounts" - }, - "chats": { - "description": "List, search, manage, and modify chats" - }, - "config": { - "description": "Manage local CLI configuration" - }, - "contacts": { - "description": "List, search, and inspect contacts" - }, - "media": { - "description": "Download message media" - }, - "messages": { - "description": "List, search, show, edit, delete, and export messages" - }, - "resolve": { - "description": "Resolve selectors into concrete candidates" - }, - "send": { - "description": "Send text, files, and reactions" - }, - "setup": { - "description": "Make a Beeper target ready" - }, - "targets": { - "description": "Manage local Desktop, managed Server, and remote Beeper targets" - }, - "verify": { - "description": "Finish setup verification or verify another device" - }, - "install": { - "description": "Install Beeper Desktop or Beeper Server" - } - } - }, "dependencies": { "@beeper/desktop-api": "github:beeper/desktop-api-js#next", - "@oclif/core": "^4.11.2", - "@oclif/plugin-autocomplete": "^3.2.49", - "@oclif/plugin-help": "^6.2.48", - "@oclif/plugin-not-found": "^3.2.85", - "@oclif/plugin-plugins": "^5.4.67", - "@oclif/plugin-warn-if-update-available": "^3.1.49", - "figures": "^6.1.0", - "ink": "^7.0.3", - "ink-spinner": "^5.0.0", "qrcode": "1.5.4", - "react": "^19.2.6", - "ws": "^8.20.1" + "ws": "^8.20.1", + "yaml": "^2.9.0" }, "devDependencies": { "@types/bun": "^1.3.3", "@types/node": "^20.0.0", - "@types/react": "^19.2.14", + "@types/qrcode": "^1.5.6", "@types/ws": "^8.18.1", "typescript": "^5.7.2" } diff --git a/packages/cli/release-please-config.json b/packages/cli/release-please-config.json deleted file mode 100644 index 0eec94e0..00000000 --- a/packages/cli/release-please-config.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "packages": { - ".": {} - }, - "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json", - "include-v-in-tag": true, - "include-component-in-tag": false, - "versioning": "prerelease", - "prerelease": true, - "bump-minor-pre-major": true, - "bump-patch-for-minor-pre-major": false, - "pull-request-header": "Automated Release PR", - "pull-request-title-pattern": "release: ${version}", - "changelog-sections": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "perf", - "section": "Performance Improvements" - }, - { - "type": "revert", - "section": "Reverts" - }, - { - "type": "chore", - "section": "Chores" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "style", - "section": "Styles" - }, - { - "type": "refactor", - "section": "Refactors" - }, - { - "type": "test", - "section": "Tests", - "hidden": true - }, - { - "type": "build", - "section": "Build System" - }, - { - "type": "ci", - "section": "Continuous Integration", - "hidden": true - } - ], - "release-type": "simple", - "extra-files": [ - "package.json", - "README.md" - ] -} diff --git a/packages/cli/safety-profiles/agent-safe.yaml b/packages/cli/safety-profiles/agent-safe.yaml new file mode 100644 index 00000000..dd397b05 --- /dev/null +++ b/packages/cli/safety-profiles/agent-safe.yaml @@ -0,0 +1,23 @@ +name: agent-safe +description: Agent workflow profile. Allows reads, setup inspection, target runtime control, default selection, and email auth. + +allow: + - version + - status + - schema + - mcp + - setup + - use + - targets.list + - targets.logs + - targets.runtime + - auth.email + - accounts.list + - contacts.list + - chats.list + - chats.show + - messages.list + - messages.search + - messages.context + - resolve + - watch diff --git a/packages/cli/safety-profiles/full.yaml b/packages/cli/safety-profiles/full.yaml new file mode 100644 index 00000000..35e6db0e --- /dev/null +++ b/packages/cli/safety-profiles/full.yaml @@ -0,0 +1,5 @@ +name: full +description: Full Beeper CLI surface. + +allow: + - all diff --git a/packages/cli/safety-profiles/readonly.yaml b/packages/cli/safety-profiles/readonly.yaml new file mode 100644 index 00000000..742bd531 --- /dev/null +++ b/packages/cli/safety-profiles/readonly.yaml @@ -0,0 +1,19 @@ +name: readonly +description: Read-only commands only. Mutating commands, setup writes, sends, deletes, installs, and auth writes are blocked. + +allow: + - version + - status + - schema + - mcp + - targets.list + - targets.logs + - accounts.list + - contacts.list + - chats.list + - chats.show + - messages.list + - messages.search + - messages.context + - resolve + - watch diff --git a/packages/cli/scripts/bootstrap b/packages/cli/scripts/bootstrap deleted file mode 100755 index c96c19ee..00000000 --- a/packages/cli/scripts/bootstrap +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then - brew bundle check >/dev/null 2>&1 || { - echo -n "==> Install Homebrew dependencies? (y/N): " - read -r response - case "$response" in - [yY][eE][sS]|[yY]) - brew bundle - ;; - *) - ;; - esac - echo - } -fi -echo "==> Checking Bun dependencies" -if [ ! -d node_modules ]; then - echo "node_modules is missing. Run bun install after approving dependency installation." - exit 1 -fi diff --git a/packages/cli/scripts/build b/packages/cli/scripts/build deleted file mode 100755 index 5615b55c..00000000 --- a/packages/cli/scripts/build +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -cd "$(dirname "$0")/.." - -echo "==> Building Beeper CLI" -bun run build diff --git a/packages/cli/scripts/build-binaries.ts b/packages/cli/scripts/build-binaries.ts deleted file mode 100644 index d0f6c3a8..00000000 --- a/packages/cli/scripts/build-binaries.ts +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env bun -import { createHash } from 'node:crypto' -import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { basename, join } from 'node:path' -import { fileURLToPath } from 'node:url' - -const root = fileURLToPath(new URL('..', import.meta.url)) -const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) -const outDir = join(root, 'dist', 'bin') -const entrypoint = join(root, 'bin', 'binary-bootstrap.js') -const payloadArchive = join(root, 'dist', 'binary-payload.tar.gz') -const targets = (process.env.BEEPER_BINARY_TARGETS || [ - 'bun-darwin-arm64', - 'bun-darwin-x64', - 'bun-linux-arm64', - 'bun-linux-x64', -].join(',')).split(',').map(target => target.trim()).filter(Boolean) - -await mkdir(outDir, { recursive: true }) -await buildPayload() - -const artifacts = [] -for (const target of targets) { - const platform = target.replace(/^bun-/, '') - const outfile = join(outDir, platform.startsWith('windows-') ? `beeper-${platform}.exe` : `beeper-${platform}`) - const result = await Bun.build({ - entrypoints: [entrypoint], - compile: { - outfile, - target, - }, - minify: true, - sourcemap: 'linked', - bytecode: true, - }) - - if (!result.success) { - for (const log of result.logs) console.error(log) - throw new Error(`Failed to build ${target}`) - } - - const sha256 = await hashFile(outfile) - artifacts.push({ file: basename(outfile), path: outfile, platform, sha256, target }) - console.log(`${outfile}`) - console.log(`sha256 ${sha256}`) -} - -await writeFile( - join(outDir, 'binaries.json'), - `${JSON.stringify({ command: 'beeper', package: pkg.name, version: pkg.version, artifacts }, null, 2)}\n`, -) - -async function hashFile(path) { - const hash = createHash('sha256') - hash.update(await readFile(path)) - return hash.digest('hex') -} - -async function buildPayload() { - const workDir = await mkdtemp(join(tmpdir(), 'beeper-cli-payload-')) - try { - await cp(join(root, 'package.json'), join(workDir, 'package.json')) - await cp(join(root, 'bin'), join(workDir, 'bin'), { recursive: true }) - await mkdir(join(workDir, 'scripts'), { recursive: true }) - await cp(join(root, 'dist'), join(workDir, 'dist'), { - recursive: true, - filter: source => !source.includes('/dist/bin/') && source !== payloadArchive, - }) - await cp(join(root, 'scripts', 'prepare-desktop-api.ts'), join(workDir, 'scripts', 'prepare-desktop-api.ts')) - await run('bun', ['install', '--production'], { cwd: workDir }) - await run('bun', ['scripts/prepare-desktop-api.ts'], { cwd: workDir }) - await rm(payloadArchive, { force: true }) - await run('tar', ['-czf', payloadArchive, '-C', workDir, '.'], { cwd: root }) - } finally { - await rm(workDir, { recursive: true, force: true }) - } -} - -async function run(command, args, options = {}) { - const child = Bun.spawn([command, ...args], { - cwd: options.cwd || root, - env: process.env, - stdin: 'inherit', - stdout: 'inherit', - stderr: 'inherit', - }) - const code = await child.exited - if (code !== 0) throw new Error(`${command} ${args.join(' ')} exited with ${code}`) -} diff --git a/packages/cli/scripts/build-homebrew-archive.ts b/packages/cli/scripts/build-homebrew-archive.ts deleted file mode 100644 index 39669de1..00000000 --- a/packages/cli/scripts/build-homebrew-archive.ts +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env bun -import { createHash } from 'node:crypto' -import { chmod, cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { basename, join } from 'node:path' -import { fileURLToPath } from 'node:url' - -const root = fileURLToPath(new URL('..', import.meta.url)) -const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) -const version = process.env.PACKAGE_VERSION || pkg.version -const binaryDir = join(root, 'dist', 'bin') -const outDir = join(root, 'dist', 'release') -const manifestPath = join(binaryDir, 'binaries.json') -const metadataPath = join(outDir, 'homebrew.json') - -await rm(outDir, { recursive: true, force: true }) -await mkdir(outDir, { recursive: true }) - -const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) -const archives = [] -for (const artifact of manifest.artifacts) { - const platform = artifact.platform - const binaryPath = artifact.path || join(binaryDir, artifact.binaryFile || `beeper-${platform}`) - const workDir = await mkdtemp(join(tmpdir(), `beeper-cli-${platform}-`)) - const archiveName = releaseArchiveName(version, platform) - const archivePath = join(outDir, archiveName) - - await mkdir(join(workDir, 'bin'), { recursive: true }) - const installedBinary = join(workDir, 'bin', 'beeper') - await cp(binaryPath, installedBinary) - await chmod(installedBinary, 0o755) - await rm(archivePath, { force: true }) - const binarySha256 = await hashFile(binaryPath) - if (platform.startsWith('darwin-')) { - await run('/usr/bin/zip', ['-X', '-r', archivePath, 'bin'], { cwd: workDir }) - } else { - await run('tar', ['-czf', archivePath, '-C', workDir, '.'], { cwd: root }) - } - const sha256 = await hashFile(archivePath) - archives.push({ archive: basename(archivePath), path: archivePath, platform, sha256 }) - artifact.binaryFile = artifact.binaryFile || artifact.file - artifact.binarySha256 = binarySha256 - artifact.file = basename(archivePath) - artifact.sha256 = sha256 - artifact.archive = basename(archivePath) - console.log(`${archivePath}`) - console.log(`sha256 ${sha256}`) - await rm(workDir, { recursive: true, force: true }) -} - -await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`) -await writeFile( - metadataPath, - `${JSON.stringify( - { - archives, - command: 'beeper', - displayName: 'Beeper CLI', - package: 'beeper-cli', - version, - }, - null, - 2, - )}\n`, -) - -function releaseArchiveName(version, platform) { - const [os, arch] = platform.split('-') - const displayOS = os === 'darwin' ? 'macos' : os - const extension = os === 'darwin' ? 'zip' : 'tar.gz' - return `beeper-cli-${version}-${displayOS}-${arch}.${extension}` -} - -async function hashFile(path) { - const hash = createHash('sha256') - hash.update(await readFile(path)) - return hash.digest('hex') -} - -async function run(command, args, options = {}) { - const child = Bun.spawn([command, ...args], { - cwd: options.cwd || root, - env: process.env, - stdin: 'inherit', - stdout: 'inherit', - stderr: 'inherit', - }) - const code = await child.exited - if (code !== 0) throw new Error(`${command} ${args.join(' ')} exited with ${code}`) -} diff --git a/packages/cli/scripts/check-api-copy.ts b/packages/cli/scripts/check-api-copy.ts deleted file mode 100644 index dca59f5a..00000000 --- a/packages/cli/scripts/check-api-copy.ts +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env bun -import {readFile} from 'node:fs/promises'; -import {existsSync} from 'node:fs'; -import {fileURLToPath} from 'node:url'; -import {join, resolve} from 'node:path'; - -const root = resolve(fileURLToPath(new URL('..', import.meta.url))); -const {apiCopy} = await import('../dist/lib/copy.js'); - -const checks = [ - ['accounts.list', 'resources/accounts/accounts.d.ts', 'list'], - ['assets.download', 'resources/assets.d.ts', 'download'], - ['assets.upload', 'resources/assets.d.ts', 'upload'], - ['chats.archive', 'resources/chats/chats.d.ts', 'archive'], - ['chats.create', 'resources/chats/chats.d.ts', 'create'], - ['chats.list', 'resources/chats/chats.d.ts', 'list'], - ['chats.markRead', 'resources/chats/chats.d.ts', 'markRead'], - ['chats.markUnread', 'resources/chats/chats.d.ts', 'markUnread'], - ['chats.notifyAnyway', 'resources/chats/chats.d.ts', 'notifyAnyway'], - ['chats.retrieve', 'resources/chats/chats.d.ts', 'retrieve'], - ['chats.search', 'resources/chats/chats.d.ts', 'search'], - ['chats.start', 'resources/chats/chats.d.ts', 'start'], - ['contacts.search', 'resources/accounts/contacts.d.ts', 'search'], - ['messages.delete', 'resources/messages.d.ts', 'delete'], - ['messages.list', 'resources/messages.d.ts', 'list'], - ['messages.retrieve', 'resources/messages.d.ts', 'retrieve'], - ['messages.search', 'resources/messages.d.ts', 'search'], - ['messages.send', 'resources/messages.d.ts', 'send'], - ['messages.update', 'resources/messages.d.ts', 'update'], - ['reactions.add', 'resources/chats/messages/reactions.d.ts', 'add'], - ['reactions.delete', 'resources/chats/messages/reactions.d.ts', 'delete'], - ['reminders.create', 'resources/chats/reminders.d.ts', 'create'], - ['reminders.delete', 'resources/chats/reminders.d.ts', 'delete'], -]; - -const failures = []; - -for (const [copyPath, sdkPath, method] of checks) { - const expected = getPath(apiCopy, copyPath); - const actual = await sdkMethodDescription(sdkPath, method); - if (expected !== actual) { - failures.push(`${copyPath}\n expected: ${expected}\n actual: ${actual}`); - } -} - -if (failures.length > 0) { - console.error(`API copy drifted from @beeper/desktop-api:\n\n${failures.join('\n\n')}`); - process.exit(1); -} - -console.log(`api-copy: ${checks.length} SDK descriptions verified`); - -function getPath(object, path) { - return path.split('.').reduce((value, key) => value?.[key], object); -} - -async function sdkMethodDescription(relativePath, method) { - const packageRoot = join(root, 'node_modules', '@beeper', 'desktop-api'); - const sourcePath = join(packageRoot, relativePath); - const source = await readFile(existsSync(sourcePath) ? sourcePath : join(packageRoot, 'dist', relativePath), 'utf8'); - const methodMatch = source.match(new RegExp(String.raw`^\s*${method}\(`, 'm')); - const methodIndex = methodMatch?.index ?? -1; - if (methodIndex === -1) throw new Error(`Could not find SDK method ${relativePath}#${method}`); - - const comments = [...source.slice(0, methodIndex).matchAll(/\/\*\*([\s\S]*?)\*\//g)]; - const match = comments.at(-1); - if (!match) throw new Error(`Could not find SDK docs for ${relativePath}#${method}`); - - const lines = match[1] - .split('\n') - .map(line => line.replace(/^\s*\*\s?/, '').trimEnd()) - - const exampleIndex = lines.findIndex(line => line.startsWith('@example')); - return lines - .slice(0, exampleIndex === -1 ? undefined : exampleIndex) - .filter(Boolean) - .join(' ') - .replace(/\s+/g, ' ') - .trim(); -} diff --git a/packages/cli/scripts/check-manifest.ts b/packages/cli/scripts/check-manifest.ts deleted file mode 100644 index 28a7108a..00000000 --- a/packages/cli/scripts/check-manifest.ts +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env bun -/** - * Validate the command manifest and the public plugin-sdk export. - * - * - every manifest entry has at least one `examples[]` entry - * - the manifest contains no duplicates - * - the manifest matches src/commands/** filenames (defense-in-depth with cli-smoke.ts) - * - the ./plugin-sdk subpath resolves at runtime and re-exports BeeperCommand - */ -import { readdirSync } from 'node:fs' -import { join } from 'node:path' -import { fileURLToPath } from 'node:url' -import { Config } from '@oclif/core/config' -import { commandManifest } from '../dist/lib/manifest.js' - -const root = fileURLToPath(new URL('..', import.meta.url)) -const failures = [] -const config = await Config.load({ root }) -const commandsByID = new Map(config.commands.map(command => [displayID(command.id), command])) - -const seen = new Set() -for (const entry of commandManifest) { - if (seen.has(entry.command)) failures.push(`Duplicate manifest entry: ${entry.command}`) - seen.add(entry.command) - if (!entry.examples?.length) failures.push(`Missing examples[] for: ${entry.command}`) - if (!entry.description) failures.push(`Missing description for: ${entry.command}`) - const command = commandsByID.get(entry.command) - const summary = command?.summary || command?.description - if (summary && entry.description !== summary) { - failures.push(`Manifest description for "${entry.command}" must match oclif summary: "${summary}"`) - } -} - -// The manifest may list commands shipped by first-party plugins (e.g. `targets tunnel` -// from @beeper/cli-plugin-cloudflare). Only enforce that every file in src/commands has -// a manifest entry — the reverse direction is allowed to include plugin-provided commands. -const internalCommands = new Set(['autocomplete']) -const commandFiles = listCommandFiles(join(root, 'src/commands')).filter(file => !internalCommands.has(fileToCommand(file))) -const fileCommands = new Set(commandFiles.map(fileToCommand)) -for (const file of fileCommands) { - if (!seen.has(file)) failures.push(`Command file has no manifest entry: ${file}`) -} - -try { - const sdk = await import('../dist/plugin-sdk.js') - if (typeof sdk.BeeperCommand !== 'function') failures.push('plugin-sdk: BeeperCommand is not exported as a class') - for (const name of ['ensureWritable', 'writeEvent', 'printData', 'printList', 'printSuccess', 'createBeeperClient', 'resolveTarget', 'readConfig', 'CLIError', 'ExitCodes', 'notFound', 'ambiguous', 'authRequired', 'notReady']) { - if (!(name in sdk)) failures.push(`plugin-sdk: missing export "${name}"`) - } -} catch (error) { - failures.push(`plugin-sdk: import failed — ${error.message}`) -} - -if (failures.length > 0) { - console.error(`check-manifest: ${failures.length} issue(s)`) - for (const issue of failures) console.error(` - ${issue}`) - process.exit(1) -} - -console.log(`check-manifest: ${commandManifest.length} commands ok, plugin-sdk surface ok`) - -function listCommandFiles(dir) { - const output = [] - for (const entry of readdirSync(dir, { withFileTypes: true })) { - // Files / directories starting with _ are private/internal (e.g. _complete used by autocomplete). - if (entry.name.startsWith('_') || entry.name === 'autocomplete.ts') continue - const path = join(dir, entry.name) - if (entry.isDirectory()) output.push(...listCommandFiles(path)) - else if (entry.isFile() && /\.(ts|tsx)$/.test(entry.name) && !entry.name.endsWith('.d.ts')) output.push(path) - } - return output -} - -function fileToCommand(file) { - const relative = file.slice(join(root, 'src/commands').length + 1) - const parts = relative.replace(/\.(ts|tsx)$/, '').split('/') - return parts.map(part => part === 'index' ? undefined : part).filter(Boolean).join(' ') -} - -function displayID(id) { - return id.replaceAll(':', ' ') -} diff --git a/packages/cli/scripts/format b/packages/cli/scripts/format deleted file mode 100755 index db2a3fa2..00000000 --- a/packages/cli/scripts/format +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -echo "==> Running gofmt -s -w" -gofmt -s -w . diff --git a/packages/cli/scripts/generate-command-map.ts b/packages/cli/scripts/generate-command-map.ts deleted file mode 100644 index ef0fb4e4..00000000 --- a/packages/cli/scripts/generate-command-map.ts +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bun -import { readdir, writeFile } from 'node:fs/promises' -import { join, relative, sep } from 'node:path' -import { fileURLToPath } from 'node:url' - -const root = fileURLToPath(new URL('..', import.meta.url)) -const commandsDir = join(root, 'src', 'commands') -const outPath = join(root, 'src', 'commands.generated.ts') - -const listAliases: Record = { - 'accounts:list': ['accounts'], - 'bridges:config': ['bridges:c'], - 'bridges:delete': ['bridges:d'], - 'bridges:list': ['bridges'], - 'bridges:login': ['bridges:l'], - 'bridges:login-password': ['bridges:p'], - 'bridges:proxy': ['bridges:x'], - 'bridges:register': ['bridges:r'], - 'bridges:whoami': ['bridges:w'], - 'chats:list': ['chats', 'accounts:chats', 'ls'], - 'contacts:list': ['contacts'], - 'messages:search': ['search'], - 'send:text': ['send'], - 'targets:list': ['targets'], -} - -const files = await listCommandFiles(commandsDir) -const canonicalEntries = files - .map(file => ({ - command: fileToCommand(file), - importPath: `./commands/${relative(commandsDir, file).split(sep).join('/').replace(/\.(ts|tsx)$/, '.js')}`, - })) - .sort((a, b) => a.command.localeCompare(b.command)) -const entries = canonicalEntries - .flatMap(entry => [entry, ...(listAliases[entry.command] ?? []).map(command => ({ command, importPath: entry.importPath }))]) - .sort((a, b) => a.command.localeCompare(b.command)) - -const importPaths = canonicalEntries.map(entry => entry.importPath) -const commandImports = new Map(importPaths.map((importPath, index) => [importPath, `Command${index}`])) -const imports = importPaths.map((importPath, index) => `import Command${index} from '${importPath}'`).join('\n') -const mapEntries = entries.map(entry => ` '${entry.command}': ${commandImports.get(entry.importPath)},`).join('\n') - -await writeFile( - outPath, - `${imports} - -export const commands = { -${mapEntries} -} -`, -) - -async function listCommandFiles(dir) { - const output = [] - for (const entry of await readdir(dir, { withFileTypes: true })) { - if (entry.name.startsWith('_')) continue - const path = join(dir, entry.name) - if (entry.isDirectory()) { - output.push(...await listCommandFiles(path)) - } else if (entry.isFile() && /\.(ts|tsx)$/.test(entry.name) && !entry.name.endsWith('.d.ts')) { - output.push(path) - } - } - return output -} - -function fileToCommand(file) { - const commandPath = relative(commandsDir, file).split(sep).join('/') - const parts = commandPath.replace(/\.(ts|tsx)$/, '').split('/') - return parts.map(part => part === 'index' ? undefined : part).filter(Boolean).join(':') -} diff --git a/packages/cli/scripts/generate-readme.ts b/packages/cli/scripts/generate-readme.ts deleted file mode 100644 index cfc17595..00000000 --- a/packages/cli/scripts/generate-readme.ts +++ /dev/null @@ -1,519 +0,0 @@ -#!/usr/bin/env bun -import {readFile, writeFile} from 'node:fs/promises'; -import {Config} from '@oclif/core/config'; -import {commandManifest} from '../dist/lib/manifest.js'; - -const config = await Config.load({root: process.cwd()}); -const check = process.argv.includes('--check'); -// Include hidden commands so manifest-listed commands still render in the README -// and pass the manifest match. -const commandsByID = new Map([...config.commands].map(command => [displayID(command.id), command])); -// Manifest entries for plugin-shipped commands (e.g. `targets tunnel` from -// @beeper/cli-plugin-cloudflare) won't be in the built oclif config unless that plugin is -// installed. Render them from the manifest entry directly instead of erroring. -const commands = commandManifest.map(item => { - const command = commandsByID.get(item.command); - if (command) return command; - return { - id: item.command.replaceAll(' ', ':'), - summary: item.description, - description: item.description, - args: {}, - flags: {}, - pluginShipped: true, - }; -}); - -const globalFlags = new Set(['base-url', 'debug', 'dry-run', 'events', 'force', 'format', 'full', 'json', 'no-input', 'quiet', 'read-only', 'results-only', 'select', 'target', 'timeout', 'yes']); -const commandList = commands.map(command => { - const id = displayID(command.id); - return `| \`${id}\` | ${escapeTable(text(command.summary || command.description || ''))} |`; -}); - -const examplesByID = new Map(commandManifest.map(item => [item.command, item.examples ?? []])); -const commandSections = commands.map(command => commandSection(command)).join('\n\n'); - -// Public URL where the Astro docs site (in `docs/`) is published. Keep this in -// sync with `site` + `base` in `docs/astro.config.mjs`. -const docsUrl = 'https://beeper.github.io/cli'; -const repoUrl = 'https://github.com/beeper/cli'; - -const intro = `
- -# beeper - -**One CLI for all your chats.** Built for you and your agent — batteries included. - -[![npm](https://img.shields.io/npm/v/beeper-cli.svg?label=npm&color=6E56F8)](https://www.npmjs.com/package/beeper-cli) -[![license](https://img.shields.io/badge/license-MIT-6E56F8.svg)](${repoUrl}/blob/main/packages/cli/LICENSE) -[![docs](https://img.shields.io/badge/docs-online-6E56F8.svg)](${docsUrl}) -[![built with Bun](https://img.shields.io/badge/built%20with-Bun-6E56F8.svg)](https://bun.sh) - -
- -Talks to Beeper Desktop on this machine, to a Beeper Server you self-host, or -to either one running somewhere else. Send and receive across the chat -networks Beeper bridges, from one CLI shaped for scripts, agents, and humans -in a hurry. - -**Supported chat networks** (via Beeper's bridges): -WhatsApp · iMessage · Telegram · Discord · Signal · Instagram DMs · -Facebook Messenger · X (Twitter) DMs · LinkedIn · Slack · -Google Messages (RCS/SMS) · Google Chat · Matrix · IRC · Bluesky. -Run \`beeper bridges list\` for the live list on your target. - -📖 **[Read the docs](${docsUrl})** · command manual: \`beeper man\` · open docs: \`beeper docs\` - -## Features - -- **Connects to your Beeper.** Local Beeper Desktop on this machine (default), a Beeper Server you install and manage via the CLI, or a remote Beeper Desktop or Beeper Server authorized over OAuth/PKCE — or a bearer token in CI. -- **Setup that does the work.** \`beeper setup\` finds Beeper Desktop, offers to launch it, adopts the session. \`--server --install\` installs and starts a headless server in one step. \`--oauth\` opens the browser. \`--remote URL\` does the rest. -- **Every chat, every network.** List, search, start, archive, pin, mute, rename, focus. Read, edit, delete, react. Send text, files, stickers, voice, typing indicators. Download media. Export to JSON or Markdown. -- **Verification first-class.** SAS/QR device verification, recovery-key unlock, \`status\`/\`doctor\` to reach an encrypted-ready target — without leaving the shell. -- **Agent-shaped automation.** \`--json\` everywhere, NDJSON \`--events\`, \`watch\` with WebSocket + outbound HMAC-signed webhooks, \`rpc\` over stdin/stdout, \`man --json\` tool manifests, raw \`api get\`/\`post\`/\`request\` for Beeper Client API endpoints we haven't wrapped yet. -- **Safe by default.** \`--read-only\` rejects every mutating command. Writes stay explicit. Plugins extend the CLI without forking it. - -## Install - -### Homebrew (recommended) - -\`\`\`sh -brew install beeper/tap/cli -\`\`\` - -The installed command is \`beeper\`. - -### npm - -\`\`\`sh -npx beeper-cli --help -npm install -g beeper-cli -\`\`\` - -The package name is \`beeper-cli\`; the installed command is \`beeper\`. - -### Build from source - -This repo is a Bun workspace. From the repo root: - -\`\`\`sh -bun install -bun --filter @beeper/cli run build -bun --filter @beeper/cli run dev -- --help -\`\`\` - -For local CLI development inside \`packages/cli\`: - -\`\`\`sh -bun run dev -- --help -\`\`\` - -Regenerate this README after command, flag, or argument changes: - -\`\`\`sh -bun run readme -\`\`\` - -## Quick start - -The happy path: Beeper Desktop is already on this machine. \`beeper setup\` finds -it, offers to launch it if it's not running, and adopts the session. - -\`\`\`text -$ beeper setup -Looking for Beeper Desktop… found, not running. -Launch it now? [Y/n] y -▎ Launched Beeper Desktop - next Run \`beeper setup\` again once it finishes starting. - -$ beeper setup -Use this Desktop session for CLI access? [Y/n] y -▎ Connected desktop - accounts whatsapp, telegram, imessage - endpoint http://127.0.0.1:23373 - -$ beeper chats list --limit 3 - 10313 Family 3 unread - 8951 Alice · - 7204 Eng standup 12 unread - -$ beeper messages search "flight" - 8951 Alice · "your flight is at 6:40, gate B23" 2d ago - 10313 Family · "what flight are you on?" 1w ago - -$ beeper send text --to Family --message "on my way" -▎ Sent Family - message "on my way" - at 2026-05-18T14:02:11Z - -$ beeper export --out ./beeper-export -▎ Exported ./beeper-export - chats 214 messages 38,901 attachments 1,205 -\`\`\` - -Recipients accept a numeric local chat ID, a full Beeper/Matrix chat ID, an -iMessage chat ID, an exact title, or search text. Ambiguous matches prompt in a -TTY; pass \`--pick N\` in scripts. - -## Connecting a target - -A *target* is the Beeper endpoint \`beeper\` talks to — local Beeper Desktop, -local Beeper Server, or a remote Beeper Desktop or Beeper Server. Pick one of -four paths. - -### 1. Local Beeper Desktop (default, recommended) - -If Beeper Desktop is installed and signed in here, \`beeper setup\` discovers it -on \`http://127.0.0.1:23373\` and adopts the existing session. If it's installed -but not running, \`setup\` offers to launch it. If it's not installed at all, -\`--install\` does that in one step. - -\`\`\`text -$ beeper setup --desktop --install -▎ Installed Beeper Desktop (stable) -▎ Launched Beeper Desktop - next Sign in to Beeper Desktop, then re-run \`beeper setup\`. - -$ beeper setup -▎ Connected desktop - accounts whatsapp, telegram -\`\`\` - -Variants: \`beeper setup --local\` to skip discovery and force the local path; -\`beeper install desktop --channel nightly\` for the nightly channel. - -### 2. Local Beeper Server (self-hosted, managed by the CLI) - -For a headless long-running setup on this machine, install and adopt a local -Beeper Server. The CLI manages the process — \`targets start/stop/restart/logs/enable\`. - -\`\`\`text -$ beeper setup --server --install -▎ Installed Beeper Server (stable) -▎ Started server on http://127.0.0.1:23373 - auth Opening browser to authorize this server… -▎ Connected server - accounts (none) - next Run \`beeper accounts add\` to connect a network. - -$ beeper accounts add -? Which bridge? whatsapp - Scan this QR code with WhatsApp on your phone: - ▄▄▄▄▄▄▄ ▄ ▄ ▄▄▄▄▄▄▄ - █ ███ █ ▄█▄ █ ███ █ - █ ███ █ ▀█▀ █ ███ █ - ▀▀▀▀▀▀▀ ▀ ▀ ▀▀▀▀▀▀▀ -▎ Connected whatsapp · +1•••4242 -\`\`\` - -Variants: \`beeper install server\`, \`beeper install server --server-env staging\`. - -### 3. Remote Desktop or Server via OAuth (PKCE) - -For a Beeper Desktop or Server running on another machine, authorize the CLI -through a browser-based OAuth/PKCE flow. - -\`\`\`text -$ beeper setup --remote https://desktop.example.com -▎ Authorizing https://desktop.example.com - flow OAuth (PKCE) — opening browser… -▎ Connected remote (desktop.example.com) - accounts whatsapp, telegram, signal -\`\`\` - -Variants: \`beeper setup --oauth\` (PKCE against the default Beeper auth); -\`beeper targets add remote work https://desktop.example.com --default\` to -register additional remotes. - -### 4. Bearer token (non-interactive / CI) - -For agents, CI, and scripts, hand the CLI a bearer token directly — no -browser, no interactive prompts. - -\`\`\`sh -BEEPER_ACCESS_TOKEN=... beeper chats list --json -BEEPER_ACCESS_TOKEN=... BEEPER_DESKTOP_BASE_URL=https://desktop.example.com \\ - beeper messages list --chat 10313 --json -\`\`\` - -Once connected, \`beeper accounts add\` walks each chat-network bridge through -its own login — QR, code, OAuth, cookie, whatever the bridge requires — so -WhatsApp, Telegram, Discord, iMessage, and the rest show up under \`accounts list\`. - -## Documentation - -Full documentation lives at **[${docsUrl.replace(/^https?:\/\//, '')}](${docsUrl})** -(built from [\`docs/\`](docs/) with Astro Starlight — a fully static site). - -| Topic | Page | Commands | -| --- | --- | --- | -| **Setup + install** | [connect](${docsUrl}/connect/) · [install](${docsUrl}/install/) · [auth](${docsUrl}/auth/) | \`setup\` · \`install desktop\` · \`install server\` · \`verify\` · \`status\` · \`doctor\` · \`auth status\` | -| **Targets** | [targets](${docsUrl}/targets/) | \`targets list\` · \`targets add desktop\` · \`targets add server\` · \`targets add remote\` · \`targets use\` · \`targets status\` · \`targets logs\` | -| **Bridges + accounts** | [accounts](${docsUrl}/accounts/) | \`bridges list\` · \`bridges show\` · \`accounts list\` · \`accounts add\` · \`accounts show\` · \`accounts use\` · \`accounts remove\` | -| **Chats** | [chats](${docsUrl}/chats/) | \`chats list\` · \`chats search\` · \`chats show\` · \`chats start\` · \`chats archive\` · \`chats pin\` · \`chats mute\` · \`chats priority\` · \`chats remind\` · \`chats rename\` · \`chats draft\` · \`chats focus\` | -| **Messages** | [messages](${docsUrl}/messages/) · [send](${docsUrl}/send/) · [presence](${docsUrl}/presence/) | \`messages list\` · \`messages search\` · \`messages export\` · \`send text\` · \`send file\` · \`send sticker\` · \`send voice\` · \`send react\` · \`presence\` | -| **Contacts + media** | [contacts](${docsUrl}/contacts/) · [media](${docsUrl}/media/) · [export](${docsUrl}/export/) | \`contacts list\` · \`contacts search\` · \`media download\` · \`export\` | -| **Automation** | [scripting](${docsUrl}/scripting/) · [watch](${docsUrl}/watch/) · [rpc](${docsUrl}/rpc/) · [api](${docsUrl}/api/) | \`watch\` · \`watch --webhook\` · \`rpc\` · \`man\` · \`api get\` · \`api post\` · \`api request\` | -| **Maintenance** | [config](${docsUrl}/config/) · [update](${docsUrl}/update/) | \`update\` · \`config\` · \`completion\` · \`docs\` · \`version\` | - -Use \`beeper docs\` to open the CLI docs and \`beeper man\` to print the local -command manual. To work on the docs site locally: \`cd docs && bun install && bun run dev\`. - -## Configuration - -Default Beeper Client API target: \`http://127.0.0.1:23373\`. CLI configuration is -stored under your user config dir; print it with \`beeper config path\`. - -**Global flags:** \`--base-url\`, \`--target\`, \`--json\`, \`--events\`, -\`--full\`, \`--timeout\`, \`--read-only\`, \`--debug\`, \`--yes\`, \`--quiet\`. - -**Environment overrides:** - -| Variable | Effect | -| --- | --- | -| \`BEEPER_ACCESS_TOKEN\` | Bearer token for the selected target. Overrides stored OAuth login. | -| \`BEEPER_DESKTOP_BASE_URL\` | Beeper Client API base URL (Desktop or Server). Defaults to \`http://127.0.0.1:23373\`. | -| \`BEEPER_READONLY\` | \`1\`/\`true\`/\`yes\`/\`on\` enables read-only mode globally. | -| \`BEEPER_CLI_CONFIG_DIR\` | Override config directory for testing or isolated profiles. | - -## Exit codes - -| Code | Meaning | -| --- | --- | -| \`0\` | Success. | -| \`1\` | Generic runtime error. | -| \`2\` | Usage error (parsing, validation, missing required flag/arg, read-only refusal). | -| \`3\` | Auth required (no stored token; sign in or set \`BEEPER_ACCESS_TOKEN\`). | -| \`4\` | Target/account not ready (\`doctor\` reports this when readiness is not \`ready\`). | -| \`5\` | Selector matched nothing (unknown target, account, chat, contact). | -| \`6\` | Ambiguous selector (multiple matches; pass an exact ID or \`--pick N\`). | - -JSON output preserves the same envelope on failure: \`{"success":false,"data":null,"error":"...","exitCode":N}\` written to stderr. - -## Addressing - -- Chat arguments accept numeric local chat IDs, full Beeper/Matrix chat IDs, iMessage chat IDs, exact titles, or search text. -- For scripts on the same target/profile, prefer the numeric local chat ID shown by \`beeper chats list\`; use the full Beeper/Matrix chat ID when the selector must work across targets or profiles. -- Numeric local chat IDs come from the selected Desktop database. Treat them as local to that target/profile. -- Ambiguous chat matches return numbered choices; pass \`--pick N\` to select one. -- Account arguments accept account IDs, network names, bridge type/id, or account user identity. -- Account filters can expand a network name to multiple matching accounts. -- \`contacts search\` and \`chats start\` can search across all accounts when \`--account\` is omitted. -- \`contacts list\` accepts the same account selectors as other account-scoped commands. - -## Output and scripting - -Most commands support: - -- app-like text by default, optimized for scanning chats, messages, contacts, accounts, and media -- \`--json\` for \`{"success":true,"data":...,"error":null}\` output on stdout -- \`--events\` for NDJSON lifecycle events on stderr from long-running commands -- \`--read-only\` to reject commands that modify Beeper or local CLI state -- \`--full\` to disable truncation -- \`--debug\` for SDK debug logging -- \`--target\` or \`--base-url\` to point at a different target - -\`man --json\` prints a compact command manifest for tools and agents. -\`rpc\` runs newline-delimited JSON command RPC over stdin/stdout. - -## Raw API access - -Raw Beeper Client API calls live under \`api\`, so scripts can reach a new -endpoint before a workflow command exists: - -\`\`\`sh -beeper api get /v1/info -beeper api post /v1/messages/{chatID}/send --body '{"text":"hello"}' -beeper api request DELETE /v1/chats/abc/messages/def/reactions --body '{"reactionKey":"👍"}' -\`\`\` - -## Plugins - -Beeper CLI supports optional oclif plugins. List recommended Beeper plugins: - -\`\`\`sh -beeper plugins available -\`\`\` - -Install a published plugin: - -\`\`\`sh -beeper plugins install @beeper/cli-plugin-cloudflare -\`\`\` - -For plugin development, import from \`@beeper/cli/plugin-sdk\` and expose oclif -commands from your package. Link a local plugin while working on it: - -\`\`\`sh -beeper plugins link ./packages/cli-plugin-cloudflare -beeper targets tunnel --help -\`\`\` - -First-party optional plugins: - -| Package | Adds | -| --- | --- | -| \`@beeper/cli-plugin-cloudflare\` | \`targets tunnel\` for exposing a selected Beeper target through Cloudflare Tunnel. | - -`; - -const inspiration = `## Inspiration - -- [wacli](https://wacli.sh/) — scriptable WhatsApp CLI whose command-line product shape we borrow from. -`; - -const license = `## License - -MIT — see [\`packages/cli/LICENSE\`](packages/cli/LICENSE). -`; - -const fullReference = `## Command Summary - -| Command | Summary | -| --- | --- | -${commandList.join('\n')} - -## Command Reference - -${commandSections} -`; - -const publishing = `## Publishing - -Beeper CLI releases ship signed macOS Bun binaries inside versioned archives and -a thin npm package that downloads, verifies, extracts, and runs the matching -GitHub Release archive. - -For now, publishing runs from a local macOS machine: - -\`\`\`sh -bun run release 0.6.1 -\`\`\` - -The local release command: - -- builds standalone Bun binaries -- signs and notarizes macOS binaries when local signing credentials are available -- uploads versioned macOS and Linux archives to the GitHub release -- publishes \`beeper-cli\` to npm as a thin binary launcher package -- updates \`beeper/homebrew-tap\` with the pinned archive SHA - -Required local credentials: - -- GitHub CLI authenticated with release and tap access -- npm auth for publishing \`beeper-cli\` -- local Developer ID signing identity, or Fastlane match access via a - \`MOBILE_SECRETS_FILE\` path exported in your shell -- \`HOMEBREW_TAP_GITHUB_TOKEN\` for updating the tap -`; - -const referencePointer = `## Full command reference - -The complete \`beeper\` command summary and per-command reference (every flag, -arg, and example) lives in [\`packages/cli/README.md\`](packages/cli/README.md). -For terminal-side reference, \`beeper man\` prints the same manual locally and -\`beeper man --json\` emits a tool manifest for agents. -`; - -const rootReadme = [intro, referencePointer, inspiration, license].join('\n'); -const packageReadme = [intro, fullReference, publishing, inspiration, license].join('\n'); - -const outputs = [ - { path: 'README.md', body: packageReadme }, - { path: '../../README.md', body: rootReadme }, -]; - -if (check) { - for (const { path, body } of outputs) { - const current = await readFile(path, 'utf8'); - if (current !== body) { - console.error(`${path} is out of date. Run bun run readme.`); - process.exit(1); - } - } -} else { - for (const { path, body } of outputs) { - await writeFile(path, body); - } -} - -function commandSection(command) { - const id = displayID(command.id); - const usage = usageFor(command); - const parts = [ - `### \`beeper ${id}\``, - text(command.summary || command.description || ''), - '', - '```sh', - usage, - '```', - ]; - - if (command.description && command.description !== command.summary) { - parts.push('', text(command.description)); - } - - const args = Object.values(command.args || {}); - if (args.length > 0) { - parts.push('', 'Arguments:', '', '| Name | Required | Description |', '| --- | --- | --- |'); - for (const arg of args) { - parts.push(`| \`${arg.name}\` | ${arg.required ? 'yes' : 'no'} | ${escapeTable(arg.description || '')} |`); - } - } - - const flags = Object.values(command.flags || {}).filter(flag => !globalFlags.has(flag.name)); - if (flags.length > 0) { - parts.push('', 'Flags:', '', '| Flag | Type | Description |', '| --- | --- | --- |'); - for (const flag of flags.sort((a, b) => a.name.localeCompare(b.name))) { - parts.push(`| \`${escapeTable(flagLabel(flag))}\` | ${flag.type || 'boolean'} | ${escapeTable(flagDescription(flag))} |`); - } - } - - const examples = examplesByID.get(id) ?? []; - if (examples.length > 0) { - parts.push('', 'Examples:', '', '```sh', ...examples, '```'); - } - - const inherited = Object.values(command.flags || {}).filter(flag => globalFlags.has(flag.name)); - if (inherited.length > 0) { - parts.push('', `Global flags: ${inherited.map(flag => `\`--${flag.name}\``).join(', ')}.`); - } - - return parts.filter((part, index, array) => part !== '' || array[index - 1] !== '').join('\n'); -} - -function displayID(id) { - return id.replaceAll(':', ' '); -} - -function usageFor(command) { - const args = Object.values(command.args || {}).map(arg => arg.required ? `<${arg.name}>` : `[${arg.name}]`); - return ['beeper', displayID(command.id), ...args].join(' '); -} - -function flagLabel(flag) { - const prefix = flag.char ? `-${flag.char}, --${flag.name}` : `--${flag.name}`; - if (flag.type === 'boolean') return prefix; - const value = flag.options?.length ? `<${flag.options.join('|')}>` : ''; - return `${prefix}=${value}${flag.multiple ? '...' : ''}`; -} - -function flagDescription(flag) { - const details = []; - if (flag.description) details.push(text(flag.description)); - if (flag.default !== undefined) details.push(`Default: ${String(flag.default)}`); - if (flag.required) details.push('Required.'); - return details.join(' '); -} - -function escapeTable(value) { - return text(value).replaceAll('|', '\\|').replace(/\s+/g, ' ').trim(); -} - -function text(value) { - return String(value) - .replaceAll('<%= config.bin %>', config.bin) - .replaceAll('<%= command.id %>', '') - .replace(/\s+/g, ' ') - .trim(); -} diff --git a/packages/cli/scripts/link b/packages/cli/scripts/link deleted file mode 100755 index aed65927..00000000 --- a/packages/cli/scripts/link +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -cd "$(dirname "$0")/.." - -echo "==> Linking Beeper CLI" -bun link diff --git a/packages/cli/scripts/lint b/packages/cli/scripts/lint deleted file mode 100755 index bbe666e4..00000000 --- a/packages/cli/scripts/lint +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -cd "$(dirname "$0")/.." - -echo "==> Typechecking Beeper CLI" -bun run typecheck diff --git a/packages/cli/scripts/mock b/packages/cli/scripts/mock deleted file mode 100755 index e5b0ace5..00000000 --- a/packages/cli/scripts/mock +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -if [[ -n "$1" && "$1" != '--'* ]]; then - URL="$1" - shift -else - URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)" -fi - -# Check if the URL is empty -if [ -z "$URL" ]; then - echo "Error: No OpenAPI spec path/url provided or found in .stats.yml" - exit 1 -fi - -echo "==> Starting mock server with URL ${URL}" - -# Run steady mock on the given spec -if [ "$1" == "--daemon" ]; then - # Pre-install the package so the download doesn't eat into the startup timeout - bunx -p @stdy/cli@0.19.6 -- steady --version - - bunx -p @stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & - - # Wait for server to come online via health endpoint (max 30s) - echo -n "Waiting for server" - attempts=0 - while ! curl --silent --fail "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1; do - if ! kill -0 $! 2>/dev/null; then - echo - cat .stdy.log - exit 1 - fi - attempts=$((attempts + 1)) - if [ "$attempts" -ge 300 ]; then - echo - echo "Timed out waiting for Steady server to start" - cat .stdy.log - exit 1 - fi - echo -n "." - sleep 0.1 - done - - echo -else - bunx -p @stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" -fi diff --git a/packages/cli/scripts/publish-homebrew-formula.ts b/packages/cli/scripts/publish-homebrew-formula.ts deleted file mode 100644 index a3583bfc..00000000 --- a/packages/cli/scripts/publish-homebrew-formula.ts +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env bun -import {existsSync} from 'node:fs'; -import {mkdir, mkdtemp, readFile, rm, writeFile} from 'node:fs/promises'; -import {tmpdir} from 'node:os'; -import {join} from 'node:path'; - -const root = new URL('..', import.meta.url).pathname; -const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')); -const metadata = JSON.parse(await readFile(new URL('../dist/release/homebrew.json', import.meta.url), 'utf8')); - -const token = process.env.HOMEBREW_TAP_GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_TOKEN; -const tapRepository = process.env.HOMEBREW_TAP_REPOSITORY || 'beeper/homebrew-tap'; -const sourceRepository = process.env.GITHUB_REPOSITORY || 'beeper/cli'; -const version = process.env.PACKAGE_VERSION || metadata.version || packageJson.version; -const formulaName = process.env.HOMEBREW_FORMULA_NAME || 'cli'; -const commandName = process.env.HOMEBREW_COMMAND_NAME || metadata.command || 'beeper'; -const formulaClass = formulaName - .split(/[-_]/) - .map(part => `${part[0].toUpperCase()}${part.slice(1)}`) - .join(''); -const tag = process.env.GITHUB_REF_NAME || `v${version}`; - -if (!token) { - throw new Error('HOMEBREW_TAP_GITHUB_TOKEN, GH_TOKEN, or GITHUB_TOKEN is required to publish the Homebrew formula.'); -} - -const cloneRoot = await mkdtemp(join(tmpdir(), 'beeper-cli-homebrew-')); -const tapPath = join(cloneRoot, 'tap'); -const remote = `https://x-access-token:${token}@github.com/${tapRepository}.git`; - -try { - await run('git', ['clone', '--depth', '1', remote, tapPath], {cwd: cloneRoot, scrub: token}); - await run('git', ['config', 'user.name', process.env.GIT_AUTHOR_NAME || 'beeper-release-bot'], {cwd: tapPath}); - await run('git', ['config', 'user.email', process.env.GIT_AUTHOR_EMAIL || 'help@beeper.com'], {cwd: tapPath}); - - const formulaDir = join(tapPath, 'Formula'); - const formulaPath = join(formulaDir, `${formulaName}.rb`); - if (!existsSync(formulaDir)) { - await mkdir(formulaDir, {recursive: true}); - } - - await writeFile( - formulaPath, - formula({formulaClass, formulaName, sourceRepository, tag, version, metadata, commandName}), - ); - await run('git', ['add', formulaPath], {cwd: tapPath}); - - const changed = await output('git', ['diff', '--cached', '--quiet'], {cwd: tapPath, allowFailure: true}); - if (changed.code === 0) { - console.log('Homebrew formula is already current.'); - } else { - await run('git', ['commit', '-m', `${formulaName} ${version}`], {cwd: tapPath}); - await run('git', ['push', 'origin', 'HEAD'], {cwd: tapPath, scrub: token}); - } -} finally { - await rm(cloneRoot, {recursive: true, force: true}); -} - -function formula({formulaClass, formulaName, sourceRepository, tag, version, metadata, commandName}) { - const archives = metadata.archives ?? [metadata] - const arm64 = archives.find(archive => archive.platform === 'darwin-arm64') ?? archives[0] - const x64 = archives.find(archive => archive.platform === 'darwin-x64') ?? arm64 - - return `class ${formulaClass} < Formula - desc "Beeper CLI" - homepage "https://developers.beeper.com/desktop-api/" - license "MIT" - version "${version}" - - on_arm do - url "https://github.com/${sourceRepository}/releases/download/${tag}/${arm64.archive}" - sha256 "${arm64.sha256}" - end - - on_intel do - url "https://github.com/${sourceRepository}/releases/download/${tag}/${x64.archive}" - sha256 "${x64.sha256}" - end - - def install - bin.install "bin/${commandName}" - end - - test do - assert_match version.to_s, shell_output("#{bin}/${commandName} --version") - end -end -`; -} - -async function run(command, args, options = {}) { - const result = await output(command, args, options); - if (result.code !== 0) { - throw new Error(`${command} ${args.join(' ')} exited with ${result.code}`); - } -} - -async function output(command, args, options = {}) { - const child = Bun.spawn([command, ...args], { - cwd: options.cwd || root, - env: process.env, - stdin: 'ignore', - stdout: 'pipe', - stderr: 'pipe', - }); - - const [stdout, stderr, code] = await Promise.all([ - collect(child.stdout, process.stdout, options.scrub), - collect(child.stderr, process.stderr, options.scrub), - child.exited, - ]); - - return {code, stdout, stderr}; -} - -async function collect(stream, sink, scrub) { - let output = ''; - const decoder = new TextDecoder(); - for await (const chunk of stream) { - const text = typeof chunk === 'string' ? chunk : decoder.decode(chunk, {stream: true}); - output += text; - sink.write(scrub ? text.replaceAll(scrub, '[token]') : text); - } - output += decoder.decode(); - return output; -} diff --git a/packages/cli/scripts/publish-local-release.ts b/packages/cli/scripts/publish-local-release.ts deleted file mode 100644 index 469d4aa5..00000000 --- a/packages/cli/scripts/publish-local-release.ts +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env bun -import { readFile } from 'node:fs/promises' -import { join } from 'node:path' -import { fileURLToPath } from 'node:url' - -const root = fileURLToPath(new URL('..', import.meta.url)) -const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) -const binaries = JSON.parse(await readFile(join(root, 'dist', 'bin', 'binaries.json'), 'utf8')) -const version = process.env.PACKAGE_VERSION || pkg.version -const tag = process.env.GITHUB_REF_NAME || process.env.TAG || `v${version}` -const repo = process.env.GITHUB_REPOSITORY || 'beeper/cli' - -await run('gh', ['auth', 'status']) -await run('npm', ['whoami']) -if (!Array.isArray(binaries.artifacts) || binaries.artifacts.length === 0) { - throw new Error('Refusing to publish npm package without binary artifacts.') -} -for (const platform of ['darwin-arm64', 'darwin-x64', 'linux-arm64', 'linux-x64']) { - if (!binaries.artifacts.some(artifact => artifact.platform === platform)) { - throw new Error(`Refusing to publish without ${platform} binary artifact.`) - } -} -await ensureRelease(tag, repo) -await run('gh', [ - 'release', - 'upload', - tag, - 'dist/bin/binaries.json', - ...await releaseArchives(), - '--repo', - repo, - '--clobber', -]) -await run('bun', ['scripts/publish-homebrew-formula.ts']) -await run('npm', ['publish', '--access', 'public'], { cwd: fileURLToPath(new URL('../../npm/', import.meta.url)) }) - -async function ensureRelease(tag, repo) { - const view = await output('gh', ['release', 'view', tag, '--repo', repo], { allowFailure: true }) - if (view.code === 0) return - await run('gh', ['release', 'create', tag, '--repo', repo, '--title', tag, '--generate-notes']) -} - -async function releaseArchives() { - const metadata = JSON.parse(await readFile(join(root, 'dist', 'release', 'homebrew.json'), 'utf8')) - return [ - ...metadata.archives.map(archive => join('dist', 'release', archive.archive)), - 'dist/release/homebrew.json', - ] -} - -async function run(command, args, options = {}) { - const result = await output(command, args, options) - if (result.code !== 0) throw new Error(`${command} ${args.join(' ')} exited with ${result.code}`) -} - -async function output(command, args, options = {}) { - const child = Bun.spawn([command, ...args], { - cwd: options.cwd || root, - env: process.env, - stdin: 'inherit', - stdout: 'inherit', - stderr: 'inherit', - }) - return { code: await child.exited } -} diff --git a/packages/cli/scripts/read-signing-secrets.rb b/packages/cli/scripts/read-signing-secrets.rb deleted file mode 100644 index 5721ee08..00000000 --- a/packages/cli/scripts/read-signing-secrets.rb +++ /dev/null @@ -1,30 +0,0 @@ -require "shellwords" -require "yaml" - -secrets = YAML.load_file(ENV.fetch("SECRETS_FILE")) -team = ENV.fetch("TEAM_ID") - -required = { - "APP_STORE_CONNECT_API_KEY_KEY_ID" => "APP_STORE_CONNECT_API_#{team}_KEY_ID", - "APP_STORE_CONNECT_API_KEY_ISSUER_ID" => "APP_STORE_CONNECT_API_#{team}_ISSUER_ID", - "APP_STORE_CONNECT_API_KEY_KEY" => "APP_STORE_CONNECT_API_#{team}_KEY_CONTENT", - "MATCH_PASSWORD" => "FASTLANE_MATCH_PASSWORD", - "MATCH_S3_ACCESS_KEY" => "FASTLANE_MATCH_S3_ACCESS_KEY", - "MATCH_S3_SECRET_ACCESS_KEY" => "FASTLANE_MATCH_S3_SECRET_ACCESS_KEY", -} - -missing = required.values.reject { |key| secrets[key] && !secrets[key].to_s.empty? } -unless missing.empty? - warn "missing required signing secret keys: #{missing.join(", ")}" - exit 1 -end - -File.write(ENV.fetch("P8_FILE"), secrets.fetch("APP_STORE_CONNECT_API_#{team}_KEY_CONTENT").to_s.gsub("\\n", "\n")) -File.chmod(0o600, ENV.fetch("P8_FILE")) - -File.open(ENV.fetch("ENV_FILE"), "w", 0o600) do |file| - required.each do |env_name, secret_name| - next if env_name == "APP_STORE_CONNECT_API_KEY_KEY" - file.puts("export #{env_name}=#{Shellwords.escape(secrets.fetch(secret_name).to_s)}") - end -end diff --git a/packages/cli/scripts/run b/packages/cli/scripts/run deleted file mode 100755 index fc3e838b..00000000 --- a/packages/cli/scripts/run +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -cd "$(dirname "$0")/.." - -bun run dev -- "$@" diff --git a/packages/cli/scripts/sign-macos-binaries.ts b/packages/cli/scripts/sign-macos-binaries.ts deleted file mode 100644 index 10cbdf44..00000000 --- a/packages/cli/scripts/sign-macos-binaries.ts +++ /dev/null @@ -1,275 +0,0 @@ -#!/usr/bin/env bun -import { existsSync } from 'node:fs' -import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { basename, join } from 'node:path' -import { fileURLToPath } from 'node:url' - -const root = fileURLToPath(new URL('..', import.meta.url)) -const outDir = join(root, 'dist', 'bin') -const manifestPath = join(outDir, 'binaries.json') -const teamID = process.env.TEAM_ID || process.env.APPLE_TEAM_ID || 'PZYM8XX95Q' -const secretsFile = process.env.MOBILE_SECRETS_FILE -const requireSigning = process.env.BEEPER_CLI_REQUIRE_MACOS_SIGNING === '1' - -if (process.platform !== 'darwin') { - if (requireSigning) throw new Error('macOS binary signing requires a macOS runner.') - console.log('Skipping macOS binary signing on non-macOS runner.') - process.exit(0) -} - -const workDir = await mkdtemp(join(tmpdir(), 'beeper-cli-signing-')) - -try { - const credentials = await prepareCredentials(workDir) - const identity = process.env.MACOS_CODESIGN_IDENTITY || process.env.IDENTITY || await findIdentity(teamID) - - if (!identity) { - if (credentials.match) { - await importDeveloperID(workDir) - } else { - if (requireSigning) throw new Error(`No Developer ID Application identity for team ${teamID}`) - console.log('Skipping macOS binary signing because no Developer ID identity is available.') - process.exit(0) - } - } - - const resolvedIdentity = identity || await findIdentity(teamID) - if (!resolvedIdentity) { - throw new Error( - `Fastlane match completed, but macOS still has no usable Developer ID Application identity for team ${teamID}. ` + - 'Run `security find-identity -v -p codesigning`; it must list a Developer ID Application certificate with an attached private key. ' + - 'If it does not, fix the local keychain or match signing storage before rerunning release.', - ) - } - - const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) - const darwinArtifacts = manifest.artifacts.filter(artifact => artifact.platform.startsWith('darwin-')) - - for (const artifact of darwinArtifacts) { - await run('/usr/bin/codesign', [ - '--force', - '--options', - 'runtime', - '--timestamp', - '--sign', - resolvedIdentity, - ...(process.env.MACOS_CODESIGN_KEYCHAIN ? ['--keychain', process.env.MACOS_CODESIGN_KEYCHAIN] : []), - artifact.path, - ]) - - await run('/usr/bin/codesign', ['--verify', '--strict', '--verbose=2', artifact.path]) - - if (credentials.notary) { - await notarize(artifact.path, credentials) - } else { - if (requireSigning) throw new Error('App Store Connect credentials are required for notarization.') - console.log('Skipping notarization because App Store Connect credentials are not available.') - } - - artifact.sha256 = await hashFile(artifact.path) - console.log(`${artifact.path}`) - console.log(`sha256 ${artifact.sha256}`) - } - - await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`) -} finally { - await rm(workDir, { recursive: true, force: true }) -} - -async function notarize(path, credentials) { - const zipPath = join(outDir, `${basename(path)}.zip`) - await run('/usr/bin/ditto', ['-c', '-k', '--keepParent', path, zipPath]) - if (credentials.p8) { - await run('/usr/bin/xcrun', [ - 'notarytool', - 'submit', - zipPath, - '--key', - credentials.p8, - '--key-id', - credentials.keyID, - '--issuer', - credentials.issuerID, - '--wait', - ]) - } else { - await run('/usr/bin/xcrun', [ - 'notarytool', - 'submit', - zipPath, - '--apple-id', - credentials.appleID, - '--password', - credentials.applePassword, - '--team-id', - credentials.teamID, - '--wait', - ]) - } - await run('/usr/bin/codesign', ['--verify', '--strict', '--verbose=2', path]) -} - -async function prepareCredentials(workDir) { - const envCredentials = { - appleID: process.env.APPLE_ID, - applePassword: process.env.APPLE_APP_SPECIFIC_PASSWORD, - teamID, - } - - if (envCredentials.appleID && envCredentials.applePassword) { - return { ...envCredentials, notary: true, match: false } - } - - if (!secretsFile || !existsSync(secretsFile)) return { notary: false, match: false } - - const envFile = join(workDir, 'secrets.env') - const p8 = join(workDir, `AuthKey_${teamID}.p8`) - await run('ruby', [new URL('./read-signing-secrets.rb', import.meta.url).pathname], { - env: { - ...process.env, - SECRETS_FILE: secretsFile, - TEAM_ID: teamID, - ENV_FILE: envFile, - P8_FILE: p8, - }, - }) - const parsed = parseEnv(await readFile(envFile, 'utf8')) - Object.assign(process.env, parsed) - return { - keyID: parsed.APP_STORE_CONNECT_API_KEY_KEY_ID, - issuerID: parsed.APP_STORE_CONNECT_API_KEY_ISSUER_ID, - p8, - notary: true, - match: Boolean(parsed.MATCH_PASSWORD && parsed.MATCH_S3_ACCESS_KEY && parsed.MATCH_S3_SECRET_ACCESS_KEY), - } -} - -async function importDeveloperID(workDir) { - const fastlaneDir = join(workDir, 'fastlane') - await mkdir(fastlaneDir, { recursive: true }) - await writeFile( - join(fastlaneDir, 'Fastfile'), - `default_platform(:mac) - -lane :import_developer_id do - setup_ci - sync_code_signing( - type: "developer_id", - platform: "macos", - team_id: ENV.fetch("BEEPER_CLI_TEAM_ID"), - app_identifier: [], - storage_mode: "s3", - s3_bucket: "a8c-fastlane-match", - s3_region: "us-east-2", - s3_access_key: ENV.fetch("MATCH_S3_ACCESS_KEY"), - s3_secret_access_key: ENV.fetch("MATCH_S3_SECRET_ACCESS_KEY"), - readonly: true - ) -end -`, - ) - const fastlane = await commandExists('fastlane') - if (fastlane) { - await run('fastlane', ['import_developer_id'], { - cwd: workDir, - env: { - ...process.env, - FASTLANE_DISABLE_COLORS: '1', - FASTLANE_HIDE_CHANGELOG: '1', - FASTLANE_SKIP_UPDATE_CHECK: '1', - BEEPER_CLI_TEAM_ID: teamID, - }, - scrub: [ - process.env.MATCH_S3_ACCESS_KEY, - process.env.MATCH_S3_SECRET_ACCESS_KEY, - process.env.MATCH_PASSWORD, - ], - }) - return - } - throw new Error('No Developer ID identity found and fastlane is unavailable.') -} - -async function findIdentity(team) { - const args = ['/usr/bin/security', 'find-identity', '-v', '-p', 'codesigning'] - const child = Bun.spawn(args, { - stdout: 'pipe', - stderr: 'pipe', - }) - const [stdout, code] = await Promise.all([new Response(child.stdout).text(), child.exited]) - if (code !== 0) return undefined - for (const line of stdout.split('\n')) { - if (!line.includes('Developer ID Application:') || !line.includes(`(${team})`)) continue - const match = line.match(/"([^"]+)"/) - if (match) return match[1] - } - return undefined -} - -async function commandExists(command) { - const child = Bun.spawn(['/usr/bin/which', command], { stdout: 'ignore', stderr: 'ignore' }) - return (await child.exited) === 0 -} - -function parseEnv(source) { - return Object.fromEntries( - source - .split('\n') - .map(line => line.match(/^export ([A-Z0-9_]+)=(.*)$/)) - .filter(Boolean) - .map(match => [match[1], shellUnescape(match[2])]), - ) -} - -function shellUnescape(value) { - if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1).replaceAll("'\\''", "'") - return value.replaceAll(/\\(.)/g, '$1') -} - -async function hashFile(path) { - const hasher = new Bun.CryptoHasher('sha256') - hasher.update(await Bun.file(path).arrayBuffer()) - return hasher.digest('hex') -} - -async function run(command, args, options = {}) { - const code = await runAllowFailure(command, args, options) - if (code !== 0) throw new Error(`${command} ${scrub(args.join(' '), options.scrub ?? [])} exited with ${code}`) -} - -async function runAllowFailure(command, args, options = {}) { - if (command.startsWith('/') && !existsSync(command)) throw new Error(`Missing command: ${command}`) - const child = Bun.spawn([command, ...args], { - cwd: options.cwd || root, - env: options.env || process.env, - stdin: 'ignore', - stdout: options.quiet || options.scrub ? 'pipe' : 'inherit', - stderr: options.quiet || options.scrub ? 'pipe' : 'inherit', - }) - const [, , code] = await Promise.all([ - options.quiet ? drain(child.stdout) : options.scrub ? collect(child.stdout, process.stdout, options.scrub) : Promise.resolve(), - options.quiet ? drain(child.stderr) : options.scrub ? collect(child.stderr, process.stderr, options.scrub) : Promise.resolve(), - child.exited, - ]) - return code -} - -async function drain(stream) { - for await (const _chunk of stream) { - } -} - -async function collect(stream, sink, scrubValues) { - const decoder = new TextDecoder() - for await (const chunk of stream) { - const text = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true }) - sink.write(scrub(text, scrubValues)) - } - const rest = decoder.decode() - if (rest) sink.write(scrub(rest, scrubValues)) -} - -function scrub(text, values) { - return values.filter(Boolean).reduce((next, value) => next.replaceAll(value, '[redacted]'), text) -} diff --git a/packages/cli/scripts/sync-bridge-manager-config.ts b/packages/cli/scripts/sync-bridge-manager-config.ts deleted file mode 100644 index ca5796a9..00000000 --- a/packages/cli/scripts/sync-bridge-manager-config.ts +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env bun -import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises' -import { homedir } from 'node:os' -import { basename, dirname, join } from 'node:path' - -type OfficialBridge = { typeName: string; names: string[] } - -const repo = process.argv[2] ?? process.env.BRIDGE_MANAGER_REPO ?? join(homedir(), 'projects', 'bridge-manager') -const root = join(import.meta.dir, '..') -const outputPath = join(root, 'src', 'lib', 'bridges', 'generated.ts') - -const bridgeutil = await readFile(join(repo, 'cmd', 'bbctl', 'bridgeutil.go'), 'utf8') -const config = await readFile(join(repo, 'cmd', 'bbctl', 'config.go'), 'utf8') -const templateDir = join(repo, 'bridgeconfig') -const templateFiles = (await readdir(templateDir)).filter(file => file.endsWith('.tpl.yaml')).sort() -const templates: Record = {} -for (const file of templateFiles) templates[file] = await readFile(join(templateDir, file), 'utf8') - -await mkdir(dirname(outputPath), { recursive: true }) -await writeFile(outputPath, `${renderGenerated({ - bridgeIPSuffix: parseStringMap(config, 'bridgeIPSuffix'), - officialBridges: parseOfficialBridges(bridgeutil), - templates, - websocketBridges: parseBoolMap(bridgeutil, 'websocketBridges'), -})}\n`) - -process.stderr.write(`Synced ${templateFiles.length} bridge templates from ${repo} to ${outputPath}\n`) - -function renderGenerated(data: { - bridgeIPSuffix: Record - officialBridges: OfficialBridge[] - templates: Record - websocketBridges: Record -}): string { - const supportedBridges = Object.keys(data.templates).map(file => basename(file, '.tpl.yaml')).sort() - return `// Generated by scripts/sync-bridge-manager-config.ts from bridge-manager. Do not edit by hand. - -export type GeneratedOfficialBridge = { typeName: string; names: string[] } - -export const generatedTemplates: Record = ${JSON.stringify(data.templates, null, 2)} as const - -export const generatedSupportedBridges = ${JSON.stringify(supportedBridges, null, 2)} as const - -export const generatedOfficialBridges: GeneratedOfficialBridge[] = ${JSON.stringify(data.officialBridges, null, 2)} - -export const generatedWebsocketBridges: Record = ${JSON.stringify(data.websocketBridges, null, 2)} - -export const generatedBridgeIPSuffix: Record = ${JSON.stringify(data.bridgeIPSuffix, null, 2)} -` -} - -function parseOfficialBridges(source: string): OfficialBridge[] { - const body = mustMatch(source, /var officialBridges = \[\]bridgeTypeToNames\{([\s\S]*?)\n\}/, 'officialBridges') - return [...body.matchAll(/\{"([^"]+)",\s*\[\]string\{([^}]*)\}\}/g)].map(match => ({ - typeName: match[1]!, - names: [...match[2]!.matchAll(/"([^"]+)"/g)].map(name => name[1]!), - })) -} - -function parseBoolMap(source: string, name: string): Record { - const body = mustMatch(source, new RegExp(`var ${name} = map\\[string\\]bool\\{([\\s\\S]*?)\\n\\}`), name) - const out: Record = {} - for (const match of body.matchAll(/"([^"]+)"\s*:\s*(true|false)/g)) out[match[1]!] = match[2] === 'true' - return out -} - -function parseStringMap(source: string, name: string): Record { - const body = mustMatch(source, new RegExp(`var ${name} = map\\[string\\]string\\{([\\s\\S]*?)\\n\\}`), name) - const out: Record = {} - for (const match of body.matchAll(/"([^"]+)"\s*:\s*"([^"]*)"/g)) out[match[1]!] = match[2]! - return out -} - -function mustMatch(source: string, pattern: RegExp, name: string): string { - const match = source.match(pattern) - if (!match) throw new Error(`Failed to parse ${name} from bridge-manager Go source`) - return match[1]! -} diff --git a/packages/cli/scripts/test b/packages/cli/scripts/test deleted file mode 100755 index cd0871ac..00000000 --- a/packages/cli/scripts/test +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -cd "$(dirname "$0")/.." - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' # No Color - -function steady_is_running() { - curl --silent "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1 -} - -kill_server_on_port() { - pids=$(lsof -t -i tcp:"$1" || echo "") - if [ "$pids" != "" ]; then - kill "$pids" - echo "Stopped $pids." - fi -} - -function is_overriding_api_base_url() { - [ -n "${TEST_API_BASE_URL:-}" ] -} - -if ! is_overriding_api_base_url && ! steady_is_running ; then - # When we exit this script, make sure to kill the background mock server process - trap 'kill_server_on_port 4010' EXIT - - # Start the dev server - ./scripts/mock --daemon -fi - -if is_overriding_api_base_url ; then - echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" - echo -elif ! steady_is_running ; then - echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Steady server" - echo -e "running against your OpenAPI spec." - echo - echo -e "To run the server, pass in the path or url of your OpenAPI" - echo -e "spec to the steady command:" - echo - echo -e " \$ ${YELLOW}bunx -p @stdy/cli@0.19.6 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" - echo - - exit 1 -else - echo -e "${GREEN}✔ Mock steady server is running with your OpenAPI spec${NC}" - echo -fi - -echo "==> Running tests" -bun run test -- "$@" diff --git a/packages/cli/scripts/unlink b/packages/cli/scripts/unlink deleted file mode 100755 index f53e006a..00000000 --- a/packages/cli/scripts/unlink +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -echo "==> Unlinking Beeper CLI" -bun unlink -g beeper-cli || true diff --git a/packages/cli/src/cli/commands.ts b/packages/cli/src/cli/commands.ts new file mode 100644 index 00000000..d498ea21 --- /dev/null +++ b/packages/cli/src/cli/commands.ts @@ -0,0 +1,1993 @@ +import { createHmac } from 'node:crypto' +import { createReadStream } from 'node:fs' +import { stdout as output } from 'node:process' +import { setTimeout as sleep } from 'node:timers/promises' +import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises' +import { basename, dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { BeeperDesktop } from '@beeper/desktop-api' +import { apiItems, apiRecord } from '../lib/api-values.js' +import { appRequest } from '../lib/app-api.js' +import { evaluateReadiness } from '../lib/app-state.js' +import { printAccountLoginStep, runGuidedAccountLogin } from '../lib/account-login.js' +import { startCloudflareTunnel, type StartedTunnel } from '../lib/cloudflare-tunnel.js' +import { authFromToken, authorizeTarget } from '../lib/desktop-auth.js' +import { AbortError, ExitCodes } from '../lib/errors.js' +import { exportBeeperData } from '../lib/export.js' +import { installDesktop, installServer } from '../lib/installations.js' +import { collectPage } from '../lib/paging.js' +import { listAccountIDs, normalizeSelector, resolveAccountID, resolveAccountIDs, resolveChatID, userQueryFromInput } from '../lib/resolve.js' +import { normalizeServerEnv } from '../lib/server-env.js' +import { finishEmailSetup, startEmailSetup } from '../lib/setup-login.js' +import { targetLiveStatus } from '../lib/target-status.js' +import { promptChoice } from '../lib/prompts.js' +import { + builtInDesktopTargetID, + listTargets, + publicTarget, + readConfig, + readTarget, + removeTarget, + resolveTarget, + updateConfig, + writeTarget, + type Target, +} from '../lib/targets.js' +import WebSocket from 'ws' +import { + desktopLogDir, + launchDesktopApp, + profileErrorLogPath, + profileLogPath, + startProfile, + stopProfile, +} from '../lib/profiles.js' +import type { CommandContext, CommandSpec, FlagSpec } from './types.js' +import { globalFlagSpecs, numberFlag, requiredStringFlag, stringFlag, stringListFlag } from './parse.js' +import { buildSchema } from './schema.js' +import { serveMcp } from './mcp.js' +import { usage, writeEvent, writeResult } from './output.js' +import { runSetup } from './setup.js' + +type WebhookConfig = { inflight: number; max: number; queue: Array<{ body: string; signature?: string }>; secret?: string; url: string } +type EventFilter = { include?: Set; exclude?: Set } +type AttachmentType = 'sticker' | 'voice-note' +type SendKind = 'file' | 'sticker' | 'text' | 'voice' +type SendPayload = { + attachmentType?: AttachmentType + duration?: number + file?: string + fileName?: string + mentions?: string[] + mimeType?: string + noPreview?: boolean + replyTo?: string + text: string + wait?: boolean + waitTimeoutMs?: number +} + +const accountFilterFlag: FlagSpec = { name: 'account', type: 'string', multiple: true, description: 'Limit to account selector' } +const candidateLimitFlag: FlagSpec = { name: 'limit', type: 'integer', default: 10, description: 'Maximum candidates' } +const chatFlag: FlagSpec = { name: 'chat', type: 'string', required: true, description: 'Chat selector' } +const pickCandidateFlag: FlagSpec = { name: 'pick', type: 'integer', description: 'Select the Nth candidate' } +const pickChatFlag: FlagSpec = { name: 'pick', type: 'integer', description: 'Pick the Nth result when selector is ambiguous' } + +const chatFlags: FlagSpec[] = [ + chatFlag, + pickChatFlag, +] + +const installFlags: FlagSpec[] = [ + { name: 'channel', type: 'string', enum: ['stable', 'nightly'], default: 'stable', description: 'Install release channel' }, + { name: 'server-env', type: 'string', enum: ['local', 'dev', 'staging', 'prod'], default: 'prod', description: 'Server environment' }, +] + +const sendChatFlags: FlagSpec[] = [ + { name: 'to', type: 'string', required: true, description: 'Chat selector' }, + pickChatFlag, +] + +const sendDeliveryFlags: FlagSpec[] = [ + ...sendChatFlags, + { name: 'reply-to', type: 'string', description: 'Send as a reply to this message ID' }, + { name: 'wait', type: 'boolean', default: false, description: 'Wait until the message leaves pending state' }, + { name: 'wait-timeout', type: 'integer', default: 30_000, description: 'Maximum wait time in ms when --wait is set' }, +] + +export const commands: CommandSpec[] = [ + { + description: 'Print CLI version', + mcp: true, + path: ['version'], + risk: 'read', + run: version, + }, + { + args: [{ name: 'target', description: 'Target name. Defaults to the selected target.' }], + description: 'Show selected target and setup readiness', + mcp: true, + path: ['status'], + risk: 'read', + run: status, + }, + { + args: [{ name: 'command', variadic: true }], + description: 'Print machine-readable command and flag schema', + mcp: true, + path: ['schema'], + risk: 'read', + run: schema, + }, + { + description: 'Run a typed MCP stdio server', + flags: [{ name: 'allow-write', type: 'boolean', default: false, description: 'Allow write-risk MCP tools' }], + path: ['mcp'], + risk: 'read', + run: mcp, + }, + { + description: 'Make the selected target ready for messaging', + examples: ['beeper setup', 'beeper setup --local', 'beeper setup --remote https://desktop.example.com', 'beeper setup --desktop --install'], + flags: [ + { name: 'local', type: 'boolean', default: false, description: 'Use the local Beeper Desktop session on this device' }, + { name: 'oauth', type: 'boolean', default: false, description: 'Authorize the target with browser OAuth/PKCE' }, + { name: 'remote', type: 'string', description: 'Connect to a remote Beeper Desktop or Server URL' }, + { name: 'server', type: 'boolean', default: false, description: 'Set up a local Beeper Server target' }, + { name: 'desktop', type: 'boolean', default: false, description: 'Set up a local Beeper Desktop target' }, + { name: 'install', type: 'boolean', default: false, description: 'Allow installing a missing local runtime' }, + ...installFlags, + { name: 'email', type: 'string', description: 'Sign in with an email address' }, + { name: 'username', type: 'string', description: 'Username to use if setup creates a new account' }, + ], + path: ['setup'], + risk: 'write', + run: runSetup, + }, + { + description: 'List configured Beeper targets', + mcp: true, + path: ['targets', 'list'], + risk: 'read', + run: targetsList, + }, + { + args: [{ name: 'name', required: true }, { name: 'url', required: true }], + description: 'Add a remote Beeper Desktop or Server target', + flags: [{ name: 'default', type: 'boolean', default: false, description: 'Set this target as the default after creation' }], + path: ['targets', 'add'], + risk: 'write', + run: targetsAdd, + }, + { + args: [{ name: 'name', description: 'Target name. Defaults to the selected target.' }], + description: 'Expose a target through Cloudflare Tunnel', + flags: [ + { name: 'cloudflared-path', type: 'string', description: 'Path to cloudflared. Also configurable with BEEPER_CLOUDFLARED_PATH.' }, + { name: 'install', type: 'boolean', default: false, description: 'Download the pinned cloudflared binary if missing or outdated' }, + { name: 'retries', type: 'integer', default: 5, description: 'Startup retries before giving up' }, + { name: 'timeout', type: 'string', description: 'Startup timeout, for example 40s or 60000ms' }, + { name: 'url-only', type: 'boolean', default: false, description: 'Print only the public tunnel URL' }, + ], + path: ['targets', 'tunnel'], + risk: 'write', + run: targetsTunnel, + }, + { + description: 'Install Beeper Desktop locally', + flags: installFlags, + path: ['install', 'desktop'], + risk: 'write', + run: installCommand, + }, + { + description: 'Install Beeper Server locally', + flags: installFlags, + path: ['install', 'server'], + risk: 'write', + run: installCommand, + }, + { + description: 'Clear stored authentication', + path: ['auth', 'logout'], + risk: 'write', + run: authLogout, + }, + { + description: 'Start email sign-in for a target', + flags: [{ name: 'email', type: 'string', required: true, description: 'Email address' }], + path: ['auth', 'email', 'start'], + risk: 'write', + run: authEmailStart, + }, + { + description: 'Finish email sign-in for a target', + flags: [ + { name: 'code', type: 'string', required: true, description: 'Email verification code' }, + { name: 'setup-request-id', type: 'string', required: true, description: 'Setup request ID from auth email start' }, + { name: 'username', type: 'string', description: 'Username to use if setup creates a new account' }, + ], + path: ['auth', 'email', 'response'], + risk: 'write', + run: authEmailResponse, + }, + { + description: 'List connected accounts', + flags: [ + accountFilterFlag, + { name: 'ids', type: 'boolean', default: false, description: 'Print only account IDs' }, + ], + mcp: true, + path: ['accounts', 'list'], + risk: 'read', + run: accountsList, + }, + { + args: [{ name: 'selector', required: true, description: 'Target name' }], + description: 'Select the default target', + path: ['use', 'target'], + risk: 'write', + run: useTarget, + }, + { + args: [{ name: 'selector', required: true, description: 'Account selector' }], + description: 'Select the default account', + path: ['use', 'account'], + risk: 'write', + run: useAccount, + }, + { + args: [{ name: 'bridge' }], + description: 'Connect a chat account by bridge', + flags: [ + { name: 'cookie', type: 'string', multiple: true, description: 'Cookie value in name=value form' }, + { name: 'field', type: 'string', multiple: true, description: 'Field value in id=value form' }, + { name: 'flow', type: 'string', description: 'Login flow ID' }, + { name: 'guided', type: 'boolean', default: true, description: 'Prompt through login steps' }, + { name: 'login-id', type: 'string', description: 'Existing login ID to re-login as' }, + { name: 'webview', type: 'boolean', default: false, description: 'Use Bun.WebView for cookie login steps' }, + { name: 'webview-backend', type: 'string', enum: ['auto', 'chrome', 'webkit'], default: 'chrome', description: 'Bun.WebView backend' }, + { name: 'webview-timeout', type: 'integer', default: 120, description: 'Seconds to wait for WebView cookie collection' }, + ], + path: ['accounts', 'add'], + risk: 'write', + run: accountsAdd, + }, + { + args: [{ name: 'selector', required: true, description: 'Target name' }], + description: 'Remove a target', + path: ['remove', 'target'], + risk: 'destructive', + run: removeTargetCommand, + }, + { + args: [{ name: 'selector', required: true, description: 'Account selector' }], + description: 'Remove an account', + path: ['remove', 'account'], + risk: 'destructive', + run: removeAccount, + }, + { + description: 'List contacts', + flags: [ + accountFilterFlag, + { name: 'ids', type: 'boolean', default: false, description: 'Print only contact user IDs' }, + { name: 'limit', type: 'integer', default: 50, description: 'Maximum contacts to print' }, + { name: 'query', type: 'string', description: 'Optional contact lookup query' }, + ], + mcp: true, + path: ['contacts', 'list'], + risk: 'read', + run: contactsList, + }, + { + description: 'List chats', + flags: [ + accountFilterFlag, + { name: 'archived', type: 'boolean', description: 'Only archived chats; use --no-archived to exclude' }, + { name: 'ids', type: 'boolean', default: false, description: 'Print preferred chat selectors' }, + { name: 'limit', type: 'integer', default: 20, description: 'Maximum chats to print' }, + { name: 'low-priority', type: 'boolean', description: 'Only low-priority chats; use --no-low-priority to exclude' }, + { name: 'muted', type: 'boolean', description: 'Only muted chats; use --no-muted to exclude' }, + { name: 'pinned', type: 'boolean', description: 'Only pinned chats; use --no-pinned to exclude' }, + { name: 'query', type: 'string', description: 'Optional chat lookup query' }, + { name: 'unread', type: 'boolean', description: 'Only unread chats; use --no-unread to exclude' }, + ], + mcp: true, + path: ['chats', 'list'], + risk: 'read', + run: chatsList, + }, + { + description: 'Show chat details', + flags: [ + chatFlag, + { name: 'max-participants', type: 'integer', description: 'Limit participants returned in chat details' }, + pickChatFlag, + ], + mcp: true, + path: ['chats', 'show'], + risk: 'read', + run: chatsShow, + }, + { + args: [{ name: 'user', required: true }], + description: 'Start a chat', + flags: [ + { name: 'account', type: 'string', description: 'Account selector' }, + { name: 'title', type: 'string', description: 'Optional initial title for a new group chat' }, + ], + path: ['chats', 'start'], + risk: 'write', + run: chatsStart, + }, + { + description: 'Archive or unarchive a chat', + flags: [...chatFlags, { name: 'clear', type: 'boolean', default: false, description: 'Unarchive the chat' }], + path: ['chats', 'archive'], + risk: 'write', + run: chatsSetFlag, + }, + { + description: 'Pin or unpin a chat', + flags: [...chatFlags, { name: 'clear', type: 'boolean', default: false, description: 'Unpin the chat' }], + path: ['chats', 'pin'], + risk: 'write', + run: chatsSetFlag, + }, + { + description: 'Mute or unmute a chat', + flags: [...chatFlags, { name: 'clear', type: 'boolean', default: false, description: 'Unmute the chat' }], + path: ['chats', 'mute'], + risk: 'write', + run: chatsSetFlag, + }, + { + description: 'Rename a chat', + flags: [...chatFlags, { name: 'title', type: 'string', required: true, description: 'Chat title' }], + path: ['chats', 'rename'], + risk: 'write', + run: chatsRename, + }, + { + description: 'Set or clear a chat description', + flags: [ + ...chatFlags, + { name: 'clear', type: 'boolean', default: false, description: 'Clear or unset the chosen state' }, + { name: 'description', type: 'string', description: 'Chat description' }, + ], + path: ['chats', 'description'], + risk: 'write', + run: chatsDescription, + }, + { + description: 'Set or clear a chat avatar', + flags: [ + ...chatFlags, + { name: 'clear', type: 'boolean', default: false, description: 'Clear the avatar' }, + { name: 'file', type: 'string', description: 'Avatar image file path' }, + ], + path: ['chats', 'avatar'], + risk: 'write', + run: chatsAvatar, + }, + { + description: 'Set chat priority', + flags: [...chatFlags, { name: 'level', type: 'string', required: true, enum: ['inbox', 'low'], description: 'Chat priority level' }], + path: ['chats', 'priority'], + risk: 'write', + run: chatsPriority, + }, + { + description: 'Mark a chat read or unread', + flags: [ + ...chatFlags, + { name: 'message', type: 'string', description: 'Read marker message ID' }, + { name: 'unread', type: 'boolean', default: false, description: 'Mark the chat unread' }, + ], + path: ['chats', 'read'], + risk: 'write', + run: chatsRead, + }, + { + description: 'Set or clear a chat draft', + flags: [ + ...chatFlags, + { name: 'clear', type: 'boolean', default: false, description: 'Clear the draft' }, + { name: 'file', type: 'string', description: 'Draft attachment file path' }, + { name: 'filename', type: 'string', description: 'Draft attachment filename' }, + { name: 'mime', type: 'string', description: 'Draft attachment MIME type' }, + { name: 'text', type: 'string', description: 'Draft text' }, + ], + path: ['chats', 'draft'], + risk: 'write', + run: chatsDraft, + }, + { + description: 'Set or clear a chat reminder', + flags: [ + ...chatFlags, + { name: 'clear', type: 'boolean', default: false, description: 'Clear the reminder' }, + { name: 'dismiss-on-message', type: 'boolean', default: false, description: 'Dismiss reminder when a new message arrives' }, + { name: 'when', type: 'string', description: 'ISO reminder timestamp' }, + ], + path: ['chats', 'remind'], + risk: 'write', + run: chatsRemind, + }, + { + description: 'Set a disappearing-message timer', + flags: [ + ...chatFlags, + { name: 'seconds', type: 'string', description: 'Disappearing-message timer in seconds, or off' }, + ], + path: ['chats', 'disappear'], + risk: 'write', + run: chatsDisappear, + }, + { + description: 'Focus a chat in Beeper', + flags: [ + ...chatFlags, + { name: 'file', type: 'string', description: 'Draft attachment file path' }, + { name: 'message', type: 'string', description: 'Message ID to focus' }, + { name: 'text', type: 'string', description: 'Draft text' }, + ], + path: ['chats', 'focus'], + risk: 'write', + run: chatsFocus, + }, + { + description: 'Notify a chat anyway', + flags: chatFlags, + path: ['chats', 'notify-anyway'], + risk: 'write', + run: chatsNotifyAnyway, + }, + { + args: [{ name: 'selector', required: true }], + description: 'Resolve an account selector', + flags: [pickCandidateFlag], + mcp: true, + path: ['resolve', 'account'], + risk: 'read', + run: resolveAccount, + }, + { + args: [{ name: 'selector', required: true }], + description: 'Resolve a bridge selector', + flags: [pickCandidateFlag], + mcp: true, + path: ['resolve', 'bridge'], + risk: 'read', + run: resolveBridge, + }, + { + args: [{ name: 'selector', required: true }], + description: 'Resolve a chat selector', + flags: [ + accountFilterFlag, + candidateLimitFlag, + pickCandidateFlag, + ], + mcp: true, + path: ['resolve', 'chat'], + risk: 'read', + run: resolveChat, + }, + { + args: [{ name: 'selector', required: true }], + description: 'Resolve a contact selector', + flags: [ + accountFilterFlag, + candidateLimitFlag, + pickCandidateFlag, + ], + mcp: true, + path: ['resolve', 'contact'], + risk: 'read', + run: resolveContact, + }, + { + args: [{ name: 'selector', required: true }], + description: 'Resolve a target selector', + flags: [pickCandidateFlag], + mcp: true, + path: ['resolve', 'target'], + risk: 'read', + run: resolveTargetCommand, + }, + { + description: 'List chat messages', + flags: [ + { name: 'after-cursor', type: 'string', description: 'Paginate messages newer than this message ID' }, + { name: 'asc', type: 'boolean', default: false, description: 'Order oldest first' }, + { name: 'before-cursor', type: 'string', description: 'Paginate messages older than this message ID' }, + chatFlag, + { name: 'ids', type: 'boolean', default: false, description: 'Print only message IDs' }, + { name: 'limit', type: 'integer', default: 50, description: 'Maximum messages to print' }, + pickChatFlag, + { name: 'sender', type: 'string', description: 'me, others, or a specific user ID' }, + ], + mcp: true, + path: ['messages', 'list'], + risk: 'read', + run: messagesList, + }, + { + description: 'Show a message with surrounding context', + flags: [ + chatFlag, + pickChatFlag, + { name: 'id', type: 'string', required: true, description: 'Message ID' }, + { name: 'after', type: 'integer', default: 10, description: 'Messages after target' }, + { name: 'before', type: 'integer', default: 10, description: 'Messages before target' }, + ], + mcp: true, + path: ['messages', 'context'], + risk: 'read', + run: messagesContext, + }, + { + description: 'Edit a message', + flags: [ + chatFlag, + pickChatFlag, + { name: 'id', type: 'string', required: true, description: 'Message ID' }, + { name: 'message', type: 'string', required: true, description: 'New message text' }, + ], + path: ['messages', 'edit'], + risk: 'write', + run: messagesEdit, + }, + { + description: 'Delete a message', + flags: [ + chatFlag, + pickChatFlag, + { name: 'id', type: 'string', required: true, description: 'Message ID' }, + { name: 'for-everyone', type: 'boolean', default: false, description: 'Delete for everyone when supported' }, + ], + path: ['messages', 'delete'], + risk: 'destructive', + run: messagesDelete, + }, + { + description: 'Stream Desktop API WebSocket events', + flags: [ + { name: 'chat', type: 'string', multiple: true, description: 'Chat ID to subscribe to; defaults to all chats' }, + { name: 'exclude-type', type: 'string', multiple: true, enum: ['chat.upserted', 'chat.deleted', 'message.upserted', 'message.deleted', 'message.stream'], description: 'Drop events of these types' }, + { name: 'include-type', type: 'string', multiple: true, enum: ['chat.upserted', 'chat.deleted', 'message.upserted', 'message.deleted', 'message.stream'], description: 'Only forward events of these types' }, + { name: 'webhook', type: 'string', description: 'Forward each event to this URL as POST' }, + { name: 'webhook-queue', type: 'integer', default: 64, description: 'Maximum pending webhook deliveries' }, + { name: 'webhook-secret', type: 'string', description: 'HMAC-SHA256 secret for X-Beeper-Signature' }, + ], + path: ['watch'], + risk: 'read', + run: watch, + }, + { + args: [{ name: 'url', required: true }], + description: 'Download message media', + flags: [{ name: 'out', type: 'string', default: '.', description: 'Output directory; - streams to stdout' }], + path: ['media', 'download'], + risk: 'write', + run: mediaDownload, + }, + { + description: 'Export accounts, chats, messages, transcripts, and attachments', + flags: [ + accountFilterFlag, + { name: 'chat', type: 'string', multiple: true, description: 'Limit to chat selector' }, + { name: 'force', type: 'boolean', default: false, description: 'Re-export completed chats' }, + { name: 'limit-chats', type: 'integer', description: 'Maximum chats to export' }, + { name: 'limit-messages', type: 'integer', description: 'Maximum messages per chat' }, + { name: 'max-participants', type: 'integer', default: 500, description: 'Maximum participants in chat.json' }, + { name: 'no-attachments', type: 'boolean', default: false, description: 'Skip downloading attachments' }, + { name: 'out', type: 'string', default: 'beeper-export', description: 'Export directory' }, + pickChatFlag, + ], + path: ['export'], + risk: 'write', + run: exportCommand, + }, + { + args: [{ name: 'query' }], + description: 'Search messages across chats', + examples: [ + 'beeper messages search "quarterly report"', + 'beeper messages search --chat "Work" --sender me --limit 20', + ], + flags: [ + accountFilterFlag, + { name: 'chat', type: 'string', multiple: true, description: 'Limit to a chat selector' }, + { name: 'chat-type', type: 'string', enum: ['group', 'single'], description: 'Only group chats or direct messages' }, + { name: 'after', type: 'string', description: 'Only messages at or after this ISO timestamp' }, + { name: 'before', type: 'string', description: 'Only messages at or before this ISO timestamp' }, + { name: 'exclude-low-priority', type: 'boolean', description: 'Exclude low-priority chats' }, + { name: 'ids', type: 'boolean', default: false, description: 'Print only message IDs' }, + { name: 'include-muted', type: 'boolean', default: true, description: 'Include muted chats' }, + { name: 'limit', type: 'integer', default: 50, description: 'Maximum results' }, + { name: 'media', type: 'string', multiple: true, enum: ['any', 'video', 'image', 'link', 'file'], description: 'Filter by media type' }, + { name: 'sender', type: 'string', description: 'me, others, or a user ID' }, + ], + mcp: true, + path: ['messages', 'search'], + risk: 'read', + run: messagesSearch, + }, + { + args: [{ name: 'method', required: true }, { name: 'path', required: true }], + description: 'Call a raw Desktop API path with any supported HTTP method', + flags: [ + { name: 'body', type: 'string', description: 'JSON request body' }, + { name: 'no-auth', type: 'boolean', default: false, description: 'Call a public API path without a bearer token' }, + ], + mcp: true, + path: ['api', 'request'], + risk: 'write', + run: apiCommand, + }, + { + description: 'Send a text message', + flags: [ + ...sendDeliveryFlags, + { name: 'message', type: 'string', required: true, description: 'Message text to send' }, + { name: 'mention', type: 'string', multiple: true, description: 'User ID to mention' }, + { name: 'no-preview', type: 'boolean', default: false, description: 'Disable automatic link preview' }, + ], + path: ['send', 'text'], + risk: 'write', + run: sendTextLike, + }, + { + description: 'Send a file message', + flags: [ + ...sendDeliveryFlags, + { name: 'file', type: 'string', required: true, description: 'Local file path to upload' }, + { name: 'caption', type: 'string', description: 'Optional caption for file messages' }, + { name: 'filename', type: 'string', description: 'Override displayed filename' }, + { name: 'mime', type: 'string', description: 'Override MIME type' }, + ], + path: ['send', 'file'], + risk: 'write', + run: sendTextLike, + }, + { + description: 'Send a sticker', + flags: [ + ...sendDeliveryFlags, + { name: 'file', type: 'string', required: true, description: 'Local sticker file path to upload' }, + { name: 'filename', type: 'string', description: 'Override displayed filename' }, + { name: 'mime', type: 'string', description: 'Override MIME type' }, + ], + path: ['send', 'sticker'], + risk: 'write', + run: sendTextLike, + }, + { + description: 'Send a voice note', + flags: [ + ...sendDeliveryFlags, + { name: 'file', type: 'string', required: true, description: 'Local voice note file path to upload' }, + { name: 'duration', type: 'integer', description: 'Duration in seconds' }, + { name: 'filename', type: 'string', description: 'Override displayed filename' }, + { name: 'mime', type: 'string', description: 'Override MIME type' }, + ], + path: ['send', 'voice'], + risk: 'write', + run: sendTextLike, + }, + { + description: 'Send or remove a reaction', + flags: [ + ...sendChatFlags, + { name: 'id', type: 'string', required: true, description: 'Message ID to react to' }, + { name: 'reaction', type: 'string', required: true, description: 'Reaction key' }, + { name: 'remove', type: 'boolean', default: false, description: 'Remove the reaction' }, + { name: 'transaction', type: 'string', description: 'Optional transaction ID for deduplication' }, + ], + path: ['send', 'react'], + risk: 'write', + run: sendReact, + }, + { + description: 'Send a typing indicator', + flags: [ + ...sendChatFlags, + { name: 'duration', type: 'integer', description: 'Seconds to keep typing before sending paused' }, + { name: 'state', type: 'string', enum: ['typing', 'paused'], default: 'typing', description: 'Presence indicator to send' }, + ], + path: ['send', 'presence'], + risk: 'write', + run: sendPresence, + }, + { + args: [{ name: 'name' }], + description: 'Start a local target runtime', + path: ['targets', 'runtime', 'start'], + risk: 'write', + run: targetsRuntime, + }, + { + args: [{ name: 'name' }], + description: 'Stop a local server runtime', + path: ['targets', 'runtime', 'stop'], + risk: 'write', + run: targetsRuntime, + }, + { + args: [{ name: 'name' }], + description: 'Restart a local server runtime', + path: ['targets', 'runtime', 'restart'], + risk: 'write', + run: targetsRuntime, + }, + { + args: [{ name: 'name' }], + description: 'Print logs for a local Beeper Desktop or Server install', + flags: [ + { name: 'lines', type: 'integer', default: 200, description: 'Lines to print from each log file' }, + { name: 'files', type: 'integer', default: 5, description: 'Desktop log files to print, newest first' }, + { name: 'all', type: 'boolean', default: false, description: 'Print all matching log files instead of only recent files' }, + ], + path: ['targets', 'logs'], + risk: 'read', + run: targetsLogs, + }, +] + +export function commandHelp(command: CommandSpec): string { + const lines = [`beeper ${command.path.join(' ')}`, '', command.description] + const args = command.args ?? [] + const flags = command.flags ?? [] + if (args.length) { + lines.push('', 'Arguments:') + for (const arg of args) lines.push(` ${arg.name}${arg.required ? '' : '?'}${arg.variadic ? '...' : ''}\t${arg.description ?? ''}`) + } + if (flags.length) { + lines.push('', 'Flags:') + for (const flag of flags) lines.push(` --${flag.name}${flag.type === 'boolean' ? '' : ' '}\t${flag.description ?? ''}`) + } + if (command.examples?.length) { + lines.push('', 'Examples:', ...command.examples.map(example => ` ${example}`)) + } + return `${lines.join('\n')}\n` +} + +export function help(): string { + const width = Math.max(...commands.map(command => command.path.join(' ').length)) + 2 + const lines = ['Usage: beeper [flags]', '', 'Commands:'] + for (const command of [...commands].sort((a, b) => a.path.join(' ').localeCompare(b.path.join(' ')))) { + lines.push(` ${command.path.join(' ').padEnd(width)}${command.description}`) + } + lines.push('', 'Global flags:') + for (const flag of globalFlagSpecs) lines.push(` --${flag.name}${flag.type === 'boolean' ? '' : ' '}\t${flag.description ?? ''}`) + return `${lines.join('\n')}\n` +} + +async function version(): Promise> { + const pkg = await packageInfo() + return { name: pkg.name, version: pkg.version } +} + +async function status(ctx: CommandContext): Promise> { + const target = await resolveTarget({ target: ctx.args[0] ?? ctx.globalFlags.target }) + return { + auth: { + authenticated: Boolean(process.env.BEEPER_ACCESS_TOKEN || target.auth?.accessToken), + clientID: target.auth?.clientID, + expiresAt: target.auth?.expiresAt, + scope: target.auth?.scope, + source: process.env.BEEPER_ACCESS_TOKEN ? 'env' : target.auth?.source ?? (target.auth?.accessToken ? 'target' : 'none'), + }, + live: await targetLiveStatus(target), + readiness: await evaluateReadiness({ baseURL: target.baseURL, target: target.id }), + target: publicTarget(target), + } +} + +async function schema(ctx: CommandContext): Promise> { + const pkg = await packageInfo() + return buildSchema(commands, String(pkg.version ?? '0'), ctx.args) +} + +async function mcp(ctx: CommandContext): Promise { + const pkg = await packageInfo() + await serveMcp(commands, ctx.globalFlags, Boolean(ctx.flags['allow-write']), String(pkg.version ?? '0')) +} + +async function targetsList(): Promise { + const config = await readConfig() + const targets = await listTargets() + const rows = targets.length ? targets : [await resolveTarget({ target: builtInDesktopTargetID })] + return Promise.all(rows.map(async target => ({ + baseURL: target.baseURL, + default: config.defaultTarget ? config.defaultTarget === target.id : target.id === builtInDesktopTargetID, + id: target.id, + localProfile: Boolean(target.dataDir), + name: target.name ?? target.id, + type: target.type, + ...await targetLiveStatus(target), + }))) +} + +async function targetsAdd(ctx: CommandContext): Promise> { + const [name, url] = ctx.args + if (!name || !url) throw usage('targets add requires name and url') + if (name === builtInDesktopTargetID) throw usage('Target name "desktop" is reserved for the built-in Beeper Desktop target') + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'targets.add', request: { default: ctx.flags.default, name, url } } + if (await readTarget(name)) throw usage(`Target "${name}" already exists`) + const target: Target = { baseURL: url, id: name, name, type: 'remote' } + await writeTarget(target) + if (ctx.flags.default) await updateConfig(config => ({ ...config, defaultTarget: target.id })) + return { target: publicTarget(target) } +} + +async function targetsTunnel(ctx: CommandContext): Promise> { + const target = await resolveTarget({ target: ctx.args[0] ?? ctx.globalFlags.target }) + const url = new URL(target.baseURL) + url.search = '' + url.hash = '' + const localURL = url.toString().replace(/\/$/, '') + const request = { + cloudflaredPath: stringFlag(ctx.flags, 'cloudflared-path') ?? process.env.BEEPER_CLOUDFLARED_PATH, + install: Boolean(ctx.flags.install), + localURL, + retries: numberFlag(ctx.flags, 'retries', 5), + target: target.id, + timeoutMs: parseDurationMs(stringFlag(ctx.flags, 'timeout')) ?? 40_000, + } + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'targets.tunnel', request } + + const started = await startCloudflareTunnel({ + cloudflaredPath: stringFlag(ctx.flags, 'cloudflared-path'), + debug: ctx.globalFlags.debug, + install: Boolean(ctx.flags.install), + retries: numberFlag(ctx.flags, 'retries', 5), + timeoutMs: parseDurationMs(stringFlag(ctx.flags, 'timeout')) ?? 40_000, + url: localURL, + }) + const result = { cloudflaredPath: started.cloudflaredPath, localURL, target: target.id, url: started.url } + if (ctx.globalFlags.events) writeEvent('tunnel.connected', result) + if (ctx.flags['url-only']) process.stdout.write(`${started.url}\n`) + else if (ctx.globalFlags.json || ctx.globalFlags.plain) writeResult(result, ctx.globalFlags) + else { + process.stdout.write(`Cloudflare Tunnel connected for ${target.id}\n${started.url} -> ${localURL}\n`) + process.stderr.write('Press Ctrl-C to stop the tunnel.\n') + } + + const exit = await waitForTunnelExit(started) + if (exit.reason === 'process' && exit.code !== 0) { + throw new Error(`cloudflared exited after the tunnel connected${exit.code === null ? '' : ` with code ${exit.code}`}.\n${started.tryMessage}`) + } + return undefined +} + +async function targetsRuntime(ctx: CommandContext): Promise> { + const action = ctx.commandPath[2] + if (action !== 'start' && action !== 'stop' && action !== 'restart') throw usage(`Unsupported runtime command: ${ctx.commandPath.join(' ')}`) + const target = await resolveTarget({ target: ctx.args[0] ?? ctx.globalFlags.target }) + if (ctx.globalFlags.dryRun) return { dry_run: true, op: `targets.runtime.${action}`, request: { target: publicTarget(target) } } + if (action === 'start' && target.type === 'desktop') return { result: await launchDesktopApp(target.dataDir ? target : undefined), target: publicTarget(target) } + if (!target.dataDir || target.type !== 'server') throw usage(`Target "${target.id}" is not a local Beeper Server install.`) + if (action === 'start') return { result: await startProfile(target), target: publicTarget(target) } + if (action === 'stop') { + await stopProfile(target) + return { stopped: true, target: publicTarget(target) } + } + await stopProfile(target).catch(() => undefined) + return { restarted: true, result: await startProfile(target), target: publicTarget(target) } +} + +async function targetsLogs(ctx: CommandContext): Promise { + const target = await resolveTarget({ target: ctx.args[0] ?? ctx.globalFlags.target }) + if (target.type === 'remote') throw usage(`Target "${target.id}" is remote and has no local logs.`) + const lines = numberFlag(ctx.flags, 'lines', 200) + if (target.type === 'server') { + if (!target.dataDir) throw usage(`Target "${target.id}" is not a local Beeper Server install.`) + await printLogFile(profileLogPath(target.id), lines) + await printLogFile(profileErrorLogPath(target.id), lines) + return + } + const files = await listLogFiles(desktopLogDir(target.dataDir ? target : undefined)) + const selected = ctx.flags.all ? files : files.slice(0, numberFlag(ctx.flags, 'files', 5)) + for (const file of selected) await printLogFile(file, lines) +} + +async function listLogFiles(dir: string): Promise { + const entries = await readdir(dir, { withFileTypes: true }).catch(() => []) + const files = await Promise.all(entries.map(async entry => { + const path = join(dir, entry.name) + if (entry.isDirectory()) return listLogFiles(path) + if (entry.isFile() && entry.name.endsWith('.log')) return [path] + return [] + })) + const paths = files.flat() + const stats = await Promise.all(paths.map(async path => ({ path, mtimeMs: (await stat(path)).mtimeMs }))) + return stats.sort((a, b) => b.mtimeMs - a.mtimeMs).map(item => item.path) +} + +async function printLogFile(path: string, lines: number): Promise { + const content = await readFile(path, 'utf8').catch(() => '') + if (!content) return + process.stdout.write(`\n==> ${path} <==\n`) + if (lines <= 0) process.stdout.write(content.endsWith('\n') ? content : `${content}\n`) + else { + const parts = content.split('\n') + const tail = parts.slice(Math.max(0, parts.length - lines - 1)).join('\n') + process.stdout.write(tail.endsWith('\n') ? tail : `${tail}\n`) + } +} + +async function installCommand(ctx: CommandContext): Promise> { + const type = ctx.commandPath[1] + if (type !== 'desktop' && type !== 'server') throw usage(`Unsupported install command: ${ctx.commandPath.join(' ')}`) + const channel = stringFlag(ctx.flags, 'channel') === 'nightly' ? 'nightly' : 'stable' + const serverEnv = normalizeServerEnv(stringFlag(ctx.flags, 'server-env') ?? 'prod') + if (ctx.globalFlags.dryRun) return { dry_run: true, op: `install.${type}`, request: { channel, serverEnv } } + if (type === 'desktop') await installDesktop({ channel, serverEnv }) + else await installServer({ channel, serverEnv }) + return { installed: type, channel, serverEnv } +} + +async function accountsList(ctx: CommandContext): Promise { + const client = await apiClient(ctx) + const selected = await resolveAccountIDs(client, stringListFlag(ctx.flags, 'account'), { allowMultiplePerInput: true, applyDefault: false }) + const config = await readConfig() + const rows = apiItems(await client.accounts.list()) + const items = rows + .filter(row => !selected?.length || selected.includes(String(row.accountID ?? row.id))) + .map(row => ({ ...row, default: (row.accountID ?? row.id) === config.defaultAccount || undefined })) + return ctx.flags.ids ? ids(items, 'accountID') : items +} + +async function useTarget(ctx: CommandContext): Promise> { + const target = await resolveTarget({ target: ctx.args[0]! }) + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'use.target', request: { defaultTarget: target.id } } + await updateConfig(config => ({ ...config, defaultTarget: target.id })) + return { defaultTarget: target.id } +} + +async function useAccount(ctx: CommandContext): Promise> { + const input = ctx.args[0]! + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'use.account', request: { defaultAccount: input } } + const client = await apiClient(ctx) + const accountID = await resolveAccountID(client, input) + await updateConfig(config => ({ ...config, defaultAccount: accountID })) + return { defaultAccount: accountID } +} + +async function accountsAdd(ctx: CommandContext): Promise { + const client = await apiClient(ctx) + let bridge = ctx.args[0] + const guided = ctx.flags.guided !== false + const nonInteractive = ctx.globalFlags.noInput + const bridges = apiItems(await client.bridges.list()) + + if (!bridge) { + if (ctx.globalFlags.json || ctx.globalFlags.plain) return bridges + if (guided && !nonInteractive && process.stdin.isTTY) { + bridge = await chooseBridge(bridges) + } else { + printAvailableBridges(bridges) + return undefined + } + } + + const accountType = resolveBridgeChoice(bridges, bridge) + if (String(accountType.status ?? 'available') !== 'available') { + const name = String(accountType.displayName ?? accountType.name ?? accountType.id) + const detail = accountType.statusText ? `: ${String(accountType.statusText)}` : '' + throw usage(`${name} is not available${detail}`) + } + + let flowID = stringFlag(ctx.flags, 'flow') + if (!flowID) { + const flows = apiItems(await client.bridges.loginFlows.list(String(accountType.id))) + if (flows.length > 1) { + if (guided && !nonInteractive && !ctx.globalFlags.json) flowID = await chooseLoginFlow(flows) + else throw usage(`Multiple sign-in methods are available for ${String(accountType.displayName ?? accountType.id)}. Pass --flow.`) + } else { + flowID = flows[0]?.id ? String(flows[0].id) : undefined + } + if (!flowID) throw usage(`No login flows returned for ${String(accountType.displayName ?? accountType.id)}.`) + } + + const cookies = parseKeyValueFlags(stringListFlag(ctx.flags, 'cookie'), '--cookie') + const fields = parseKeyValueFlags(stringListFlag(ctx.flags, 'field'), '--field') + const request = { + bridgeID: String(accountType.id), + bridgeName: accountType.displayName ?? accountType.name, + cookieKeys: Object.keys(cookies), + fieldKeys: Object.keys(fields), + flowID, + guided, + loginID: stringFlag(ctx.flags, 'login-id'), + nonInteractive, + webview: Boolean(ctx.flags.webview), + webviewBackend: stringFlag(ctx.flags, 'webview-backend') ?? 'chrome', + } + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'accounts.add', request } + + const step = await client.bridges.loginSessions.create(String(accountType.id), { + flowID, + loginID: stringFlag(ctx.flags, 'login-id'), + }) + const result = guided + ? await runGuidedAccountLogin(client, String(accountType.id), step, { + cookies, + fields, + nonInteractive, + webview: Boolean(ctx.flags.webview), + webviewBackend: request.webviewBackend as 'auto' | 'chrome' | 'webkit', + webviewTimeoutMs: numberFlag(ctx.flags, 'webview-timeout', 120) * 1000, + }) + : step + if (ctx.globalFlags.json || ctx.globalFlags.plain) return result + await printAccountLoginStep(result) + return undefined +} + +async function removeTargetCommand(ctx: CommandContext): Promise> { + const input = ctx.args[0]! + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'remove.target', request: { id: input } } + await removeTarget(input) + return { id: input, removed: true } +} + +async function removeAccount(ctx: CommandContext): Promise> { + const input = ctx.args[0]! + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'remove.account', request: { account: input } } + const client = await apiClient(ctx) + const accountID = await resolveAccountID(client, input) + if (client.accounts.delete) await client.accounts.delete(accountID) + else if (client.accounts.remove) await client.accounts.remove(accountID) + else throw usage('This Desktop API does not expose account removal.') + return { accountID, removed: true } +} + +async function contactsList(ctx: CommandContext): Promise { + const client = await apiClient(ctx) + const accountIDs = await resolveAccountIDs(client, stringListFlag(ctx.flags, 'account'), { allowMultiplePerInput: true }) ?? await listAccountIDs(client) + const limit = numberFlag(ctx.flags, 'limit', 50) + const query = stringFlag(ctx.flags, 'query') + const items: Array> = [] + for (const accountID of accountIDs) { + const remaining = limit - items.length + if (remaining <= 0) break + const contacts = await collectPage(client.accounts.contacts.list(accountID, { query }), remaining) + items.push(...contacts.map(item => ({ ...apiRecord(item), accountID }))) + } + return ctx.flags.ids ? ids(items, 'userID') : items +} + +async function chatsList(ctx: CommandContext): Promise { + const client = await apiClient(ctx) + const accountIDs = await resolveAccountIDs(client, stringListFlag(ctx.flags, 'account'), { allowMultiplePerInput: true }) + const query = stringFlag(ctx.flags, 'query') + if (query) { + const items = (await collectPage(client.chats.search({ accountIDs, query }), numberFlag(ctx.flags, 'limit', 20))) + .map(apiRecord) + .filter(row => matchesChatFilters(row, ctx)) + return ctx.flags.ids ? ids(items, 'localChatID') : items + } + const items: Record[] = [] + for await (const item of client.chats.list({ accountIDs })) { + const row = apiRecord(item) + if (matchesChatFilters(row, ctx)) items.push(row) + if (items.length >= numberFlag(ctx.flags, 'limit', 20)) break + } + return ctx.flags.ids ? ids(items, 'localChatID') : items +} + +async function chatsShow(ctx: CommandContext): Promise { + const client = await apiClient(ctx) + const chatID = await resolveChatID(client, stringFlag(ctx.flags, 'chat')!, chatResolutionOptions(ctx)) + return client.chats.retrieve(chatID, { maxParticipantCount: numberFlag(ctx.flags, 'max-participants', 0) || undefined }) +} + +async function chatsStart(ctx: CommandContext): Promise { + const user = ctx.args[0] + if (!user) throw usage('chats start requires user') + const account = stringFlag(ctx.flags, 'account') + const title = stringFlag(ctx.flags, 'title') + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'chats.start', request: { account, title, user: userQueryFromInput(user) } } + const client = await apiClient(ctx) + const accountID = account ? await resolveAccountID(client, account) : await defaultAccountID(client) + const payload = { accountID, title, user: userQueryFromInput(user) } + return client.chats.start(payload) +} + +async function chatsUpdate(ctx: CommandContext, op: string, update: Record): Promise { + if (ctx.globalFlags.dryRun) return { dry_run: true, op: `chats.${op}`, request: { chat: stringFlag(ctx.flags, 'chat'), pick: ctx.flags.pick, ...update } } + const client = await apiClient(ctx) + const chatID = await chatIDFromFlag(client, ctx, 'chat') + return client.chats.update(chatID, update) +} + +async function chatsSetFlag(ctx: CommandContext): Promise { + const action = ctx.commandPath[1] ?? '' + const field = ({ archive: 'isArchived', mute: 'isMuted', pin: 'isPinned' } as const)[action] + if (!field) throw usage(`Unsupported chat command: ${ctx.commandPath.join(' ')}`) + return chatsUpdate(ctx, action, { [field]: !ctx.flags.clear }) +} + +async function chatsRename(ctx: CommandContext): Promise { + return chatsUpdate(ctx, 'rename', { title: stringFlag(ctx.flags, 'title') }) +} + +async function chatsDescription(ctx: CommandContext): Promise { + const clear = Boolean(ctx.flags.clear) + const description = stringFlag(ctx.flags, 'description') + if (!clear && !description) throw usage('Provide --description or --clear') + return chatsUpdate(ctx, 'description', { description: clear ? null : description }) +} + +async function chatsAvatar(ctx: CommandContext): Promise { + const clear = Boolean(ctx.flags.clear) + const file = stringFlag(ctx.flags, 'file') + if (!clear && !file) throw usage('Provide --file or --clear') + return chatsUpdate(ctx, 'avatar', { imgURL: clear ? null : file }) +} + +async function chatsPriority(ctx: CommandContext): Promise { + const level = stringFlag(ctx.flags, 'level')! + const update = level === 'inbox' ? { isArchived: false, isLowPriority: false } : { isLowPriority: true } + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'chats.priority', request: { chat: stringFlag(ctx.flags, 'chat'), level, pick: ctx.flags.pick, update } } + const client = await apiClient(ctx) + const chatID = await chatIDFromFlag(client, ctx, 'chat') + return client.chats.update(chatID, update) +} + +async function chatsRead(ctx: CommandContext): Promise { + const messageID = stringFlag(ctx.flags, 'message') + const read = !ctx.flags.unread + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'chats.read', request: { chat: stringFlag(ctx.flags, 'chat'), messageID, pick: ctx.flags.pick, read } } + const client = await apiClient(ctx) + const chatID = await chatIDFromFlag(client, ctx, 'chat') + return read ? client.chats.markRead(chatID, { messageID }) : client.chats.markUnread(chatID, { messageID }) +} + +async function chatsDraft(ctx: CommandContext): Promise { + const clear = Boolean(ctx.flags.clear) + if (!clear && ctx.flags.text === undefined) throw usage('Provide --text TEXT, optionally with --file PATH, or --clear.') + if (clear && (ctx.flags.text !== undefined || ctx.flags.file)) throw usage('--clear cannot be combined with --text or --file.') + if (clear) { + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'chats.draft', request: { chat: stringFlag(ctx.flags, 'chat'), draft: null, pick: ctx.flags.pick } } + const client = await apiClient(ctx) + const chatID = await chatIDFromFlag(client, ctx, 'chat') + return client.chats.update(chatID, { draft: null }) + } + const draft = { file: stringFlag(ctx.flags, 'file'), fileName: stringFlag(ctx.flags, 'filename'), mimeType: stringFlag(ctx.flags, 'mime'), text: stringFlag(ctx.flags, 'text') } + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'chats.draft', request: { chat: stringFlag(ctx.flags, 'chat'), draft, pick: ctx.flags.pick } } + const client = await apiClient(ctx) + const chatID = await chatIDFromFlag(client, ctx, 'chat') + const upload = draft.file ? await client.assets.upload({ file: createReadStream(draft.file), fileName: draft.fileName, mimeType: draft.mimeType }) : undefined + return client.chats.update(chatID, { draft: { text: draft.text, attachments: upload?.uploadID ? { [upload.uploadID]: upload } : undefined } }) +} + +async function chatsRemind(ctx: CommandContext): Promise { + if (ctx.flags.clear) { + if (ctx.flags.when || ctx.flags['dismiss-on-message']) throw usage('--clear cannot be combined with --when or --dismiss-on-message') + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'chats.remind', request: { chat: stringFlag(ctx.flags, 'chat'), pick: ctx.flags.pick, reminder: null } } + const client = await apiClient(ctx) + const chatID = await chatIDFromFlag(client, ctx, 'chat') + await client.chats.reminders.delete(chatID) + return { chatID, reminderCleared: true } + } + const when = requiredStringFlag(ctx.flags, 'when') + const reminder = { dismissOnIncomingMessage: Boolean(ctx.flags['dismiss-on-message']) || undefined, remindAt: when } + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'chats.remind', request: { chat: stringFlag(ctx.flags, 'chat'), pick: ctx.flags.pick, reminder } } + const client = await apiClient(ctx) + const chatID = await chatIDFromFlag(client, ctx, 'chat') + await client.chats.reminders.create(chatID, { reminder }) + return { chatID, remindAt: when, reminderSet: true } +} + +async function chatsDisappear(ctx: CommandContext): Promise { + const raw = requiredStringFlag(ctx.flags, 'seconds').toLowerCase() + const messageExpirySeconds = raw === 'off' ? null : /^\d+$/.test(raw) ? Number(raw) : NaN + if (messageExpirySeconds !== null && (!Number.isSafeInteger(messageExpirySeconds) || messageExpirySeconds < 0)) throw usage('--seconds must be a positive integer or "off"') + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'chats.disappear', request: { chat: stringFlag(ctx.flags, 'chat'), messageExpirySeconds, pick: ctx.flags.pick } } + return chatsUpdate(ctx, 'disappear', { messageExpirySeconds }) +} + +async function chatsFocus(ctx: CommandContext): Promise { + if (ctx.globalFlags.dryRun) { + return { + dry_run: true, + op: 'chats.focus', + request: { + chat: stringFlag(ctx.flags, 'chat'), + draftAttachmentPath: stringFlag(ctx.flags, 'file'), + draftText: stringFlag(ctx.flags, 'text'), + messageID: stringFlag(ctx.flags, 'message'), + pick: ctx.flags.pick, + }, + } + } + const client = await apiClient(ctx) + const chatID = await chatIDFromFlag(client, ctx, 'chat') + const request = { + chatID, + draftAttachmentPath: stringFlag(ctx.flags, 'file'), + draftText: stringFlag(ctx.flags, 'text'), + messageID: stringFlag(ctx.flags, 'message'), + } + return client.focus(request) +} + +async function chatsNotifyAnyway(ctx: CommandContext): Promise { + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'chats.notify-anyway', request: { chat: stringFlag(ctx.flags, 'chat'), pick: ctx.flags.pick } } + const client = await apiClient(ctx) + const chatID = await chatIDFromFlag(client, ctx, 'chat') + return client.chats.notifyAnyway(chatID) +} + +async function resolveAccount(ctx: CommandContext): Promise { + const selector = ctx.args[0]! + const client = await apiClient(ctx) + const rows = apiItems(await client.accounts.list()) + const ids = await resolveAccountIDs(client, [selector], { allowMultiplePerInput: true, applyDefault: false }) + const candidates = rows.filter(row => ids?.includes(String(row.accountID ?? row.id))) + return resolution(ctx, 'account', selector, candidates.map(account => ({ + accountID: account.accountID, + bridge: account.bridge, + id: account.accountID ?? account.id, + network: account.network, + raw: account, + user: account.user, + }))) +} + +async function resolveChat(ctx: CommandContext): Promise { + const selector = ctx.args[0]! + const client = await apiClient(ctx) + const accountIDs = await resolveAccountIDs(client, stringListFlag(ctx.flags, 'account'), { allowMultiplePerInput: true }) + const candidates = await collectPage(client.chats.search({ accountIDs, query: selector, scope: 'titles' }), numberFlag(ctx.flags, 'limit', 10)) + const normalized = normalizeSelector(selector) + const exact = candidates.map(apiRecord).filter(chat => + normalizeSelector(chat.id) === normalized || + normalizeSelector(chat.localChatID) === normalized || + normalizeSelector(chat.title) === normalized + ) + const matches = exact.length ? exact : candidates.map(apiRecord) + return resolution(ctx, 'chat', selector, matches.map(chat => ({ + accountID: chat.accountID, + id: chat.id, + localChatID: chat.localChatID, + network: chat.network, + raw: chat, + title: chat.title, + }))) +} + +async function resolveContact(ctx: CommandContext): Promise { + const selector = ctx.args[0]! + const client = await apiClient(ctx) + const accountIDs = await resolveAccountIDs(client, stringListFlag(ctx.flags, 'account'), { allowMultiplePerInput: true }) ?? await listAccountIDs(client) + const candidates: Record[] = [] + for (const accountID of accountIDs) { + try { + const result = await client.accounts.contacts.search(accountID, { query: selector }) + candidates.push(...apiItems(result).slice(0, numberFlag(ctx.flags, 'limit', 10)).map(item => ({ ...item, accountID }))) + } catch (error) { + if (!ignorableLookupError(error)) throw error + } + } + return resolution(ctx, 'contact', selector, candidates.map(contact => ({ + accountID: contact.accountID, + displayName: contact.displayName ?? contact.fullName ?? contact.name, + email: contact.email, + id: contact.id, + phoneNumber: contact.phoneNumber, + username: contact.username, + }))) +} + +async function resolveTargetCommand(ctx: CommandContext): Promise { + const selector = ctx.args[0]! + const normalized = normalizeSelector(selector) + const targets = await listTargets() + const rows = targets.some(target => target.id === builtInDesktopTargetID) + ? targets + : [await resolveTarget({ target: builtInDesktopTargetID }), ...targets] + const candidates = rows.filter(target => + normalizeSelector(target.id) === normalized || + normalizeSelector(target.name) === normalized || + normalizeSelector(target.type) === normalized || + normalizeSelector(target.baseURL).includes(normalized) + ) + return resolution(ctx, 'target', selector, candidates.map(target => ({ + baseURL: target.baseURL, + id: target.id, + localProfile: Boolean(target.dataDir), + name: target.name, + raw: publicTarget(target), + type: target.type, + }))) +} + +async function resolveBridge(ctx: CommandContext): Promise { + const selector = ctx.args[0]! + const client = await apiClient(ctx) + const rows = apiItems(await client.bridges.list()) + const normalized = normalizeSelector(selector) + const candidates = rows.filter(bridge => + normalizeSelector(bridge.id) === normalized || + normalizeSelector(bridge.type) === normalized || + normalizeSelector(bridge.provider) === normalized || + normalizeSelector(bridge.name) === normalized || + normalizeSelector(bridge.displayName) === normalized || + normalizeSelector(bridge.id).includes(normalized) || + normalizeSelector(bridge.displayName).includes(normalized) + ) + return resolution(ctx, 'bridge', selector, candidates.map(bridge => ({ + displayName: bridge.displayName ?? bridge.name, + id: bridge.id, + provider: bridge.provider, + raw: bridge, + status: bridge.status, + type: bridge.type, + }))) +} + +async function messagesList(ctx: CommandContext): Promise { + const chat = stringFlag(ctx.flags, 'chat')! + const before = stringFlag(ctx.flags, 'before-cursor') + const after = stringFlag(ctx.flags, 'after-cursor') + if (before && after) throw usage('Use only one of --before-cursor or --after-cursor') + const client = await apiClient(ctx) + const chatID = await resolveChatID(client, chat, chatResolutionOptions(ctx)) + let items = await collectMessages(client.messages.list(chatID, { + cursor: before ?? after, + direction: before ? 'before' : after ? 'after' : undefined, + }), numberFlag(ctx.flags, 'limit', 50), stringFlag(ctx.flags, 'sender')) + if (ctx.flags.asc) items = [...items].reverse() + return ctx.flags.ids ? ids(items.map(apiRecord), 'messageID') : items +} + +async function messagesContext(ctx: CommandContext): Promise { + const id = stringFlag(ctx.flags, 'id')! + if (ctx.globalFlags.dryRun) { + return { dry_run: true, op: 'messages.context', request: { after: numberFlag(ctx.flags, 'after', 10), before: numberFlag(ctx.flags, 'before', 10), chat: stringFlag(ctx.flags, 'chat'), messageID: id, pick: ctx.flags.pick } } + } + const client = await apiClient(ctx) + const chatID = await chatIDFromFlag(client, ctx, 'chat') + const message = client.messages.retrieve ? await client.messages.retrieve(id, { chatID }) : undefined + const before = await collectPage(client.messages.list(chatID, { cursor: id, direction: 'before' }), numberFlag(ctx.flags, 'before', 10)) + const after = await collectPage(client.messages.list(chatID, { cursor: id, direction: 'after' }), numberFlag(ctx.flags, 'after', 10)) + return { after, before, chatID, message, messageID: id } +} + +async function messagesEdit(ctx: CommandContext): Promise { + const id = stringFlag(ctx.flags, 'id')! + const text = stringFlag(ctx.flags, 'message')! + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'messages.edit', request: { chat: stringFlag(ctx.flags, 'chat'), messageID: id, pick: ctx.flags.pick, text } } + const client = await apiClient(ctx) + const chatID = await chatIDFromFlag(client, ctx, 'chat') + return client.messages.update(id, { chatID, text }) +} + +async function messagesDelete(ctx: CommandContext): Promise { + const id = stringFlag(ctx.flags, 'id')! + const forEveryone = Boolean(ctx.flags['for-everyone']) + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'messages.delete', request: { chat: stringFlag(ctx.flags, 'chat'), forEveryone, messageID: id, pick: ctx.flags.pick } } + const client = await apiClient(ctx) + const chatID = await chatIDFromFlag(client, ctx, 'chat') + await client.messages.delete(id, { chatID, forEveryone: forEveryone || undefined }) + return { chatID, deleted: true, forEveryone, messageID: id } +} + +async function watch(ctx: CommandContext): Promise { + if (ctx.flags['webhook-secret'] && !ctx.flags.webhook) throw usage('--webhook-secret requires --webhook URL') + const include = stringListFlag(ctx.flags, 'include-type') + const exclude = stringListFlag(ctx.flags, 'exclude-type') + if (include.length && exclude.length) throw usage('Use either --include-type or --exclude-type, not both.') + const filter: EventFilter = { + include: include.length ? new Set(include) : undefined, + exclude: exclude.length ? new Set(exclude) : undefined, + } + const target = await resolveTarget({ target: ctx.globalFlags.target }) + const token = await targetToken(target, true) + const baseURL = target.baseURL + const info = await fetch(new URL('/v1/info', baseURL)) + if (!info.ok) throw usage(`Failed to fetch /v1/info: HTTP ${info.status}`) + const metadata = await info.json() as { endpoints?: { ws_events?: string } } + const url = new URL(metadata.endpoints?.ws_events || '/v1/ws', baseURL) + url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:' + + const subscribed = stringListFlag(ctx.flags, 'chat') + const chatIDs = subscribed.length ? subscribed : ['*'] + const webhookURL = stringFlag(ctx.flags, 'webhook') + const webhook = webhookURL + ? { inflight: 0, max: numberFlag(ctx.flags, 'webhook-queue', 64), queue: [], secret: stringFlag(ctx.flags, 'webhook-secret'), url: webhookURL } satisfies WebhookConfig + : undefined + const ws = new WebSocket(url, { headers: { Authorization: `Bearer ${token}` } }) + + ws.addEventListener('open', () => { + if (ctx.globalFlags.events) writeEvent('watch.open', { subscribed: chatIDs }) + ws.send(JSON.stringify({ chatIDs, type: 'subscriptions.set' })) + }) + ws.addEventListener('message', event => { + const body = typeof event.data === 'string' ? event.data : event.data.toString() + if (!passesFilter(body, filter)) return + if (ctx.globalFlags.events) writeEvent('watch.message') + writeWatchEvent(body, ctx.globalFlags.json || ctx.globalFlags.plain) + if (webhook) forwardWebhook(webhook, body, ctx.globalFlags.events) + }) + ws.addEventListener('error', () => { + if (ctx.globalFlags.events) writeEvent('watch.error', { message: 'WebSocket connection failed' }) + }) + ws.addEventListener('close', event => { + if (ctx.globalFlags.events) writeEvent('watch.close', { code: event.code, reason: event.reason }) + }) + + await new Promise(resolve => { + process.once('SIGINT', () => { + ws.close(1000) + resolve() + }) + ws.addEventListener('close', () => resolve()) + }) +} + +async function mediaDownload(ctx: CommandContext): Promise { + const url = ctx.args[0] + if (!url) throw usage('media download requires url') + const out = stringFlag(ctx.flags, 'out') ?? '.' + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'media.download', request: { out, url } } + + const client = await apiClient(ctx) + const response = await client.assets.serve({ url }) + if (!response.ok) throw usage(`Failed to download media: HTTP ${response.status}`) + const buffer = Buffer.from(await response.arrayBuffer()) + if (out === '-') { + output.write(buffer) + return undefined + } + + await mkdir(out, { recursive: true }) + const path = join(out, basename(new URL(url).pathname) || 'media') + await writeFile(path, buffer) + return { bytes: buffer.length, path } +} + +async function exportCommand(ctx: CommandContext): Promise { + const accountSelectors = stringListFlag(ctx.flags, 'account') + const chatSelectors = stringListFlag(ctx.flags, 'chat') + const request = { + accounts: accountSelectors, + chats: chatSelectors, + downloadAttachments: !ctx.flags['no-attachments'], + force: Boolean(ctx.flags.force), + limitChats: ctx.flags['limit-chats'], + limitMessages: ctx.flags['limit-messages'], + maxParticipants: numberFlag(ctx.flags, 'max-participants', 500), + outDir: stringFlag(ctx.flags, 'out') ?? 'beeper-export', + pick: ctx.flags.pick, + } + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'export', request } + + const client = await apiClient(ctx) + const accountIDs = await resolveAccountIDs(client, accountSelectors, { allowMultiplePerInput: true }) + const chatIDs = chatSelectors.length + ? await Promise.all(chatSelectors.map(chat => resolveChatID(client, chat, chatResolutionOptions(ctx, accountIDs)))) + : undefined + const manifest = await exportBeeperData(client, { + accountIDs, + chatIDs, + downloadAttachments: request.downloadAttachments, + force: request.force, + limitChats: typeof request.limitChats === 'number' ? request.limitChats : undefined, + limitMessages: typeof request.limitMessages === 'number' ? request.limitMessages : undefined, + maxParticipants: request.maxParticipants, + onProgress: message => { + if (ctx.globalFlags.events) writeEvent('export.progress', { message }) + if (!ctx.globalFlags.json && !ctx.globalFlags.plain) process.stderr.write(`${message}\n`) + }, + outDir: request.outDir, + }) + return { ...manifest, outDir: request.outDir } +} + +async function messagesSearch(ctx: CommandContext): Promise { + const accountSelectors = stringListFlag(ctx.flags, 'account') + const chatSelectors = stringListFlag(ctx.flags, 'chat') + const mediaTypes = stringListFlag(ctx.flags, 'media') as Array<'any' | 'video' | 'image' | 'link' | 'file'> + const hasFilter = Boolean( + accountSelectors.length || chatSelectors.length || ctx.flags['chat-type'] + || ctx.flags.after || ctx.flags.before || mediaTypes.length || ctx.flags.sender, + ) + if (!ctx.args[0] && !hasFilter) { + throw usage('Provide a search query or at least one filter flag (--chat, --sender, --media, etc.).') + } + const client = await apiClient(ctx) + const accountIDs = await resolveAccountIDs(client, accountSelectors, { allowMultiplePerInput: true }) + const chatIDs = chatSelectors.length + ? await Promise.all(chatSelectors.map(chat => resolveChatID(client, chat, chatResolutionOptions(ctx, accountIDs)))) + : undefined + const items = await collectPage(client.messages.search({ + accountIDs, + chatIDs, + chatType: stringFlag(ctx.flags, 'chat-type') as 'group' | 'single' | undefined, + dateAfter: stringFlag(ctx.flags, 'after'), + dateBefore: stringFlag(ctx.flags, 'before'), + excludeLowPriority: ctx.flags['exclude-low-priority'], + includeMuted: ctx.flags['include-muted'], + mediaTypes: mediaTypes.length ? mediaTypes : undefined, + query: ctx.args[0], + sender: stringFlag(ctx.flags, 'sender') as 'me' | 'others' | (string & {}) | undefined, + }), numberFlag(ctx.flags, 'limit', 50)) + return ctx.flags.ids ? ids(items.map(apiRecord), 'messageID') : items +} + +async function apiCommand(ctx: CommandContext): Promise { + const method = String(ctx.args[0] ?? '').toUpperCase() + const path = ctx.args[1] + if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) throw usage('api request method must be one of: GET, POST, PUT, PATCH, DELETE') + if (!path) throw usage('api request requires path') + const body = method === 'GET' ? undefined : jsonBody(ctx) + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'api.request', request: { body, method, noAuth: ctx.flags['no-auth'], path, target: ctx.globalFlags.target } } + return appRequest(method, path, { body, target: ctx.globalFlags.target, token: ctx.flags['no-auth'] ? false : undefined }) +} + +async function sendTextLike(ctx: CommandContext): Promise { + const kind = ctx.commandPath[1] + if (kind !== 'file' && kind !== 'sticker' && kind !== 'text' && kind !== 'voice') throw usage(`Unsupported send command: ${ctx.commandPath.join(' ')}`) + const to = stringFlag(ctx.flags, 'to')! + const payload = sendPayload(ctx, kind) + if (ctx.globalFlags.dryRun) return { dry_run: true, op: `send.${kind}`, request: { chat: to, ...payload } } + + const client = await apiClient(ctx) + const chatID = await resolveChatID(client, to, chatResolutionOptions(ctx)) + return sendMessage(client, { ...payload, chatID }) +} + +async function sendMessage(client: any, options: SendPayload & { + chatID: string +}): Promise> { + const uploaded = options.file + ? await client.assets.upload({ + file: createReadStream(options.file), + fileName: options.fileName, + mimeType: options.mimeType, + }) + : undefined + + if (options.file && !uploaded?.uploadID) throw new Error('Upload did not return an uploadID') + + const pending = await client.messages.send(options.chatID, { + attachment: uploaded?.uploadID + ? { + uploadID: uploaded.uploadID, + type: options.attachmentType, + duration: options.duration ?? uploaded.duration, + fileName: uploaded.fileName, + mimeType: options.mimeType ?? uploaded.mimeType, + size: uploaded.width && uploaded.height ? { height: uploaded.height, width: uploaded.width } : undefined, + } + : undefined, + replyToMessageID: options.replyTo, + text: options.text, + mentions: options.mentions?.length ? options.mentions : undefined, + disableLinkPreview: options.noPreview || undefined, + }) + + if (!options.wait) { + return { + ...pending, + accepted: true, + state: 'accepted', + chatID: options.chatID, + hint: 'Desktop accepted the send request. Pass --wait to wait for the final message or failure.', + } + } + return { + accepted: true, + state: 'resolved', + chatID: options.chatID, + pendingMessageID: pending.pendingMessageID, + message: await waitForMessage(client, options.chatID, pending.pendingMessageID, options.waitTimeoutMs), + } +} + +async function waitForMessage(client: any, chatID: string, pendingMessageID: string, timeoutMs = 30_000): Promise { + const started = Date.now() + let lastError: unknown + while (Date.now() - started < timeoutMs) { + try { + return await client.messages.retrieve(pendingMessageID, { chatID }) + } catch (error) { + lastError = error + await sleep(750) + } + } + throw new Error(`Timed out waiting for ${pendingMessageID}${lastError instanceof Error ? `: ${lastError.message}` : ''}`) +} + +async function sendReact(ctx: CommandContext): Promise { + const id = stringFlag(ctx.flags, 'id')! + const reaction = stringFlag(ctx.flags, 'reaction')! + const transactionID = stringFlag(ctx.flags, 'transaction') + const remove = Boolean(ctx.flags.remove) + if (remove && transactionID) throw usage('--transaction cannot be combined with --remove') + if (ctx.globalFlags.dryRun) { + return { dry_run: true, op: 'send.react', request: { chat: stringFlag(ctx.flags, 'to'), messageID: id, pick: ctx.flags.pick, reactionKey: reaction, remove, transactionID } } + } + const client = await apiClient(ctx) + const chatID = await chatIDFromFlag(client, ctx, 'to') + if (remove) return client.chats.messages.reactions.delete(reaction, { chatID, messageID: id }) + return client.chats.messages.reactions.add(id, { chatID, reactionKey: reaction, transactionID }) +} + +async function authLogout(ctx: CommandContext): Promise> { + const target = await resolveTarget({ target: ctx.globalFlags.target }) + const token = target.auth?.accessToken + if (ctx.globalFlags.dryRun) { + return { dry_run: true, op: 'auth.logout', request: { baseURL: target.baseURL, hadToken: Boolean(token), revokeToken: Boolean(token), target: target.id } } + } + if (process.env.BEEPER_ACCESS_TOKEN && !target.auth?.accessToken) { + throw usage('auth logout cannot clear BEEPER_ACCESS_TOKEN from the environment; unset it in the calling process.') + } + let revoked = false + if (token) { + const response = await fetch(new URL('/oauth/revoke', target.baseURL), { + body: new URLSearchParams({ token, token_type_hint: 'access_token' }), + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + method: 'POST', + signal: AbortSignal.timeout(5000), + }).catch(() => undefined) + revoked = Boolean(response?.ok) + await writeTarget({ ...target, auth: undefined }) + } + return { hadToken: Boolean(token), loggedOut: true, revoked } +} + +async function authEmailStart(ctx: CommandContext): Promise { + const target = await resolveTarget({ target: ctx.globalFlags.target }) + const email = stringFlag(ctx.flags, 'email')! + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'auth.email.start', request: { email, target: target.id } } + return startEmailSetup(target, email) +} + +async function authEmailResponse(ctx: CommandContext): Promise { + const target = await resolveTarget({ target: ctx.globalFlags.target }) + const code = stringFlag(ctx.flags, 'code')! + const setupRequestID = stringFlag(ctx.flags, 'setup-request-id')! + if (ctx.globalFlags.dryRun) { + return { dry_run: true, op: 'auth.email.response', request: { baseURL: target.baseURL, force: ctx.globalFlags.force, setupRequestID, target: target.id, username: stringFlag(ctx.flags, 'username') } } + } + return finishEmailSetup(target, { + code, + json: ctx.globalFlags.json, + setupRequestID, + username: stringFlag(ctx.flags, 'username'), + force: ctx.globalFlags.force, + }) +} + +async function apiClient(ctx: CommandContext): Promise { + const target = await resolveTarget({ target: ctx.globalFlags.target }) + return new BeeperDesktop({ + accessToken: await targetToken(target, target.id === 'desktop'), + baseURL: target.baseURL, + logLevel: ctx.globalFlags.debug ? 'debug' : 'warn', + }) +} + +async function targetToken(target: Target, scan?: boolean): Promise { + const token = process.env.BEEPER_ACCESS_TOKEN || target.auth?.accessToken + if (token) return token + const auth = authFromToken( + await authorizeTarget({ baseURL: target.baseURL, scan }), + target.type === 'remote' ? 'remote-oauth' : 'desktop-oauth', + ) + await writeTarget({ ...target, auth }) + return auth.accessToken +} + +function jsonBody(ctx: CommandContext): Record { + const raw = stringFlag(ctx.flags, 'body') ?? '{}' + try { + const parsed = JSON.parse(raw) + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('body must be a JSON object') + return parsed as Record + } catch (error) { + const detail = error instanceof Error ? error.message : String(error) + throw usage(`--body is not valid JSON: ${detail}`) + } +} + +function sendPayload(ctx: CommandContext, kind: SendKind): SendPayload { + if (kind === 'text') { + return { + mentions: stringListFlag(ctx.flags, 'mention'), + noPreview: Boolean(ctx.flags['no-preview']), + replyTo: stringFlag(ctx.flags, 'reply-to'), + text: stringFlag(ctx.flags, 'message')!, + wait: Boolean(ctx.flags.wait), + waitTimeoutMs: numberFlag(ctx.flags, 'wait-timeout', 30_000), + } + } + const file = stringFlag(ctx.flags, 'file')! + const attachmentType: AttachmentType | undefined = kind === 'sticker' ? 'sticker' : kind === 'voice' ? 'voice-note' : undefined + return { + attachmentType, + duration: kind === 'voice' ? numberFlag(ctx.flags, 'duration', 0) || undefined : undefined, + file, + fileName: stringFlag(ctx.flags, 'filename'), + mimeType: stringFlag(ctx.flags, 'mime') ?? (kind === 'sticker' ? 'image/webp' : kind === 'voice' ? 'audio/ogg' : undefined), + replyTo: stringFlag(ctx.flags, 'reply-to'), + text: kind === 'file' ? stringFlag(ctx.flags, 'caption') ?? '' : '', + wait: Boolean(ctx.flags.wait), + waitTimeoutMs: numberFlag(ctx.flags, 'wait-timeout', 30_000), + } +} + +async function sendPresence(ctx: CommandContext): Promise { + const state = (stringFlag(ctx.flags, 'state') ?? 'typing') as 'typing' | 'paused' + const duration = ctx.flags.duration === undefined ? undefined : numberFlag(ctx.flags, 'duration', 0) + if (duration !== undefined && duration <= 0) throw usage('--duration must be a positive integer') + if (duration !== undefined && state !== 'typing') throw usage('--duration only applies when --state is typing') + const to = stringFlag(ctx.flags, 'to')! + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'send.presence', request: { chat: to, durationSeconds: duration, pick: ctx.flags.pick, state } } + + const client = await apiClient(ctx) + const chatID = await chatIDFromFlag(client, ctx, 'to') + const post = (nextState: 'typing' | 'paused') => + client.post(`/v1/chats/${encodeURIComponent(chatID)}/typing`, { body: { state: nextState } }) + await post(state) + if (duration !== undefined) { + await sleep(duration * 1000) + await post('paused') + return { chatID, durationSeconds: duration, sent: true, state: 'paused' } + } + return { chatID, sent: true, state } +} + +async function chatIDFromFlag(client: any, ctx: CommandContext, name: 'chat' | 'to'): Promise { + return resolveChatID(client, stringFlag(ctx.flags, name)!, chatResolutionOptions(ctx)) +} + +function chatResolutionOptions(ctx: CommandContext, accountIDs?: string[]): { accountIDs?: string[]; noInput?: boolean; pick?: number } { + return { accountIDs, noInput: ctx.globalFlags.noInput, pick: numberFlag(ctx.flags, 'pick', 0) || undefined } +} + +async function defaultAccountID(client: any): Promise { + const accountIDs = await listAccountIDs(client) + if (accountIDs.includes('matrix')) return 'matrix' + if (accountIDs.length === 1 && accountIDs[0]) return accountIDs[0] + throw usage('Use --account to choose which account should start the chat.') +} + +function parseDurationMs(value?: string): number | undefined { + if (!value) return undefined + const match = value.trim().match(/^(\d+(?:\.\d+)?)(ms|s|m)?$/i) + if (!match) throw usage(`Invalid duration "${value}". Use values like 500ms, 30s, or 2m.`) + const amount = Number(match[1]) + const unit = (match[2] ?? 'ms').toLowerCase() + if (unit === 'ms') return amount + if (unit === 's') return amount * 1000 + if (unit === 'm') return amount * 60_000 + return amount +} + +async function waitForTunnelExit(started: StartedTunnel): Promise<{ code: number | null; reason: 'process' | 'signal' }> { + return new Promise(resolve => { + const finish = () => { + started.stop() + resolve({ code: 0, reason: 'signal' }) + } + process.once('SIGINT', finish) + process.once('SIGTERM', finish) + started.done.then(({ code }) => { + process.off('SIGINT', finish) + process.off('SIGTERM', finish) + resolve({ code, reason: 'process' }) + }) + }) +} + +function writeWatchEvent(body: string, raw: boolean): void { + if (raw) { + process.stdout.write(`${body}\n`) + return + } + try { + const parsed = JSON.parse(body) as Record + process.stdout.write([ + String(parsed.type ?? 'event'), + parsed.chatID ? `chat=${parsed.chatID}` : undefined, + parsed.messageID ? `message=${parsed.messageID}` : undefined, + String(parsed.timestamp ?? new Date().toISOString()), + ].filter(Boolean).join('\t') + '\n') + } catch { + process.stdout.write(`raw\t${new Date().toISOString()}\n`) + } +} + +function passesFilter(body: string, filter?: EventFilter): boolean { + if (!filter || (!filter.include && !filter.exclude)) return true + let type: string | undefined + try { + const parsed = JSON.parse(body) as { type?: unknown } + if (typeof parsed.type === 'string') type = parsed.type + } catch { + return true + } + if (!type) return true + if (filter.include && !filter.include.has(type)) return false + if (filter.exclude && filter.exclude.has(type)) return false + return true +} + +function forwardWebhook(webhook: WebhookConfig, body: string, events: boolean): void { + if (webhook.inflight + webhook.queue.length >= webhook.max) { + if (events) writeEvent('watch.webhook_drop', { reason: 'queue_full', size: webhook.queue.length }) + process.stderr.write(`warning: webhook queue full (${webhook.max}); dropped event\n`) + return + } + const signature = webhook.secret ? `sha256=${createHmac('sha256', webhook.secret).update(body).digest('hex')}` : undefined + webhook.queue.push({ body, signature }) + void drainWebhook(webhook, events) +} + +async function drainWebhook(webhook: WebhookConfig, events: boolean): Promise { + while (webhook.queue.length > 0) { + const item = webhook.queue.shift()! + webhook.inflight += 1 + try { + const headers: Record = { 'content-type': 'application/json' } + if (item.signature) headers['x-beeper-signature'] = item.signature + const response = await fetch(webhook.url, { body: item.body, headers, method: 'POST', signal: AbortSignal.timeout(10_000) }) + if (!response.ok) { + if (events) writeEvent('watch.webhook_error', { status: response.status }) + process.stderr.write(`warning: webhook POST ${webhook.url} returned ${response.status}\n`) + } + } catch (error) { + if (events) writeEvent('watch.webhook_error', { message: (error as Error).message }) + process.stderr.write(`warning: webhook POST failed: ${(error as Error).message}\n`) + } finally { + webhook.inflight -= 1 + } + } +} + +async function chooseBridge(items: Record[]): Promise { + const available = items.filter(item => String(item.status ?? 'available') === 'available') + if (!available.length) throw usage('No available bridges to connect.') + output.write('Choose a bridge to connect an account:\n') + available.forEach((item, index) => { + const id = String(item.id) + const name = String(item.displayName ?? item.name ?? id) + const multiple = item.supportsMultipleAccounts ? 'multiple allowed' : 'single account' + output.write(` ${index + 1}. ${name} (${id}) - ${multiple}\n`) + }) + return promptChoice('Select a bridge: ', available.map(item => String(item.id)), { output }) +} + +function printAvailableBridges(items: Record[]): void { + const sections: Array<[string, Record[]]> = [ + ['On-Device Accounts', items.filter(item => item.provider === 'local')], + ['Beeper Cloud Accounts', items.filter(item => item.provider === 'cloud')], + ['Self-Hosted Accounts', items.filter(item => item.provider === 'self-hosted')], + ] + output.write('Choose a bridge to connect an account:\n\n') + for (const [title, bridges] of sections) { + if (!bridges.length) continue + output.write(`${title}\n`) + for (const bridge of bridges) { + const id = String(bridge.id) + const name = String(bridge.displayName ?? bridge.name ?? id) + const state = String(bridge.status ?? 'available') + const status = bridge.statusText ?? (state === 'available' ? undefined : state === 'connected' ? `${name} Connected` : state.replaceAll('_', ' ')) + const multiple = bridge.supportsMultipleAccounts ? 'multiple allowed' : 'single account' + output.write(` ${name} (${id}) - ${multiple}${status ? ` - ${String(status)}` : ''}\n`) + if (String(bridge.status ?? 'available') === 'available') output.write(` beeper accounts add ${id}\n`) + } + output.write('\n') + } +} + +function resolveBridgeChoice(items: Record[], input: string): Record { + const keys = (item: Record) => [item.id, item.displayName, item.name, item.network, item.provider, item.type] + const normalized = normalizeSelector(input) + const exact = items.filter(item => keys(item).some(value => normalizeSelector(value) === normalized)) + if (exact.length === 1) return exact[0]! + if (exact.length > 1) throw ambiguousBridge(input, exact) + const partial = items.filter(item => keys(item).some(value => normalizeSelector(value).includes(normalized))) + if (partial.length === 1) return partial[0]! + if (partial.length > 1) throw ambiguousBridge(input, partial) + throw usage(`Unknown bridge "${input}". Run "beeper resolve bridge ${input}" to inspect matches.`) +} + +function ambiguousBridge(input: string, matches: Record[]): Error { + return usage(`Bridge "${input}" is ambiguous. Use one of: ${matches.map(item => `${String(item.displayName ?? item.name ?? item.id)} (${String(item.id)})`).join(', ')}`) +} + +function parseKeyValueFlags(values: string[], flagName: string): Record { + const parsed: Record = {} + for (const value of values) { + const equalsIndex = value.indexOf('=') + if (equalsIndex <= 0) throw usage(`${flagName} must use name=value form.`) + parsed[value.slice(0, equalsIndex)] = value.slice(equalsIndex + 1) + } + return parsed +} + +async function chooseLoginFlow(flows: Record[]): Promise { + output.write('Choose how you want to sign in:\n') + flows.forEach((flow, index) => { + const description = flow.description ? ` - ${String(flow.description)}` : '' + output.write(` ${index + 1}. ${String(flow.name ?? flow.id)}${description}\n`) + }) + return promptChoice('Select a sign-in method: ', flows.map(flow => String(flow.id)), { output }) +} + +function ids(items: Record[], preferred: string): string[] { + return items + .map(item => item[preferred] ?? item.localChatID ?? item.rowID ?? item.id ?? item.chatID ?? item.messageID ?? item.accountID ?? item.userID) + .filter((value): value is string | number => typeof value === 'string' || typeof value === 'number') + .map(String) +} + +function resolution(ctx: CommandContext, kind: string, selector: string, candidates: Record[]): Record { + if (!candidates.length) { + throw new AbortError(`No ${kind} matches "${selector}"`, ExitCodes.NotFound, undefined, 'not_found') + } + const pick = numberFlag(ctx.flags, 'pick', 0) + const selected = pick ? candidates[pick - 1] : candidates.length === 1 ? candidates[0] : undefined + if (pick && !selected) { + throw new AbortError(`--pick ${pick} is outside the ${candidates.length} matching ${kind}s`, ExitCodes.NotFound, undefined, 'not_found') + } + return { + candidates: candidates.map((candidate, index) => ({ pick: index + 1, ...candidate })), + kind, + selected: selected ? { pick: candidates.indexOf(selected) + 1, ...selected } : null, + selector, + } +} + +function ignorableLookupError(error: unknown): boolean { + if (!(error instanceof Error)) return false + const shaped = error as Error & { status?: number; statusCode?: number } + const status = shaped.status ?? shaped.statusCode + return status === 400 || status === 404 || /\b(400|404)\b|not supported|not found/i.test(error.message) +} + +function matchesChatFilters(row: Record, ctx: CommandContext): boolean { + if (ctx.flags.archived !== undefined && Boolean(row.isArchived) !== ctx.flags.archived) return false + if (ctx.flags.pinned !== undefined && Boolean(row.isPinned) !== ctx.flags.pinned) return false + if (ctx.flags.muted !== undefined && Boolean(row.isMuted) !== ctx.flags.muted) return false + if (ctx.flags['low-priority'] !== undefined && Boolean(row.isLowPriority) !== ctx.flags['low-priority']) return false + if (ctx.flags.unread !== undefined) { + const unread = Number(row.unreadCount ?? 0) > 0 || Boolean(row.isMarkedUnread) + if (unread !== ctx.flags.unread) return false + } + return true +} + +async function collectMessages(iterable: AsyncIterable, limit: number, sender?: string): Promise { + if (!sender) return collectPage(iterable, limit) + const items: unknown[] = [] + for await (const item of iterable) { + if (matchesSender(item, sender)) items.push(item) + if (items.length >= limit) break + } + return items +} + +function matchesSender(item: unknown, sender: string): boolean { + if (!item || typeof item !== 'object') return false + const row = item as { isSender?: boolean; senderID?: string } + if (sender === 'me') return row.isSender === true + if (sender === 'others') return row.isSender !== true + return row.senderID === sender +} + +async function packageInfo(): Promise> { + const root = dirname(dirname(dirname(fileURLToPath(import.meta.url)))) + return JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) as Record +} diff --git a/packages/cli/src/cli/main.ts b/packages/cli/src/cli/main.ts new file mode 100644 index 00000000..72983aa9 --- /dev/null +++ b/packages/cli/src/cli/main.ts @@ -0,0 +1,33 @@ +import { ExitCodes } from '../lib/errors.js' +import { commands, commandHelp, help } from './commands.js' +import { enforcePolicy } from './policy.js' +import { parseCommand } from './parse.js' +import { usage, writeError, writeResult } from './output.js' + +export async function runCli(argv = process.argv.slice(2)): Promise { + let parsed + try { + parsed = parseCommand(argv, commands) + if (parsed.globalFlags.json && parsed.globalFlags.plain) throw usage('cannot combine --json and --plain') + if (parsed.helpOnly) { + process.stdout.write(help()) + return + } + if (!parsed.command) throw new Error('missing command') + if (parsed.flags.help) { + process.stdout.write(commandHelp(parsed.command)) + return + } + enforcePolicy(parsed.command, parsed.globalFlags) + const result = await parsed.command.run({ + args: parsed.positionals, + commandPath: parsed.command.path, + flags: parsed.flags, + globalFlags: parsed.globalFlags, + }) + writeResult(result, parsed.globalFlags) + } catch (error) { + const flags = parsed?.globalFlags ?? { events: argv.includes('--events'), json: argv.includes('--json') } + process.exitCode = writeError(error, flags) || ExitCodes.Generic + } +} diff --git a/packages/cli/src/cli/mcp.ts b/packages/cli/src/cli/mcp.ts new file mode 100644 index 00000000..db5ecd8f --- /dev/null +++ b/packages/cli/src/cli/mcp.ts @@ -0,0 +1,122 @@ +import type { CommandSpec, FlagSpec, GlobalFlags } from './types.js' +import { enforcePolicy } from './policy.js' +import { wrapUntrusted } from './output.js' +import { parseFlagValue, validateCommandInput } from './parse.js' + +type JsonRpcRequest = { + id?: number | string + method?: string + params?: Record +} + +export async function serveMcp(commands: CommandSpec[], flags: GlobalFlags, allowWrite: boolean, version: string): Promise { + const buffer: string[] = [] + process.stdin.setEncoding('utf8') + for await (const chunk of process.stdin) { + buffer.push(String(chunk)) + let joined = buffer.join('') + let index = joined.indexOf('\n') + while (index !== -1) { + const line = joined.slice(0, index).trim() + joined = joined.slice(index + 1) + if (line) await handleLine(commands, flags, allowWrite, version, line) + index = joined.indexOf('\n') + } + buffer.length = 0 + if (joined) buffer.push(joined) + } + const finalLine = buffer.join('').trim() + if (finalLine) await handleLine(commands, flags, allowWrite, version, finalLine) +} + +function mcpTools(commands: CommandSpec[]): Record[] { + return commands + .filter(command => command.mcp) + .map(command => ({ + description: command.description, + inputSchema: { + additionalProperties: false, + properties: Object.fromEntries([ + ...(command.args ?? []).map(arg => [arg.name, { description: arg.description, type: 'string' }]), + ...(command.flags ?? []).map(flag => [flag.name, inputSchemaForFlag(flag)]), + ]), + required: [ + ...(command.args ?? []).filter(arg => arg.required).map(arg => arg.name), + ...(command.flags ?? []).filter(flag => flag.required).map(flag => flag.name), + ], + type: 'object', + }, + name: command.path.join('_'), + })) +} + +async function handleLine(commands: CommandSpec[], flags: GlobalFlags, allowWrite: boolean, version: string, line: string): Promise { + let request: JsonRpcRequest = {} + try { + request = JSON.parse(line) as JsonRpcRequest + if (request.method === 'initialize') { + respond(request.id, { capabilities: { tools: {} }, protocolVersion: '2024-11-05', serverInfo: { name: 'beeper', version } }) + return + } + if (request.method === 'tools/list') { + respond(request.id, { tools: mcpTools(commands) }) + return + } + if (request.method === 'tools/call') { + const name = String(request.params?.name ?? '') + const tool = commands.find(command => command.mcp && command.path.join('_') === name) + if (!tool) throw new Error(`unknown MCP tool: ${name}`) + if (tool.risk !== 'read' && !allowWrite) throw new Error(`MCP tool "${name}" requires mcp --allow-write`) + const args = request.params?.arguments && typeof request.params.arguments === 'object' + ? request.params.arguments as Record + : {} + const globalFlags = { ...flags, json: true, wrapUntrusted: true } + const positionals = positionalsFor(tool, args) + const toolFlags = flagsFor(tool, args) + validateCommandInput(tool, toolFlags, positionals) + enforcePolicy(tool, globalFlags) + const result = await tool.run({ args: positionals, commandPath: tool.path, flags: toolFlags, globalFlags }) + respond(request.id, { content: [{ text: JSON.stringify(wrapUntrusted(result)), type: 'text' }] }) + return + } + if (request.id !== undefined) respond(request.id, {}) + } catch (error) { + respondError(request.id, error instanceof Error ? error.message : String(error)) + } +} + +function inputSchemaForFlag(flag: FlagSpec): Record { + const schema = { + description: flag.description, + enum: flag.enum, + type: flag.type === 'integer' ? 'integer' : flag.type, + } + return flag.multiple ? { description: flag.description, items: schema, type: 'array' } : schema +} + +function positionalsFor(command: CommandSpec, input: Record): string[] { + return (command.args ?? []) + .map(arg => input[arg.name]) + .filter(value => value !== undefined) + .map(String) +} + +function flagsFor(command: CommandSpec, input: Record): Record { + const flags: Record = {} + for (const flag of command.flags ?? []) { + const value = input[flag.name] ?? flag.default + if (value === undefined) continue + flags[flag.name] = flag.multiple + ? (Array.isArray(value) ? value : [value]).map(item => parseFlagValue(flag, item)) + : parseFlagValue(flag, value) + } + return flags +} + +function respond(id: JsonRpcRequest['id'], result: unknown): void { + process.stdout.write(`${JSON.stringify({ id, jsonrpc: '2.0', result })}\n`) +} + +function respondError(id: JsonRpcRequest['id'], message: string): void { + process.stdout.write(`${JSON.stringify({ error: { code: -32000, message }, id, jsonrpc: '2.0' })}\n`) +} diff --git a/packages/cli/src/cli/output.ts b/packages/cli/src/cli/output.ts new file mode 100644 index 00000000..e8721c8e --- /dev/null +++ b/packages/cli/src/cli/output.ts @@ -0,0 +1,155 @@ +import { AbortError, CLIError, ExitCodes } from '../lib/errors.js' +import type { GlobalFlags } from './types.js' + +type ErrorShape = { + code: string + exitCode: number + hint?: string + kind: 'abort' | 'bug' + message: string +} + +export function writeResult(value: unknown, flags: GlobalFlags): void { + if (value === undefined) return + if (flags.json) process.stdout.write(`${JSON.stringify(flags.wrapUntrusted ? wrapUntrusted(value) : value, null, 2)}\n`) + else writeText(value, flags.plain) +} + +export function writeEvent(event: string, data: Record = {}): void { + process.stderr.write(`${JSON.stringify({ event, data, ts: Date.now() })}\n`) +} + +function formatError(error: unknown): ErrorShape { + const err = normalizeError(error) + const isBug = !(err instanceof CLIError) + const exitCode = err instanceof CLIError ? err.exitCode : ExitCodes.Generic + return { + code: err instanceof CLIError && err.code ? err.code : errorCode(exitCode, isBug), + exitCode, + hint: err instanceof CLIError ? err.tryMessage : undefined, + kind: isBug ? 'bug' : 'abort', + message: err.message, + } +} + +export function writeError(error: unknown, flags: Pick): number { + const formatted = formatError(error) + if (flags.events) { + writeEvent('error', formatted) + return formatted.exitCode + } + if (flags.json) { + process.stderr.write(`${JSON.stringify({ error: formatted })}\n`) + return formatted.exitCode + } + process.stderr.write(`${sanitizeHuman(formatted.message)}\n`) + if (formatted.hint) process.stderr.write(`hint: ${sanitizeHuman(formatted.hint)}\n`) + return formatted.exitCode +} + +export function usage(message: string): AbortError { + return new AbortError(message, ExitCodes.Usage, undefined, 'usage_error') +} + +function sanitizeHuman(value: string): string { + let out = '' + let inEscape = false + for (const char of value) { + const code = char.charCodeAt(0) + if (inEscape) { + if (code >= 0x40 && code <= 0x7e) inEscape = false + continue + } + if (char === '\x1b') { + inEscape = true + if (!out.endsWith(' ')) out += ' ' + continue + } + if (code < 0x20 || code === 0x7f || (code >= 0x80 && code <= 0x9f)) { + if (!out.endsWith(' ')) out += ' ' + continue + } + out += char + } + return out.trim() +} + +function normalizeError(error: unknown): Error { + if (error instanceof Error) return error + return new Error(String(error)) +} + +function errorCode(code: number, isBug: boolean): string { + if (isBug) return 'internal_error' + if (code === ExitCodes.AuthRequired) return 'auth_required' + if (code === ExitCodes.CommandNotFound) return 'command_not_found' + if (code === ExitCodes.NotFound) return 'not_found' + if (code === ExitCodes.NotReady) return 'not_ready' + if (code === ExitCodes.Usage) return 'usage_error' + return 'runtime_error' +} + +function writeText(value: unknown, plain = false): void { + if (value === undefined) return + if (Array.isArray(value)) { + for (const item of value) writeText(item, plain) + return + } + if (!value || typeof value !== 'object') { + process.stdout.write(`${String(value ?? '')}\n`) + return + } + for (const [key, item] of Object.entries(value as Record)) { + if (item === undefined) continue + const cell = humanCell(item) + process.stdout.write(plain ? `${key}\t${cell.replaceAll('\n', '\\n').replaceAll('\t', '\\t')}\n` : `${key}: ${cell}\n`) + } +} + +function humanCell(value: unknown): string { + if (Array.isArray(value)) return value.map(humanCell).join(', ') + if (value && typeof value === 'object') return JSON.stringify(value) + return String(value ?? '') +} + +export function wrapUntrusted(value: unknown): unknown { + return wrapValue(value, []) +} + +function wrapValue(value: unknown, path: string[]): unknown { + if (Array.isArray(value)) return value.map(item => wrapValue(item, path)) + if (value && typeof value === 'object') { + const out: Record = {} + let wrapped = false + for (const [key, item] of Object.entries(value as Record)) { + const next = wrapValueForKey(item, [...path, key], key) + if (next !== item) wrapped = true + out[key] = next + } + if (path.length === 0 && wrapped) out.externalContent = { source: 'beeper_api', untrusted: true, wrapped: true } + return out + } + return value +} + +function wrapValueForKey(value: unknown, path: string[], key: string): unknown { + if (typeof value === 'string' && shouldWrapString(path, key, value)) { + return wrapText(value) + } + return wrapValue(value, path) +} + +function shouldWrapString(path: string[], key: string, value: string): boolean { + if (!value) return false + const normalized = key.replaceAll(/[-_]/g, '').toLowerCase() + if (['id', 'chatid', 'messageid', 'roomid', 'url', 'uri', 'createdat', 'updatedat', 'status'].includes(normalized)) return false + if (['about', 'body', 'caption', 'description', 'displayname', 'message', 'name', 'subject', 'text', 'title', 'topic', 'value'].includes(normalized)) return true + return path.some(part => ['messages', 'rows', 'values'].includes(part.replaceAll(/[-_]/g, '').toLowerCase())) +} + +function wrapText(value: string): string { + const sanitized = value + .replaceAll(/<<<\s*(?:END[\s_]+)?EXTERNAL[\s_]+UNTRUSTED[\s_]+CONTENT[^>]*>>>/gi, '[[UNTRUSTED_MARKER_SANITIZED]]') + .replaceAll(/<\|[^>]+?\|>/g, '[REMOVED_SPECIAL_TOKEN]') + return `<<>>\n${sanitized}\n<<>>` +} diff --git a/packages/cli/src/cli/parse.ts b/packages/cli/src/cli/parse.ts new file mode 100644 index 00000000..e98fb38b --- /dev/null +++ b/packages/cli/src/cli/parse.ts @@ -0,0 +1,183 @@ +import type { CommandSpec, FlagSpec, GlobalFlags } from './types.js' +import { usage } from './output.js' + +type ParsedCommand = { + command?: CommandSpec + flags: Record + globalFlags: GlobalFlags + helpOnly?: boolean + positionals: string[] +} + +export const globalFlagSpecs: FlagSpec[] = [ + { name: 'debug', type: 'boolean', default: false }, + { name: 'dry-run', type: 'boolean', default: false }, + { name: 'events', type: 'boolean', default: false }, + { name: 'force', type: 'boolean', default: false }, + { name: 'json', type: 'boolean', default: false }, + { name: 'no-input', type: 'boolean', default: false }, + { name: 'plain', type: 'boolean', default: false }, + { name: 'safety-profile', type: 'string', description: 'Safety profile name or YAML path' }, + { name: 'target', type: 'string' }, + { name: 'wrap-untrusted', type: 'boolean', default: false }, +] + +export function parseCommand(argv: string[], commands: CommandSpec[]): ParsedCommand { + const helpRequested = argv.includes('--help') + const global = parseGlobalFlags(argv) + const tokens = parseArgv(argv, globalFlagSpecs, { allowUnknownFlags: true }).positionals + const pathTokens = tokens.filter(token => !token.startsWith('-')) + if (pathTokens.length === 0) { + return { flags: {}, globalFlags: global, helpOnly: true, positionals: [] } + } + + const command = findCommand(commands, pathTokens) + if (!command) throw usage(`unknown command "${pathTokens.join(' ')}"`) + const pathLength = command.path.length + const commandArgs = tokens.slice(pathLength) + if (helpRequested) return { command, flags: { help: true }, globalFlags: global, positionals: commandArgs } + + const { flags, positionals } = parseArgv(commandArgs, command.flags ?? []) + validateCommandInput(command, flags, positionals) + return { command, flags, globalFlags: global, positionals } +} + +export function stringFlag(flags: Record, name: string): string | undefined { + const value = flags[name] + return typeof value === 'string' ? value : undefined +} + +export function requiredStringFlag(flags: Record, name: string): string { + const value = stringFlag(flags, name) + if (!value) throw usage(`--${name} is required`) + return value +} + +export function numberFlag(flags: Record, name: string, fallback: number): number { + const value = flags[name] + return typeof value === 'number' && Number.isFinite(value) ? value : fallback +} + +export function stringListFlag(flags: Record, name: string): string[] { + const value = flags[name] + if (Array.isArray(value)) return value.map(String).filter(Boolean) + return typeof value === 'string' && value ? [value] : [] +} + +export function parseFlagValue(flag: FlagSpec, value: unknown): boolean | number | string { + if (flag.type === 'integer') { + const text = String(value).trim() + const parsed = /^-?\d+$/.test(text) ? Number(text) : NaN + if (!Number.isSafeInteger(parsed)) throw usage(`--${flag.name} must be an integer`) + return parsed + } + const parsed = flag.type === 'boolean' + ? typeof value === 'boolean' ? value : String(value) !== 'false' + : String(value) + if (flag.enum && !flag.enum.includes(String(parsed))) throw usage(`--${flag.name} must be one of: ${flag.enum.join(', ')}`) + return parsed +} + +function parseGlobalFlags(argv: string[]): GlobalFlags { + const raw = parseArgv(argv, globalFlagSpecs, { allowUnknownFlags: true }).flags + return { + debug: raw.debug === true, + dryRun: raw['dry-run'] === true, + events: raw.events === true, + force: raw.force === true, + json: raw.json === true, + noInput: raw['no-input'] === true, + plain: raw.plain === true, + safetyProfile: typeof raw['safety-profile'] === 'string' && raw['safety-profile'] ? raw['safety-profile'] : undefined, + target: typeof raw.target === 'string' && raw.target ? raw.target : undefined, + wrapUntrusted: raw['wrap-untrusted'] === true, + } +} + +function parseArgv( + argv: string[], + specs: FlagSpec[], + options: { allowUnknownFlags?: boolean } = {}, +): { flags: Record; positionals: string[] } { + const byName = flagMap(specs) + const flags: Record = {} + const positionals: string[] = [] + for (const spec of specs) { + if (spec.default !== undefined) flags[spec.name] = spec.default + } + + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index] + if (!token) continue + if (token === '--') { + positionals.push(...argv.slice(index + 1)) + break + } + if (!token.startsWith('-')) { + positionals.push(token) + continue + } + const parsed = flagToken(token) + const noPrefix = parsed.name.startsWith('no-') ? parsed.name.slice(3) : undefined + const spec = byName.get(parsed.name) ?? (noPrefix ? byName.get(noPrefix) : undefined) + if (!spec) { + if (!options.allowUnknownFlags) throw usage(`unknown flag --${parsed.name}`) + positionals.push(token) + continue + } + if (spec.type === 'boolean') { + const value = noPrefix && !byName.has(parsed.name) ? false : parsed.value === undefined ? true : parsed.value !== 'false' + setFlag(flags, spec, parseFlagValue(spec, value)) + continue + } + const value = parsed.value ?? argv[index + 1] + if (value === undefined || value.startsWith('-')) throw usage(`--${spec.name} requires a value`) + setFlag(flags, spec, parseFlagValue(spec, value)) + if (parsed.value === undefined) index += 1 + } + return { flags, positionals } +} + +function findCommand(commands: CommandSpec[], tokens: string[]): CommandSpec | undefined { + return commands + .filter(command => command.path.every((part, index) => tokens[index] === part)) + .sort((a, b) => b.path.length - a.path.length)[0] +} + +function validatePositionals(command: CommandSpec, values: string[]): void { + const args = command.args ?? [] + const required = args.filter(arg => arg.required).length + const variadic = args.some(arg => arg.variadic) + if (values.length < required) throw usage(`${command.path.join(' ')} requires ${args[values.length]?.name ?? 'more arguments'}`) + if (!variadic && values.length > args.length) throw usage(`${command.path.join(' ')} got too many arguments`) +} + +export function validateCommandInput(command: CommandSpec, flags: Record, positionals: string[]): void { + validatePositionals(command, positionals) + for (const flag of command.flags ?? []) { + const value = flags[flag.name] + if (flag.required && (value === undefined || value === '')) throw usage(`--${flag.name} is required`) + } +} + +function flagMap(specs: FlagSpec[]): Map { + const out = new Map() + for (const spec of specs) out.set(spec.name, spec) + return out +} + +function flagToken(token: string): { name: string; value?: string } { + const trimmed = token.replace(/^-+/, '') + const index = trimmed.indexOf('=') + if (index === -1) return { name: trimmed } + return { name: trimmed.slice(0, index), value: trimmed.slice(index + 1) } +} + +function setFlag(out: Record, spec: FlagSpec, value: boolean | number | string): void { + if (!spec.multiple) { + out[spec.name] = value + return + } + const current = Array.isArray(out[spec.name]) ? out[spec.name] as unknown[] : [] + out[spec.name] = [...current, value] +} diff --git a/packages/cli/src/cli/policy.ts b/packages/cli/src/cli/policy.ts new file mode 100644 index 00000000..998226b9 --- /dev/null +++ b/packages/cli/src/cli/policy.ts @@ -0,0 +1,53 @@ +import { existsSync, readFileSync } from 'node:fs' +import { dirname, isAbsolute, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { parse as parseYAML } from 'yaml' +import type { CommandSpec, GlobalFlags } from './types.js' +import { usage } from './output.js' + +export function enforcePolicy(command: CommandSpec, flags: GlobalFlags): void { + const profile = flags.safetyProfile ? loadSafetyProfile(flags.safetyProfile) : undefined + if (profile && !matchesPrefix(profile.allow, command.path)) { + throw usage(`command "${command.path.join(' ')}" is blocked by safety profile "${profile.name}"`) + } +} + +function loadSafetyProfile(nameOrPath: string): { allow: Set; name: string } { + const path = resolveProfilePath(nameOrPath) + if (!path) throw usage(`unknown safety profile "${nameOrPath}"`) + const root = parseYAML(readFileSync(path, 'utf8')) as Record | undefined + return { + allow: rules(root?.allow), + name: typeof root?.name === 'string' && root.name.trim() ? root.name.trim() : 'unnamed', + } +} + +function resolveProfilePath(nameOrPath: string): string | undefined { + if (isAbsolute(nameOrPath) || nameOrPath.includes('/')) return existsSync(nameOrPath) ? nameOrPath : undefined + const filename = nameOrPath.endsWith('.yaml') ? nameOrPath : `${nameOrPath}.yaml` + const here = dirname(fileURLToPath(import.meta.url)) + const path = join(dirname(dirname(here)), 'safety-profiles', filename) + return existsSync(path) ? path : undefined +} + +function rules(value: unknown): Set { + const out = new Set() + if (!Array.isArray(value)) return out + for (const item of value) { + const rule = normalizeRule(String(item)) + if (rule) out.add(rule) + } + return out +} + +function matchesPrefix(rules: Set, path: string[]): boolean { + if (rules.has('*') || rules.has('all')) return true + for (let index = 1; index <= path.length; index += 1) { + if (rules.has(path.slice(0, index).join('.'))) return true + } + return false +} + +function normalizeRule(value: string): string { + return value.trim().toLowerCase().replaceAll(/\s+/g, '.').replaceAll(/^\.+|\.+$/g, '') +} diff --git a/packages/cli/src/cli/schema.ts b/packages/cli/src/cli/schema.ts new file mode 100644 index 00000000..aba5c399 --- /dev/null +++ b/packages/cli/src/cli/schema.ts @@ -0,0 +1,102 @@ +import type { ArgSpec, CommandSpec, FlagSpec } from './types.js' +import { globalFlagSpecs } from './parse.js' + +type SchemaDoc = { + build: string + command: SchemaNode + schema_version: 1 +} + +type SchemaNode = { + flags?: SchemaFlag[] + help: string + name: string + path: string + positionals?: SchemaArg[] + requirements?: string[] + subcommands?: SchemaNode[] + type: 'application' | 'command' + usage?: string +} + +type SchemaFlag = { + default?: boolean | number | string + enum?: string[] + help?: string + multiple?: boolean + name: string + required?: boolean + type: string +} + +type SchemaArg = { + help?: string + name: string + required?: boolean + type: 'string' + variadic?: boolean +} + +export function buildSchema(commands: CommandSpec[], version: string, requested: string[] = []): SchemaDoc { + const filtered = requested.length + ? commands.filter(command => command.path.join('.').startsWith(requested.join('.'))) + : commands + return { + build: version, + command: nodeFor(filtered, requested, requested.length ? requested.at(-1) ?? 'beeper' : 'beeper'), + schema_version: 1, + } +} + +function nodeFor(commands: CommandSpec[], prefix: string[], name: string): SchemaNode { + const exact = commands.find(command => command.path.join('.') === prefix.join('.')) + const childNames = new Set() + for (const command of commands) { + const child = command.path[prefix.length] + if (child) childNames.add(child) + } + const children = [...childNames] + .sort() + .map(child => nodeFor(commands.filter(command => command.path[prefix.length] === child), [...prefix, child], child)) + + return { + flags: prefix.length === 0 ? schemaFlags(globalFlagSpecs) : schemaFlags(exact?.flags ?? []), + help: exact?.description ?? 'Beeper CLI', + name, + path: prefix.join(' '), + positionals: exact?.args?.map(schemaArg), + requirements: exact ? requirements(exact) : undefined, + subcommands: children.length ? children : undefined, + type: prefix.length === 0 ? 'application' : 'command', + usage: exact ? `beeper ${exact.path.join(' ')}` : undefined, + } +} + +function schemaFlags(flags: FlagSpec[]): SchemaFlag[] { + return flags.map(flag => ({ + default: flag.default, + enum: flag.enum, + help: flag.description, + multiple: flag.multiple, + name: flag.name, + required: flag.required, + type: flag.type, + })) +} + +function schemaArg(arg: ArgSpec): SchemaArg { + return { + help: arg.description, + name: arg.name, + required: arg.required, + type: 'string', + variadic: arg.variadic, + } +} + +function requirements(command: CommandSpec): string[] | undefined { + const out: string[] = [] + if (command.risk === 'write') out.push('write') + if (command.risk === 'destructive') out.push('destructive', 'force') + return out.length ? out : undefined +} diff --git a/packages/cli/src/cli/setup.ts b/packages/cli/src/cli/setup.ts new file mode 100644 index 00000000..68ccca2e --- /dev/null +++ b/packages/cli/src/cli/setup.ts @@ -0,0 +1,639 @@ +import { access } from 'node:fs/promises' +import { driveVerification, evaluateReadiness, type Readiness } from '../lib/app-state.js' +import { authFromToken, authorizeTarget, findLocalDesktop } from '../lib/desktop-auth.js' +import { installDesktop, installServer, readInstallations, type Installations } from '../lib/installations.js' +import { connectedAccountSummary, findLocalDesktopSession, localConnectedAccountSummary, localDesktopReadiness, type LocalDesktopSession } from '../lib/local-desktop.js' +import { renderStartupLogo } from '../lib/logo.js' +import { promptChoice, promptConfirm, promptText } from '../lib/prompts.js' +import { findDesktopAppPath, launchDesktopApp, startProfile } from '../lib/profiles.js' +import { SERVER_ENV_API_BASE_URLS, normalizeServerEnv } from '../lib/server-env.js' +import { finishEmailSetup, startEmailSetup, type SetupLoginResult } from '../lib/setup-login.js' +import { + builtInDesktopTargetID, + createDefaultDesktopTarget, + createProfileTarget, + listTargets, + publicTarget, + readConfig, + readTarget, + updateConfig, + writeTarget, + type ManagedTargetType, + type Target, +} from '../lib/targets.js' +import type { CommandContext } from './types.js' +import { usage, writeEvent } from './output.js' +import { stringFlag } from './parse.js' + +type SetupFlags = { + 'server-env': string + channel: string + debug: boolean + desktop: boolean + email?: string + events: boolean + install: boolean + json: boolean + local: boolean + oauth: boolean + remote?: string + server: boolean + target?: string + username?: string + force: boolean +} + +type PreparedLocalDesktopSetup = { + accounts: string[] + readiness: Readiness + session: LocalDesktopSession + target: Target +} + +type DesktopSetupDetection = + | { kind: 'session-found'; local: PreparedLocalDesktopSetup; serverInstalled: boolean } + | { kind: 'installed-not-running'; serverInstalled: boolean } + | { kind: 'running-signed-out'; readiness?: Readiness; serverInstalled: boolean } + | { kind: 'session-unreadable'; reason: string; readiness?: Readiness; serverInstalled: boolean } + | { kind: 'not-installed'; serverInstalled: boolean } + +type SetupAction = { command: string; id: string } + +export async function runSetup(ctx: CommandContext): Promise { + const flags = setupFlags(ctx) + const targetModeCount = [Boolean(flags.remote), flags.server, flags.desktop].filter(Boolean).length + if (targetModeCount > 1) throw usage('Specify at most one of --remote, --server, or --desktop') + const authModeCount = [flags.local, flags.oauth, Boolean(flags.email)].filter(Boolean).length + if (authModeCount > 1) throw usage('Specify at most one of --local, --oauth, or --email') + if ((flags.local || flags.oauth) && (flags.remote || flags.server || flags.desktop)) { + throw usage('Use --local or --oauth with an existing target, not with --remote, --server, or --desktop.') + } + if (ctx.globalFlags.dryRun) { + return { + dry_run: true, + op: 'setup', + request: { + authMode: flags.local ? 'local' : flags.oauth ? 'oauth' : flags.email ? 'email' : 'auto', + channel: flags.channel, + email: flags.email, + install: flags.install, + remote: flags.remote, + serverEnv: flags['server-env'], + target: flags.target, + targetMode: flags.remote ? 'remote' : flags.server ? 'server' : flags.desktop ? 'desktop' : 'selected', + username: flags.username, + force: flags.force, + }, + } + } + if (flags.events) writeEvent('setup_step', { step: 'start', target: flags.target }) + + if (flags.remote) return setupRemote(flags) + if (flags.server) return setupManaged('server', flags) + if (flags.desktop) return setupManaged('desktop', flags) + + const target = await setupTarget(flags) + if (flags.local) { + const prepared = await prepareLocalDesktopSetup(target, flags) + return printSetupResult(await commitLocalDesktopSetup(prepared), flags) + } + if (flags.oauth) return printSetupResult(await setupOAuthTarget(target, flags), flags) + if (flags.email) return printSetupResult(await setupEmailTarget(target, flags), flags) + return setupDefault(target, flags) +} + +function setupFlags(ctx: CommandContext): SetupFlags { + return { + 'server-env': stringFlag(ctx.flags, 'server-env') || 'prod', + channel: stringFlag(ctx.flags, 'channel') || 'stable', + debug: ctx.globalFlags.debug, + desktop: ctx.flags.desktop === true, + email: stringFlag(ctx.flags, 'email'), + events: ctx.globalFlags.events, + install: ctx.flags.install === true, + json: ctx.globalFlags.json, + local: ctx.flags.local === true, + oauth: ctx.flags.oauth === true, + remote: stringFlag(ctx.flags, 'remote'), + server: ctx.flags.server === true, + target: ctx.globalFlags.target, + username: stringFlag(ctx.flags, 'username'), + force: ctx.globalFlags.force, + } +} + +async function setupDefault(target: Target, flags: SetupFlags): Promise { + const setupCmd = setupCommand(target) + if (interactive(flags)) { + process.stdout.write(`${renderStartupLogo()}\n\n`) + process.stdout.write('Setup\n\n') + if (target.id !== builtInDesktopTargetID || flags.target) process.stdout.write(`Continuing setup for ${target.name ?? target.id}.\n\n`) + } + if (target.type === 'desktop') { + const detected = await detectDesktopSetup(target, flags) + if (detected.kind === 'session-found') { + const local = detected.local + if (flags.force) return printSetupResult(await commitLocalDesktopSetup(local), flags) + if (!interactive(flags)) return setupSessionFoundOutput(local, setupCmd, detected.serverInstalled) + printLocalDesktopPreview(local) + if (await promptConfirm('Use this Desktop session for CLI access?', true)) { + return printSetupResult(await commitLocalDesktopSetup(local), flags) + } + return printInteractiveSetupStatus( + local.readiness.state === 'ready' ? 'Beeper Desktop is ready' : `Setup paused: ${local.readiness.state}`, + setupDetailForReadiness(local.readiness), + ) + } + if (!interactive(flags)) return setupStateOutput(detected, target) + if (detected.kind === 'installed-not-running') { + printStatus('Found Beeper Desktop on this device.', 'installed, not running') + if (flags.force || await promptConfirm('Launch Beeper Desktop now?', true)) return launchAndPoll(target, setupCmd, flags) + } else if (detected.kind === 'running-signed-out') { + printStatus('Found Beeper Desktop on this device.', 'running, signed out') + if (flags.force || await promptConfirm('Open Beeper Desktop so you can sign in?', true)) return launchAndPoll(target, setupCmd, flags) + } else if (detected.kind === 'session-unreadable') { + printStatus('Found Beeper Desktop on this device.', 'signed in, but CLI could not read the local session') + process.stdout.write('You can still connect through Beeper Desktop.\n') + if (flags.debug) process.stdout.write(`\n${detected.reason}\n`) + process.stdout.write('\n') + if (flags.force || await promptConfirm('Connect through Beeper Desktop instead?', true)) { + return printSetupResult(await setupOAuthTarget(target, flags), flags) + } + } else if (detected.kind === 'not-installed') { + return setupFromChoice(flags) + } + } + + const readiness = await evaluateReadiness({ baseURL: target.baseURL, target: target.id }) + if (readiness.state === 'target-unreachable' && target.type !== 'desktop') { + if (!interactive(flags)) { + return currentTargetBrokenOutput(target, readiness, await isServerInstalled()) + } + if (await handleBrokenCurrentTarget(target, readiness, flags)) return undefined + } + if (readiness.state === 'target-unreachable' && target.type === 'desktop' && interactive(flags)) { + if (flags.force || await promptConfirm('Beeper Desktop is not reachable. Launch it now?', true)) { + return launchAndPoll(target, setupCmd, flags) + } + } + if (!interactive(flags)) return { readiness, target: publicTarget(target) } + return printInteractiveSetupStatus( + readiness.state === 'ready' ? 'Target ready' : `Setup paused: ${readiness.state}`, + setupDetailForReadiness(readiness), + ) +} + +async function setupRemote(flags: SetupFlags): Promise { + const name = flags.target ?? await uniqueRemoteName(flags.remote!) + if (interactive(flags)) { + process.stdout.write('Connecting to Desktop API on another device.\n\n') + process.stdout.write(`Name: ${name}\n`) + process.stdout.write(`URL: ${flags.remote!}\n\n`) + } + const target: Target = { + baseURL: flags.remote!, + id: name, + name, + type: 'remote', + } + const result = flags.email ? await setupEmailTarget(target, flags) : await setupOAuthTarget(target, flags) + if (!flags.target) await updateConfig(config => ({ ...config, defaultTarget: config.defaultTarget ?? target.id })) + return printSetupResult(result, flags) +} + +async function setupManaged(type: ManagedTargetType, flags: SetupFlags): Promise { + if (flags.install) { + if (!interactive(flags) && !flags.force) throw usage('Install requires --install --force in non-interactive mode.') + await installWithCopy(type, flags) + } + const id = flags.target ?? type + const target = await readTarget(id) ?? await createProfileTarget(type, id, { serverEnv: flags['server-env'], port: undefined }) + if (!flags.target) await updateConfig(config => ({ ...config, defaultTarget: config.defaultTarget ?? target.id })) + await startProfile(target).catch(error => { + if (type === 'desktop') return undefined + throw error + }) + if (flags.email) return printSetupResult(await setupEmailTarget(target, flags), flags) + return { readiness: await evaluateReadiness({ baseURL: target.baseURL, target: target.id }), target: publicTarget(target) } +} + +async function printSetupResult(result: SetupLoginResult, flags: SetupFlags): Promise { + result = await maybeDriveOnboarding(result, flags) + if (!interactive(flags)) return result + process.stdout.write(result.readiness.state === 'ready' + ? `Connected to ${result.target.name ?? result.target.id}\n` + : `Connected; setup paused: ${result.readiness.state}\n`) + const readinessDetail = setupDetailForReadiness(result.readiness) + const detail = result.accounts.length && readinessDetail + ? `Connected accounts: ${result.accounts.join(', ')}\n${readinessDetail}` + : result.accounts.length + ? `Connected accounts: ${result.accounts.join(', ')}` + : readinessDetail + if (detail) process.stdout.write(`${detail}\n`) + if (result.readiness.state === 'ready') { + process.stdout.write('\nNext:\n') + process.stdout.write(' beeper chats list\n') + process.stdout.write(' beeper send text --to --message "hello"\n') + } + return undefined +} + +async function setupFromChoice(flags: SetupFlags): Promise { + const serverInstalled = await isServerInstalled() + process.stdout.write('No usable Beeper Desktop session was found on this device.\n\n') + process.stdout.write('How do you want to connect Beeper CLI?\n\n') + process.stdout.write(' 1. Install Beeper Desktop\n') + process.stdout.write(` 2. ${serverInstalled ? 'Use installed local Beeper Server' : 'Install local Beeper Server'}\n`) + process.stdout.write(' 3. Connect with Desktop API on another device\n\n') + const defaultChoice = serverInstalled ? '2' : '1' + const choice = await promptChoice(`Choose [${defaultChoice}]: `, ['1', '2', '3'], { defaultValue: defaultChoice }) + if (choice === '1') { + if (!await promptConfirm('Install Beeper Desktop stable from beeper.com?', true)) return undefined + await installWithCopy('desktop', { ...flags, channel: 'stable' }) + const target = await setupTarget({ ...flags, desktop: true }) + return launchAndPoll(target, setupCommand(target), flags) + } + if (choice === '2') { + if (!serverInstalled) { + if (!await promptConfirm('Install local Beeper Server stable from beeper.com?', true)) return undefined + await installWithCopy('server', { ...flags, channel: 'stable', 'server-env': 'prod' }) + } + return setupManaged('server', { ...flags, install: false, server: true, channel: 'stable' }) + } + const url = await promptText('Desktop API URL: ') + if (!url) throw usage('Remote URL is required.') + return setupRemote({ ...flags, remote: url }) +} + +async function handleBrokenCurrentTarget(target: Target, readiness: Readiness, flags: SetupFlags): Promise { + const serverInstalled = await isServerInstalled() + process.stdout.write(`Beeper CLI is set up for ${target.name ?? target.id}, but it is not reachable.\n\n`) + if (readiness.message) process.stdout.write(`${readiness.message}\n\n`) + process.stdout.write('What do you want to do?\n\n') + process.stdout.write(` 1. Retry ${target.name ?? target.id}\n`) + process.stdout.write(' 2. Use Beeper Desktop on this device\n') + process.stdout.write(` 3. ${serverInstalled ? 'Use installed local Beeper Server' : 'Install local Beeper Server'}\n`) + process.stdout.write(' 4. Connect with Desktop API on another device\n\n') + const choice = await promptChoice('Choose [1]: ', ['1', '2', '3', '4'], { defaultValue: '1' }) + if (choice === '1') return false + if (choice === '2') { + const desktop = await createDefaultDesktopTarget() + await setupDefault(desktop, { ...flags, target: desktop.id }) + return true + } + if (choice === '3') { + if (!serverInstalled) { + if (!await promptConfirm('Install local Beeper Server stable from beeper.com?', true)) return true + await installWithCopy('server', { ...flags, channel: 'stable', 'server-env': 'prod' }) + } + await setupManaged('server', { ...flags, install: false, server: true, channel: 'stable' }) + return true + } + const url = await promptText('Desktop API URL: ') + if (!url) throw usage('Remote URL is required.') + await setupRemote({ ...flags, remote: url }) + return true +} + +async function setupTarget(flags: SetupFlags): Promise { + if (flags.target) { + const target = await readTarget(flags.target) + if (!target) throw usage(`Unknown Beeper target "${flags.target}". Run \`beeper targets list\`.`) + return target + } + const config = await readConfig() + if (config.defaultTarget) { + const target = await readTarget(config.defaultTarget) + if (target) return target + } + const desktop = await readTarget(builtInDesktopTargetID) + if (desktop) return desktop + const detected = await findLocalDesktop({ scan: true, timeoutMs: 300 }).catch(() => undefined) + return createDefaultDesktopTarget(detected?.baseURL) +} + +async function prepareLocalDesktopSetup(target: Target, flags: SetupFlags): Promise { + if (flags.events) writeEvent('setup_step', { step: 'local-desktop', target: target.id }) + const desktop = await findLocalDesktop({ baseURL: target.baseURL, scan: target.id === builtInDesktopTargetID, timeoutMs: 500 }).catch(() => undefined) + const resolvedTarget: Target = { + ...target, + baseURL: desktop?.baseURL ?? target.baseURL, + name: target.name ?? 'Beeper Desktop', + type: 'desktop', + } + const session = await findLocalDesktopSession(resolvedTarget) + const readiness = localDesktopReadiness(session) + const accounts = await localConnectedAccountSummary(session.dataDir).catch(() => []) + return { accounts, readiness, session, target: resolvedTarget } +} + +async function detectDesktopSetup(target: Target, flags: SetupFlags): Promise { + printProgress(flags, 'Checking Beeper Desktop') + const installations = await readInstallations().catch((): Installations => ({})) + const serverInstalled = await isServerInstalled(installations) + const appInstalled = Boolean(await findDesktopAppPath(installations)) + printProgress(flags, 'Reading local Desktop session') + const local = await prepareLocalDesktopSetup(target, flags).catch(error => ({ error })) + if (!('error' in local)) return { kind: 'session-found', local, serverInstalled } + + printProgress(flags, 'Checking Desktop readiness') + const desktop = await findLocalDesktop({ baseURL: target.baseURL, scan: target.id === builtInDesktopTargetID, timeoutMs: 500 }).catch(() => undefined) + if (desktop) { + const readiness = await evaluateReadiness({ baseURL: desktop.baseURL, target: target.id, token: false }) + if (readiness.state === 'needs-login') return { kind: 'running-signed-out', readiness, serverInstalled } + return { + kind: 'session-unreadable', + reason: local.error instanceof Error ? local.error.message : String(local.error), + readiness, + serverInstalled, + } + } + return appInstalled ? { kind: 'installed-not-running', serverInstalled } : { kind: 'not-installed', serverInstalled } +} + +async function isServerInstalled(installations?: Installations): Promise { + if (process.env.BEEPER_SERVER_BIN) return true + const installation = installations ?? await readInstallations().catch((): Installations => ({})) + return Boolean(installation.server?.path && await access(installation.server.path).then(() => true, () => false)) +} + +async function commitLocalDesktopSetup(prepared: PreparedLocalDesktopSetup): Promise { + await writeTarget({ ...prepared.target, auth: prepared.session.auth }) + await updateConfig(config => ({ ...config, defaultTarget: config.defaultTarget ?? prepared.target.id })) + return { + accounts: prepared.accounts, + readiness: prepared.readiness, + target: publicTarget({ ...prepared.target, auth: prepared.session.auth }), + } +} + +async function setupOAuthTarget(target: Target, flags: SetupFlags): Promise { + if (flags.events) writeEvent('setup_step', { step: 'oauth', target: target.id }) + if (!interactive(flags) && !flags.force) throw usage('OAuth setup requires an interactive terminal or --force to open the browser.') + const auth = authFromToken(await authorizeTarget({ + baseURL: target.baseURL, + scan: target.type === 'desktop' && target.id === builtInDesktopTargetID, + }), target.type === 'remote' ? 'remote-oauth' : 'desktop-oauth') + await writeTarget({ ...target, auth }) + const [readiness, accounts] = await Promise.all([ + evaluateReadiness({ baseURL: target.baseURL, target: target.id, token: auth.accessToken }), + connectedAccountSummary(target, auth).catch(() => []), + ]) + return { accounts, readiness, target: publicTarget({ ...target, auth }) } +} + +async function setupEmailTarget(target: Target, flags: SetupFlags): Promise { + if (flags.events) writeEvent('setup_step', { step: 'email', target: target.id }) + const email = flags.email + if (!email) throw usage('Email setup requires --email.') + if (!interactive(flags)) throw usage('Email setup prompts for the verification code. For automation, use `beeper auth email start` and `beeper auth email response`.') + const start = await startEmailSetup(target, email) + return finishEmailSetup(target, { + code: await promptText('Email code: '), + force: flags.force, + json: flags.json, + setupRequestID: start.setupRequestID, + username: flags.username, + }) +} + +function printLocalDesktopPreview(prepared: PreparedLocalDesktopSetup): void { + process.stdout.write('Found Beeper Desktop on this device.\n\n') + process.stdout.write(`Status: ${prepared.readiness.state === 'ready' ? 'signed in and ready' : prepared.readiness.state}\n`) + if (prepared.session.userID) process.stdout.write(`Signed in as: ${prepared.session.userID}\n`) + if (prepared.accounts.length) process.stdout.write(`Connected accounts: ${prepared.accounts.join(', ')}\n`) + process.stdout.write('\n') +} + +function setupSessionFoundOutput(local: PreparedLocalDesktopSetup, setupCmd: string, serverInstalled: boolean): Record { + const availableActions = [ + { id: 'use-desktop-session', command: `${setupCmd} --local` }, + { id: 'desktop-oauth', command: `${setupCmd} --oauth` }, + { id: 'connect-remote', command: 'beeper setup --remote ' }, + ] + if (serverInstalled) availableActions.push(installedServerAction(true)) + return { + availableActions, + localDesktop: { + authSource: local.session.auth.source, + baseURL: local.target.baseURL, + connectedAccounts: local.accounts, + dataDir: local.session.dataDir, + signedInAs: local.session.userID, + }, + message: local.readiness.state === 'ready' + ? 'Beeper Desktop is signed in and ready.' + : 'Beeper Desktop is signed in, but setup is not finished.', + readiness: local.readiness, + recommendedAction: { id: 'use-desktop-session', command: `${setupCmd} --local` }, + state: local.readiness.state === 'ready' ? 'desktop-ready' : 'desktop-session-found', + target: publicTarget(local.target), + } +} + +function printStatus(title: string, status: string): void { + process.stdout.write(`${title}\n\n`) + process.stdout.write(`Status: ${status}\n\n`) +} + +function printProgress(flags: SetupFlags, message: string): void { + if (!interactive(flags)) return + process.stdout.write(`${message}...\n`) +} + +function printInteractiveSetupStatus(message: string, detail?: string): undefined { + process.stdout.write(`${message}\n`) + if (detail) process.stdout.write(`${detail}\n`) + return undefined +} + +async function launchAndPoll(target: Target, setupCmd: string, flags: SetupFlags): Promise { + if (flags.events) writeEvent('setup_step', { step: 'launch', target: target.id }) + if (interactive(flags)) process.stdout.write('Opening Beeper Desktop...\n') + await launchDesktopApp(target) + const readiness = await pollReadiness(target, 10_000) + const detail = readiness.state === 'target-unreachable' + ? `Run \`${setupCmd}\` again after Beeper Desktop finishes starting.` + : setupDetailForReadiness(readiness) + if (!interactive(flags)) return { readiness, target: publicTarget(target) } + process.stdout.write('Launched Beeper Desktop\n') + if (detail) process.stdout.write(`${detail}\n`) + if (readiness.state === 'target-unreachable') { + process.stdout.write('\nNext:\n') + process.stdout.write(` ${setupCmd}\n`) + process.stdout.write(' beeper status\n') + } + return undefined +} + +async function pollReadiness(target: Target, timeoutMs: number): Promise { + const started = Date.now() + let readiness = await evaluateReadiness({ baseURL: target.baseURL, target: target.id, token: false }) + while (readiness.state === 'target-unreachable' && Date.now() - started < timeoutMs) { + await new Promise(resolve => setTimeout(resolve, 500)) + readiness = await evaluateReadiness({ baseURL: target.baseURL, target: target.id, token: false }) + } + return readiness +} + +async function maybeDriveOnboarding(result: SetupLoginResult, flags: SetupFlags): Promise { + if (!interactive(flags)) return result + if (result.readiness.state !== 'needs-verification' && result.readiness.state !== 'verification-in-progress') return result + process.stdout.write('Continuing verification...\n\n') + await driveVerification({ baseURL: result.target.baseURL, target: result.target.id, force: flags.force }) + return { + ...result, + readiness: await evaluateReadiness({ baseURL: result.target.baseURL, target: result.target.id }), + target: result.target, + } +} + +async function installWithCopy(type: ManagedTargetType, flags: SetupFlags): Promise { + const label = type === 'desktop' ? 'Beeper Desktop' : 'local Beeper Server' + const channel = flags.channel === 'nightly' ? 'nightly' : 'stable' + const serverEnv = normalizeServerEnv(flags['server-env']) + const source = type === 'server' ? new URL(SERVER_ENV_API_BASE_URLS[serverEnv]).host : 'beeper.com' + if (interactive(flags)) process.stdout.write(`Installing ${label} ${channel} from ${source}...\n`) + if (type === 'desktop') await installDesktop({ channel, serverEnv }) + else await installServer({ channel, serverEnv }) + if (interactive(flags)) process.stdout.write(`Installed ${label} ${channel}.\n\n`) +} + +function interactive(flags: SetupFlags): boolean { + return !flags.json && process.stdin.isTTY +} + +function setupStateOutput(detected: Exclude, target: Target): Record { + if (detected.kind === 'installed-not-running') { + const serverAction = installedServerAction(detected.serverInstalled) + return setupActionEnvelope({ + availableActions: [ + { id: 'launch-desktop', command: 'beeper setup --desktop --force' }, + { id: 'connect-remote', command: 'beeper setup --remote ' }, + serverAction, + ], + message: 'Beeper Desktop is installed but not running.', + recommendedAction: { id: 'launch-desktop', command: 'beeper setup --desktop --force' }, + state: 'desktop-installed-not-running', + target, + }) + } + if (detected.kind === 'running-signed-out') { + const availableActions = [ + { id: 'open-desktop', command: 'beeper setup --desktop --force' }, + { id: 'connect-remote', command: 'beeper setup --remote ' }, + ] + if (detected.serverInstalled) availableActions.push(installedServerAction(true)) + return setupActionEnvelope({ + availableActions, + message: 'Beeper Desktop is running but not signed in.', + readiness: detected.readiness, + recommendedAction: { id: 'open-desktop', command: 'beeper setup --desktop --force' }, + state: 'desktop-running-signed-out', + target, + }) + } + if (detected.kind === 'session-unreadable') { + const availableActions = [ + { id: 'desktop-oauth', command: 'beeper setup --oauth --force' }, + { id: 'connect-remote', command: 'beeper setup --remote ' }, + ] + if (detected.serverInstalled) availableActions.push(installedServerAction(true)) + return setupActionEnvelope({ + availableActions, + detail: detected.reason, + message: 'Beeper Desktop is running, but CLI could not read the local session.', + readiness: detected.readiness, + recommendedAction: { id: 'desktop-oauth', command: 'beeper setup --oauth --force' }, + state: 'desktop-running-session-unreadable', + target, + }) + } + const serverAction = installedServerAction(detected.serverInstalled) + return setupActionEnvelope({ + availableActions: [ + { id: 'install-desktop', command: 'beeper setup --desktop --install --force' }, + serverAction, + { id: 'connect-remote', command: 'beeper setup --remote ' }, + ], + message: 'No Beeper Desktop installation was found on this device.', + recommendedAction: detected.serverInstalled ? serverAction : { id: 'install-desktop', command: 'beeper setup --desktop --install --force' }, + state: 'desktop-not-installed', + target, + }) +} + +function installedServerAction(installed: boolean): SetupAction { + return installed + ? { id: 'use-installed-server', command: 'beeper setup --server --force' } + : { id: 'install-server', command: 'beeper setup --server --install --force' } +} + +function currentTargetBrokenOutput(target: Target, readiness: Readiness, serverInstalled: boolean): Record { + return { + availableActions: [ + { id: 'retry-current', command: `beeper setup --target ${target.id}` }, + { id: 'use-desktop', command: 'beeper setup --desktop' }, + installedServerAction(serverInstalled), + { id: 'connect-remote', command: 'beeper setup --remote ' }, + ], + message: `Beeper CLI is set up for ${target.name ?? target.id}, but it is not reachable.`, + readiness, + recommendedAction: { id: 'retry-current', command: `beeper setup --target ${target.id}` }, + state: 'current-target-unreachable', + target: publicTarget(target), + } +} + +function setupActionEnvelope(options: { + availableActions: SetupAction[] + detail?: string + message: string + readiness?: Readiness + recommendedAction: SetupAction + state: string + target: Target +}): Record { + return { + availableActions: options.availableActions, + detail: options.detail, + message: options.message, + readiness: options.readiness, + recommendedAction: options.recommendedAction, + state: options.state, + target: publicTarget(options.target), + } +} + +function setupDetailForReadiness(readiness: Readiness): string | undefined { + if (readiness.state === 'needs-login') return 'Sign in to Beeper Desktop, then run `beeper setup` again.' + if (readiness.state === 'needs-verification' || readiness.state === 'verification-in-progress') return 'Continue verification to finish setup.' + if (readiness.state === 'needs-recovery-key' || readiness.state === 'needs-secrets') return 'Finish recovery in Beeper, then run `beeper setup` again.' + if (readiness.state === 'needs-cross-signing-setup') return 'Finish cross-signing setup in Beeper, then run `beeper setup` again.' + if (readiness.state === 'needs-first-sync' || readiness.state === 'initializing') return 'Beeper is still syncing. You can rerun `beeper setup` at any time.' + return readiness.message +} + +async function uniqueRemoteName(url: string): Promise { + const base = remoteName(url) + const targets = await listTargets() + const ids = new Set(targets.map(target => target.id)) + if (!ids.has(base)) return base + for (let index = 2; index < 100; index += 1) { + const id = `${base}-${index}` + if (!ids.has(id)) return id + } + return `remote-${Date.now()}` +} + +function setupCommand(target: Target): string { + return target.id === builtInDesktopTargetID ? 'beeper setup' : `beeper setup --target ${target.id}` +} + +function remoteName(url: string): string { + try { + return new URL(url).hostname.replace(/[^a-zA-Z0-9._-]/g, '-') || 'remote' + } catch { + return 'remote' + } +} diff --git a/packages/cli/src/cli/types.ts b/packages/cli/src/cli/types.ts new file mode 100644 index 00000000..e3bb43d8 --- /dev/null +++ b/packages/cli/src/cli/types.ts @@ -0,0 +1,49 @@ +export type FlagSpec = { + name: string + default?: boolean | number | string + description?: string + enum?: string[] + multiple?: boolean + required?: boolean + type: 'boolean' | 'string' | 'integer' +} + +export type ArgSpec = { + name: string + description?: string + required?: boolean + variadic?: boolean +} + +export type CommandRisk = 'read' | 'write' | 'destructive' + +export type CommandContext = { + args: string[] + commandPath: string[] + flags: Record + globalFlags: GlobalFlags +} + +export type CommandSpec = { + args?: ArgSpec[] + description: string + examples?: string[] + flags?: FlagSpec[] + mcp?: boolean + path: string[] + risk: CommandRisk + run(ctx: CommandContext): Promise +} + +export type GlobalFlags = { + debug: boolean + dryRun: boolean + events: boolean + force: boolean + json: boolean + noInput: boolean + plain: boolean + safetyProfile?: string + target?: string + wrapUntrusted: boolean +} diff --git a/packages/cli/src/commands.generated.ts b/packages/cli/src/commands.generated.ts deleted file mode 100644 index b327a90e..00000000 --- a/packages/cli/src/commands.generated.ts +++ /dev/null @@ -1,251 +0,0 @@ -import Command0 from './commands/accounts/add.js' -import Command1 from './commands/accounts/list.js' -import Command2 from './commands/accounts/remove.js' -import Command3 from './commands/accounts/show.js' -import Command4 from './commands/accounts/use.js' -import Command5 from './commands/api/get.js' -import Command6 from './commands/api/post.js' -import Command7 from './commands/api/request.js' -import Command8 from './commands/auth/email/response.js' -import Command9 from './commands/auth/email/start.js' -import Command10 from './commands/auth/logout.js' -import Command11 from './commands/auth/status.js' -import Command12 from './commands/autocomplete.js' -import Command13 from './commands/bridges/config.js' -import Command14 from './commands/bridges/delete.js' -import Command15 from './commands/bridges/list.js' -import Command16 from './commands/bridges/login.js' -import Command17 from './commands/bridges/login-password.js' -import Command18 from './commands/bridges/logout.js' -import Command19 from './commands/bridges/proxy.js' -import Command20 from './commands/bridges/register.js' -import Command21 from './commands/bridges/run.js' -import Command22 from './commands/bridges/show.js' -import Command23 from './commands/bridges/whoami.js' -import Command24 from './commands/chats/archive.js' -import Command25 from './commands/chats/avatar.js' -import Command26 from './commands/chats/description.js' -import Command27 from './commands/chats/disappear.js' -import Command28 from './commands/chats/draft.js' -import Command29 from './commands/chats/focus.js' -import Command30 from './commands/chats/list.js' -import Command31 from './commands/chats/mark-read.js' -import Command32 from './commands/chats/mark-unread.js' -import Command33 from './commands/chats/mute.js' -import Command34 from './commands/chats/notify-anyway.js' -import Command35 from './commands/chats/pin.js' -import Command36 from './commands/chats/priority.js' -import Command37 from './commands/chats/remind.js' -import Command38 from './commands/chats/rename.js' -import Command39 from './commands/chats/search.js' -import Command40 from './commands/chats/show.js' -import Command41 from './commands/chats/start.js' -import Command42 from './commands/chats/unarchive.js' -import Command43 from './commands/chats/unmute.js' -import Command44 from './commands/chats/unpin.js' -import Command45 from './commands/chats/unremind.js' -import Command46 from './commands/completion.js' -import Command47 from './commands/config/get.js' -import Command48 from './commands/config/path.js' -import Command49 from './commands/config/reset.js' -import Command50 from './commands/config/set.js' -import Command51 from './commands/contacts/list.js' -import Command52 from './commands/contacts/search.js' -import Command53 from './commands/contacts/show.js' -import Command54 from './commands/docs.js' -import Command55 from './commands/doctor.js' -import Command56 from './commands/export.js' -import Command57 from './commands/install/desktop.js' -import Command58 from './commands/install/server.js' -import Command59 from './commands/man.js' -import Command60 from './commands/media/download.js' -import Command61 from './commands/messages/context.js' -import Command62 from './commands/messages/delete.js' -import Command63 from './commands/messages/edit.js' -import Command64 from './commands/messages/export.js' -import Command65 from './commands/messages/list.js' -import Command66 from './commands/messages/search.js' -import Command67 from './commands/messages/show.js' -import Command68 from './commands/plugins.js' -import Command69 from './commands/plugins/available.js' -import Command70 from './commands/presence.js' -import Command71 from './commands/resolve/account.js' -import Command72 from './commands/resolve/bridge.js' -import Command73 from './commands/resolve/chat.js' -import Command74 from './commands/resolve/contact.js' -import Command75 from './commands/resolve/target.js' -import Command76 from './commands/rpc.js' -import Command77 from './commands/schema.js' -import Command78 from './commands/send/file.js' -import Command79 from './commands/send/react.js' -import Command80 from './commands/send/sticker.js' -import Command81 from './commands/send/text.js' -import Command82 from './commands/send/unreact.js' -import Command83 from './commands/send/voice.js' -import Command84 from './commands/setup.js' -import Command85 from './commands/status.js' -import Command86 from './commands/targets/add/desktop.js' -import Command87 from './commands/targets/add/remote.js' -import Command88 from './commands/targets/add/server.js' -import Command89 from './commands/targets/disable.js' -import Command90 from './commands/targets/enable.js' -import Command91 from './commands/targets/list.js' -import Command92 from './commands/targets/logs.js' -import Command93 from './commands/targets/remove.js' -import Command94 from './commands/targets/restart.js' -import Command95 from './commands/targets/show.js' -import Command96 from './commands/targets/start.js' -import Command97 from './commands/targets/status.js' -import Command98 from './commands/targets/stop.js' -import Command99 from './commands/targets/use.js' -import Command100 from './commands/update.js' -import Command101 from './commands/verify.js' -import Command102 from './commands/verify/approve.js' -import Command103 from './commands/verify/cancel.js' -import Command104 from './commands/verify/list.js' -import Command105 from './commands/verify/qr-confirm.js' -import Command106 from './commands/verify/qr-scan.js' -import Command107 from './commands/verify/recovery-key.js' -import Command108 from './commands/verify/reset-recovery-key.js' -import Command109 from './commands/verify/sas.js' -import Command110 from './commands/verify/sas-confirm.js' -import Command111 from './commands/verify/show.js' -import Command112 from './commands/verify/start.js' -import Command113 from './commands/verify/status.js' -import Command114 from './commands/version.js' -import Command115 from './commands/watch.js' - -export const commands = { - 'accounts': Command1, - 'accounts:add': Command0, - 'accounts:chats': Command30, - 'accounts:list': Command1, - 'accounts:remove': Command2, - 'accounts:show': Command3, - 'accounts:use': Command4, - 'api:get': Command5, - 'api:post': Command6, - 'api:request': Command7, - 'auth:email:response': Command8, - 'auth:email:start': Command9, - 'auth:logout': Command10, - 'auth:status': Command11, - 'autocomplete': Command12, - 'bridges': Command15, - 'bridges:c': Command13, - 'bridges:config': Command13, - 'bridges:d': Command14, - 'bridges:delete': Command14, - 'bridges:l': Command16, - 'bridges:list': Command15, - 'bridges:login': Command16, - 'bridges:login-password': Command17, - 'bridges:logout': Command18, - 'bridges:p': Command17, - 'bridges:proxy': Command19, - 'bridges:r': Command20, - 'bridges:register': Command20, - 'bridges:run': Command21, - 'bridges:show': Command22, - 'bridges:w': Command23, - 'bridges:whoami': Command23, - 'bridges:x': Command19, - 'chats': Command30, - 'chats:archive': Command24, - 'chats:avatar': Command25, - 'chats:description': Command26, - 'chats:disappear': Command27, - 'chats:draft': Command28, - 'chats:focus': Command29, - 'chats:list': Command30, - 'chats:mark-read': Command31, - 'chats:mark-unread': Command32, - 'chats:mute': Command33, - 'chats:notify-anyway': Command34, - 'chats:pin': Command35, - 'chats:priority': Command36, - 'chats:remind': Command37, - 'chats:rename': Command38, - 'chats:search': Command39, - 'chats:show': Command40, - 'chats:start': Command41, - 'chats:unarchive': Command42, - 'chats:unmute': Command43, - 'chats:unpin': Command44, - 'chats:unremind': Command45, - 'completion': Command46, - 'config:get': Command47, - 'config:path': Command48, - 'config:reset': Command49, - 'config:set': Command50, - 'contacts': Command51, - 'contacts:list': Command51, - 'contacts:search': Command52, - 'contacts:show': Command53, - 'docs': Command54, - 'doctor': Command55, - 'export': Command56, - 'install:desktop': Command57, - 'install:server': Command58, - 'ls': Command30, - 'man': Command59, - 'media:download': Command60, - 'messages:context': Command61, - 'messages:delete': Command62, - 'messages:edit': Command63, - 'messages:export': Command64, - 'messages:list': Command65, - 'messages:search': Command66, - 'messages:show': Command67, - 'plugins': Command68, - 'plugins:available': Command69, - 'presence': Command70, - 'resolve:account': Command71, - 'resolve:bridge': Command72, - 'resolve:chat': Command73, - 'resolve:contact': Command74, - 'resolve:target': Command75, - 'rpc': Command76, - 'schema': Command77, - 'search': Command66, - 'send': Command81, - 'send:file': Command78, - 'send:react': Command79, - 'send:sticker': Command80, - 'send:text': Command81, - 'send:unreact': Command82, - 'send:voice': Command83, - 'setup': Command84, - 'status': Command85, - 'targets': Command91, - 'targets:add:desktop': Command86, - 'targets:add:remote': Command87, - 'targets:add:server': Command88, - 'targets:disable': Command89, - 'targets:enable': Command90, - 'targets:list': Command91, - 'targets:logs': Command92, - 'targets:remove': Command93, - 'targets:restart': Command94, - 'targets:show': Command95, - 'targets:start': Command96, - 'targets:status': Command97, - 'targets:stop': Command98, - 'targets:use': Command99, - 'update': Command100, - 'verify': Command101, - 'verify:approve': Command102, - 'verify:cancel': Command103, - 'verify:list': Command104, - 'verify:qr-confirm': Command105, - 'verify:qr-scan': Command106, - 'verify:recovery-key': Command107, - 'verify:reset-recovery-key': Command108, - 'verify:sas': Command109, - 'verify:sas-confirm': Command110, - 'verify:show': Command111, - 'verify:start': Command112, - 'verify:status': Command113, - 'version': Command114, - 'watch': Command115, -} diff --git a/packages/cli/src/commands/_complete.ts b/packages/cli/src/commands/_complete.ts deleted file mode 100644 index a422828a..00000000 --- a/packages/cli/src/commands/_complete.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { Args, Command, Flags } from '@oclif/core' -import { resolveTarget, listTargets } from '../lib/targets.js' -import { BeeperDesktop } from '@beeper/desktop-api' - -type Kind = 'chat' | 'account' | 'contact' | 'target' - -export default class Complete extends Command { - static override hidden = true - static override summary = 'Internal: emit completion suggestions for chats/contacts/accounts/targets' - static override description = 'Used by the semantic shell completion wrapper. Prints one suggestion per line as `id\\tdescription`. Stays silent on any error so the shell completion never produces noise.' - static override args = { - kind: Args.string({ required: true, description: 'chat|account|contact|target', options: ['chat', 'account', 'contact', 'target'] }), - } - static override flags = { - query: Flags.string({ description: 'Filter suggestions by a substring (already-typed prefix)' }), - target: Flags.string({ description: 'Target name override' }), - limit: Flags.integer({ default: 25, description: 'Max suggestions to print' }), - 'timeout-ms': Flags.integer({ default: 1500, description: 'Live-fetch timeout' }), - } - - async run(): Promise { - const { args, flags } = await this.parse(Complete) - const kind = args.kind as Kind - try { - const lines = await Promise.race([ - emit(kind, flags), - new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), flags['timeout-ms'])), - ]) - const filtered = flags.query ? lines.filter(line => fuzzy(line, flags.query!)) : lines - for (const line of filtered.slice(0, flags.limit)) process.stdout.write(`${line}\n`) - } catch { - // intentionally silent: completion noise is worse than no suggestions - } - } -} - -async function emit(kind: Kind, flags: { target?: string }): Promise { - if (kind === 'target') { - const targets = await listTargets() - return targets.map(t => `${t.id}\t${t.type} ${t.baseURL}`) - } - const target = await resolveTarget({ target: flags.target }) - const token = target.auth?.accessToken - if (!token) return [] - const client = new BeeperDesktop({ baseURL: target.baseURL, accessToken: token, logLevel: 'warn' }) - - if (kind === 'account') { - const list = await client.accounts.list() - const rows = Array.isArray(list) ? list : ((list as { items?: unknown[] }).items ?? []) - return rows - .map((row): string | undefined => { - if (!row || typeof row !== 'object') return undefined - const r = row as Record - const label = [r.network, r.user && typeof r.user === 'object' ? (r.user as Record).displayName : undefined].filter(Boolean).join(' ') - return r.accountID ? `${String(r.accountID)}\t${label}` : undefined - }) - .filter((line): line is string => !!line) - } - - if (kind === 'chat') { - const out: string[] = [] - for await (const chat of client.chats.list({ limit: 50 } as never)) { - const c = chat as unknown as Record - const id = c.localChatID || c.id - const title = (c.title as string | undefined) ?? '' - const network = (c.network as string | undefined) ?? '' - if (id) out.push(`${String(id)}\t${title}${network ? ` (${network})` : ''}`) - if (out.length >= 50) break - } - return out - } - - if (kind === 'contact') { - const accountsList = await client.accounts.list() - const accountIDs = (Array.isArray(accountsList) ? accountsList : ((accountsList as { items?: unknown[] }).items ?? [])) - .map((row: unknown) => (row && typeof row === 'object' ? (row as Record).accountID : undefined)) - .filter((id): id is string => typeof id === 'string') - const out: string[] = [] - for (const accountID of accountIDs.slice(0, 3)) { - const page = await client.accounts.contacts.list(accountID, { limit: 25 } as never) - const rows = Array.isArray(page) ? page : ((page as { items?: unknown[] }).items ?? []) - for (const contact of rows) { - const c = contact as Record - const id = c.id || c.username - const name = (c.fullName as string | undefined) || (c.displayName as string | undefined) || '' - if (id) out.push(`${String(id)}\t${name}`) - if (out.length >= 50) break - } - if (out.length >= 50) break - } - return out - } - - return [] -} - -function fuzzy(line: string, query: string): boolean { - const q = query.trim().toLowerCase() - if (!q) return true - return line.toLowerCase().includes(q) -} diff --git a/packages/cli/src/commands/accounts/add.ts b/packages/cli/src/commands/accounts/add.ts deleted file mode 100644 index d28138e0..00000000 --- a/packages/cli/src/commands/accounts/add.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { createInterface } from 'node:readline/promises' -import { stdin as input, stdout as output } from 'node:process' -import { Args, Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import type { Bridge, LoginFlow } from '@beeper/desktop-api/resources/bridges.js' -import { createClient } from '../../lib/client.js' -import { printAccountLoginStep, runGuidedAccountLogin } from '../../lib/account-login.js' -import { printData, printDryRun } from '../../lib/output.js' - -type AccountType = Bridge - -export default class AccountsAdd extends BeeperCommand { - static override summary = 'Connect a chat account by bridge' - static override description = '`accounts add` without an argument opens the guided bridge chooser. Pass a bridge ID when you already know which chat network connector to use.' - static override args = { - bridge: Args.string({ description: 'Bridge ID, network, or type to connect. Omit to list available bridges.' }), - } - static override flags = { - cookie: Flags.string({ description: 'Cookie value for non-interactive login, in name=value form. Repeat for multiple cookies.', multiple: true }), - field: Flags.string({ description: 'Field value for non-interactive login, in id=value form. Repeat for multiple fields.', multiple: true }), - flow: Flags.string({ description: 'Login flow ID. If omitted, Desktop chooses the default flow.' }), - guided: Flags.boolean({ default: true, allowNo: true, description: 'Prompt through login steps until completion' }), - 'login-id': Flags.string({ description: 'Existing login ID to re-login as' }), - 'non-interactive': Flags.boolean({ default: false, description: 'Do not prompt; require --flow, --field, and --cookie values when needed.' }), - webview: Flags.boolean({ default: false, description: 'Use Bun.WebView to collect cookie login fields when a cookie step is returned.' }), - 'webview-backend': Flags.string({ default: 'chrome', description: 'Bun.WebView backend for cookie login steps.', options: ['auto', 'chrome', 'webkit'] }), - 'webview-timeout': Flags.integer({ default: 120, description: 'Seconds to wait for Bun.WebView cookie collection.' }), - } - - async run(): Promise { - const { args, flags } = await this.parse(AccountsAdd) - const client = await createClient(flags) - - if (!args.bridge) { - const bridges = await client.bridges.list() - if (flags.json) { - await printData(bridges, 'json') - return - } - if (flags.guided && !flags['non-interactive'] && process.stdin.isTTY) { - args.bridge = await chooseAccountType(bridges.items) - } else { - printAvailableAccounts(bridges.items) - return - } - } - - const bridges = await client.bridges.list() - const accountType = resolveAccountType(bridges.items, args.bridge) - if (accountType.status !== 'available') { - const suffix = accountType.statusText ? `: ${accountType.statusText}` : '' - throw new Error(`${accountType.displayName} is not available${suffix}`) - } - - let flowID = flags.flow - if (!flowID) { - const flows = await client.bridges.loginFlows.list(accountType.id) - const loginFlows = flows.items - if (loginFlows.length > 1) { - if (flags.guided && !flags.json && !flags['non-interactive']) flowID = await chooseLoginFlow(loginFlows) - else throw new Error(`Multiple sign-in methods are available for ${accountType.displayName}. Pass --flow.`) - } else { - flowID = loginFlows[0]?.id - } - if (!flowID) throw new Error(`No login flows returned for ${accountType.displayName}.`) - if (!flags.json && loginFlows.length > 1) this.log(`Using flow ${flowID}`) - } - - if (flags['dry-run']) { - await printDryRun('accounts.add', { - bridgeID: accountType.id, - bridgeName: accountType.displayName, - flowID, - loginID: flags['login-id'], - guided: flags.guided, - nonInteractive: flags['non-interactive'], - cookieKeys: Object.keys(parseKeyValueFlags(flags.cookie, '--cookie')), - fieldKeys: Object.keys(parseKeyValueFlags(flags.field, '--field')), - webview: flags.webview, - webviewBackend: flags['webview-backend'], - }, flags.json ? 'json' : 'human') - return - } - - ensureWritable(flags) - const step = await client.bridges.loginSessions.create(accountType.id, { - flowID, - loginID: flags['login-id'], - }) - const result = flags.guided ? await runGuidedAccountLogin(client, accountType.id, step, { - cookies: parseKeyValueFlags(flags.cookie, '--cookie'), - fields: parseKeyValueFlags(flags.field, '--field'), - nonInteractive: flags['non-interactive'], - webview: flags.webview, - webviewBackend: flags['webview-backend'] as 'auto' | 'chrome' | 'webkit', - webviewTimeoutMs: flags['webview-timeout'] * 1000, - }) : step - if (flags.json) await printData(result, 'json') - else await printAccountLoginStep(result) - } -} - -async function chooseAccountType(items: AccountType[]): Promise { - const available = items.filter(item => item.status === 'available') - if (!available.length) throw new Error('No available bridges to connect.') - - process.stdout.write('Choose a bridge to connect an account:\n') - available.forEach((account, index) => { - const multiple = account.supportsMultipleAccounts ? 'multiple allowed' : 'single account' - process.stdout.write(` ${index + 1}. ${account.displayName} (${account.id}) - ${multiple}\n`) - }) - - const rl = createInterface({ input, output }) - try { - for (;;) { - const answer = (await rl.question('Select a bridge: ')).trim() - const selected = /^\d+$/.test(answer) ? Number.parseInt(answer, 10) : Number.NaN - if (Number.isInteger(selected) && selected >= 1 && selected <= available.length) return available[selected - 1]!.id - const byID = available.find(account => account.id === answer) - if (byID) return byID.id - process.stdout.write('Choose one of the listed bridges.\n') - } - } finally { - rl.close() - } -} - -function printAvailableAccounts(items: AccountType[]): void { - const sections: Array<[string, AccountType[]]> = [ - ['On-Device Accounts', items.filter(item => item.provider === 'local')], - ['Beeper Cloud Accounts', items.filter(item => item.provider === 'cloud')], - ['Self-Hosted Accounts', items.filter(item => item.provider === 'self-hosted')], - ] - - process.stdout.write('Choose a bridge to connect an account:\n\n') - for (const [title, accounts] of sections) { - if (!accounts.length) continue - process.stdout.write(`${title}\n`) - for (const account of accounts) { - const status = account.statusText ?? statusLabel(account) - const command = account.status === 'available' ? `beeper accounts add ${account.id}` : undefined - const multiple = account.supportsMultipleAccounts ? 'multiple allowed' : 'single account' - process.stdout.write(` ${account.displayName} (${account.id}) - ${multiple}${status ? ` - ${status}` : ''}\n`) - if (command) process.stdout.write(` ${command}\n`) - } - process.stdout.write('\n') - } - process.stdout.write('Run `beeper bridges list` for the scriptable catalog or `beeper bridges show ` for login flows.\n') -} - -function resolveAccountType(items: AccountType[], input: string): AccountType { - const normalizedInput = normalize(input) - const exact = items.filter(item => [ - item.id, - item.displayName, - item.network, - item.type, - ].some(value => normalize(value) === normalizedInput)) - - if (exact.length === 1) return exact[0]! - if (exact.length > 1) throw ambiguousAccountType(input, exact) - - const partial = items.filter(item => [ - item.id, - item.displayName, - item.network, - item.type, - ].some(value => normalize(value).includes(normalizedInput))) - - if (partial.length === 1) return partial[0]! - if (partial.length > 1) throw ambiguousAccountType(input, partial) - throw new Error(`Unknown bridge "${input}". Run \`beeper bridges list\` to list available bridges.`) -} - -function ambiguousAccountType(input: string, matches: AccountType[]): Error { - const options = matches.map(item => `${item.displayName} (${item.id})`).join(', ') - return new Error(`Account type ${input} is ambiguous. Use one of: ${options}`) -} - -function statusLabel(account: AccountType): string | undefined { - if (account.status === 'available') return undefined - if (account.status === 'connected') return `${account.displayName} Connected` - return account.status.replaceAll('_', ' ') -} - -function normalize(value: string | undefined): string { - return (value ?? '').toLowerCase().replaceAll(/[^a-z0-9]+/g, '') -} - -function parseKeyValueFlags(values: string[] | undefined, flagName: string): Record { - const parsed: Record = {} - for (const value of values ?? []) { - const equalsIndex = value.indexOf('=') - if (equalsIndex <= 0) throw new Error(`${flagName} must use name=value form.`) - parsed[value.slice(0, equalsIndex)] = value.slice(equalsIndex + 1) - } - - return parsed -} - -async function chooseLoginFlow(flows: LoginFlow[]): Promise { - process.stdout.write('Choose how you want to sign in:\n') - flows.forEach((flow, index) => { - const description = flow.description ? ` - ${flow.description}` : '' - process.stdout.write(` ${index + 1}. ${flow.name}${description}\n`) - }) - - const rl = createInterface({ input, output }) - try { - for (;;) { - const answer = (await rl.question('Select a sign-in method: ')).trim() - const selected = Number.parseInt(answer, 10) - if (Number.isInteger(selected) && selected >= 1 && selected <= flows.length) return flows[selected - 1]!.id - const byID = flows.find(flow => flow.id === answer) - if (byID) return byID.id - process.stdout.write('Choose one of the listed sign-in methods.\n') - } - } finally { - rl.close() - } -} diff --git a/packages/cli/src/commands/accounts/list.ts b/packages/cli/src/commands/accounts/list.ts deleted file mode 100644 index 45f70197..00000000 --- a/packages/cli/src/commands/accounts/list.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printList } from '../../lib/output.js' -import { resolveAccountIDs } from '../../lib/resolve.js' -import { readConfig } from '../../lib/targets.js' - -export default class AccountsList extends BeeperCommand { - static override summary = 'List connected accounts' - static override flags = { - account: Flags.string({ multiple: true, description: 'Filter by account selector' }), - ids: Flags.boolean({ default: false, description: 'Print only account IDs' }), - } - async run(): Promise { - const { flags } = await this.parse(AccountsList) - const client = await createClient(flags) - // Account filter is an explicit override here; do not auto-apply defaultAccount. - const selected = await resolveAccountIDs(client, flags.account, { allowMultiplePerInput: true, applyDefault: false }) - const config = await readConfig() - const response = await client.accounts.list() - const rows = Array.isArray(response) ? response : ((response as any).items ?? []) - const filtered = selected?.length ? rows.filter((row: any) => selected.includes(row.accountID ?? row.id)) : rows - const items = filtered.map((row: any) => ({ - ...row, - default: (row.accountID ?? row.id) === config.defaultAccount || undefined, - })) - if (flags.ids) for (const item of items) process.stdout.write(`${String((item as any).accountID ?? (item as any).id)}\n`) - else await printList(items, flags.json ? 'json' : 'human', { title: 'No accounts connected', subtitle: 'Run beeper accounts add to add one.' }) - } -} diff --git a/packages/cli/src/commands/accounts/remove.ts b/packages/cli/src/commands/accounts/remove.ts deleted file mode 100644 index 93e20aa5..00000000 --- a/packages/cli/src/commands/accounts/remove.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Args } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printDryRun, printSuccess } from '../../lib/output.js' -import { resolveAccountID } from '../../lib/resolve.js' - -export default class AccountsRemove extends BeeperCommand { - static override summary = 'Remove an account' - static override args = { - account: Args.string({ required: true, description: 'Account selector (ID, network, bridge, or user identity)' }), - } - async run(): Promise { - const { args, flags } = await this.parse(AccountsRemove) - ensureWritable(flags) - const client = await createClient(flags) - const accountID = await resolveAccountID(client, args.account) - if (flags['dry-run']) { - await printDryRun('accounts.remove', { accountID }, flags.json ? 'json' : 'human') - return - } - const accounts = client.accounts as any - if (accounts.delete) await accounts.delete(accountID) - else if (accounts.remove) await accounts.remove(accountID) - else throw new Error('This Desktop API does not expose account removal.') - await printSuccess({ message: `Removed account: ${accountID}`, data: { accountID } }, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/accounts/show.ts b/packages/cli/src/commands/accounts/show.ts deleted file mode 100644 index dc54a30d..00000000 --- a/packages/cli/src/commands/accounts/show.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Args } from '@oclif/core' -import { BeeperCommand } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' -import { resolveAccountID } from '../../lib/resolve.js' - -export default class AccountsShow extends BeeperCommand { - static override summary = 'Show account details' - static override args = { - account: Args.string({ required: true, description: 'Account selector (ID, network, bridge, or user identity)' }), - } - async run(): Promise { - const { args, flags } = await this.parse(AccountsShow) - const client = await createClient(flags) - const accountID = await resolveAccountID(client, args.account) - const account = client.accounts.retrieve ? await client.accounts.retrieve(accountID) : (await client.accounts.list()).find((item: any) => (item.accountID ?? item.id) === accountID) - await printData(account, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/accounts/use.ts b/packages/cli/src/commands/accounts/use.ts deleted file mode 100644 index 441f560c..00000000 --- a/packages/cli/src/commands/accounts/use.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Args } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printDryRun, printSuccess } from '../../lib/output.js' -import { resolveAccountID } from '../../lib/resolve.js' -import { updateConfig } from '../../lib/targets.js' - -export default class AccountsUse extends BeeperCommand { - static override summary = 'Select a default account for account-scoped commands' - static override description = 'Persists the choice in CLI config. Account-scoped commands that take --account fall back to this default when --account is omitted. Use `beeper accounts use ""` (or `beeper config set defaultAccount ""`) to clear.' - static override args = { - account: Args.string({ required: true, description: 'Account selector (ID, network, bridge, user identity), or "" to clear.' }), - } - async run(): Promise { - const { args, flags } = await this.parse(AccountsUse) - ensureWritable(flags) - if (args.account === '') { - if (flags['dry-run']) { - await printDryRun('accounts.use', { defaultAccount: undefined }, flags.json ? 'json' : 'human') - return - } - await updateConfig(config => ({ ...config, defaultAccount: undefined })) - await printSuccess({ message: 'Cleared default account' }, flags.json ? 'json' : 'human') - return - } - const client = await createClient(flags) - const accountID = await resolveAccountID(client, args.account) - if (flags['dry-run']) { - await printDryRun('accounts.use', { defaultAccount: accountID }, flags.json ? 'json' : 'human') - return - } - await updateConfig(config => ({ ...config, defaultAccount: accountID })) - await printSuccess({ message: `Default account: ${accountID}`, data: { accountID } }, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/api/get.ts b/packages/cli/src/commands/api/get.ts deleted file mode 100644 index 850156d4..00000000 --- a/packages/cli/src/commands/api/get.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Args, Flags } from '@oclif/core' -import { BeeperCommand } from '../../lib/command.js' -import { appRequest } from '../../lib/app-api.js' -import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' - -export default class ApiGet extends BeeperCommand { - static override summary = 'Call a raw Desktop API GET path' - static override args = { - path: Args.string({ description: 'API path, for example /v1/info', required: true }), - } - static override flags = { - json: Flags.boolean({ default: true, allowNo: true, description: 'Print JSON' }), - 'no-auth': Flags.boolean({ default: false, description: 'Call a public API path without a bearer token' }), - } - - async run(): Promise { - const { args, flags } = await this.parse(ApiGet) - const format = flags.json ? 'json' : 'human' - if (flags['no-auth']) { - await printData(await appRequest('GET', args.path, { baseURL: flags['base-url'], target: flags.target, token: false }), format) - return - } - const client = await createClient(flags) - await printData(await client.get(args.path), format) - } -} diff --git a/packages/cli/src/commands/api/post.ts b/packages/cli/src/commands/api/post.ts deleted file mode 100644 index c381a3bf..00000000 --- a/packages/cli/src/commands/api/post.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Args, Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { appRequest } from '../../lib/app-api.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' - -export default class ApiPost extends BeeperCommand { - static override summary = 'Call a raw Desktop API POST path with a JSON body' - static override args = { - path: Args.string({ description: 'API path, for example /v1/messages/{chatID}/send', required: true }), - } - static override flags = { - body: Flags.string({ default: '{}', description: 'JSON request body' }), - json: Flags.boolean({ default: true, allowNo: true, description: 'Print JSON' }), - 'no-auth': Flags.boolean({ default: false, description: 'Call a public API path without a bearer token' }), - } - - async run(): Promise { - const { args, flags } = await this.parse(ApiPost) - ensureWritable(flags) - const format = flags.json ? 'json' : 'human' - let body: Record - try { - body = JSON.parse(flags.body) as Record - } catch { - throw new Error(`--body is not valid JSON: ${flags.body}`) - } - if (flags['dry-run']) { - await printDryRun('api.post', { method: 'POST', path: args.path, body, noAuth: flags['no-auth'], target: flags.target, baseURL: flags['base-url'] }, format) - return - } - if (flags['no-auth']) { - await printData(await appRequest('POST', args.path, { baseURL: flags['base-url'], body, target: flags.target, token: false }), format) - return - } - const client = await createClient(flags) - await printData(await client.post(args.path, { body }), format) - } -} diff --git a/packages/cli/src/commands/api/request.ts b/packages/cli/src/commands/api/request.ts deleted file mode 100644 index c551e450..00000000 --- a/packages/cli/src/commands/api/request.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Args, Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { appRequest, type AppRequestMethod } from '../../lib/app-api.js' -import { printData, printDryRun } from '../../lib/output.js' - -export default class ApiRequest extends BeeperCommand { - static override summary = 'Call a raw Desktop API path with any supported HTTP method' - static override args = { - method: Args.string({ description: 'HTTP method', options: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], required: true }), - path: Args.string({ description: 'API path, for example /v1/info', required: true }), - } - static override flags = { - body: Flags.string({ description: 'JSON request body' }), - json: Flags.boolean({ default: true, allowNo: true, description: 'Print JSON' }), - 'no-auth': Flags.boolean({ default: false, description: 'Call a public API path without a bearer token' }), - } - - async run(): Promise { - const { args, flags } = await this.parse(ApiRequest) - const method = args.method as AppRequestMethod - if (method !== 'GET') ensureWritable(flags) - const format = flags.json ? 'json' : 'human' - const body = flags.body ? JSON.parse(flags.body) as Record : undefined - if (flags['dry-run'] && method !== 'GET') { - await printDryRun('api.request', { method, path: args.path, body, noAuth: flags['no-auth'], target: flags.target, baseURL: flags['base-url'] }, format) - return - } - if (flags['no-auth']) { - await printData(await appRequest(method, args.path, { baseURL: flags['base-url'], body, target: flags.target, token: false }), format) - return - } - await printData(await appRequest(method, args.path, { baseURL: flags['base-url'], body, target: flags.target }), format) - } -} diff --git a/packages/cli/src/commands/auth/email/response.ts b/packages/cli/src/commands/auth/email/response.ts deleted file mode 100644 index 3268af76..00000000 --- a/packages/cli/src/commands/auth/email/response.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../../lib/command.js' -import { resolveTarget } from '../../../lib/targets.js' -import { finishEmailSetup } from '../../../lib/setup-login.js' -import { printData, printDryRun } from '../../../lib/output.js' - -export default class AuthEmailResponse extends BeeperCommand { - static override summary = 'Finish email sign-in with a verification code' - static override flags = { - code: Flags.string({ required: true, description: 'Email verification code' }), - 'setup-request-id': Flags.string({ required: true, description: 'Setup request ID from auth email start' }), - username: Flags.string({ description: 'Username to use if setup creates a new account' }), - yes: Flags.boolean({ default: false, description: 'Accept required registration prompts non-interactively' }), - } - - async run(): Promise { - const { flags } = await this.parse(AuthEmailResponse) - ensureWritable(flags) - const format = flags.json ? 'json' : 'human' - const target = await resolveTarget({ target: flags.target, baseURL: flags['base-url'] }) - if (flags['dry-run']) { - await printDryRun('auth.email.response', { target: target.id, baseURL: target.baseURL, setupRequestID: flags['setup-request-id'], username: flags.username, yes: flags.yes }, format) - return - } - const data = await finishEmailSetup(target, { - code: flags.code, - json: flags.json, - setupRequestID: flags['setup-request-id'], - username: flags.username, - yes: flags.yes, - }) - await printData(data, format) - } -} diff --git a/packages/cli/src/commands/auth/email/start.ts b/packages/cli/src/commands/auth/email/start.ts deleted file mode 100644 index 0baca3db..00000000 --- a/packages/cli/src/commands/auth/email/start.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand } from '../../../lib/command.js' -import { resolveTarget } from '../../../lib/targets.js' -import { startEmailSetup } from '../../../lib/setup-login.js' -import { printData } from '../../../lib/output.js' - -export default class AuthEmailStart extends BeeperCommand { - static override summary = 'Start email sign-in for a target' - static override flags = { - email: Flags.string({ required: true, description: 'Email address to sign in with' }), - } - - async run(): Promise { - const { flags } = await this.parse(AuthEmailStart) - const target = await resolveTarget({ target: flags.target, baseURL: flags['base-url'] }) - const data = await startEmailSetup(target, flags.email) - await printData(data, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/auth/logout.ts b/packages/cli/src/commands/auth/logout.ts deleted file mode 100644 index fc8756d5..00000000 --- a/packages/cli/src/commands/auth/logout.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { clearTargetAuth, resolveTarget } from '../../lib/targets.js' -import { printDryRun, printSuccess } from '../../lib/output.js' - -export default class AuthLogout extends BeeperCommand { - static override summary = 'Clear stored authentication' - - async run(): Promise { - const { flags } = await this.parse(AuthLogout) - if (!flags['dry-run']) ensureWritable(flags) - const format = flags.json ? 'json' : 'human' - const target = await resolveTarget({ target: flags.target, baseURL: flags['base-url'] }) - const token = target.auth?.accessToken - if (flags['dry-run']) { - await printDryRun('auth.logout', { target: target.id, baseURL: target.baseURL, hadToken: Boolean(token), revokeToken: Boolean(token) }, format) - return - } - if (process.env.BEEPER_ACCESS_TOKEN && !target.auth?.accessToken) { - throw new Error('auth logout cannot clear BEEPER_ACCESS_TOKEN from the environment; unset it in the calling process.') - } - let revoked = false - if (token) { - const response = await fetch(new URL('/oauth/revoke', target.baseURL), { - method: 'POST', - headers: { 'content-type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ token, token_type_hint: 'access_token' }), - signal: AbortSignal.timeout(5000), - }).catch(() => undefined) - revoked = Boolean(response?.ok) - await clearTargetAuth(target) - } - await printSuccess({ message: 'Logged out', detail: token ? 'local token cleared' : 'no token was stored', data: { revoked, hadToken: Boolean(token) } }, format) - } -} diff --git a/packages/cli/src/commands/auth/status.ts b/packages/cli/src/commands/auth/status.ts deleted file mode 100644 index 0d0f94e1..00000000 --- a/packages/cli/src/commands/auth/status.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { BeeperCommand } from '../../lib/command.js' -import { resolveTarget } from '../../lib/targets.js' -import { printData } from '../../lib/output.js' - -export default class AuthStatus extends BeeperCommand { - static override summary = 'Show stored auth for the selected target' - - async run(): Promise { - const { flags } = await this.parse(AuthStatus) - const target = await resolveTarget({ target: flags.target, baseURL: flags['base-url'] }) - const authenticated = Boolean(process.env.BEEPER_ACCESS_TOKEN || target.auth?.accessToken) - const data = { - authenticated, - target: target.id, - baseURL: target.baseURL, - source: process.env.BEEPER_ACCESS_TOKEN ? 'env' : target.auth?.source ?? (target.auth?.accessToken ? 'target' : 'none'), - clientID: target.auth?.clientID, - expiresAt: target.auth?.expiresAt, - scope: target.auth?.scope, - } - await printData(data, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/autocomplete.ts b/packages/cli/src/commands/autocomplete.ts deleted file mode 100644 index 85e69a8b..00000000 --- a/packages/cli/src/commands/autocomplete.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Command } from '@oclif/core' - -export default class Autocomplete extends Command { - static override hidden = true - - async run(): Promise { - const autocompletePlugin = this.config.plugins.get('@oclif/plugin-autocomplete') as any - const command = await autocompletePlugin?.findCommand?.('autocomplete', { must: true }) - if (!command?.run) throw new Error('Autocomplete plugin is not available. Run `beeper completion` for setup help.') - await command.run(this.argv, this.config) - } -} diff --git a/packages/cli/src/commands/bridges/config.ts b/packages/cli/src/commands/bridges/config.ts deleted file mode 100644 index 0574e4df..00000000 --- a/packages/cli/src/commands/bridges/config.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Args, Flags } from '@oclif/core' -import { ensureWritable } from '../../lib/command.js' -import { BridgeCommand } from '../../lib/bridges/command.js' -import { generateBridgeConfig, outputFile, prepareBridgeEnv } from '../../lib/bridges/manager.js' - -export default class BridgesConfig extends BridgeCommand { - static override summary = 'Generate a config for an official Beeper bridge' - static override aliases = ['bridges:c'] - static override args = { bridge: Args.string({ required: true, description: 'Self-hosted bridge name, usually sh-...' }) } - static override flags = { - env: Flags.string({ description: 'Beeper environment or domain (prod, staging, dev, local, or a domain)' }), - 'no-state': Flags.boolean({ default: false, description: "Don't send a bridge state update" }), - output: Flags.string({ char: 'o', default: process.env.BEEPER_BRIDGE_CONFIG_FILE ?? '-', description: 'Path to save generated config file to. Use - for stdout.' }), - param: Flags.string({ char: 'p', multiple: true, description: 'Bridge-specific config option in key=value form. Repeatable.' }), - type: Flags.string({ char: 't', default: process.env.BEEPER_BRIDGE_TYPE, description: 'The type of bridge being registered.' }), - } - - async run(): Promise { - const { args, flags } = await this.parse(BridgesConfig) - ensureWritable(flags) - const env = await prepareBridgeEnv(flags) - const cfg = await generateBridgeConfig(env, args.bridge, { - force: flags.force, - noState: flags['no-state'], - params: flags.param, - type: flags.type, - }) - await outputFile('Config', cfg.config ?? '', flags.output) - printStartupHint(cfg.bridgeType, flags.output, cfg.homeserver_url, cfg.your_user_id) - } -} - -function printStartupHint(bridgeType: string, outputPath: string, homeserverURL: string, userID: string): void { - const configPath = outputPath === '-' || !outputPath ? '' : outputPath - let startupCommand = '' - let installInstructions = '' - if (['imessage', 'whatsapp', 'discord', 'slack', 'gmessages', 'gvoice', 'signal', 'meta', 'twitter', 'bluesky', 'linkedin'].includes(bridgeType)) { - startupCommand = `mautrix-${bridgeType}` - if (configPath !== 'config.yaml' && configPath !== '') startupCommand += ` -c ${configPath}` - installInstructions = `https://docs.mau.fi/bridges/go/setup.html?bridge=${bridgeType}#installation` - } else if (bridgeType === 'imessagego') { - startupCommand = 'beeper-imessage' - if (configPath !== 'config.yaml' && configPath !== '') startupCommand += ` -c ${configPath}` - } else if (bridgeType === 'heisenbridge') { - startupCommand = `python -m heisenbridge -c ${configPath} -o ${userID} ${homeserverURL.replace('https://', 'wss://')}` - installInstructions = 'https://github.com/beeper/bridge-manager/wiki/Heisenbridge' - } - if (startupCommand) process.stderr.write(`\nStartup command: ${startupCommand}\n`) - if (installInstructions) process.stderr.write(`See ${installInstructions} for bridge installation instructions\n`) -} diff --git a/packages/cli/src/commands/bridges/delete.ts b/packages/cli/src/commands/bridges/delete.ts deleted file mode 100644 index 5f873299..00000000 --- a/packages/cli/src/commands/bridges/delete.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Args, Flags } from '@oclif/core' -import { createInterface } from 'node:readline/promises' -import { stdin as input, stdout as output } from 'node:process' -import { readdir, rm } from 'node:fs/promises' -import { join } from 'node:path' -import { ensureWritable, isForce } from '../../lib/command.js' -import { BridgeCommand } from '../../lib/bridges/command.js' -import { bridgeDataDir, deleteBridge, prepareBridgeEnv, validateBridgeID, whoami } from '../../lib/bridges/manager.js' - -export default class BridgesDelete extends BridgeCommand { - static override summary = 'Delete a bridge and all associated rooms on the Beeper servers' - static override aliases = ['bridges:d'] - static override args = { bridge: Args.string({ required: true, description: 'Bridge name' }) } - static override flags = { - env: Flags.string({ description: 'Beeper environment or domain (prod, staging, dev, local, or a domain)' }), - 'local-dev': Flags.boolean({ char: 'l', default: process.env.BEEPER_BRIDGE_LOCAL === '1', description: 'Delete bridge database and config from the current working directory' }), - } - - async run(): Promise { - const { args, flags } = await this.parse(BridgesDelete) - ensureWritable(flags) - if (args.bridge === 'hungryserv') throw new Error("You really shouldn't do that") - validateBridgeID(args.bridge) - const env = await prepareBridgeEnv(flags) - const bridgeDir = flags['local-dev'] ? process.cwd() : join(bridgeDataDir(env.envName), args.bridge) - - if (!flags.force) { - const info = await whoami(env) - const bridgeInfo = info.user.bridges?.[args.bridge] - if (!bridgeInfo) throw new Error(`You don't have a ${args.bridge} bridge.`) - if (!bridgeInfo.bridgeState?.isSelfHosted) throw new Error(`Your ${args.bridge} bridge is not self-hosted.`) - } - - if (!isForce(flags)) { - const confirmed = await confirm(`Are you sure you want to permanently delete ${args.bridge}?`) - if (!confirmed) throw new Error('bridge delete cancelled') - } - await deleteBridge(env, args.bridge) - process.stdout.write('Started deleting bridge\n') - await deleteLocalBridgeData(bridgeDir, !flags['local-dev']) - } -} - -async function confirm(message: string): Promise { - const rl = createInterface({ input, output }) - try { - const answer = (await rl.question(`${message} [y/N] `)).trim().toLowerCase() - return answer === 'y' || answer === 'yes' - } finally { - rl.close() - } -} - -async function deleteLocalBridgeData(bridgeDir: string, deleteWholeDir: boolean): Promise { - if (deleteWholeDir) { - await rm(bridgeDir, { force: true, recursive: true }) - process.stderr.write(`Deleted local bridge data from ${bridgeDir}\n`) - return - } - for (const item of await readdir(bridgeDir).catch(() => [])) { - if (isLocalBridgeFile(item)) await rm(join(bridgeDir, item), { force: true }) - } - process.stderr.write(`Deleted local bridge data from ${bridgeDir}\n`) -} - -function isLocalBridgeFile(name: string): boolean { - return name === 'config.yaml' || name.endsWith('.db') || name.endsWith('.db-shm') || name.endsWith('.db-wal') -} diff --git a/packages/cli/src/commands/bridges/list.ts b/packages/cli/src/commands/bridges/list.ts deleted file mode 100644 index ac34a86a..00000000 --- a/packages/cli/src/commands/bridges/list.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Flags } from '@oclif/core' -import { BridgeCommand } from '../../lib/bridges/command.js' -import { printList } from '../../lib/output.js' -import { prepareBridgeEnv } from '../../lib/bridges/manager.js' - -export default class BridgesList extends BridgeCommand { - static override summary = 'List self-hosted bridge types' - static override description = '`bridges list` lists the bridge-manager templates available for `beeper bridges config` and `beeper bridges run`.' - static override flags = { - env: Flags.string({ description: 'Beeper environment or domain (prod, staging, dev, local, or a domain)' }), - } - - async run(): Promise { - const { flags } = await this.parse(BridgesList) - const env = await prepareBridgeEnv(flags) - const items = env.catalog.supportedBridges.map(type => { - const official = env.catalog.officialBridges.find(item => item.typeName === type) - return { - id: type, - bridgeType: type, - names: official?.names ?? [], - websocket: Boolean(env.catalog.websocketBridges[type]), - template: `${type}.tpl.yaml`, - } - }) - - await printList(items, flags.json ? 'json' : 'human', { - title: 'No bridges matched', - subtitle: 'Add templates with BEEPER_BRIDGE_TEMPLATE_DIR or ~/.beeper/bridges/templates.', - suggestions: [{ command: 'beeper bridges config sh-discord --type discord', hint: 'generate a self-hosted bridge config' }], - }) - } -} diff --git a/packages/cli/src/commands/bridges/login-password.ts b/packages/cli/src/commands/bridges/login-password.ts deleted file mode 100644 index b7ed8d20..00000000 --- a/packages/cli/src/commands/bridges/login-password.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Flags } from '@oclif/core' -import { createInterface } from 'node:readline/promises' -import { stdin as input, stdout as output } from 'node:process' -import { ensureWritable, isNoInput } from '../../lib/command.js' -import { BridgeCommand } from '../../lib/bridges/command.js' -import { loginWithPassword, prepareBridgeTargetEnv } from '../../lib/bridges/manager.js' -import { printSuccess } from '../../lib/output.js' - -export default class BridgesLoginPassword extends BridgeCommand { - static override summary = 'Log into the Beeper server using username and password' - static override aliases = ['bridges:p'] - static override flags = { - env: Flags.string({ description: 'Beeper environment or domain (prod, staging, dev, local, or a domain)' }), - password: Flags.string({ char: 'p', default: process.env.BEEPER_PASSWORD, description: 'The Beeper password' }), - username: Flags.string({ char: 'u', default: process.env.BEEPER_USERNAME, description: 'The Beeper username to log in as' }), - } - - async run(): Promise { - const { flags } = await this.parse(BridgesLoginPassword) - ensureWritable(flags) - const env = await prepareBridgeTargetEnv(flags) - const username = flags.username || await prompt('Username:') - const password = flags.password || await prompt('Password:') - const login = await loginWithPassword(env, username, password) - await printSuccess({ message: `Successfully logged in as ${login.userID}`, data: { target: env.target.id, userID: login.userID } }, flags.json ? 'json' : 'human') - } -} - -async function prompt(message: string): Promise { - if (isNoInput() || !process.stdin.isTTY) throw new Error(`${message.replace(/:$/, '')} is required.`) - const rl = createInterface({ input, output }) - try { - return (await rl.question(`${message} `)).trim() - } finally { - rl.close() - } -} diff --git a/packages/cli/src/commands/bridges/login.ts b/packages/cli/src/commands/bridges/login.ts deleted file mode 100644 index 12dee329..00000000 --- a/packages/cli/src/commands/bridges/login.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Flags } from '@oclif/core' -import { createInterface } from 'node:readline/promises' -import { stdin as input, stdout as output } from 'node:process' -import { ensureWritable, isNoInput } from '../../lib/command.js' -import { BridgeCommand } from '../../lib/bridges/command.js' -import { loginWithEmail, prepareBridgeTargetEnv } from '../../lib/bridges/manager.js' -import { printSuccess } from '../../lib/output.js' - -export default class BridgesLogin extends BridgeCommand { - static override summary = 'Log into the Beeper server for bridge-manager APIs' - static override aliases = ['bridges:l'] - static override flags = { - email: Flags.string({ default: process.env.BEEPER_EMAIL, description: 'The Beeper account email to log in with' }), - env: Flags.string({ description: 'Beeper environment or domain (prod, staging, dev, local, or a domain)' }), - 'no-desktop': Flags.boolean({ default: process.env.BBCTL_NO_DESKTOP_LOGIN === '1', description: 'Accepted for bbctl compatibility; Desktop login is not used by this command' }), - } - - async run(): Promise { - const { flags } = await this.parse(BridgesLogin) - ensureWritable(flags) - const env = await prepareBridgeTargetEnv(flags) - const email = flags.email || await prompt('Email:') - const login = await loginWithEmail(env, email, () => prompt('Enter login code sent to your email:')) - await printSuccess({ message: `Successfully logged in as ${login.userID}`, data: { target: env.target.id, userID: login.userID } }, flags.json ? 'json' : 'human') - } -} - -async function prompt(message: string): Promise { - if (isNoInput() || !process.stdin.isTTY) throw new Error(`${message.replace(/:$/, '')} is required.`) - const rl = createInterface({ input, output }) - try { - return (await rl.question(`${message} `)).trim() - } finally { - rl.close() - } -} diff --git a/packages/cli/src/commands/bridges/logout.ts b/packages/cli/src/commands/bridges/logout.ts deleted file mode 100644 index 4e46ad38..00000000 --- a/packages/cli/src/commands/bridges/logout.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Flags } from '@oclif/core' -import { ensureWritable, isForce } from '../../lib/command.js' -import { BridgeCommand } from '../../lib/bridges/command.js' -import { logoutBridgeTarget, prepareBridgeEnv } from '../../lib/bridges/manager.js' -import { printSuccess } from '../../lib/output.js' - -export default class BridgesLogout extends BridgeCommand { - static override summary = 'Log out from the Beeper server for bridge-manager APIs' - static override flags = { - env: Flags.string({ description: 'Beeper environment or domain (prod, staging, dev, local, or a domain)' }), - } - - async run(): Promise { - const { flags } = await this.parse(BridgesLogout) - ensureWritable(flags) - const env = await prepareBridgeEnv(flags) - await logoutBridgeTarget(env, isForce(flags)) - await printSuccess({ message: 'Logged out successfully', data: { target: env.target.id } }, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/bridges/proxy.ts b/packages/cli/src/commands/bridges/proxy.ts deleted file mode 100644 index 27b3af97..00000000 --- a/packages/cli/src/commands/bridges/proxy.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Flags } from '@oclif/core' -import { BridgeCommand } from '../../lib/bridges/command.js' -import { prepareBridgeEnv, whoami } from '../../lib/bridges/manager.js' -import { proxyAppserviceWebsocket } from '../../lib/bridges/websocket-proxy.js' - -export default class BridgesProxy extends BridgeCommand { - static override summary = 'Connect to an appservice websocket and proxy it to a local appservice HTTP server' - static override aliases = ['bridges:x'] - static override flags = { - env: Flags.string({ description: 'Beeper environment or domain (prod, staging, dev, local, or a domain)' }), - registration: Flags.string({ char: 'r', required: true, default: process.env.BEEPER_BRIDGE_REGISTRATION_FILE, description: 'Registration file containing as_token, hs_token, and local appservice URL' }), - } - - async run(): Promise { - const { flags } = await this.parse(BridgesProxy) - const env = await prepareBridgeEnv(flags) - const info = await whoami(env) - await proxyAppserviceWebsocket({ - homeserverURL: `https://matrix.${env.domain}/_hungryserv/${encodeURIComponent(info.userInfo.username)}`, - registrationPath: flags.registration, - }) - } -} diff --git a/packages/cli/src/commands/bridges/register.ts b/packages/cli/src/commands/bridges/register.ts deleted file mode 100644 index 789fbb65..00000000 --- a/packages/cli/src/commands/bridges/register.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Args, Flags } from '@oclif/core' -import { ensureWritable } from '../../lib/command.js' -import { BridgeCommand } from '../../lib/bridges/command.js' -import { outputFile, prepareBridgeEnv, registerBridge, registrationToYAML, validateBridgeName, writeRegistrationJSON } from '../../lib/bridges/manager.js' - -export default class BridgesRegister extends BridgeCommand { - static override summary = 'Register a third-party bridge and print the appservice registration' - static override aliases = ['bridges:r'] - static override args = { bridge: Args.string({ required: true, description: 'Self-hosted bridge name, usually sh-...' }) } - static override flags = { - address: Flags.string({ char: 'a', default: process.env.BEEPER_BRIDGE_ADDRESS, description: 'HTTPS address where Beeper can push events. Omit to use websocket.' }), - env: Flags.string({ description: 'Beeper environment or domain (prod, staging, dev, local, or a domain)' }), - get: Flags.boolean({ char: 'g', default: false, description: "Only get existing registrations, don't create if missing" }), - json: Flags.boolean({ char: 'j', default: process.env.BEEPER_BRIDGE_REGISTRATION_JSON === '1', description: 'Return all data as JSON instead of registration YAML' }), - 'no-state': Flags.boolean({ default: false, description: "Don't send a bridge state update" }), - output: Flags.string({ char: 'o', default: process.env.BEEPER_BRIDGE_REGISTRATION_FILE ?? '-', description: 'Path to save generated registration file to. Use - for stdout.' }), - } - - async run(): Promise { - const { args, flags } = await this.parse(BridgesRegister) - ensureWritable(flags) - validateBridgeName(args.bridge, flags.force) - const env = await prepareBridgeEnv(flags) - const output = await registerBridge(env, args.bridge, { - address: flags.address, - force: flags.force, - get: flags.get, - noState: flags['no-state'], - }) - if (flags.json) { - await writeRegistrationJSON(output) - return - } - await outputFile('Registration', registrationToYAML(output.registration), flags.output) - process.stderr.write('\nAdditional bridge configuration details:\n') - process.stderr.write(`* Homeserver domain: ${output.homeserver_domain}\n`) - process.stderr.write(`* Homeserver URL: ${output.homeserver_url}\n`) - process.stderr.write(`* Your user ID: ${output.your_user_id}\n`) - } -} diff --git a/packages/cli/src/commands/bridges/run.ts b/packages/cli/src/commands/bridges/run.ts deleted file mode 100644 index c1ce3049..00000000 --- a/packages/cli/src/commands/bridges/run.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { Args, Flags } from '@oclif/core' -import { constants as fsConstants } from 'node:fs' -import { access, mkdir, writeFile } from 'node:fs/promises' -import { join } from 'node:path' -import { platform } from 'node:os' -import { spawn } from 'node:child_process' -import { once } from 'node:events' -import { ensureWritable } from '../../lib/command.js' -import { BridgeCommand } from '../../lib/bridges/command.js' -import { - bridgeDataDir, - compileGoBridge, - generateBridgeConfig, - prepareBridgeEnv, - registerBridge, - runCommand, - setupPythonVenv, - updateGoBridge, - whoami, - type GeneratedBridgeConfig, -} from '../../lib/bridges/manager.js' -import { runProxyLoop } from '../../lib/bridges/websocket-proxy.js' - -export default class BridgesRun extends BridgeCommand { - static override summary = 'Run an official Beeper bridge' - static override args = { bridge: Args.string({ required: true, description: 'Self-hosted bridge name, usually sh-...' }) } - static override flags = { - compile: Flags.boolean({ default: process.env.BEEPER_BRIDGE_COMPILE === '1', description: 'Clone the bridge repository and compile locally instead of downloading a CI binary' }), - 'config-file': Flags.string({ char: 'c', default: process.env.BEEPER_BRIDGE_CONFIG_FILE ?? 'config.yaml', description: 'File name to save the config to' }), - 'custom-startup-command': Flags.string({ default: process.env.BEEPER_BRIDGE_CUSTOM_STARTUP_COMMAND, description: 'Custom binary or script to run for startup. Disables update checks.' }), - env: Flags.string({ description: 'Beeper environment or domain (prod, staging, dev, local, or a domain)' }), - 'local-dev': Flags.boolean({ char: 'l', default: process.env.BEEPER_BRIDGE_LOCAL === '1', description: 'Run the bridge in the current working directory' }), - 'no-override-config': Flags.boolean({ default: process.env.BEEPER_BRIDGE_NO_OVERRIDE_CONFIG === '1', description: "Don't override config file if it already exists" }), - 'no-state': Flags.boolean({ default: false, description: "Don't send a bridge state update" }), - 'no-update': Flags.boolean({ char: 'n', default: process.env.BEEPER_BRIDGE_NO_UPDATE === '1', description: "Don't update the bridge even if it is out of date" }), - param: Flags.string({ char: 'p', multiple: true, description: 'Bridge-specific config option in key=value form. Repeatable.' }), - type: Flags.string({ char: 't', default: process.env.BEEPER_BRIDGE_TYPE, description: 'The type of bridge to run.' }), - } - - async run(): Promise { - const { args, flags } = await this.parse(BridgesRun) - ensureWritable(flags) - const env = await prepareBridgeEnv(flags) - const dataDir = bridgeDataDir(env.envName) - const bridgeDir = flags['local-dev'] ? process.cwd() : join(dataDir, args.bridge) - await mkdir(join(bridgeDir, 'logs'), { recursive: true, mode: 0o700 }) - - const configPath = join(bridgeDir, flags['config-file']) - const shouldWriteConfig = !(flags['no-override-config'] || flags['local-dev']) || !await exists(configPath) - let cfg: GeneratedBridgeConfig - if (shouldWriteConfig) { - cfg = await generateBridgeConfig(env, args.bridge, { - force: flags.force, - noState: flags['no-state'], - params: flags.param, - type: flags.type, - }) - await writeFile(configPath, cfg.config ?? '', { mode: 0o600 }) - } else { - const info = await whoami(env) - const bridgeType = info.user.bridges?.[args.bridge]?.bridgeState?.bridgeType - if (!bridgeType) { - cfg = await generateBridgeConfig(env, args.bridge, { - force: flags.force, - noState: flags['no-state'], - params: flags.param, - type: flags.type, - }) - await writeFile(configPath, cfg.config ?? '', { mode: 0o600 }) - } else { - const reg = await registerBridge(env, args.bridge, { bridgeType, force: flags.force, get: true, noState: flags['no-state'] }) - cfg = { ...reg, bridgeType } - } - process.stderr.write(`Config already exists, not overriding - delete ${configPath} to regenerate it\n`) - } - - const startup = await prepareStartup({ - bridgeDir, - cfg, - compile: flags.compile, - configFile: flags['config-file'], - customStartupCommand: flags['custom-startup-command'], - dataDir, - localDev: flags['local-dev'], - noUpdate: flags['no-update'], - }) - process.stderr.write(`Starting ${cfg.bridgeType}\n`) - await runBridgeProcess({ ...startup, bridgeDir, cfg, env }) - } -} - -async function prepareStartup(options: { - bridgeDir: string - cfg: GeneratedBridgeConfig - compile: boolean - configFile: string - customStartupCommand?: string - dataDir: string - localDev: boolean - noUpdate: boolean -}): Promise<{ command: string; args: string[]; needsWebsocketProxy: boolean }> { - const goBridges = ['imessage', 'imessagego', 'whatsapp', 'discord', 'slack', 'gmessages', 'gvoice', 'signal', 'meta', 'twitter', 'bluesky', 'linkedin', 'telegram'] - if (goBridges.includes(options.cfg.bridgeType)) { - const binaryName = options.cfg.bridgeType === 'imessagego' ? 'beeper-imessage' : `mautrix-${options.cfg.bridgeType}` - let command = join(options.dataDir, 'binaries', binaryName) - if (options.customStartupCommand) { - command = options.customStartupCommand - } else if (options.localDev) { - command = join(options.bridgeDir, binaryName) - await runCommand('./build.sh', [], options.bridgeDir) - } else if (options.compile) { - const buildDir = join(options.dataDir, 'compile', binaryName) - command = join(buildDir, binaryName) - await compileGoBridge(buildDir, command, options.cfg.bridgeType, options.noUpdate) - } else { - await updateGoBridge(command, options.cfg.bridgeType, options.noUpdate) - } - return { command, args: ['-c', options.configFile], needsWebsocketProxy: false } - } - if (options.cfg.bridgeType === 'googlechat') { - const command = options.customStartupCommand ?? join(await setupPythonVenv(options.bridgeDir, options.cfg.bridgeType, options.localDev), 'bin', 'python3') - return { command, args: ['-m', 'mautrix_googlechat', '-c', options.configFile], needsWebsocketProxy: true } - } - if (options.cfg.bridgeType === 'heisenbridge') { - const command = options.customStartupCommand ?? join(await setupPythonVenv(options.bridgeDir, options.cfg.bridgeType, options.localDev), 'bin', 'python3') - return { - command, - args: ['-m', 'heisenbridge', '-c', options.configFile, '-o', options.cfg.your_user_id, options.cfg.homeserver_url.replace('https://', 'wss://')], - needsWebsocketProxy: false, - } - } - throw new Error('Unsupported bridge type for beeper bridges run') -} - -async function runBridgeProcess(options: { - args: string[] - bridgeDir: string - cfg: GeneratedBridgeConfig - command: string - env: { domain: string } - needsWebsocketProxy: boolean -}): Promise { - const controller = new AbortController() - const child = spawn(options.command, options.args, { - cwd: options.bridgeDir, - detached: platform() === 'linux', - stdio: 'inherit', - }) - let proxyDone: Promise | undefined - if (options.needsWebsocketProxy) { - proxyDone = runProxyLoop(controller.signal, options.cfg.homeserver_url, options.cfg.registration) - proxyDone.catch(error => { - process.stderr.write(`Websocket proxy exited: ${(error as Error).message}\n`) - child.kill('SIGTERM') - }) - } - const shutdown = () => { - controller.abort() - if (platform() === 'linux' && child.pid) process.kill(-child.pid, 'SIGTERM') - else child.kill('SIGTERM') - setTimeout(() => child.kill('SIGKILL'), 3000).unref() - } - process.once('SIGINT', shutdown) - process.once('SIGTERM', shutdown) - const [code, signal] = await once(child, 'exit') as [number | null, NodeJS.Signals | null] - controller.abort() - if (proxyDone) await proxyDone.catch(() => undefined) - if (code !== 0) throw new Error(`${options.command} exited with ${signal ?? code}`) - process.stderr.write('Bridge exited\n') -} - -async function exists(path: string): Promise { - try { - await access(path, fsConstants.F_OK) - return true - } catch { - return false - } -} diff --git a/packages/cli/src/commands/bridges/show.ts b/packages/cli/src/commands/bridges/show.ts deleted file mode 100644 index b2d5055e..00000000 --- a/packages/cli/src/commands/bridges/show.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Args, Flags } from '@oclif/core' -import { BridgeCommand } from '../../lib/bridges/command.js' -import { printData } from '../../lib/output.js' -import { prepareBridgeEnv } from '../../lib/bridges/manager.js' - -export default class BridgesShow extends BridgeCommand { - static override summary = 'Show self-hosted bridge type details' - static override args = { - bridge: Args.string({ required: true, description: 'Bridge type' }), - } - static override flags = { - env: Flags.string({ description: 'Beeper environment or domain (prod, staging, dev, local, or a domain)' }), - template: Flags.boolean({ default: false, description: 'Print the raw template' }), - } - - async run(): Promise { - const { args, flags } = await this.parse(BridgesShow) - const env = await prepareBridgeEnv(flags) - const templateName = `${args.bridge}.tpl.yaml` - const template = env.catalog.templates[templateName] - if (!template) throw new Error(`Unknown bridge type "${args.bridge}".`) - if (flags.template && !flags.json) { - process.stdout.write(template) - return - } - const official = env.catalog.officialBridges.find(item => item.typeName === args.bridge) - await printData({ - id: args.bridge, - bridgeType: args.bridge, - names: official?.names ?? [], - websocket: Boolean(env.catalog.websocketBridges[args.bridge]), - ipSuffix: env.catalog.bridgeIPSuffix[args.bridge], - template: templateName, - templateBody: flags.template ? template : undefined, - }, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/bridges/whoami.ts b/packages/cli/src/commands/bridges/whoami.ts deleted file mode 100644 index da087bf9..00000000 --- a/packages/cli/src/commands/bridges/whoami.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Flags } from '@oclif/core' -import { BridgeCommand } from '../../lib/bridges/command.js' -import { hungryURL, prepareBridgeEnv, whoami, type WhoamiBridge, type WhoamiResponse } from '../../lib/bridges/manager.js' -import { printData } from '../../lib/output.js' - -export default class BridgesWhoami extends BridgeCommand { - static override summary = 'Show Beeper account details for bridge-manager APIs' - static override aliases = ['bridges:w'] - static override flags = { - env: Flags.string({ description: 'Beeper environment or domain (prod, staging, dev, local, or a domain)' }), - raw: Flags.boolean({ char: 'r', default: process.env.BEEPER_WHOAMI_RAW === '1', description: 'Get raw JSON output instead of pretty-printed bridge status' }), - } - - async run(): Promise { - const { flags } = await this.parse(BridgesWhoami) - const env = await prepareBridgeEnv(flags) - const data = await whoami(env) - if (flags.raw) { - process.stdout.write(`${JSON.stringify(data, null, 2)}\n`) - return - } - if (flags.json) { - await printData(data, 'json') - return - } - printWhoami(data, env.domain) - } -} - -function printWhoami(data: WhoamiResponse, domain: string): void { - const user = data.userInfo - process.stdout.write(`User ID: @${user.username}:${domain}\n`) - if (user.isAdmin) process.stdout.write('Admin: true\n') - if (user.isFree) process.stdout.write('Free: true\n') - if (user.fullName) process.stdout.write(`Name: ${user.fullName}\n`) - if (user.email) process.stdout.write(`Email: ${user.email}\n`) - if (user.supportRoomId) process.stdout.write(`Support room ID: ${user.supportRoomId}\n`) - if (user.createdAt) process.stdout.write(`Registered at: ${new Date(user.createdAt).toLocaleString()}\n`) - process.stdout.write('Cloud bridge details:\n') - if (user.channel) process.stdout.write(` Update channel: ${user.channel}\n`) - if (user.bridgeClusterId) process.stdout.write(` Cluster ID: ${user.bridgeClusterId}\n`) - process.stdout.write(` Hungryserv URL: ${hungryURL(domain, user.username)}\n`) - process.stdout.write('Bridges:\n') - if (data.user.hungryserv) process.stdout.write(` ${formatBridge('hungryserv', data.user.hungryserv)}\n`) - for (const name of Object.keys(data.user.bridges ?? {}).sort()) { - process.stdout.write(` ${formatBridge(name, data.user.bridges![name]!)}\n`) - } -} - -function formatBridge(name: string, bridge: WhoamiBridge): string { - const parts = [name] - const version = parseBridgeImage(name, bridge.version) - if (version) parts.push(`(version: ${version})`) - if (bridge.bridgeState?.isSelfHosted) { - const typeName = bridge.bridgeState.bridgeType && !name.includes(bridge.bridgeState.bridgeType) ? `${bridge.bridgeState.bridgeType}, ` : '' - parts.push(`(${typeName}self-hosted)`) - } - parts.push(`- ${bridge.bridgeState?.stateEvent ?? 'UNKNOWN'}`) - const remote = formatBridgeRemotes(name, bridge) - if (remote) parts.push(`- ${remote}`) - return parts.join(' ') -} - -function formatBridgeRemotes(name: string, bridge: WhoamiBridge): string { - if (['hungryserv', 'androidsms', 'imessage'].includes(name)) return '' - const states = Object.values(bridge.remoteState ?? {}) - if (!states.length) return bridge.bridgeState?.isSelfHosted ? '' : 'not logged in' - if (states.length > 1) return 'multiple remotes' - const state = states[0]! - return `remote: ${state.stateEvent ?? 'UNKNOWN'} (${state.remoteName ?? ''} / ${state.remoteID ?? ''})` -} - -function parseBridgeImage(name: string, image: string | undefined): string { - if (!image || image === '?') return '' - if (name === 'imessagecloud') return image.slice(0, 8) - const match = image.match(/^docker\.beeper-tools\.com\/(?:bridge\/)?([a-z]+):(v2-)?([0-9a-f]{40})(?:-amd64)?$/) - return match ? `${match[2] ?? ''}${match[3]!.slice(0, 8)}` : image -} diff --git a/packages/cli/src/commands/chats/archive.ts b/packages/cli/src/commands/chats/archive.ts deleted file mode 100644 index da099632..00000000 --- a/packages/cli/src/commands/chats/archive.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class ChatsArchive extends BeeperCommand { - static override summary = 'Archive a chat' - static override flags = { chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), } - async run(): Promise { - const { flags } = await this.parse(ChatsArchive) - ensureWritable(flags) - - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - if (flags['dry-run']) { - await printDryRun('chats.archive', { chatID, isArchived: true }, flags.json ? 'json' : 'human') - return - } - await printData(await client.chats.update(chatID, { isArchived: true }), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/chats/avatar.ts b/packages/cli/src/commands/chats/avatar.ts deleted file mode 100644 index 7c97c2bc..00000000 --- a/packages/cli/src/commands/chats/avatar.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class ChatsAvatar extends BeeperCommand { - static override summary = 'Set a chat avatar' - static override flags = { chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), file: Flags.string({ description: 'Image file to upload as the new avatar' }), clear: Flags.boolean({ default: false, description: 'Clear the existing avatar instead of setting a new one' }), } - async run(): Promise { - const { flags } = await this.parse(ChatsAvatar) - ensureWritable(flags) - if (!flags.clear && !flags.file) throw new Error('Provide --file or --clear') - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - if (flags['dry-run']) { - await printDryRun('chats.avatar', { chatID, imgURL: flags.clear ? null : flags.file }, flags.json ? 'json' : 'human') - return - } - await printData(await client.chats.update(chatID, { imgURL: flags.clear ? null : flags.file }), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/chats/description.ts b/packages/cli/src/commands/chats/description.ts deleted file mode 100644 index 0cb2022a..00000000 --- a/packages/cli/src/commands/chats/description.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class ChatsDescription extends BeeperCommand { - static override summary = 'Set a chat description' - static override flags = { chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), description: Flags.string({ description: 'New chat description' }), clear: Flags.boolean({ default: false, description: 'Clear the existing description instead of setting one' }), } - async run(): Promise { - const { flags } = await this.parse(ChatsDescription) - ensureWritable(flags) - if (!flags.clear && !flags.description) throw new Error('Provide --description or --clear') - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - if (flags['dry-run']) { - await printDryRun('chats.description', { chatID, description: flags.clear ? null : flags.description }, flags.json ? 'json' : 'human') - return - } - await printData(await client.chats.update(chatID, { description: flags.clear ? null : flags.description }), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/chats/disappear.ts b/packages/cli/src/commands/chats/disappear.ts deleted file mode 100644 index 6e9237c7..00000000 --- a/packages/cli/src/commands/chats/disappear.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class ChatsDisappear extends BeeperCommand { - static override summary = 'Set disappearing-message expiry' - static override examples = [ - 'beeper chats disappear --chat "Mom" --seconds 86400', - 'beeper chats disappear --chat "Work Group" --seconds off', - ] - static override flags = { - chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), - pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), - seconds: Flags.string({ required: true, description: 'Timer in seconds, or "off" to disable' }), - } - async run(): Promise { - const { flags } = await this.parse(ChatsDisappear) - const expiry = flags.seconds.toLowerCase() === 'off' ? null : Number(flags.seconds) - if (expiry !== null && (!Number.isInteger(expiry) || expiry < 0)) throw new Error('--seconds must be a positive integer or "off"') - if (flags['dry-run']) { - await printDryRun('chats.disappear', { chat: flags.chat, pick: flags.pick, messageExpirySeconds: expiry }, flags.json ? 'json' : 'human') - return - } - ensureWritable(flags) - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - await printData(await client.chats.update(chatID, { messageExpirySeconds: expiry }), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/chats/draft.ts b/packages/cli/src/commands/chats/draft.ts deleted file mode 100644 index ec4f22ed..00000000 --- a/packages/cli/src/commands/chats/draft.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Flags } from '@oclif/core' -import { createReadStream } from 'node:fs' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class ChatsDraft extends BeeperCommand { - static override summary = 'Set or clear a chat draft' - static override flags = { - chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), - pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), - text: Flags.string({ description: 'Draft text. Omit and pass --clear to remove the draft.' }), - file: Flags.string({ description: 'Attachment file to upload with the draft' }), - filename: Flags.string({ description: 'Override the displayed filename of the attachment' }), - mime: Flags.string({ description: 'Override MIME type detection for the attachment' }), - clear: Flags.boolean({ default: false, description: 'Clear the existing draft instead of setting one' }), - } - async run(): Promise { - const { flags } = await this.parse(ChatsDraft) - ensureWritable(flags) - if (!flags.clear && flags.text === undefined) throw new Error('Provide --text TEXT (and optionally --file PATH) or --clear.') - if (flags.clear && (flags.text !== undefined || flags.file)) throw new Error('--clear cannot be combined with --text or --file.') - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - if (flags.clear) { - if (flags['dry-run']) { - await printDryRun('chats.draft', { chatID, draft: null }, flags.json ? 'json' : 'human') - return - } - await printData(await client.chats.update(chatID, { draft: null }), flags.json ? 'json' : 'human') - return - } - if (flags['dry-run']) { - await printDryRun('chats.draft', { chatID, draft: { text: flags.text!, file: flags.file, fileName: flags.filename, mimeType: flags.mime } }, flags.json ? 'json' : 'human') - return - } - const upload = flags.file ? await client.assets.upload({ file: createReadStream(flags.file), fileName: flags.filename, mimeType: flags.mime }) : undefined - await printData(await client.chats.update(chatID, { draft: { text: flags.text!, attachments: upload?.uploadID ? { [upload.uploadID]: upload as any } : undefined } }), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/chats/focus.ts b/packages/cli/src/commands/chats/focus.ts deleted file mode 100644 index c9e811b8..00000000 --- a/packages/cli/src/commands/chats/focus.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class ChatsFocus extends BeeperCommand { - static override summary = 'Focus Beeper Desktop on a chat' - static override flags = { - chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), - pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), - message: Flags.string({ description: 'Scroll Desktop to this message ID after focusing' }), - draft: Flags.string({ description: 'Prefill the chat composer with this draft text' }), - attachment: Flags.string({ description: 'Prefill the chat composer with this attachment file path' }), - } - async run(): Promise { - const { flags } = await this.parse(ChatsFocus) - ensureWritable(flags) - - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - if (flags['dry-run']) { - await printDryRun('chats.focus', { chatID, messageID: flags.message, draftText: flags.draft, draftAttachmentPath: flags.attachment }, flags.json ? 'json' : 'human') - return - } - await printData(await client.focus({ chatID, messageID: flags.message, draftText: flags.draft, draftAttachmentPath: flags.attachment }), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/chats/list.ts b/packages/cli/src/commands/chats/list.ts deleted file mode 100644 index 42d0235b..00000000 --- a/packages/cli/src/commands/chats/list.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { cliCopy } from '../../lib/copy.js' -import { printIDs, printList } from '../../lib/output.js' -import { resolveAccountIDs } from '../../lib/resolve.js' - -export default class ChatsList extends BeeperCommand { - static override summary = 'List chats' - static override flags = { - account: Flags.string({ multiple: true, description: `Limit to ${cliCopy.args.accountSelector}` }), - ids: Flags.boolean({ default: false, description: 'Print preferred chat selectors, using numeric local chat IDs when available' }), - limit: Flags.integer({ default: 20, description: 'Maximum chats to print' }), - archived: Flags.boolean({ allowNo: true, description: 'Only archived chats (--no-archived to exclude)' }), - pinned: Flags.boolean({ allowNo: true, description: 'Only pinned chats (--no-pinned to exclude)' }), - muted: Flags.boolean({ allowNo: true, description: 'Only muted chats (--no-muted to exclude)' }), - unread: Flags.boolean({ allowNo: true, description: 'Only chats with unread messages (--no-unread to exclude)' }), - 'low-priority': Flags.boolean({ allowNo: true, description: 'Only Low Priority chats (--no-low-priority to exclude)' }), - } - async run(): Promise { - const { flags } = await this.parse(ChatsList) - const client = await createClient(flags) - const accountIDs = await resolveAccountIDs(client, flags.account, { allowMultiplePerInput: true }) - - const hasStateFilter = ( - flags.archived !== undefined || flags.pinned !== undefined - || flags.muted !== undefined || flags.unread !== undefined - || flags['low-priority'] !== undefined - ) - - const matchesFilters = (row: Record): boolean => { - if (!hasStateFilter) return true - if (flags.archived !== undefined && Boolean(row.isArchived) !== flags.archived) return false - if (flags.pinned !== undefined && Boolean(row.isPinned) !== flags.pinned) return false - if (flags.muted !== undefined && Boolean(row.isMuted) !== flags.muted) return false - if (flags['low-priority'] !== undefined && Boolean(row.isLowPriority) !== flags['low-priority']) return false - if (flags.unread !== undefined) { - const unread = Number(row.unreadCount ?? 0) > 0 || Boolean(row.isMarkedUnread) - if (unread !== flags.unread) return false - } - return true - } - - const items: Array> = [] - for await (const item of client.chats.list({ accountIDs })) { - const row = item as unknown as Record - if (matchesFilters(row)) items.push(row) - if (items.length >= flags.limit) break - } - - if (flags.ids) printIDs(items) - else await printList(items, flags.json ? 'json' : 'human', { title: 'No chats matched', subtitle: hasStateFilter ? 'Try relaxing the filter flags.' : 'Connect an account or sync existing chats.' }) - } -} diff --git a/packages/cli/src/commands/chats/mark-read.ts b/packages/cli/src/commands/chats/mark-read.ts deleted file mode 100644 index 82dceed9..00000000 --- a/packages/cli/src/commands/chats/mark-read.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class ChatsMarkRead extends BeeperCommand { - static override summary = 'Mark a chat as read' - static override flags = { chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), message: Flags.string({ description: 'Mark read at (or unread starting from) this message ID' }), } - async run(): Promise { - const { flags } = await this.parse(ChatsMarkRead) - ensureWritable(flags) - - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - if (flags['dry-run']) { - await printDryRun('chats.mark-read', { chatID, messageID: flags.message }, flags.json ? 'json' : 'human') - return - } - await printData(await client.chats.markRead(chatID, { messageID: flags.message }), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/chats/mark-unread.ts b/packages/cli/src/commands/chats/mark-unread.ts deleted file mode 100644 index e27c9839..00000000 --- a/packages/cli/src/commands/chats/mark-unread.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class ChatsMarkUnread extends BeeperCommand { - static override summary = 'Mark a chat as unread' - static override flags = { chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), message: Flags.string({ description: 'Mark read at (or unread starting from) this message ID' }), } - async run(): Promise { - const { flags } = await this.parse(ChatsMarkUnread) - ensureWritable(flags) - - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - if (flags['dry-run']) { - await printDryRun('chats.mark-unread', { chatID, messageID: flags.message }, flags.json ? 'json' : 'human') - return - } - await printData(await client.chats.markUnread(chatID, { messageID: flags.message }), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/chats/mute.ts b/packages/cli/src/commands/chats/mute.ts deleted file mode 100644 index 39d6e338..00000000 --- a/packages/cli/src/commands/chats/mute.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class ChatsMute extends BeeperCommand { - static override summary = 'Mute a chat' - static override flags = { - chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), - pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), - } - async run(): Promise { - const { flags } = await this.parse(ChatsMute) - ensureWritable(flags) - - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - if (flags['dry-run']) { - await printDryRun('chats.mute', { chatID, isMuted: true }, flags.json ? 'json' : 'human') - return - } - await printData(await client.chats.update(chatID, { isMuted: true }), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/chats/notify-anyway.ts b/packages/cli/src/commands/chats/notify-anyway.ts deleted file mode 100644 index 4174098d..00000000 --- a/packages/cli/src/commands/chats/notify-anyway.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class ChatsNotifyAnyway extends BeeperCommand { - static override summary = 'Send an iMessage Notify Anyway alert' - static override flags = { chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), } - async run(): Promise { - const { flags } = await this.parse(ChatsNotifyAnyway) - ensureWritable(flags) - - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - if (flags['dry-run']) { - await printDryRun('chats.notify-anyway', { chatID }, flags.json ? 'json' : 'human') - return - } - await printData(await client.chats.notifyAnyway(chatID), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/chats/pin.ts b/packages/cli/src/commands/chats/pin.ts deleted file mode 100644 index 7ee34d16..00000000 --- a/packages/cli/src/commands/chats/pin.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class ChatsPin extends BeeperCommand { - static override summary = 'Pin a chat' - static override flags = { chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), } - async run(): Promise { - const { flags } = await this.parse(ChatsPin) - ensureWritable(flags) - - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - if (flags['dry-run']) { - await printDryRun('chats.pin', { chatID, isPinned: true }, flags.json ? 'json' : 'human') - return - } - await printData(await client.chats.update(chatID, { isPinned: true }), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/chats/priority.ts b/packages/cli/src/commands/chats/priority.ts deleted file mode 100644 index 1ae932f9..00000000 --- a/packages/cli/src/commands/chats/priority.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class ChatsPriority extends BeeperCommand { - static override summary = 'Move a chat to the Inbox or Low Priority' - static override flags = { - chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), - pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), - level: Flags.string({ required: true, options: ['inbox', 'low'], description: 'Destination: inbox (default mailbox) or low (Low Priority)' }), - } - async run(): Promise { - const { flags } = await this.parse(ChatsPriority) - ensureWritable(flags) - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - const update = flags.level === 'inbox' - ? { isArchived: false, isLowPriority: false } - : { isLowPriority: true } - if (flags['dry-run']) { - await printDryRun('chats.priority', { chatID, level: flags.level, update }, flags.json ? 'json' : 'human') - return - } - await printData(await client.chats.update(chatID, update), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/chats/remind.ts b/packages/cli/src/commands/chats/remind.ts deleted file mode 100644 index f2599030..00000000 --- a/packages/cli/src/commands/chats/remind.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printDryRun, printSuccess } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class ChatsRemind extends BeeperCommand { - static override summary = 'Set a chat reminder' - static override examples = [ - 'beeper chats remind --chat "Mom" --when 2024-12-25T09:00:00Z', - 'beeper chats remind --chat "Work" --when 2024-12-25T09:00:00Z --dismiss-on-message', - ] - static override flags = { chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), when: Flags.string({ required: true, description: 'ISO timestamp when the reminder should trigger' }), 'dismiss-on-message': Flags.boolean({ default: false, description: 'Dismiss the reminder automatically when a new message arrives' }), } - async run(): Promise { - const { flags } = await this.parse(ChatsRemind) - ensureWritable(flags) - - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - if (flags['dry-run']) { - await printDryRun('chats.remind', { chatID, reminder: { remindAt: flags.when, dismissOnIncomingMessage: flags['dismiss-on-message'] || undefined } }, flags.json ? 'json' : 'human') - return - } - await client.chats.reminders.create(chatID, { reminder: { remindAt: flags.when, dismissOnIncomingMessage: flags['dismiss-on-message'] || undefined } }) - await printSuccess({ message: 'Reminder set', detail: flags.when, data: { chatID, remindAt: flags.when } }, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/chats/rename.ts b/packages/cli/src/commands/chats/rename.ts deleted file mode 100644 index d965bff8..00000000 --- a/packages/cli/src/commands/chats/rename.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class ChatsRename extends BeeperCommand { - static override summary = 'Rename a chat' - static override flags = { - chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), - pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), - title: Flags.string({ required: true, description: 'New chat title' }), - } - async run(): Promise { - const { flags } = await this.parse(ChatsRename) - ensureWritable(flags) - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - if (flags['dry-run']) { - await printDryRun('chats.rename', { chatID, title: flags.title }, flags.json ? 'json' : 'human') - return - } - await printData(await client.chats.update(chatID, { title: flags.title }), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/chats/search.ts b/packages/cli/src/commands/chats/search.ts deleted file mode 100644 index afc2aad5..00000000 --- a/packages/cli/src/commands/chats/search.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Args, Flags } from '@oclif/core' -import { BeeperCommand } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { collectPage, printIDs, printList } from '../../lib/output.js' -import { resolveAccountIDs } from '../../lib/resolve.js' - -export default class ChatsSearch extends BeeperCommand { - static override summary = 'Search chats' - static override args = { query: Args.string({ required: true, description: 'Search query (title, participant, or network)' }) } - static override flags = { - account: Flags.string({ multiple: true, description: 'Limit to Account ID, network, bridge, or account user' }), - ids: Flags.boolean({ default: false, description: 'Print preferred chat selectors, using numeric local chat IDs when available' }), - limit: Flags.integer({ default: 20, description: 'Maximum chats to print' }), - } - async run(): Promise { - const { args, flags } = await this.parse(ChatsSearch) - const client = await createClient(flags) - const accountIDs = await resolveAccountIDs(client, flags.account, { allowMultiplePerInput: true }) - const items = await collectPage(client.chats.search({ query: args.query, accountIDs }), flags.limit) - if (flags.ids) printIDs(items) - else await printList(items, flags.json ? 'json' : 'human', { title: 'No chats matched', subtitle: `Nothing found for "${args.query}".` }) - } -} diff --git a/packages/cli/src/commands/chats/show.ts b/packages/cli/src/commands/chats/show.ts deleted file mode 100644 index 12b1613c..00000000 --- a/packages/cli/src/commands/chats/show.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class ChatsShow extends BeeperCommand { - static override summary = 'Show chat details' - static override flags = { chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), 'max-participants': Flags.integer({ description: 'Limit number of participants returned in chat details' }) } - async run(): Promise { - const { flags } = await this.parse(ChatsShow) - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - await printData(await client.chats.retrieve(chatID, { maxParticipantCount: flags['max-participants'] }), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/chats/start.ts b/packages/cli/src/commands/chats/start.ts deleted file mode 100644 index 170bd82b..00000000 --- a/packages/cli/src/commands/chats/start.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Args, Flags } from '@oclif/core' -import type { ChatStartParams } from '@beeper/desktop-api/resources/chats' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -import { listAccountIDs, resolveAccountID, userQueryFromInput } from '../../lib/resolve.js' - -export default class ChatsStart extends BeeperCommand { - static override summary = 'Start a chat' - static override args = { user: Args.string({ required: true, description: 'User ID, phone number, email, or display name' }) } - static override flags = { - account: Flags.string({ description: 'Account selector. Defaults to the single available account or the matrix account.' }), - title: Flags.string({ description: 'Optional initial title for a new group chat' }), - } - - async run(): Promise { - const { args, flags } = await this.parse(ChatsStart) - ensureWritable(flags) - const client = await createClient(flags) - const accountID = flags.account ? await resolveAccountID(client, flags.account) : await defaultAccountID(client) - const user = userQueryFromInput(args.user) - const payload: ChatStartParams & { title?: string } = { accountID, user, title: flags.title } - if (flags['dry-run']) { - await printDryRun('chats.start', payload as unknown as Record, flags.json ? 'json' : 'human') - return - } - await printData(await client.chats.start(payload), flags.json ? 'json' : 'human') - } -} - -async function defaultAccountID(client: any): Promise { - const accountIDs = await listAccountIDs(client) - if (accountIDs.includes('matrix')) return 'matrix' - if (accountIDs.length === 1 && accountIDs[0]) return accountIDs[0] - throw new Error('Use --account to choose which account should start the chat.') -} diff --git a/packages/cli/src/commands/chats/unarchive.ts b/packages/cli/src/commands/chats/unarchive.ts deleted file mode 100644 index 17f55f9e..00000000 --- a/packages/cli/src/commands/chats/unarchive.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class ChatsUnarchive extends BeeperCommand { - static override summary = 'Unarchive a chat' - static override flags = { chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), } - async run(): Promise { - const { flags } = await this.parse(ChatsUnarchive) - ensureWritable(flags) - - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - if (flags['dry-run']) { - await printDryRun('chats.unarchive', { chatID, isArchived: false }, flags.json ? 'json' : 'human') - return - } - await printData(await client.chats.update(chatID, { isArchived: false }), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/chats/unmute.ts b/packages/cli/src/commands/chats/unmute.ts deleted file mode 100644 index 6466e892..00000000 --- a/packages/cli/src/commands/chats/unmute.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class ChatsUnmute extends BeeperCommand { - static override summary = 'Unmute a chat' - static override flags = { chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), } - async run(): Promise { - const { flags } = await this.parse(ChatsUnmute) - ensureWritable(flags) - - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - if (flags['dry-run']) { - await printDryRun('chats.unmute', { chatID, isMuted: false }, flags.json ? 'json' : 'human') - return - } - await printData(await client.chats.update(chatID, { isMuted: false }), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/chats/unpin.ts b/packages/cli/src/commands/chats/unpin.ts deleted file mode 100644 index 1053c681..00000000 --- a/packages/cli/src/commands/chats/unpin.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class ChatsUnpin extends BeeperCommand { - static override summary = 'Unpin a chat' - static override flags = { chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), } - async run(): Promise { - const { flags } = await this.parse(ChatsUnpin) - ensureWritable(flags) - - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - if (flags['dry-run']) { - await printDryRun('chats.unpin', { chatID, isPinned: false }, flags.json ? 'json' : 'human') - return - } - await printData(await client.chats.update(chatID, { isPinned: false }), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/chats/unremind.ts b/packages/cli/src/commands/chats/unremind.ts deleted file mode 100644 index 5fa5e9ef..00000000 --- a/packages/cli/src/commands/chats/unremind.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printDryRun, printSuccess } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class ChatsUnremind extends BeeperCommand { - static override summary = 'Clear a chat reminder' - static override flags = { chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), } - async run(): Promise { - const { flags } = await this.parse(ChatsUnremind) - ensureWritable(flags) - - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - if (flags['dry-run']) { - await printDryRun('chats.unremind', { chatID }, flags.json ? 'json' : 'human') - return - } - await client.chats.reminders.delete(chatID) - await printSuccess({ message: 'Reminder cleared', data: { chatID } }, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/completion.ts b/packages/cli/src/commands/completion.ts deleted file mode 100644 index d1aa82bd..00000000 --- a/packages/cli/src/commands/completion.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Args, Command, Flags } from '@oclif/core' - -const ZSH_SNIPPET = `# beeper semantic completion (zsh) - source after \`beeper completion\` -# Augments static command completion with live suggestions for --chat / --to / --account / --target. -_beeper_complete_kind() { - local kind="$1" - local -a lines - local IFS=$'\\n' - lines=( $(beeper _complete "$kind" --query "$PREFIX" --limit 25 2>/dev/null) ) - local -a values descs - for line in "$lines[@]"; do - values+=("$\{line%%\\t*\}") - descs+=("$\{line\}") - done - _describe -t "$kind" "$kind" descs values -} -_beeper_chat() { _beeper_complete_kind chat } -_beeper_account() { _beeper_complete_kind account } -_beeper_target() { _beeper_complete_kind target } -_beeper_contact() { _beeper_complete_kind contact } - -zstyle ':completion:*:*:beeper:*:option-chat-1' extra-verbose yes -compdef '_arguments \\ - "--chat=[Chat ID or title]:chat:_beeper_chat" \\ - "--to=[Chat or contact]:chat:_beeper_chat" \\ - "--account=[Account]:account:_beeper_account" \\ - "--target=[Target name]:target:_beeper_target" \\ - "-t+[Target name]:target:_beeper_target"' beeper -` - -const BASH_SNIPPET = `# beeper semantic completion (bash) - source after \`beeper completion\` -# Augments static command completion with live suggestions for --chat / --to / --account / --target. -_beeper_semantic_kind() { - local kind="$1" cur="$2" - local IFS=$'\\n' - COMPREPLY+=( $(beeper _complete "$kind" --query "$cur" --limit 25 2>/dev/null | cut -f1) ) -} -_beeper_semantic_dispatch() { - local prev="$3" cur="$2" - case "$prev" in - --chat|--to) _beeper_semantic_kind chat "$cur" ;; - --account) _beeper_semantic_kind account "$cur" ;; - --target|-t) _beeper_semantic_kind target "$cur" ;; - --contact) _beeper_semantic_kind contact "$cur" ;; - esac -} -# Chain after the static beeper completion: call it, then add semantic suggestions. -complete -o nospace -o default -F _beeper_semantic_dispatch beeper -` - -export default class Completion extends Command { - static override summary = 'Print shell completion setup' - static override description = `Print static shell completion setup for bash, zsh, fish, or PowerShell. - -Pass \`--semantic\` to print a small supplementary snippet that adds live suggestions for \`--chat\`, \`--to\`, \`--account\`, and \`--target\` by calling back into \`beeper _complete\`. Source it after the static completion setup.` - static override args = { - shell: Args.string({ description: 'Shell to set up (bash, zsh, fish, or powershell)', required: false }), - } - static override flags = { - 'refresh-cache': Flags.boolean({ char: 'r', default: false, description: 'Refresh the autocomplete cache before printing setup' }), - semantic: Flags.boolean({ default: false, description: 'Print a semantic-completion snippet (chats/accounts/targets) for bash or zsh' }), - } - - async run(): Promise { - const { args, flags } = await this.parse(Completion) - if (flags.semantic) { - const shell = (args.shell ?? process.env.SHELL ?? '').toLowerCase() - if (shell.includes('zsh')) { - process.stdout.write(`${ZSH_SNIPPET}\n`) - return - } - if (shell.includes('bash')) { - process.stdout.write(`${BASH_SNIPPET}\n`) - return - } - process.stderr.write('Semantic completion is currently supported for bash and zsh. Pass `bash` or `zsh` explicitly.\n') - this.exit(2) - } - const argv: string[] = [] - if (args.shell) argv.push(args.shell) - if (flags['refresh-cache']) argv.push('--refresh-cache') - await this.config.runCommand('autocomplete', argv) - } -} diff --git a/packages/cli/src/commands/config/get.ts b/packages/cli/src/commands/config/get.ts deleted file mode 100644 index 121c9b5b..00000000 --- a/packages/cli/src/commands/config/get.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Args } from '@oclif/core' -import { BeeperCommand } from '../../lib/command.js' -import { readConfig } from '../../lib/targets.js' -import { printConfig, printData } from '../../lib/output.js' - -export default class ConfigGet extends BeeperCommand { - static override summary = 'Print CLI configuration' - static override args = { - key: Args.string({ description: 'Optional config key to print', options: ['baseURL', 'auth', 'defaultTarget', 'defaultAccount'], required: false }), - } - - async run(): Promise { - const { args, flags } = await this.parse(ConfigGet) - const config = await readConfig() - const safeConfig = { - ...config, - auth: config.auth ? { ...config.auth, accessToken: '[redacted]' } : config.auth, - } - const format = flags.json ? 'json' : 'human' - if (args.key) { - await printData(safeConfig[args.key as keyof typeof safeConfig], format) - return - } - await printConfig(safeConfig as unknown as Record, format) - } -} diff --git a/packages/cli/src/commands/config/path.ts b/packages/cli/src/commands/config/path.ts deleted file mode 100644 index 944b9270..00000000 --- a/packages/cli/src/commands/config/path.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { BeeperCommand } from '../../lib/command.js' -import { configPath } from '../../lib/targets.js' - -export default class ConfigPath extends BeeperCommand { - static override summary = 'Print the CLI config path' - - async run(): Promise { - const { flags } = await this.parse(ConfigPath) - const path = configPath() - if (flags.json) { - process.stdout.write(`${JSON.stringify({ path }, null, 2)}\n`) - return - } - // Plain path so it's pipeable (xargs / cat / cd). - process.stdout.write(`${path}\n`) - } -} diff --git a/packages/cli/src/commands/config/reset.ts b/packages/cli/src/commands/config/reset.ts deleted file mode 100644 index 8d3b9636..00000000 --- a/packages/cli/src/commands/config/reset.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { resetConfig } from '../../lib/targets.js' -import { printDryRun, printSuccess } from '../../lib/output.js' - -export default class ConfigReset extends BeeperCommand { - static override summary = 'Reset CLI configuration' - - async run(): Promise { - const { flags } = await this.parse(ConfigReset) - ensureWritable(flags) - const format = flags.json ? 'json' : 'human' - if (flags['dry-run']) { - await printDryRun('config.reset', {}, format) - return - } - await resetConfig() - await printSuccess({ message: 'Config reset' }, format) - } -} diff --git a/packages/cli/src/commands/config/set.ts b/packages/cli/src/commands/config/set.ts deleted file mode 100644 index 2cd95099..00000000 --- a/packages/cli/src/commands/config/set.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Args } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { updateConfig } from '../../lib/targets.js' -import { printDryRun, printSuccess } from '../../lib/output.js' - -export default class ConfigSet extends BeeperCommand { - static override summary = 'Set a CLI configuration value' - static override args = { - key: Args.string({ description: 'Config key to set', options: ['defaultTarget', 'defaultAccount'], required: true }), - value: Args.string({ description: 'Config value (pass "" to clear)', required: true }), - } - - async run(): Promise { - const { args, flags } = await this.parse(ConfigSet) - ensureWritable(flags) - const format = flags.json ? 'json' : 'human' - const nextValue = args.value === '' ? undefined : args.value - if (flags['dry-run']) { - await printDryRun('config.set', { [args.key]: nextValue }, format) - return - } - await updateConfig(config => ({ ...config, [args.key]: nextValue })) - await printSuccess({ - message: nextValue === undefined ? `Cleared ${args.key}` : `Set ${args.key}`, - detail: nextValue, - data: { [args.key]: nextValue }, - }, format) - } -} diff --git a/packages/cli/src/commands/contacts/list.ts b/packages/cli/src/commands/contacts/list.ts deleted file mode 100644 index 9fac2b25..00000000 --- a/packages/cli/src/commands/contacts/list.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { apiCopy, cliCopy } from '../../lib/copy.js' -import { collectPage, printIDs, printList } from '../../lib/output.js' -import { listAccountIDs, resolveAccountIDs } from '../../lib/resolve.js' -import { withInkSpinner as withSpinner } from '../../lib/ink/spinner.js' - -export default class ContactsList extends BeeperCommand { - static override summary = 'List contacts' - static override description = apiCopy.contacts.list - static override args = {} - - static override flags = { - ids: Flags.boolean({ default: false, description: 'Print only contact user IDs' }), - limit: Flags.integer({ default: 50, description: 'Maximum contacts to print' }), - account: Flags.string({ multiple: true, description: `Limit to ${cliCopy.args.accountSelector}` }), - query: Flags.string({ description: 'Optional blended contact lookup query' }), - } - - async run(): Promise { - const { flags } = await this.parse(ContactsList) - const client = await createClient(flags) - const accountIDs = await resolveAccountIDs(client, flags.account, { allowMultiplePerInput: true }) ?? await listAccountIDs(client) - const useSpinner = !flags.json && !flags.ids - const load = async (): Promise>> => { - const collected: Array> = [] - for (const accountID of accountIDs) { - const remaining = flags.limit - collected.length - if (remaining <= 0) break - const contacts = await collectPage(client.accounts.contacts.list(accountID, { query: flags.query }), remaining) - collected.push(...contacts.map(item => ({ ...(item as unknown as Record), accountID }))) - if (collected.length >= flags.limit) break - } - return collected - } - const items = useSpinner - ? await withSpinner(`Loading contacts${flags.query ? ` matching "${flags.query}"` : ''}…`, load, { - done: value => `${value.length} contact${value.length === 1 ? '' : 's'}`, - }) - : await load() - if (flags.ids) { - printIDs(items.map(item => ({ id: item.userID ?? item.id }))) - return - } - await printList(items, flags.json ? 'json' : 'human', { - title: 'No contacts found', - subtitle: flags.query ? `Nothing matched "${flags.query}".` : 'This account has no contacts to list.', - suggestions: [ - { command: 'beeper contacts search ', hint: 'narrow with a search' }, - { command: 'beeper accounts', hint: 'check the account is online' }, - ], - }) - } -} diff --git a/packages/cli/src/commands/contacts/search.ts b/packages/cli/src/commands/contacts/search.ts deleted file mode 100644 index 2c9d1d08..00000000 --- a/packages/cli/src/commands/contacts/search.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Args, Flags } from '@oclif/core' -import { BeeperCommand } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { apiCopy, cliCopy } from '../../lib/copy.js' -import { isMachineReadableOutput, printList } from '../../lib/output.js' -import { listAccountIDs, resolveAccountIDs } from '../../lib/resolve.js' -import { withInkSpinner as withSpinner } from '../../lib/ink/spinner.js' - -export default class ContactsSearch extends BeeperCommand { - static override summary = 'Search contacts' - static override description = apiCopy.contacts.search - static override args = { - query: Args.string({ description: 'Contact search query', required: true }), - } - static override flags = { - account: Flags.string({ multiple: true, description: `${cliCopy.args.accountSelector}. Omit to search every account.` }), - } - - async run(): Promise { - const { args, flags } = await this.parse(ContactsSearch) - const client = await createClient(flags) - const accountIDs = await resolveAccountIDs(client, flags.account, { allowMultiplePerInput: true }) ?? await listAccountIDs(client) - const load = async (): Promise>> => { - const collected: Array> = [] - for (const accountID of accountIDs) { - try { - const result = await client.accounts.contacts.search(accountID, { query: args.query }) - collected.push(...result.items.map((item: unknown) => ({ ...(item as Record), accountID }))) - } catch { - // Some networks reject exact lookups for some identifiers; keep trying the rest. - } - } - return collected - } - const useSpinner = !isMachineReadableOutput(flags.json ? 'json' : 'human') - const results = useSpinner - ? await withSpinner(`Searching contacts for "${args.query}"…`, load, { - done: value => `${value.length} match${value.length === 1 ? '' : 'es'} across ${accountIDs.length} account${accountIDs.length === 1 ? '' : 's'}`, - }) - : await load() - await printList(results, flags.json ? 'json' : 'human', { - title: 'No contacts matched', - subtitle: `Nothing across your ${accountIDs.length} account${accountIDs.length === 1 ? '' : 's'} matched "${args.query}".`, - suggestions: [ - { command: 'beeper accounts', hint: 'verify which accounts are connected' }, - { command: `beeper contact ${args.query}`, hint: 'exact lookup on one account' }, - ], - }) - } -} diff --git a/packages/cli/src/commands/contacts/show.ts b/packages/cli/src/commands/contacts/show.ts deleted file mode 100644 index 8d6cf7c4..00000000 --- a/packages/cli/src/commands/contacts/show.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Args, Flags } from '@oclif/core' -import { BeeperCommand } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { notFound } from '../../lib/errors.js' -import { collectPage, printData } from '../../lib/output.js' -import { resolveAccountIDs } from '../../lib/resolve.js' -export default class ContactsShow extends BeeperCommand { - static override summary = 'Show contact details' - static override args = { - id: Args.string({ required: true, description: 'Contact user ID, display name, or phone/handle' }), - } - static override flags = { - account: Flags.string({ multiple: true, description: 'Limit to account ID, network, bridge, or account user' }), - } - async run(): Promise { - const { args, flags } = await this.parse(ContactsShow) - const client = await createClient(flags) - const accountIDs = await resolveAccountIDs(client, flags.account, { allowMultiplePerInput: true }) - for (const accountID of accountIDs ?? []) { - const matches = await collectPage(client.accounts.contacts.list(accountID, { query: args.id }), 10) - const match = matches.find((item: any) => [item.userID, item.id, item.name, item.displayName].includes(args.id)) - if (match) return printData({ accountID, contact: match }, flags.json ? 'json' : 'human') - } - throw notFound(`Contact not found: ${args.id}`) - } -} diff --git a/packages/cli/src/commands/docs.ts b/packages/cli/src/commands/docs.ts deleted file mode 100644 index 8565291e..00000000 --- a/packages/cli/src/commands/docs.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { BeeperCommand } from '../lib/command.js' -import { printData } from '../lib/output.js' -export default class Docs extends BeeperCommand { - static override summary = 'Open Beeper CLI docs' - async run(): Promise { - const { flags } = await this.parse(Docs) - await printData({ url: 'https://developers.beeper.com/desktop-api-reference' }, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/doctor.ts b/packages/cli/src/commands/doctor.ts deleted file mode 100644 index a07a5581..00000000 --- a/packages/cli/src/commands/doctor.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { BeeperCommand } from '../lib/command.js' -import { evaluateReadiness } from '../lib/app-state.js' -import { ExitCodes } from '../lib/errors.js' -import { resolveTarget } from '../lib/targets.js' -import { targetLiveStatus } from '../lib/target-status.js' -import { printData } from '../lib/output.js' -export default class Doctor extends BeeperCommand { - static override summary = 'Probe the target live and report diagnostics' - static override description = 'Active reachability check plus readiness diagnostics. Exits non-zero when the target is not ready. For a cheap snapshot use `beeper status`.' - async run(): Promise { - const { flags } = await this.parse(Doctor) - const target = await resolveTarget({ target: flags.target, baseURL: flags['base-url'] }) - const [targetStatus, readiness] = await Promise.all([ - targetLiveStatus(target), - evaluateReadiness({ baseURL: target.baseURL, target: target.id }), - ]) - const checks = { target: targetStatus, readiness } - await printData({ ok: checks.readiness.state === 'ready', checks }, flags.json ? 'json' : 'human') - if (checks.readiness.state !== 'ready') this.exit(ExitCodes.NotReady) - } -} diff --git a/packages/cli/src/commands/export.ts b/packages/cli/src/commands/export.ts deleted file mode 100644 index ef7af0ae..00000000 --- a/packages/cli/src/commands/export.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../lib/command.js' -import { createClient } from '../lib/client.js' -import { exportBeeperData } from '../lib/export/index.js' -import { printDryRun } from '../lib/output.js' -import { resolveAccountIDs, resolveChatID } from '../lib/resolve.js' - -export default class Export extends BeeperCommand { - static override summary = 'Export accounts, chats, messages, Markdown transcripts, and attachments' - static override description = [ - 'Creates a resumable Beeper Desktop export using the official Desktop API SDK.', - 'The export directory contains accounts.json, chats.json, manifest.json, and one directory per chat with chat.json, messages.json, messages.markdown, messages.html, downloaded attachments, and checkpoint state for interrupted runs.', - ].join('\n') - - static override flags = { - account: Flags.string({ multiple: true, description: 'Limit to an account selector. Repeat to include more accounts.' }), - chat: Flags.string({ multiple: true, description: 'Limit to a chat selector. Repeat to include more chats.' }), - force: Flags.boolean({ default: false, description: 'Re-export chats even if checkpoint state says they are complete.' }), - 'limit-chats': Flags.integer({ description: 'Maximum chats to export. Intended for testing large exports.' }), - 'limit-messages': Flags.integer({ description: 'Maximum messages per chat. Intended for testing large exports.' }), - 'max-participants': Flags.integer({ default: 500, description: 'Maximum participants to include in each chat.json.' }), - 'no-attachments': Flags.boolean({ default: false, description: 'Skip downloading message attachments.' }), - out: Flags.directory({ char: 'o', default: 'beeper-export', description: 'Export directory.' }), - pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), - quiet: Flags.boolean({ default: false, description: 'Suppress progress output.' }), - } - - async run(): Promise { - const { flags } = await this.parse(Export) - ensureWritable(flags) - const client = await createClient(flags) - const accountIDs = await resolveAccountIDs(client, flags.account, { allowMultiplePerInput: true }) - const chatIDs = flags.chat?.length - ? await Promise.all(flags.chat.map(chat => resolveChatID(client, chat, { accountIDs, pick: flags.pick }))) - : undefined - if (flags['dry-run']) { - await printDryRun('export', { - accountIDs, - chatIDs, - downloadAttachments: !flags['no-attachments'], - force: flags.force, - limitChats: flags['limit-chats'], - limitMessages: flags['limit-messages'], - maxParticipants: flags['max-participants'], - outDir: flags.out, - }, flags.json ? 'json' : 'human') - return - } - - const manifest = await exportBeeperData(client, { - accountIDs, - chatIDs, - downloadAttachments: !flags['no-attachments'], - events: flags.events, - force: flags.force, - limitChats: flags['limit-chats'], - limitMessages: flags['limit-messages'], - maxParticipants: flags['max-participants'], - outDir: flags.out, - quiet: flags.quiet, - }) - - this.log(`Exported ${manifest.chatCount} chats, ${manifest.messageCount} messages, ${manifest.attachmentCount} attachments to ${flags.out}`) - } -} diff --git a/packages/cli/src/commands/install/desktop.ts b/packages/cli/src/commands/install/desktop.ts deleted file mode 100644 index 37901fb8..00000000 --- a/packages/cli/src/commands/install/desktop.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { installDesktop, type InstallChannel } from '../../lib/installations.js' -import { printDryRun, printSuccess } from '../../lib/output.js' - -export default class SetupInstallDesktop extends BeeperCommand { - static override summary = 'Install Beeper Desktop locally' - static override flags = { - channel: Flags.string({ options: ['stable', 'nightly'], default: 'stable', description: 'Desktop release channel' }), - } - - async run(): Promise { - const { flags } = await this.parse(SetupInstallDesktop) - ensureWritable(flags) - if (flags['dry-run']) { - await printDryRun('install.desktop', { channel: flags.channel }, flags.json ? 'json' : 'human') - return - } - const installation = await installDesktop({ channel: flags.channel as InstallChannel }) - await printSuccess({ - message: `Installed Beeper Desktop ${installation.version ?? ''}`.trim(), - detail: installation.path, - data: installation, - }, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/install/server.ts b/packages/cli/src/commands/install/server.ts deleted file mode 100644 index 78c7bc65..00000000 --- a/packages/cli/src/commands/install/server.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { installServer, type InstallChannel } from '../../lib/installations.js' -import { pathSetupHint } from '../../lib/env.js' -import { printDryRun, printSuccess } from '../../lib/output.js' -import { SERVER_ENVIRONMENTS, normalizeServerEnv } from '../../lib/server-env.js' - -export default class SetupInstallServer extends BeeperCommand { - static override summary = 'Install Beeper Server locally' - static override flags = { - channel: Flags.string({ options: ['stable', 'nightly'], default: 'stable', description: 'Server release channel' }), - 'server-env': Flags.string({ default: 'prod', description: 'Server environment: local, dev, staging, or prod', options: [...SERVER_ENVIRONMENTS], parse: async value => normalizeServerEnv(value) }), - } - - async run(): Promise { - const { flags } = await this.parse(SetupInstallServer) - ensureWritable(flags) - if (flags['dry-run']) { - await printDryRun('install.server', { channel: flags.channel, serverEnv: flags['server-env'] }, flags.json ? 'json' : 'human') - return - } - const installation = await installServer({ channel: flags.channel as InstallChannel, serverEnv: flags['server-env'] }) - await printSuccess({ - message: `Installed Beeper Server ${installation.version ?? ''}`.trim(), - detail: pathSetupHint() ?? installation.path, - data: installation, - }, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/man.ts b/packages/cli/src/commands/man.ts deleted file mode 100644 index c8fd9508..00000000 --- a/packages/cli/src/commands/man.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { BeeperCommand } from '../lib/command.js' -import { metadataForCommand } from '../lib/command-metadata.js' -import { commandManifest } from '../lib/manifest.js' -import { printCommands } from '../lib/output.js' -export default class Man extends BeeperCommand { - static override summary = 'Print the command manual' - async run(): Promise { - const { flags } = await this.parse(Man) - const commandsByID = new Map(this.config.commands.map(command => [command.id.replaceAll(':', ' '), command])) - const commands = commandManifest.map(item => { - const command = commandsByID.get(item.command) - return { - ...item, - description: command?.summary || command?.description || item.description, - ...metadataForCommand(item.command), - } - }) - await printCommands(commands, flags.json ? 'json' : 'human', { title: 'Beeper CLI' }) - } -} diff --git a/packages/cli/src/commands/media/download.ts b/packages/cli/src/commands/media/download.ts deleted file mode 100644 index 3c86c8d7..00000000 --- a/packages/cli/src/commands/media/download.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Args, Flags } from '@oclif/core' -import { mkdir, writeFile } from 'node:fs/promises' -import { basename, join } from 'node:path' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printDryRun, printSuccess } from '../../lib/output.js' -export default class MediaDownload extends BeeperCommand { - static override summary = 'Download message media' - static override args = { url: Args.string({ required: true, description: 'mxc:// or localmxc:// URL' }) } - static override flags = { - out: Flags.string({ char: 'o', default: '.', description: 'Output directory; pass - to stream the file to stdout' }), - } - async run(): Promise { - const { args, flags } = await this.parse(MediaDownload) - const format = flags.json ? 'json' : 'human' - if (flags['dry-run'] && flags.out !== '-') { - ensureWritable(flags) - await printDryRun('media.download', { url: args.url, out: flags.out }, format) - return - } - const client = await createClient(flags) - const response = await client.assets.serve({ url: args.url }) - const buffer = Buffer.from(await response.arrayBuffer()) - if (flags.out === '-') { - process.stdout.write(buffer) - return - } - ensureWritable(flags) - await mkdir(flags.out, { recursive: true }) - const path = join(flags.out, basename(new URL(args.url).pathname) || 'media') - await writeFile(path, buffer) - await printSuccess({ message: 'Downloaded media', detail: path, data: { path, bytes: buffer.length } }, format) - } -} diff --git a/packages/cli/src/commands/messages/context.ts b/packages/cli/src/commands/messages/context.ts deleted file mode 100644 index a70a23fa..00000000 --- a/packages/cli/src/commands/messages/context.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { collectPage, printData } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class MessagesContext extends BeeperCommand { - static override summary = 'Show message context' - static override flags = { - chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), - id: Flags.string({ required: true, description: 'Target message ID to center the window on' }), - before: Flags.integer({ default: 10, description: 'Number of messages to include before the target' }), - after: Flags.integer({ default: 10, description: 'Number of messages to include after the target' }), - pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), - } - async run(): Promise { - const { flags } = await this.parse(MessagesContext) - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - const before = await collectPage(client.messages.list(chatID, { cursor: flags.id, direction: 'before' }), flags.before) - const after = await collectPage(client.messages.list(chatID, { cursor: flags.id, direction: 'after' }), flags.after) - await printData({ chatID, messageID: flags.id, before, after }, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/messages/delete.ts b/packages/cli/src/commands/messages/delete.ts deleted file mode 100644 index ac011a6f..00000000 --- a/packages/cli/src/commands/messages/delete.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printDryRun, printSuccess } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class MessagesDelete extends BeeperCommand { - static override summary = 'Delete a message' - static override flags = { - chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), - id: Flags.string({ required: true, description: 'Message ID to delete (final message ID; pending IDs are rejected)' }), - pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), - 'for-everyone': Flags.boolean({ default: false, description: 'Delete for everyone when the network supports it (otherwise deletes only for you)' }), - } - async run(): Promise { - const { flags } = await this.parse(MessagesDelete) - ensureWritable(flags) - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - if (flags['dry-run']) { - await printDryRun('messages.delete', { chatID, messageID: flags.id, forEveryone: flags['for-everyone'] }, flags.json ? 'json' : 'human') - return - } - await client.messages.delete(flags.id, { chatID, forEveryone: flags['for-everyone'] || undefined }) - await printSuccess({ message: flags['for-everyone'] ? 'Deleted for everyone' : 'Deleted', data: { chatID, messageID: flags.id, forEveryone: flags['for-everyone'] } }, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/messages/edit.ts b/packages/cli/src/commands/messages/edit.ts deleted file mode 100644 index a473017b..00000000 --- a/packages/cli/src/commands/messages/edit.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class MessagesEdit extends BeeperCommand { - static override summary = 'Edit a message' - static override flags = { - chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), - id: Flags.string({ required: true, description: 'Message ID to edit (must be one of your own messages with no attachments)' }), - pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), - message: Flags.string({ required: true, description: 'New message text' }), - } - async run(): Promise { - const { flags } = await this.parse(MessagesEdit) - ensureWritable(flags) - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - if (flags['dry-run']) { - await printDryRun('messages.edit', { chatID, messageID: flags.id, text: flags.message }, flags.json ? 'json' : 'human') - return - } - await printData(await client.messages.update(flags.id, { chatID, text: flags.message }), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/messages/export.ts b/packages/cli/src/commands/messages/export.ts deleted file mode 100644 index 03d8ac38..00000000 --- a/packages/cli/src/commands/messages/export.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { writeFile } from 'node:fs/promises' -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printDryRun } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class MessagesExport extends BeeperCommand { - static override summary = 'Export one chat to JSON' - static override description = 'Lightweight per-chat JSON export. For a full export with transcripts, attachments, and multiple chats, use `beeper export`.' - static override flags = { - chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), - pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), - 'before-cursor': Flags.string({ description: 'Paginate messages older than this message ID' }), - 'after-cursor': Flags.string({ description: 'Paginate messages newer than this message ID' }), - after: Flags.string({ description: 'Only messages at or after this ISO timestamp (client-side filter)' }), - before: Flags.string({ description: 'Only messages at or before this ISO timestamp (client-side filter)' }), - limit: Flags.integer({ description: 'Maximum messages to export' }), - output: Flags.string({ char: 'o', default: '-', description: 'Output path; - writes JSON to stdout' }), - asc: Flags.boolean({ default: false, description: 'Order oldest first (default: newest first)' }), - } - - async run(): Promise { - const { flags } = await this.parse(MessagesExport) - if (flags['before-cursor'] && flags['after-cursor']) throw new Error('Use only one of --before-cursor or --after-cursor') - if (flags['dry-run']) { - await printDryRun('messages.export', { - chat: flags.chat, - pick: flags.pick, - output: flags.output, - beforeCursor: flags['before-cursor'], - afterCursor: flags['after-cursor'], - after: flags.after, - before: flags.before, - limit: flags.limit, - asc: flags.asc, - }, flags.json ? 'json' : 'human') - return - } - if (flags.output !== '-') ensureWritable(flags) - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - const cursor = flags['before-cursor'] ?? flags['after-cursor'] - const direction = flags['before-cursor'] ? 'before' : flags['after-cursor'] ? 'after' : undefined - const afterTs = flags.after ? Date.parse(flags.after) : undefined - const beforeTs = flags.before ? Date.parse(flags.before) : undefined - if (afterTs !== undefined && Number.isNaN(afterTs)) throw new Error(`--after is not a valid ISO timestamp: ${flags.after}`) - if (beforeTs !== undefined && Number.isNaN(beforeTs)) throw new Error(`--before is not a valid ISO timestamp: ${flags.before}`) - const items: unknown[] = [] - for await (const item of client.messages.list(chatID, { cursor, direction })) { - const ts = Date.parse((item as { timestamp?: string }).timestamp ?? '') - if (!Number.isNaN(ts)) { - if (afterTs !== undefined && ts < afterTs) continue - if (beforeTs !== undefined && ts > beforeTs) continue - } - items.push(item) - if (flags.limit !== undefined && items.length >= flags.limit) break - } - const messages = flags.asc ? [...items].reverse() : items - const envelope = { - exportedAt: new Date().toISOString(), - chatID, - after: flags.after, - before: flags.before, - count: messages.length, - messages, - } - const text = `${JSON.stringify(envelope, null, 2)}\n` - if (flags.output === '-') process.stdout.write(text) - else await writeFile(flags.output, text) - } -} diff --git a/packages/cli/src/commands/messages/list.ts b/packages/cli/src/commands/messages/list.ts deleted file mode 100644 index d3798d62..00000000 --- a/packages/cli/src/commands/messages/list.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { collectPage, printIDs, printList } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class MessagesList extends BeeperCommand { - static override summary = 'List chat messages' - static override flags = { - chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), - 'before-cursor': Flags.string({ description: 'Paginate messages older than this message ID' }), - 'after-cursor': Flags.string({ description: 'Paginate messages newer than this message ID' }), - sender: Flags.string({ description: 'Filter by sender: me, others, or a specific user ID (client-side)' }), - asc: Flags.boolean({ default: false, description: 'Order oldest first (default: newest first)' }), - ids: Flags.boolean({ default: false, description: 'Print only message IDs' }), - limit: Flags.integer({ default: 50, description: 'Maximum messages to print' }), - pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), - } - async run(): Promise { - const { flags } = await this.parse(MessagesList) - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - const before = flags['before-cursor'] - const after = flags['after-cursor'] - if (before && after) throw new Error('Use only one of --before-cursor or --after-cursor') - let items = await collectFiltered(client.messages.list(chatID, { cursor: before ?? after, direction: before ? 'before' : after ? 'after' : undefined }), flags.limit, flags.sender) - if (flags.asc) items = [...items].reverse() - if (flags.ids) printIDs(items) - else await printList(items, flags.json ? 'json' : 'human', { title: 'No messages yet', subtitle: 'This chat is empty.' }) - } -} - -async function collectFiltered(iterable: AsyncIterable, limit: number, sender: string | undefined): Promise { - if (!sender) return collectPage(iterable, limit) - const items: unknown[] = [] - for await (const item of iterable) { - if (matchesSender(item, sender)) items.push(item) - if (items.length >= limit) break - } - return items -} - -export function matchesSender(item: unknown, sender: string): boolean { - if (!item || typeof item !== 'object') return false - const row = item as { isSender?: boolean; senderID?: string } - if (sender === 'me') return row.isSender === true - if (sender === 'others') return row.isSender !== true - return row.senderID === sender -} diff --git a/packages/cli/src/commands/messages/search.ts b/packages/cli/src/commands/messages/search.ts deleted file mode 100644 index 2515087b..00000000 --- a/packages/cli/src/commands/messages/search.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Args, Flags } from '@oclif/core' -import { BeeperCommand } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { usageError } from '../../lib/errors.js' -import { collectPage, isMachineReadableOutput, printIDs, printList } from '../../lib/output.js' -import { resolveAccountIDs, resolveChatID } from '../../lib/resolve.js' -import { withInkSpinner as withSpinner } from '../../lib/ink/spinner.js' - -export default class MessagesSearch extends BeeperCommand { - static override summary = 'Search messages across chats' - static override examples = [ - 'beeper messages search "quarterly report"', - 'beeper messages search --chat "Work" --sender me --limit 20', - 'beeper messages search --media image --after 2024-01-01T00:00:00Z', - 'beeper messages search --chat-type group --sender others "meeting"', - ] - static override args = { - query: Args.string({ description: 'Search text (literal word match)', required: false }), - } - static override flags = { - account: Flags.string({ multiple: true, description: 'Limit to an account selector. Repeat for multiple.' }), - chat: Flags.string({ multiple: true, description: 'Limit to a chat selector. Repeat for multiple.' }), - 'chat-type': Flags.string({ options: ['group', 'single'], description: 'Only group chats or direct messages' }), - after: Flags.string({ description: 'Only messages at or after this ISO timestamp' }), - before: Flags.string({ description: 'Only messages at or before this ISO timestamp' }), - 'exclude-low-priority': Flags.boolean({ allowNo: true, description: 'Exclude low-priority chats' }), - ids: Flags.boolean({ default: false, description: 'Print only message IDs' }), - 'include-muted': Flags.boolean({ allowNo: true, default: true, description: 'Include muted chats' }), - limit: Flags.integer({ default: 50, description: 'Maximum results' }), - media: Flags.string({ multiple: true, options: ['any', 'video', 'image', 'link', 'file'], description: 'Filter by media type. Repeat for multiple.' }), - sender: Flags.string({ description: 'me, others, or a user ID' }), - } - - async run(): Promise { - const { args, flags } = await this.parse(MessagesSearch) - const hasFilter = Boolean( - flags.account?.length || flags.chat?.length || flags['chat-type'] - || flags.after || flags.before || flags.media?.length - || flags.sender, - ) - if (!args.query && !hasFilter) { - throw usageError('Provide a search query or at least one filter flag (--chat, --sender, --media, etc.).') - } - const client = await createClient(flags) - const accountIDs = await resolveAccountIDs(client, flags.account, { allowMultiplePerInput: true }) - const chatIDs = flags.chat?.length - ? await Promise.all(flags.chat.map(chat => resolveChatID(client, chat, { accountIDs }))) - : undefined - const params = { - accountIDs, - chatIDs, - chatType: flags['chat-type'] as 'group' | 'single' | undefined, - dateAfter: flags.after, - dateBefore: flags.before, - excludeLowPriority: flags['exclude-low-priority'], - includeMuted: flags['include-muted'], - mediaTypes: flags.media as Array<'any' | 'video' | 'image' | 'link' | 'file'> | undefined, - query: args.query, - sender: flags.sender as 'me' | 'others' | (string & {}) | undefined, - } - const useSpinner = !isMachineReadableOutput(flags.ids ? 'ids' : flags.json ? 'json' : 'human') - const label = args.query ? `Searching messages for "${args.query}"…` : 'Searching messages…' - const collect = () => collectPage(client.messages.search(params), flags.limit) - const items = useSpinner - ? await withSpinner(label, collect, { - done: value => `${value.length} match${value.length === 1 ? '' : 'es'}`, - }) - : await collect() - if (flags.ids) { - printIDs(items) - return - } - await printList(items, flags.json ? 'json' : 'human', { - title: 'No messages matched', - subtitle: args.query ? `Nothing found for "${args.query}".` : 'Try a different filter combination.', - suggestions: [ - { command: 'beeper messages list --chat ', hint: 'list messages from a chat' }, - { command: 'beeper chats search ""', hint: 'search chats instead' }, - ], - }) - } -} diff --git a/packages/cli/src/commands/messages/show.ts b/packages/cli/src/commands/messages/show.ts deleted file mode 100644 index d46a3f01..00000000 --- a/packages/cli/src/commands/messages/show.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class MessagesShow extends BeeperCommand { - static override summary = 'Show one message' - static override flags = { - chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), - id: Flags.string({ required: true, description: 'Message ID, pendingMessageID, or Matrix event ID' }), - pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), - } - async run(): Promise { - const { flags } = await this.parse(MessagesShow) - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - if (client.messages.retrieve) await printData(await client.messages.retrieve(flags.id, { chatID }), flags.json ? 'json' : 'human') - else throw new Error('This Desktop API does not expose message lookup.') - } -} diff --git a/packages/cli/src/commands/plugins.ts b/packages/cli/src/commands/plugins.ts deleted file mode 100644 index 2f0a8c64..00000000 --- a/packages/cli/src/commands/plugins.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Command } from '@oclif/core' - -export default class Plugins extends Command { - static override summary = 'Manage Beeper CLI plugins' - static override description = 'List recommended Beeper CLI plugins, or use oclif plugin commands to install, link, update, and remove plugins.' - - async run(): Promise { - this.log('Recommended plugins:') - this.log(' beeper plugins available') - this.log('') - this.log('Plugin management:') - this.log(' beeper plugins install ') - this.log(' beeper plugins link ') - this.log(' beeper plugins uninstall ') - } -} diff --git a/packages/cli/src/commands/plugins/available.ts b/packages/cli/src/commands/plugins/available.ts deleted file mode 100644 index e2f16dd6..00000000 --- a/packages/cli/src/commands/plugins/available.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { BeeperCommand } from '../../lib/command.js' -import { printData } from '../../lib/output.js' -import { recommendedPlugins } from '../../lib/recommended-plugins.js' - -export default class PluginsAvailable extends BeeperCommand { - static override summary = 'List recommended optional Beeper CLI plugins' - - async run(): Promise { - const { flags } = await this.parse(PluginsAvailable) - const installed = new Set(this.config.plugins.keys()) - const corePlugins = new Set((this.config.pjson.oclif.plugins ?? []) as string[]) - const plugins = recommendedPlugins.map(plugin => { - const isInstalled = installed.has(plugin.name) - return { - ...plugin, - installed: isInstalled, - status: isInstalled ? 'installed' : 'not installed', - core: corePlugins.has(plugin.name), - } - }) - - await printData(plugins, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/presence.ts b/packages/cli/src/commands/presence.ts deleted file mode 100644 index fe85d783..00000000 --- a/packages/cli/src/commands/presence.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { setTimeout as delay } from 'node:timers/promises' -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../lib/command.js' -import { createClient } from '../lib/client.js' -import { printDryRun, printSuccess } from '../lib/output.js' -import { resolveChatID } from '../lib/resolve.js' - -export default class Presence extends BeeperCommand { - static override summary = 'Send a typing (or paused) indicator to a chat' - static override description = 'Requires server-side support. Networks without typing notifications return an error.' - static override flags = { - chat: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), - pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), - state: Flags.string({ default: 'typing', options: ['typing', 'paused'], description: 'Indicator to send' }), - duration: Flags.integer({ description: 'When --state is typing, send paused automatically after this many seconds' }), - } - - async run(): Promise { - const { flags } = await this.parse(Presence) - if (flags.duration !== undefined && flags.duration <= 0) throw new Error('--duration must be a positive integer (seconds)') - if (flags.duration !== undefined && flags.state !== 'typing') throw new Error('--duration only applies when --state is typing') - if (flags['dry-run']) { - await printDryRun('presence', { chat: flags.chat, pick: flags.pick, state: flags.state, durationSeconds: flags.duration }, flags.json ? 'json' : 'human') - return - } - - ensureWritable(flags) - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick }) - const post = (state: 'typing' | 'paused') => - client.post(`/v1/chats/${encodeURIComponent(chatID)}/typing`, { body: { state } }) - - await post(flags.state as 'typing' | 'paused') - if (flags.duration !== undefined) { - await delay(flags.duration * 1000) - await post('paused') - await printSuccess({ message: `Sent typing then paused after ${flags.duration}s`, data: { chatID, state: 'paused', durationSeconds: flags.duration } }, flags.json ? 'json' : 'human') - return - } - - await printSuccess({ message: `Sent ${flags.state} indicator`, data: { chatID, state: flags.state } }, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/resolve/account.ts b/packages/cli/src/commands/resolve/account.ts deleted file mode 100644 index ade03dc9..00000000 --- a/packages/cli/src/commands/resolve/account.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Args, Flags } from '@oclif/core' -import { BeeperCommand } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { notFound } from '../../lib/errors.js' -import { printData } from '../../lib/output.js' -import { resolveAccountIDs } from '../../lib/resolve.js' - -export default class ResolveAccount extends BeeperCommand { - static override summary = 'Resolve an account selector' - static override args = { - selector: Args.string({ required: true, description: 'Account ID, network, bridge, or account user selector' }), - } - static override flags = { - pick: Flags.integer({ description: 'Select the Nth candidate (1-indexed)' }), - } - - async run(): Promise { - const { args, flags } = await this.parse(ResolveAccount) - const client = await createClient(flags) - const response = await client.accounts.list() - const rows = Array.isArray(response) ? response : ((response as any).items ?? []) - const ids = await resolveAccountIDs(client, [args.selector], { allowMultiplePerInput: true, applyDefault: false }) - const candidates = rows.filter((row: any) => ids?.includes(String(row.accountID ?? row.id))) - if (!candidates.length) throw notFound(`No account matches "${args.selector}"`, { selector: args.selector, kind: 'account' }) - const pick = flags.pick - const selected = pick !== undefined ? candidates[pick - 1] : candidates.length === 1 ? candidates[0] : undefined - if (pick !== undefined && !selected) throw notFound(`--pick ${pick} is outside the ${candidates.length} matching accounts`, { selector: args.selector, pick, count: candidates.length }) - await printData({ - selector: args.selector, - kind: 'account', - selected: selected ? accountCandidate(selected, candidates.indexOf(selected) + 1) : null, - candidates: candidates.map((account: any, index: number) => accountCandidate(account, index + 1)), - }, flags.json ? 'json' : 'human') - } -} - -function accountCandidate(account: any, pick: number): Record { - return { - pick, - id: account.accountID ?? account.id, - accountID: account.accountID, - network: account.network, - bridge: account.bridge, - user: account.user, - raw: account, - } -} diff --git a/packages/cli/src/commands/resolve/bridge.ts b/packages/cli/src/commands/resolve/bridge.ts deleted file mode 100644 index 44781dfe..00000000 --- a/packages/cli/src/commands/resolve/bridge.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Args, Flags } from '@oclif/core' -import { BeeperCommand } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { notFound } from '../../lib/errors.js' -import { printData } from '../../lib/output.js' - -export default class ResolveBridge extends BeeperCommand { - static override summary = 'Resolve a bridge selector' - static override args = { - selector: Args.string({ required: true, description: 'Bridge ID, type, provider, or display name' }), - } - static override flags = { - pick: Flags.integer({ description: 'Select the Nth candidate (1-indexed)' }), - } - - async run(): Promise { - const { args, flags } = await this.parse(ResolveBridge) - const client = await createClient(flags) - const response = await client.bridges.list() - const rows = ((response as unknown as { items?: Array> }).items ?? []) - const normalized = normalize(args.selector) - const candidates = rows.filter(bridge => - normalize(bridge.id) === normalized || - normalize(bridge.type) === normalized || - normalize(bridge.provider) === normalized || - normalize(bridge.name) === normalized || - normalize(bridge.displayName) === normalized || - normalize(bridge.id).includes(normalized) || - normalize(bridge.displayName).includes(normalized) - ) - if (!candidates.length) throw notFound(`No bridge matches "${args.selector}"`, { selector: args.selector, kind: 'bridge' }) - const pick = flags.pick - const selected = pick !== undefined ? candidates[pick - 1] : candidates.length === 1 ? candidates[0] : undefined - if (pick !== undefined && !selected) throw notFound(`--pick ${pick} is outside the ${candidates.length} matching bridges`, { selector: args.selector, pick, count: candidates.length }) - await printData({ - selector: args.selector, - kind: 'bridge', - selected: selected ? bridgeCandidate(selected, candidates.indexOf(selected) + 1) : null, - candidates: candidates.map((bridge, index) => bridgeCandidate(bridge, index + 1)), - }, flags.json ? 'json' : 'human') - } -} - -function bridgeCandidate(bridge: Record, pick: number): Record { - return { pick, id: bridge.id, type: bridge.type, provider: bridge.provider, displayName: bridge.displayName ?? bridge.name, status: bridge.status, raw: bridge } -} - -function normalize(value: unknown): string { - return String(value ?? '').trim().toLowerCase().replace(/[\s._-]+/g, '') -} diff --git a/packages/cli/src/commands/resolve/chat.ts b/packages/cli/src/commands/resolve/chat.ts deleted file mode 100644 index 4586e190..00000000 --- a/packages/cli/src/commands/resolve/chat.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Args, Flags } from '@oclif/core' -import { BeeperCommand } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { notFound } from '../../lib/errors.js' -import { collectPage, printData } from '../../lib/output.js' -import { resolveAccountIDs } from '../../lib/resolve.js' - -export default class ResolveChat extends BeeperCommand { - static override summary = 'Resolve a chat selector to concrete chat candidates' - static override args = { - selector: Args.string({ required: true, description: 'Chat ID, local ID, exact title, or search text' }), - } - static override flags = { - account: Flags.string({ multiple: true, description: 'Limit to account selector. Repeat for multiple.' }), - pick: Flags.integer({ description: 'Select the Nth candidate (1-indexed)' }), - limit: Flags.integer({ default: 10, description: 'Maximum candidates to return' }), - } - - async run(): Promise { - const { args, flags } = await this.parse(ResolveChat) - const client = await createClient(flags) - const accountIDs = await resolveAccountIDs(client, flags.account, { allowMultiplePerInput: true }) - const candidates = await collectPage(client.chats.search({ accountIDs, query: args.selector, scope: 'titles' }), flags.limit) - const normalized = normalize(args.selector) - const exact = candidates.filter(chat => - normalize(chat.id) === normalized || - normalize(chat.localChatID) === normalized || - normalize(chat.title) === normalized - ) - const matches = exact.length ? exact : candidates - if (!matches.length) throw notFound(`No chat matches "${args.selector}"`, { selector: args.selector, kind: 'chat' }) - const selected = flags.pick ? matches[flags.pick - 1] : matches.length === 1 ? matches[0] : undefined - if (flags.pick && !selected) throw notFound(`--pick ${flags.pick} is outside the ${matches.length} matching chats`, { selector: args.selector, pick: flags.pick, count: matches.length }) - await printData({ - selector: args.selector, - kind: 'chat', - selected: selected ? chatCandidate(selected, matches.indexOf(selected) + 1) : null, - candidates: matches.map((chat, index) => chatCandidate(chat, index + 1)), - }, flags.json ? 'json' : 'human') - } -} - -type Chat = Record - -function chatCandidate(chat: Chat, pick: number): Record { - return { - pick, - id: chat.id, - localChatID: chat.localChatID, - title: chat.title, - network: chat.network, - accountID: chat.accountID, - raw: chat, - } -} - -function normalize(value: unknown): string { - return String(value ?? '').trim().toLowerCase().replace(/[\s._-]+/g, '') -} diff --git a/packages/cli/src/commands/resolve/contact.ts b/packages/cli/src/commands/resolve/contact.ts deleted file mode 100644 index 8cfd2491..00000000 --- a/packages/cli/src/commands/resolve/contact.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Args, Flags } from '@oclif/core' -import { BeeperCommand } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { notFound } from '../../lib/errors.js' -import { printData } from '../../lib/output.js' -import { listAccountIDs, resolveAccountIDs } from '../../lib/resolve.js' - -export default class ResolveContact extends BeeperCommand { - static override summary = 'Resolve a contact selector' - static override args = { - selector: Args.string({ required: true, description: 'Contact name, username, phone, email, or ID' }), - } - static override flags = { - account: Flags.string({ multiple: true, description: 'Limit to account selector. Repeat for multiple.' }), - pick: Flags.integer({ description: 'Select the Nth candidate (1-indexed)' }), - limit: Flags.integer({ default: 10, description: 'Maximum candidates to return per account' }), - } - - async run(): Promise { - const { args, flags } = await this.parse(ResolveContact) - const client = await createClient(flags) - const accountIDs = await resolveAccountIDs(client, flags.account, { allowMultiplePerInput: true }) ?? await listAccountIDs(client) - const candidates: Array> = [] - for (const accountID of accountIDs) { - try { - const result = await client.accounts.contacts.search(accountID, { query: args.selector }) - candidates.push(...result.items.slice(0, flags.limit).map((item: unknown) => ({ ...(item as Record), accountID }))) - } catch (error) { - if (shouldIgnoreLookupError(error)) continue - throw error - } - } - if (!candidates.length) throw notFound(`No contact matches "${args.selector}"`, { selector: args.selector, kind: 'contact' }) - const pick = flags.pick - const selected = pick !== undefined ? candidates[pick - 1] : candidates.length === 1 ? candidates[0] : undefined - if (pick !== undefined && !selected) throw notFound(`--pick ${pick} is outside the ${candidates.length} matching contacts`, { selector: args.selector, pick, count: candidates.length }) - await printData({ - selector: args.selector, - kind: 'contact', - selected: selected ? contactCandidate(selected, candidates.indexOf(selected) + 1) : null, - candidates: candidates.map((contact, index) => contactCandidate(contact, index + 1)), - }, flags.json ? 'json' : 'human') - } -} - -function contactCandidate(contact: Record, pick: number): Record { - return { - pick, - id: contact.id, - accountID: contact.accountID, - displayName: contact.displayName ?? contact.fullName ?? contact.name, - username: contact.username, - phoneNumber: contact.phoneNumber, - email: contact.email, - } -} - -function shouldIgnoreLookupError(error: unknown): boolean { - if (!(error instanceof Error)) return false - const status = (error as Error & { status?: number; statusCode?: number }).status ?? (error as Error & { status?: number; statusCode?: number }).statusCode - if (status === 400 || status === 404) return true - return /\b(400|404)\b|not supported|not found/i.test(error.message) -} diff --git a/packages/cli/src/commands/resolve/target.ts b/packages/cli/src/commands/resolve/target.ts deleted file mode 100644 index a6b8b325..00000000 --- a/packages/cli/src/commands/resolve/target.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Args, Flags } from '@oclif/core' -import { BeeperCommand } from '../../lib/command.js' -import { notFound } from '../../lib/errors.js' -import { printData } from '../../lib/output.js' -import { builtInDesktopTargetID, listTargets, readConfig, type Target } from '../../lib/targets.js' - -export default class ResolveTarget extends BeeperCommand { - static override summary = 'Resolve a target selector' - static override args = { - selector: Args.string({ required: true, description: 'Target name, ID, type, or base URL' }), - } - static override flags = { - pick: Flags.integer({ description: 'Select the Nth candidate (1-indexed)' }), - } - - async run(): Promise { - const { args, flags } = await this.parse(ResolveTarget) - const config = await readConfig() - const builtIn: Target = { - id: builtInDesktopTargetID, - type: 'desktop', - name: 'Beeper Desktop', - baseURL: process.env.BEEPER_DESKTOP_BASE_URL || config.baseURL || 'http://127.0.0.1:23373', - auth: config.auth, - } - const targets = [builtIn, ...await listTargets()] - const normalized = normalize(args.selector) - const candidates = targets.filter(target => - normalize(target.id) === normalized || - normalize(target.name) === normalized || - normalize(target.type) === normalized || - normalize(target.baseURL).includes(normalized) - ) - if (!candidates.length) throw notFound(`No target matches "${args.selector}"`, { selector: args.selector, kind: 'target' }) - const selected = flags.pick ? candidates[flags.pick - 1] : candidates.length === 1 ? candidates[0] : undefined - if (flags.pick && !selected) throw notFound(`--pick ${flags.pick} is outside the ${candidates.length} matching targets`, { selector: args.selector, pick: flags.pick, count: candidates.length }) - await printData({ - selector: args.selector, - kind: 'target', - selected: selected ? targetCandidate(selected, candidates.indexOf(selected) + 1) : null, - candidates: candidates.map((target, index) => targetCandidate(target, index + 1)), - }, flags.json ? 'json' : 'human') - } -} - -function targetCandidate(target: Target, pick: number): Record { - return { pick, id: target.id, name: target.name, type: target.type, baseURL: target.baseURL, managed: target.managed, raw: target } -} - -function normalize(value: unknown): string { - return String(value ?? '').trim().toLowerCase().replace(/[\s._-]+/g, '') -} diff --git a/packages/cli/src/commands/rpc.ts b/packages/cli/src/commands/rpc.ts deleted file mode 100644 index 08fd3389..00000000 --- a/packages/cli/src/commands/rpc.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { BeeperCommand } from '../lib/command.js' -import { createInterface } from 'node:readline/promises' -import { stdin as input } from 'node:process' -import { splitCommandLine } from '../lib/argv.js' -import { runCli } from '../lib/runner.js' - -type RPCRequest = { - args?: string[] - argv?: string[] - command?: string - id?: string | number | null -} - -export default class RPC extends BeeperCommand { - static override summary = 'Run newline-delimited JSON command RPC over stdin/stdout' - static override description = 'Reads JSON lines like {"id":1,"command":"send text --to 10313 --message hello"} or {"id":1,"args":["status","--json"]}.' - - async run(): Promise { - const rl = createInterface({ input }) - - for await (const line of rl) { - if (!line.trim()) continue - let requestID: string | number | null = null - - try { - const request = JSON.parse(line) as RPCRequest - requestID = request.id ?? null - const args = normalizeArgs(request) - if (args[0] === 'rpc' || args[0] === 'shell') throw new Error(`Unsupported nested command: ${args[0]}`) - const result = await runCli(args) - process.stdout.write(`${JSON.stringify({ - id: requestID, - ok: result.code === 0, - code: result.code, - signal: result.signal, - stdout: result.stdout, - stderr: result.stderr, - })}\n`) - } catch (error) { - process.stdout.write(`${JSON.stringify({ - id: requestID, - ok: false, - error: error instanceof Error ? error.message : String(error), - })}\n`) - } - } - } -} - -function normalizeArgs(request: RPCRequest): string[] { - const args = request.args ?? request.argv ?? (request.command ? splitCommandLine(request.command) : undefined) - if (!args || args.length === 0) throw new Error('Expected args, argv, or command') - return args -} diff --git a/packages/cli/src/commands/schema.ts b/packages/cli/src/commands/schema.ts deleted file mode 100644 index f990d858..00000000 --- a/packages/cli/src/commands/schema.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { Args } from '@oclif/core' -import { BeeperCommand } from '../lib/command.js' -import { metadataForCommand } from '../lib/command-metadata.js' -import { commandManifest } from '../lib/manifest.js' -import { printData } from '../lib/output.js' - -type RawCommand = { - id: string - aliases?: string[] - args?: Record - description?: string - flags?: Record - hidden?: boolean - pluginName?: string - summary?: string -} - -export default class Schema extends BeeperCommand { - static override strict = false - static override summary = 'Print machine-readable command/flag schema' - static override description = 'Agent-first schema for commands, flags, args, examples, mutation metadata, selectors, output shapes, and related commands.' - static override args = { - command: Args.string({ required: false, description: 'Optional command path, such as "messages search"' }), - } - - async run(): Promise { - const { argv } = await this.parse(Schema) - const pathArgs = argv as string[] - const requested = pathArgs.length > 0 ? pathArgs.join(' ') : undefined - const manifestByCommand = new Map(commandManifest.map(item => [item.command, item])) - const commands = (this.config.commands as RawCommand[]) - .filter(command => !command.hidden) - .map(command => { - const path = command.id.replaceAll(':', ' ') - const manifest = manifestByCommand.get(path) - const metadata = metadataForCommand(path) - return { - path, - id: command.id, - aliases: (command.aliases ?? []).map(alias => alias.replaceAll(':', ' ')), - summary: command.summary ?? manifest?.description ?? command.description ?? '', - description: command.description ?? manifest?.description ?? command.summary ?? '', - examples: manifest?.examples ?? [], - args: normalizeFields(command.args), - flags: normalizeFields(command.flags), - ...metadata, - supports: { - dryRun: metadata.mutates, - force: metadata.mutates, - format: true, - noInput: true, - readOnly: true, - select: true, - }, - outputShape: outputShape(metadata.output), - } - }) - .sort((a, b) => a.path.localeCompare(b.path)) - - const filtered = requested - ? commands.filter(command => command.path === requested || command.path.startsWith(`${requested} `)) - : commands - - await printData({ - schemaVersion: 1, - bin: this.config.bin, - version: this.config.version, - defaults: { - stdout: 'primary command output only', - stderr: 'diagnostics, progress, events, and structured errors', - nonTTYFormat: 'json', - ttyFormat: 'table', - }, - formats: ['json', 'jsonl', 'table', 'text', 'ids'], - exitCodes: { - 0: 'success', - 1: 'generic runtime error', - 2: 'usage error', - 3: 'auth required', - 4: 'target/account not ready', - 5: 'selector matched nothing', - 6: 'ambiguous selector', - 127: 'declined did-you-mean suggestion', - }, - commands: filtered, - }, 'json') - } -} - -function normalizeFields(fields: Record | undefined): Array> { - if (!fields) return [] - return Object.entries(fields).map(([name, raw]) => normalizeField(name, raw)) -} - -function normalizeField(name: string, raw: unknown): Record { - const record = raw && typeof raw === 'object' ? raw as Record : {} - return { - name, - description: record.description ?? record.summary ?? '', - required: Boolean(record.required), - multiple: Boolean(record.multiple), - default: record.default, - options: record.options, - char: record.char, - type: typeName(record), - } -} - -function typeName(record: Record): string { - if (Array.isArray(record.options)) return 'enum' - if (record.type === 'boolean' || record.type === 'option') return String(record.type) - if (typeof record.parse === 'function') return 'string' - if (typeof record.default === 'boolean') return 'boolean' - if (typeof record.default === 'number') return 'integer' - return 'string' -} - -function outputShape(kind: string): Record { - const envelope = { ok: true, data: '', error: null, meta: '' } - switch (kind) { - case 'list': { - return { kind, envelope, data: 'array' } - } - - case 'send-result': { - return { kind, envelope, data: { chatID: 'string', pendingMessageID: 'string?', state: 'string?' } } - } - - case 'stream': { - return { kind, data: 'jsonl events or RPC lines' } - } - - case 'success': { - return { kind, envelope, data: { message: 'string', detail: 'string?', data: 'object?' } } - } - - default: { - return { kind, envelope, data: 'object' } - } - } -} diff --git a/packages/cli/src/commands/send/file.ts b/packages/cli/src/commands/send/file.ts deleted file mode 100644 index 2e5442fc..00000000 --- a/packages/cli/src/commands/send/file.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' -import { sendMessage } from '../../lib/send-message.js' - -export default class SendFile extends BeeperCommand { - static override summary = 'Send a file' - static override description = 'Returns when Desktop accepts the send request. Pass `--wait` to wait until the message leaves the pending state or fails.' - static override examples = [ - 'beeper send file --to 10313 --file ./photo.jpg --caption "Look at this"', - 'beeper send file --to alice@whatsapp --file ./report.pdf', - 'beeper send file --to 8951 --file ./clip.mp4 --reply-to ', - ] - static override flags = { - to: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), - file: Flags.string({ required: true, description: 'Local file path to upload (max 500 MB)' }), - caption: Flags.string({ description: 'Optional caption to send alongside the file' }), - filename: Flags.string({ description: 'Override the displayed filename' }), - mime: Flags.string({ description: 'Override MIME type detection' }), - pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), - 'reply-to': Flags.string({ description: 'Send as a reply to this message ID' }), - wait: Flags.boolean({ default: false, description: 'Wait for the message to leave the pending state (or fail) before returning' }), - 'wait-timeout': Flags.integer({ default: 30_000, description: 'Maximum wait time in ms when --wait is set' }), - } - async run(): Promise { - const { flags } = await this.parse(SendFile) - ensureWritable(flags) - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.to, { pick: flags.pick }) - const request = { chatID, file: flags.file, fileName: flags.filename, mimeType: flags.mime, replyTo: flags['reply-to'], text: flags.caption || '', wait: flags.wait, waitTimeoutMs: flags['wait-timeout'] } - if (flags['dry-run']) { - await printDryRun('send.file', request, flags.json ? 'json' : 'human') - return - } - - await printData(await sendMessage(client, request), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/send/react.ts b/packages/cli/src/commands/send/react.ts deleted file mode 100644 index c4d5cea5..00000000 --- a/packages/cli/src/commands/send/react.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class SendReact extends BeeperCommand { - static override summary = 'Send a reaction to a message' - static override flags = { - to: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), - id: Flags.string({ required: true, description: 'Message ID to react to' }), - reaction: Flags.string({ required: true, description: 'Reaction key (emoji, shortcode, or custom emoji key)' }), - pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), - transaction: Flags.string({ description: 'Optional transaction ID for deduplication' }), - } - async run(): Promise { - const { flags } = await this.parse(SendReact) - ensureWritable(flags) - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.to, { pick: flags.pick }) - const request = { chatID, messageID: flags.id, reactionKey: flags.reaction, transactionID: flags.transaction } - if (flags['dry-run']) { - await printDryRun('send.react', request, flags.json ? 'json' : 'human') - return - } - await printData( - await client.chats.messages.reactions.add(flags.id, { chatID, reactionKey: flags.reaction, transactionID: flags.transaction }), - flags.json ? 'json' : 'human', - ) - } -} diff --git a/packages/cli/src/commands/send/sticker.ts b/packages/cli/src/commands/send/sticker.ts deleted file mode 100644 index 284c8de0..00000000 --- a/packages/cli/src/commands/send/sticker.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' -import { sendMessage } from '../../lib/send-message.js' - -export default class SendSticker extends BeeperCommand { - static override summary = 'Send a sticker' - static override description = 'Uploads the file and sends as a sticker message. Defaults --mime to image/webp.' - static override flags = { - to: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), - file: Flags.string({ required: true, description: 'Sticker file (typically 512x512 WebP)' }), - filename: Flags.string({ description: 'Override the displayed filename' }), - mime: Flags.string({ default: 'image/webp', description: 'MIME type for the sticker (default: image/webp)' }), - pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), - 'reply-to': Flags.string({ description: 'Send as a reply to this message ID' }), - wait: Flags.boolean({ default: false, description: 'Wait for the message to leave the pending state (or fail) before returning' }), - 'wait-timeout': Flags.integer({ default: 30_000, description: 'Maximum wait time in ms when --wait is set' }), - } - async run(): Promise { - const { flags } = await this.parse(SendSticker) - ensureWritable(flags) - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.to, { pick: flags.pick }) - const request = { - chatID, - file: flags.file, - fileName: flags.filename, - mimeType: flags.mime, - replyTo: flags['reply-to'], - text: '', - attachmentType: 'sticker' as const, - wait: flags.wait, - waitTimeoutMs: flags['wait-timeout'], - } - if (flags['dry-run']) { - await printDryRun('send.sticker', request, flags.json ? 'json' : 'human') - return - } - await printData( - await sendMessage(client, request), - flags.json ? 'json' : 'human', - ) - } -} diff --git a/packages/cli/src/commands/send/text.ts b/packages/cli/src/commands/send/text.ts deleted file mode 100644 index 3363907a..00000000 --- a/packages/cli/src/commands/send/text.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' -import { sendMessage } from '../../lib/send-message.js' - -export default class SendText extends BeeperCommand { - static override summary = 'Send a text message' - static override description = 'Returns when Desktop accepts the send request. Pass `--wait` to wait until the message leaves the pending state or fails.' - static override examples = [ - 'beeper send text --to 10313 --message "On my way"', - 'beeper send text --to 8951 --message "See @alice" --mention alice@whatsapp', - 'beeper send text --to alice@whatsapp --message "Got it" --reply-to ', - ] - static override flags = { - to: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), - message: Flags.string({ required: true, description: 'Message text to send' }), - pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), - 'reply-to': Flags.string({ description: 'Send as a reply to this message ID' }), - mention: Flags.string({ multiple: true, description: 'User ID to @-mention (repeatable)' }), - 'no-preview': Flags.boolean({ default: false, description: 'Disable automatic link preview for URLs in the message' }), - wait: Flags.boolean({ default: false, description: 'Wait for the message to leave the pending state (or fail) before returning' }), - 'wait-timeout': Flags.integer({ default: 30_000, description: 'Maximum wait time in ms when --wait is set' }), - } - async run(): Promise { - const { flags } = await this.parse(SendText) - ensureWritable(flags) - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.to, { pick: flags.pick }) - const request = { chatID, text: flags.message, replyTo: flags['reply-to'], mentions: flags.mention, noPreview: flags['no-preview'], wait: flags.wait, waitTimeoutMs: flags['wait-timeout'] } - if (flags['dry-run']) { - await printDryRun('send.text', request, flags.json ? 'json' : 'human') - return - } - - await printData(await sendMessage(client, request), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/send/unreact.ts b/packages/cli/src/commands/send/unreact.ts deleted file mode 100644 index c1494ea6..00000000 --- a/packages/cli/src/commands/send/unreact.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' - -export default class SendUnreact extends BeeperCommand { - static override summary = 'Remove a reaction from a message' - static override flags = { - to: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), - id: Flags.string({ required: true, description: 'Message ID whose reaction to remove' }), - reaction: Flags.string({ required: true, description: 'Reaction key to remove (emoji, shortcode, or custom emoji key)' }), - pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), - transaction: Flags.string({ description: 'Optional transaction ID for deduplication' }), - } - - async run(): Promise { - const { flags } = await this.parse(SendUnreact) - ensureWritable(flags) - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.to, { pick: flags.pick }) - const request = { chatID, messageID: flags.id, reactionKey: flags.reaction } - if (flags['dry-run']) { - await printDryRun('send.unreact', request, flags.json ? 'json' : 'human') - return - } - await printData( - await client.chats.messages.reactions.delete(flags.reaction, { chatID, messageID: flags.id }), - flags.json ? 'json' : 'human', - ) - } -} diff --git a/packages/cli/src/commands/send/voice.ts b/packages/cli/src/commands/send/voice.ts deleted file mode 100644 index e89f8aa9..00000000 --- a/packages/cli/src/commands/send/voice.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -import { resolveChatID } from '../../lib/resolve.js' -import { sendMessage } from '../../lib/send-message.js' - -export default class SendVoice extends BeeperCommand { - static override summary = 'Send a voice note' - static override description = 'Uploads the audio file and sends as a voice note. Defaults --mime to audio/ogg.' - static override flags = { - to: Flags.string({ required: true, description: 'Chat selector (ID, local ID, title, or search text)' }), - file: Flags.string({ required: true, description: 'Voice note audio file (OGG/Opus recommended)' }), - duration: Flags.integer({ description: 'Voice note duration in seconds (overrides upload-detected duration)' }), - filename: Flags.string({ description: 'Override the displayed filename' }), - mime: Flags.string({ default: 'audio/ogg', description: 'MIME type for the voice note (default: audio/ogg)' }), - pick: Flags.integer({ description: 'Pick the Nth result when the selector is ambiguous (1-indexed)' }), - 'reply-to': Flags.string({ description: 'Send as a reply to this message ID' }), - wait: Flags.boolean({ default: false, description: 'Wait for the message to leave the pending state (or fail) before returning' }), - 'wait-timeout': Flags.integer({ default: 30_000, description: 'Maximum wait time in ms when --wait is set' }), - } - async run(): Promise { - const { flags } = await this.parse(SendVoice) - const dryRunRequest = { - chat: flags.to, - pick: flags.pick, - file: flags.file, - fileName: flags.filename, - mimeType: flags.mime, - replyTo: flags['reply-to'], - text: '', - attachmentType: 'voice-note' as const, - duration: flags.duration, - wait: flags.wait, - waitTimeoutMs: flags['wait-timeout'], - } - if (flags['dry-run']) { - await printDryRun('send.voice', dryRunRequest, flags.json ? 'json' : 'human') - return - } - - ensureWritable(flags) - const client = await createClient(flags) - const chatID = await resolveChatID(client, flags.to, { pick: flags.pick }) - await printData( - await sendMessage(client, { ...dryRunRequest, chatID }), - flags.json ? 'json' : 'human', - ) - } -} diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts deleted file mode 100644 index f1ab07a3..00000000 --- a/packages/cli/src/commands/setup.ts +++ /dev/null @@ -1,781 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable, writeEvent } from '../lib/command.js' -import { driveVerification, evaluateReadiness, type Readiness } from '../lib/app-state.js' -import { ensureDesktopToken, findLocalDesktop } from '../lib/desktop-auth.js' -import { promptText, promptYesNoDefaultYes } from '../lib/app-api.js' -import { installDesktop, installServer, readInstallations } from '../lib/installations.js' -import { connectedAccountSummary, findLocalDesktopSession, localConnectedAccountSummary, localDesktopReadiness, type LocalDesktopSession } from '../lib/local-desktop.js' -import { loginWithPKCE } from '../lib/oauth.js' -import { findDesktopAppPath, launchDesktopApp, startProfile } from '../lib/profiles.js' -import { interactiveEmailSetup } from '../lib/setup-login.js' -import { renderStartupLogo } from '../lib/logo.js' -import { SERVER_ENVIRONMENTS, SERVER_ENV_API_BASE_URLS, normalizeServerEnv } from '../lib/server-env.js' -import { - builtInDesktopTargetID, - createProfileTarget, - customTargetID, - readConfig, - readTarget, - listTargets, - pathExists, - saveTargetAuth, - updateConfig, - writeTarget, - type AuthSource, - type Target, -} from '../lib/targets.js' -import { printData, printDryRun, printSuccess } from '../lib/output.js' - -export default class Setup extends BeeperCommand { - static override summary = 'Make the selected target ready for messaging' - static override flags = { - local: Flags.boolean({ default: false, description: 'Use the local Beeper Desktop session on this device' }), - oauth: Flags.boolean({ default: false, description: 'Authorize the target with browser OAuth/PKCE' }), - remote: Flags.string({ description: 'Connect to a remote Beeper Desktop or Server URL' }), - server: Flags.boolean({ default: false, description: 'Set up a local Beeper Server target' }), - desktop: Flags.boolean({ default: false, description: 'Set up a local Beeper Desktop target' }), - install: Flags.boolean({ default: false, description: 'Allow installing missing managed runtime' }), - channel: Flags.string({ options: ['stable', 'nightly'], default: 'stable', description: 'Install release channel' }), - 'server-env': Flags.string({ default: 'prod', description: 'Server environment: local, dev, staging, or prod', options: [...SERVER_ENVIRONMENTS], parse: async value => normalizeServerEnv(value) }), - email: Flags.string({ description: 'Sign in with an email address' }), - username: Flags.string({ description: 'Username to use if setup creates a new account' }), - } - - static override examples = [ - 'beeper setup --local', - 'beeper setup --oauth', - 'beeper setup --remote https://my-beeper.example.com', - 'beeper setup --server --install', - 'beeper setup --desktop --install', - ] - - async run(): Promise { - const { flags } = await this.parse(Setup) - ensureWritable(flags) - const targetModeCount = [Boolean(flags.remote), flags.server, flags.desktop].filter(Boolean).length - if (targetModeCount > 1) throw new Error('Specify at most one of --remote, --server, or --desktop') - const authModeCount = [flags.local, flags.oauth, Boolean(flags.email)].filter(Boolean).length - if (authModeCount > 1) throw new Error('Specify at most one of --local, --oauth, or --email') - if ((flags.local || flags.oauth) && (flags.remote || flags.server || flags.desktop)) { - throw new Error('Use --local or --oauth with an existing target, not with --remote, --server, or --desktop.') - } - if (flags['dry-run']) { - await printDryRun('setup', { - target: flags.target, - baseURL: flags['base-url'], - targetMode: flags.remote ? 'remote' : flags.server ? 'server' : flags.desktop ? 'desktop' : 'selected', - authMode: flags.local ? 'local' : flags.oauth ? 'oauth' : flags.email ? 'email' : 'auto', - remote: flags.remote, - install: flags.install, - channel: flags.channel, - serverEnv: flags['server-env'], - email: flags.email, - username: flags.username, - yes: flags.yes, - }, flags.json ? 'json' : 'human') - return - } - if (flags.events) writeEvent('setup_step', { step: 'start', target: flags.target }) - - if (flags.remote) { - await this.setupRemote(flags) - return - } - if (flags.server) { - await this.setupManaged('server', flags) - return - } - if (flags.desktop) { - await this.setupManaged('desktop', flags) - return - } - - const target = await setupTarget(flags) - if (flags.local) { - await this.setupLocal(target, flags) - return - } - if (flags.oauth) { - await this.setupOAuth(target, flags) - return - } - if (flags.email) { - await this.setupEmail(target, flags) - return - } - - await this.setupDefault(target, flags) - } - - private async setupDefault(target: Target, flags: SetupFlags): Promise { - const setupCmd = setupCommand(target) - printSetupHeader(flags) - printResumeBanner(target, flags) - if (target.type === 'desktop') { - const detected = await detectDesktopSetup(target, flags) - if (detected.kind === 'session-found') { - const local = detected.local - if (flags.yes) { - await this.printSetupResult(await commitLocalDesktopSetup(local), flags) - return - } - if (flags.json || !process.stdin.isTTY) { - await printData(setupSessionFoundOutput(local, setupCmd, detected.serverInstalled), flags.json ? 'json' : 'human') - return - } - printLocalDesktopPreview(local) - if (await promptYesNoDefaultYes('Use this Desktop session for CLI access?')) { - await this.printSetupResult(await commitLocalDesktopSetup(local), flags) - return - } - await printSuccess({ - message: local.readiness.state === 'ready' ? 'Beeper Desktop is ready' : `Setup paused: ${local.readiness.state}`, - detail: setupDetailForReadiness(local.readiness, local.target), - data: { target: publicTarget(local.target), readiness: local.readiness }, - }, 'human') - return - } else if (flags.json || !process.stdin.isTTY) { - await printData(setupStateOutput(detected, target), flags.json ? 'json' : 'human') - return - } else if (detected.kind === 'installed-not-running') { - printStatus('Found Beeper Desktop on this device.', 'installed, not running') - const shouldLaunch = flags.yes || await promptYesNoDefaultYes('Launch Beeper Desktop now?') - if (shouldLaunch) { - await launchAndPoll(target, setupCmd, flags) - return - } - } else if (detected.kind === 'running-signed-out') { - printStatus('Found Beeper Desktop on this device.', 'running, signed out') - const shouldOpen = flags.yes || await promptYesNoDefaultYes('Open Beeper Desktop so you can sign in?') - if (shouldOpen) { - await launchAndPoll(target, setupCmd, flags) - return - } - } else if (detected.kind === 'session-unreadable') { - printStatus('Found Beeper Desktop on this device.', 'signed in, but CLI could not read the local session') - process.stdout.write('You can still connect through Beeper Desktop.\n') - if (flags.debug) process.stdout.write(`\n${detected.reason}\n`) - process.stdout.write('\n') - const useOAuth = flags.yes || await promptYesNoDefaultYes('Connect through Beeper Desktop instead?') - if (useOAuth) { - await this.setupOAuth(target, flags) - return - } - } else if (detected.kind === 'not-installed') { - await this.setupFromChoice(flags) - return - } - } - - const readiness = await evaluateReadiness({ baseURL: target.baseURL, target: target.id }) - if (readiness.state === 'target-unreachable' && target.type !== 'desktop') { - if (flags.json || !process.stdin.isTTY) { - const serverInstalled = await isServerInstalled() - await printData(currentTargetBrokenOutput(target, readiness, serverInstalled), flags.json ? 'json' : 'human') - return - } - if (await this.handleBrokenCurrentTarget(target, readiness, flags)) return - } - if (readiness.state === 'target-unreachable' && target.type === 'desktop' && !flags.json && process.stdin.isTTY) { - const shouldLaunch = flags.yes || await promptYesNoDefaultYes('Beeper Desktop is not reachable. Launch it now?') - if (shouldLaunch) { - await launchAndPoll(target, setupCmd, flags) - return - } - } - - if (flags.json || !process.stdin.isTTY) { - await printData({ target: publicTarget(target), readiness }, flags.json ? 'json' : 'human') - return - } - - await printSuccess({ - message: readiness.state === 'ready' ? 'Target ready' : `Setup paused: ${readiness.state}`, - detail: setupDetailForReadiness(readiness, target), - data: { target: publicTarget(target), readiness }, - }, 'human') - } - - private async setupLocal(target: Target, flags: SetupFlags): Promise { - const result = await setupLocalDesktop(target, flags) - await this.printSetupResult(result, flags) - } - - private async setupOAuth(target: Target, flags: SetupFlags): Promise { - const result = await setupOAuthTarget(target, flags) - await this.printSetupResult(result, flags) - } - - private async setupEmail(target: Target, flags: SetupFlags): Promise { - const result = await setupEmailTarget(target, flags) - await this.printSetupResult(result, flags) - } - - private async setupRemote(flags: SetupFlags): Promise { - const name = flags.target ?? await uniqueRemoteName(flags.remote!) - if (!flags.json && process.stdin.isTTY) { - process.stdout.write('Connecting to Desktop API on another device.\n\n') - process.stdout.write(`Name: ${name}\n`) - process.stdout.write(`URL: ${flags.remote!}\n\n`) - } - const target: Target = { - id: name, - name, - type: 'remote', - baseURL: flags.remote!, - managed: false, - } - const result = flags.email ? await setupEmailTarget(target, flags) : await setupOAuthTarget(target, flags, 'remote-oauth') - await writeTarget(target) - if (!flags.target) await updateConfig(config => ({ ...config, defaultTarget: config.defaultTarget ?? target.id })) - await this.printSetupResult(result, flags) - } - - private async setupManaged(type: 'desktop' | 'server', flags: SetupFlags): Promise { - if (flags.install) { - if ((flags.json || !process.stdin.isTTY) && !flags.yes) throw new Error('Install requires --install --yes in non-interactive mode.') - await installWithCopy(type, flags) - } - const id = flags.target ?? type - const target = await readTarget(id) ?? await createProfileTarget(type, id, { serverEnv: flags['server-env'], port: undefined }) - if (!flags.target) await updateConfig(config => ({ ...config, defaultTarget: config.defaultTarget ?? target.id })) - await startProfile(target).catch(error => { - if (type === 'desktop') return undefined - throw error - }) - if (flags.email) { - await this.setupEmail(target, flags) - return - } - const readiness = await evaluateReadiness({ baseURL: target.baseURL, target: target.id }) - await printData({ target: publicTarget(target), readiness }, flags.json ? 'json' : 'human') - } - - private async printSetupResult(result: SetupResult, flags: SetupFlags): Promise { - result = await maybeDriveOnboarding(result, flags) - if (flags.json || !process.stdin.isTTY) { - await printData(result, flags.json ? 'json' : 'human') - return - } - await printSuccess({ - message: result.readiness.state === 'ready' - ? `Connected to ${result.target.name ?? result.target.id}` - : `Connected; setup paused: ${result.readiness.state}`, - detail: setupResultDetail(result), - data: result, - }, 'human') - if (result.readiness.state === 'ready') printNextSteps() - } - - private async setupFromChoice(flags: SetupFlags): Promise { - const serverInstalled = await isServerInstalled() - process.stdout.write('No usable Beeper Desktop session was found on this device.\n\n') - process.stdout.write('How do you want to connect Beeper CLI?\n\n') - process.stdout.write(' 1. Install Beeper Desktop\n') - process.stdout.write(` 2. ${serverInstalled ? 'Use installed local Beeper Server' : 'Install local Beeper Server'}\n`) - process.stdout.write(' 3. Connect with Desktop API on another device\n\n') - const defaultChoice = serverInstalled ? '2' : '1' - const choice = await promptChoice(`Choose [${defaultChoice}]: `, ['1', '2', '3'], defaultChoice) - if (choice === '1') { - if (!await promptYesNoDefaultYes('Install Beeper Desktop stable from beeper.com?')) return - await installWithCopy('desktop', { ...flags, channel: 'stable' }) - const target = await setupTarget({ ...flags, desktop: true }) - await launchAndPoll(target, setupCommand(target), flags) - return - } - if (choice === '2') { - if (!serverInstalled) { - if (!await promptYesNoDefaultYes('Install local Beeper Server stable from beeper.com?')) return - await installWithCopy('server', { ...flags, channel: 'stable', 'server-env': 'prod' }) - } - await this.setupManaged('server', { ...flags, install: false, server: true, channel: 'stable' }) - return - } - const url = await promptText('Desktop API URL: ') - if (!url) throw new Error('Remote URL is required.') - await this.setupRemote({ ...flags, remote: url }) - } - - private async handleBrokenCurrentTarget(target: Target, readiness: Readiness, flags: SetupFlags): Promise { - const serverInstalled = await isServerInstalled() - process.stdout.write(`Beeper CLI is set up for ${target.name ?? target.id}, but it is not reachable.\n\n`) - if (readiness.message) process.stdout.write(`${readiness.message}\n\n`) - process.stdout.write('What do you want to do?\n\n') - process.stdout.write(` 1. Retry ${target.name ?? target.id}\n`) - process.stdout.write(' 2. Use Beeper Desktop on this device\n') - process.stdout.write(` 3. ${serverInstalled ? 'Use installed local Beeper Server' : 'Install local Beeper Server'}\n`) - process.stdout.write(' 4. Connect with Desktop API on another device\n\n') - const choice = await promptChoice('Choose [1]: ', ['1', '2', '3', '4'], '1') - if (choice === '1') return false - if (choice === '2') { - const desktop = await defaultDesktopTarget() - await this.setupDefault(desktop, { ...flags, target: desktop.id }) - return true - } - if (choice === '3') { - if (!serverInstalled) { - if (!await promptYesNoDefaultYes('Install local Beeper Server stable from beeper.com?')) return true - await installWithCopy('server', { ...flags, channel: 'stable', 'server-env': 'prod' }) - } - await this.setupManaged('server', { ...flags, install: false, server: true, channel: 'stable' }) - return true - } - const url = await promptText('Desktop API URL: ') - if (!url) throw new Error('Remote URL is required.') - await this.setupRemote({ ...flags, remote: url }) - return true - } -} - -type SetupFlags = { - 'base-url'?: string - channel?: string - debug?: boolean - desktop?: boolean - events?: boolean - install?: boolean - json?: boolean - local?: boolean - oauth?: boolean - email?: string - remote?: string - server?: boolean - 'server-env'?: string - target?: string - username?: string - yes?: boolean -} - -type SetupResult = { - accounts: string[] - authSource?: AuthSource - readiness: Awaited> - target: ReturnType -} - -type PreparedLocalDesktopSetup = { - accounts: string[] - readiness: Readiness - session: LocalDesktopSession - target: Target -} - -type DesktopSetupDetection = - | { kind: 'session-found'; local: PreparedLocalDesktopSetup; serverInstalled: boolean } - | { kind: 'installed-not-running'; serverInstalled: boolean } - | { kind: 'running-signed-out'; readiness?: Readiness; serverInstalled: boolean } - | { kind: 'session-unreadable'; reason: string; readiness?: Readiness; serverInstalled: boolean } - | { kind: 'not-installed'; serverInstalled: boolean } - -async function setupTarget(flags: SetupFlags): Promise { - if (flags['base-url']) return { id: customTargetID, type: 'desktop', baseURL: flags['base-url'] } - if (flags.target) { - const target = await readTarget(flags.target) - if (!target) throw new Error(`Unknown Beeper target "${flags.target}". Run \`beeper targets list\`.`) - return target - } - const config = await readConfig() - if (config.defaultTarget) { - const target = await readTarget(config.defaultTarget) - if (target) return target - } - const desktop = await readTarget(builtInDesktopTargetID) - if (desktop) return desktop - return defaultDesktopTarget() -} - -async function defaultDesktopTarget(): Promise { - const detected = await findLocalDesktop({ scan: true, timeoutMs: 300 }).catch(() => undefined) - const target: Target = { - id: builtInDesktopTargetID, - type: 'desktop', - name: 'Beeper Desktop', - baseURL: detected?.baseURL ?? 'http://127.0.0.1:23373', - managed: false, - runtime: { install: 'desktop', port: 23373 }, - } - await writeTarget(target) - await updateConfig(next => ({ ...next, defaultTarget: next.defaultTarget ?? target.id })) - return target -} - -async function setupLocalDesktop(target: Target, flags: SetupFlags): Promise { - return commitLocalDesktopSetup(await prepareLocalDesktopSetup(target, flags)) -} - -async function prepareLocalDesktopSetup(target: Target, flags: SetupFlags): Promise { - if (flags.events) writeEvent('setup_step', { step: 'local-desktop', target: target.id }) - const desktop = await findLocalDesktop({ baseURL: target.baseURL, scan: target.id === builtInDesktopTargetID, timeoutMs: 500 }).catch(() => undefined) - const resolvedTarget: Target = { - ...target, - id: target.id === customTargetID ? builtInDesktopTargetID : target.id, - type: 'desktop', - name: target.name ?? 'Beeper Desktop', - baseURL: desktop?.baseURL ?? target.baseURL, - managed: target.managed ?? false, - } - const session = await findLocalDesktopSession(resolvedTarget) - const readiness = localDesktopReadiness(session) - const accounts = await localConnectedAccountSummary(session.dataDir).catch(() => []) - return { accounts, readiness, session, target: resolvedTarget } -} - -async function detectDesktopSetup(target: Target, flags: SetupFlags): Promise { - printProgress(flags, 'Checking Beeper Desktop') - const installations = await readInstallations().catch((): Awaited> => ({})) - const serverInstalled = await isServerInstalled(installations) - const appInstalled = Boolean(installations.desktop?.path || await findDesktopAppPath()) - printProgress(flags, 'Reading local Desktop session') - const local = await prepareLocalDesktopSetup(target, flags).catch(error => ({ error })) - if (!('error' in local)) return { kind: 'session-found', local, serverInstalled } - - printProgress(flags, 'Checking Desktop readiness') - const desktop = await findLocalDesktop({ baseURL: target.baseURL, scan: target.id === builtInDesktopTargetID, timeoutMs: 500 }).catch(() => undefined) - if (desktop) { - const readiness = await evaluateReadiness({ baseURL: desktop.baseURL, target: target.id, token: false }) - if (readiness.state === 'needs-login') return { kind: 'running-signed-out', readiness, serverInstalled } - return { - kind: 'session-unreadable', - reason: local.error instanceof Error ? local.error.message : String(local.error), - readiness, - serverInstalled, - } - } - - return appInstalled ? { kind: 'installed-not-running', serverInstalled } : { kind: 'not-installed', serverInstalled } -} - -async function isServerInstalled(installations?: Awaited>): Promise { - if (process.env.BEEPER_SERVER_BIN) return true - const installation = installations ?? await readInstallations().catch((): Awaited> => ({})) - return Boolean(installation.server?.path && await pathExists(installation.server.path)) -} - -async function commitLocalDesktopSetup(prepared: PreparedLocalDesktopSetup): Promise { - await writeTarget(prepared.target) - await saveTargetAuth(prepared.target, prepared.session.auth) - await updateConfig(config => ({ ...config, defaultTarget: config.defaultTarget ?? prepared.target.id })) - return { - accounts: prepared.accounts, - authSource: prepared.session.auth.source, - readiness: prepared.readiness, - target: publicTarget({ ...prepared.target, auth: prepared.session.auth }), - } -} - -async function setupOAuthTarget(target: Target, flags: SetupFlags, source?: AuthSource): Promise { - if (flags.events) writeEvent('setup_step', { step: 'oauth', target: target.id }) - if ((flags.json || !process.stdin.isTTY) && !flags.yes) throw new Error('OAuth setup requires an interactive terminal or --yes to open the browser.') - const authSource = source ?? (target.type === 'remote' ? 'remote-oauth' : 'desktop-oauth') - const token = target.type === 'desktop' && target.id === builtInDesktopTargetID - ? await ensureDesktopToken({ baseURL: target.baseURL, save: false, scan: true }) - : await loginWithPKCE({ - baseURL: target.baseURL, - clientName: 'Beeper CLI', - openBrowser: true, - save: false, - scope: 'read write', - source: authSource, - }) - const auth = typeof token === 'string' - ? { accessToken: token, source: authSource, tokenType: 'Bearer' as const } - : { - accessToken: token.access_token, - clientID: token.clientID, - expiresAt: token.expires_in ? new Date(Date.now() + token.expires_in * 1000).toISOString() : undefined, - scope: token.scope, - source: authSource, - tokenType: token.token_type, - } - await writeTarget(target) - await saveTargetAuth(target, auth) - const [readiness, accounts] = await Promise.all([ - evaluateReadiness({ baseURL: target.baseURL, target: target.id, token: auth.accessToken }), - connectedAccountSummary(target, auth).catch(() => []), - ]) - return { accounts, authSource, readiness, target: publicTarget({ ...target, auth }) } -} - -async function setupEmailTarget(target: Target, flags: SetupFlags): Promise { - if (flags.events) writeEvent('setup_step', { step: 'email', target: target.id }) - const email = flags.email - if (!email) throw new Error('Email setup requires --email.') - if (flags.json || !process.stdin.isTTY) throw new Error('Email setup prompts for the verification code. For automation, use `beeper auth email start` and `beeper auth email response`.') - return interactiveEmailSetup(target, { email, username: flags.username, yes: flags.yes, json: flags.json }) -} - -function publicTarget(target: Target): Omit & { auth?: { source?: AuthSource; tokenType?: 'Bearer' } } { - const { auth, ...rest } = target - return { ...rest, auth: auth ? { source: auth.source, tokenType: auth.tokenType } : undefined } -} - -function localDesktopPreview(prepared: PreparedLocalDesktopSetup): Record { - return { - authSource: prepared.session.auth.source, - baseURL: prepared.target.baseURL, - dataDir: prepared.session.dataDir, - signedInAs: prepared.session.userID, - connectedAccounts: prepared.accounts, - } -} - -function printLocalDesktopPreview(prepared: PreparedLocalDesktopSetup): void { - process.stdout.write('Found Beeper Desktop on this device.\n\n') - process.stdout.write(`Status: ${prepared.readiness.state === 'ready' ? 'signed in and ready' : prepared.readiness.state}\n`) - if (prepared.session.userID) process.stdout.write(`Signed in as: ${prepared.session.userID}\n`) - if (prepared.accounts.length) process.stdout.write(`Connected accounts: ${prepared.accounts.join(', ')}\n`) - process.stdout.write('\n') -} - -function setupSessionFoundOutput(local: PreparedLocalDesktopSetup, setupCmd: string, serverInstalled: boolean): Record { - const availableActions = [ - action('use-desktop-session', `${setupCmd} --local`), - action('desktop-oauth', `${setupCmd} --oauth`), - action('connect-remote', 'beeper setup --remote '), - ] - if (serverInstalled) availableActions.push(installedServerAction(true)) - return { - state: local.readiness.state === 'ready' ? 'desktop-ready' : 'desktop-session-found', - message: local.readiness.state === 'ready' - ? 'Beeper Desktop is signed in and ready.' - : 'Beeper Desktop is signed in, but setup is not finished.', - target: publicTarget(local.target), - readiness: local.readiness, - localDesktop: localDesktopPreview(local), - recommendedAction: action('use-desktop-session', `${setupCmd} --local`), - availableActions, - } -} - -function printSetupHeader(flags: SetupFlags): void { - if (flags.json || !process.stdin.isTTY || process.env.BEEPER_QUIET === '1') return - process.stdout.write(`${renderStartupLogo()}\n\n`) - process.stdout.write('Setup\n\n') -} - -function printResumeBanner(target: Target, flags: SetupFlags): void { - if (flags.json || !process.stdin.isTTY || process.env.BEEPER_QUIET === '1') return - if (target.id !== builtInDesktopTargetID || flags.target) process.stdout.write(`Continuing setup for ${target.name ?? target.id}.\n\n`) -} - -function printStatus(title: string, status: string): void { - process.stdout.write(`${title}\n\n`) - process.stdout.write(`Status: ${status}\n\n`) -} - -function printProgress(flags: SetupFlags, message: string): void { - if (flags.json || !process.stdin.isTTY || process.env.BEEPER_QUIET === '1') return - process.stdout.write(`${message}...\n`) -} - -async function promptChoice(label: string, allowed: string[], fallback: string): Promise { - const value = await promptText(label) - const normalized = value || fallback - if (!allowed.includes(normalized)) throw new Error(`Choose one of: ${allowed.join(', ')}`) - return normalized -} - -async function launchAndPoll(target: Target, setupCmd: string, flags: SetupFlags): Promise { - if (flags.events) writeEvent('setup_step', { step: 'launch', target: target.id }) - if (!flags.json && process.stdin.isTTY) process.stdout.write('Opening Beeper Desktop...\n') - await launchDesktopApp(target) - const readiness = await pollReadiness(target, 10_000) - const detail = readiness.state === 'target-unreachable' - ? `Run \`${setupCmd}\` again after Beeper Desktop finishes starting.` - : setupDetailForReadiness(readiness, target) - await printSuccess({ - message: 'Launched Beeper Desktop', - detail, - data: { target: publicTarget(target), readiness }, - }, flags.json ? 'json' : 'human') - if (!flags.json && process.stdin.isTTY && readiness.state === 'target-unreachable') { - process.stdout.write('\nNext:\n') - process.stdout.write(` ${setupCmd}\n`) - process.stdout.write(' beeper doctor\n') - } -} - -async function pollReadiness(target: Target, timeoutMs: number): Promise { - const started = Date.now() - let readiness = await evaluateReadiness({ baseURL: target.baseURL, target: target.id, token: false }) - while (readiness.state === 'target-unreachable' && Date.now() - started < timeoutMs) { - await new Promise(resolve => setTimeout(resolve, 500)) - readiness = await evaluateReadiness({ baseURL: target.baseURL, target: target.id, token: false }) - } - return readiness -} - -async function maybeDriveOnboarding(result: SetupResult, flags: SetupFlags): Promise { - if (flags.json || !process.stdin.isTTY) return result - if (result.readiness.state !== 'needs-verification' && result.readiness.state !== 'verification-in-progress') return result - process.stdout.write('Continuing verification...\n\n') - await driveVerification({ baseURL: result.target.baseURL, target: result.target.id, yes: flags.yes }) - return { - ...result, - readiness: await evaluateReadiness({ baseURL: result.target.baseURL, target: result.target.id }), - target: result.target, - } -} - -async function installWithCopy(type: 'desktop' | 'server', flags: SetupFlags): Promise { - const label = type === 'desktop' ? 'Beeper Desktop' : 'local Beeper Server' - const channel = flags.channel === 'nightly' ? 'nightly' : 'stable' - const serverEnv = normalizeServerEnv(flags['server-env']) - const source = type === 'server' ? new URL(SERVER_ENV_API_BASE_URLS[serverEnv]).host : 'beeper.com' - if (!flags.json && process.stdin.isTTY) process.stdout.write(`Installing ${label} ${channel} from ${source}...\n`) - if (type === 'desktop') await installDesktop({ channel, serverEnv }) - else await installServer({ channel, serverEnv }) - if (!flags.json && process.stdin.isTTY) process.stdout.write(`Installed ${label} ${channel}.\n\n`) -} - -function setupResultDetail(result: SetupResult): string | undefined { - const detail = setupDetailForReadiness(result.readiness, result.target) - if (result.accounts.length && detail) return `Connected accounts: ${result.accounts.join(', ')}\n${detail}` - if (result.accounts.length) return `Connected accounts: ${result.accounts.join(', ')}` - return detail -} - -function printNextSteps(): void { - process.stdout.write('\nNext:\n') - process.stdout.write(' beeper chats list\n') - process.stdout.write(' beeper send text --to "hello"\n') -} - -function setupStateOutput(detected: Exclude, target: Target): Record { - if (detected.kind === 'installed-not-running') { - const serverAction = installedServerAction(detected.serverInstalled) - return setupActionEnvelope({ - state: 'desktop-installed-not-running', - message: 'Beeper Desktop is installed but not running.', - target, - recommendedAction: action('launch-desktop', 'beeper setup --desktop --yes'), - availableActions: [ - action('launch-desktop', 'beeper setup --desktop --yes'), - action('connect-remote', 'beeper setup --remote '), - serverAction, - ], - }) - } - if (detected.kind === 'running-signed-out') { - const availableActions = [ - action('open-desktop', 'beeper setup --desktop --yes'), - action('connect-remote', 'beeper setup --remote '), - ] - if (detected.serverInstalled) availableActions.push(installedServerAction(true)) - return setupActionEnvelope({ - state: 'desktop-running-signed-out', - message: 'Beeper Desktop is running but not signed in.', - target, - readiness: detected.readiness, - recommendedAction: action('open-desktop', 'beeper setup --desktop --yes'), - availableActions, - }) - } - if (detected.kind === 'session-unreadable') { - const availableActions = [ - action('desktop-oauth', 'beeper setup --oauth --yes'), - action('connect-remote', 'beeper setup --remote '), - ] - if (detected.serverInstalled) availableActions.push(installedServerAction(true)) - return setupActionEnvelope({ - state: 'desktop-running-session-unreadable', - message: 'Beeper Desktop is running, but CLI could not read the local session.', - target, - readiness: detected.readiness, - detail: detected.reason, - recommendedAction: action('desktop-oauth', 'beeper setup --oauth --yes'), - availableActions, - }) - } - const serverAction = installedServerAction(detected.serverInstalled) - return setupActionEnvelope({ - state: 'desktop-not-installed', - message: 'No Beeper Desktop installation was found on this device.', - target, - recommendedAction: detected.serverInstalled ? serverAction : action('install-desktop', 'beeper setup --desktop --install --yes'), - availableActions: [ - action('install-desktop', 'beeper setup --desktop --install --yes'), - serverAction, - action('connect-remote', 'beeper setup --remote '), - ], - }) -} - -function installedServerAction(installed: boolean): { id: string; command: string } { - return installed - ? action('use-installed-server', 'beeper setup --server --yes') - : action('install-server', 'beeper setup --server --install --yes') -} - -function currentTargetBrokenOutput(target: Target, readiness: Readiness, serverInstalled: boolean): Record { - return { - state: 'current-target-unreachable', - message: `Beeper CLI is set up for ${target.name ?? target.id}, but it is not reachable.`, - target: publicTarget(target), - readiness, - recommendedAction: action('retry-current', `beeper setup -t ${target.id}`), - availableActions: [ - action('retry-current', `beeper setup -t ${target.id}`), - action('use-desktop', 'beeper setup --desktop'), - installedServerAction(serverInstalled), - action('connect-remote', 'beeper setup --remote '), - ], - } -} - -function setupActionEnvelope(options: { - state: string - message: string - target: Target - detail?: string - readiness?: Readiness - recommendedAction: ReturnType - availableActions: Array> -}): Record { - return { - state: options.state, - message: options.message, - detail: options.detail, - target: publicTarget(options.target), - readiness: options.readiness, - recommendedAction: options.recommendedAction, - availableActions: options.availableActions, - } -} - -function action(id: string, command: string): { id: string; command: string } { - return { id, command } -} - -function setupDetailForReadiness(readiness: Readiness, target: Pick): string | undefined { - if (readiness.state === 'needs-login') return 'Sign in to Beeper Desktop, then run `beeper setup` again.' - if (readiness.state === 'needs-verification' || readiness.state === 'verification-in-progress') return 'Continue verification to finish setup.' - if (readiness.state === 'needs-recovery-key' || readiness.state === 'needs-secrets') return `Run \`beeper verify recovery-key${target.id === builtInDesktopTargetID ? '' : ` -t ${target.id}`}\`.` - if (readiness.state === 'needs-cross-signing-setup') return `Run \`beeper verify reset-recovery-key${target.id === builtInDesktopTargetID ? '' : ` -t ${target.id}`}\`.` - if (readiness.state === 'needs-first-sync' || readiness.state === 'initializing') return 'Beeper is still syncing. You can rerun `beeper setup` at any time.' - return readiness.message -} - -async function uniqueRemoteName(url: string): Promise { - const base = remoteName(url) - const targets = await listTargets() - const ids = new Set(targets.map(target => target.id)) - if (!ids.has(base)) return base - for (let index = 2; index < 100; index += 1) { - const id = `${base}-${index}` - if (!ids.has(id)) return id - } - return `remote-${Date.now()}` -} - -function setupCommand(target: Target): string { - return target.id === builtInDesktopTargetID ? 'beeper setup' : `beeper setup -t ${target.id}` -} - -function remoteName(url: string): string { - try { - return new URL(url).hostname.replace(/[^a-zA-Z0-9._-]/g, '-') || 'remote' - } catch { - return 'remote' - } -} diff --git a/packages/cli/src/commands/status.ts b/packages/cli/src/commands/status.ts deleted file mode 100644 index 3f7b93ce..00000000 --- a/packages/cli/src/commands/status.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { BeeperCommand } from '../lib/command.js' -import { evaluateReadiness } from '../lib/app-state.js' -import { resolveTarget } from '../lib/targets.js' -import { printData } from '../lib/output.js' -export default class Status extends BeeperCommand { - static override summary = 'Show selected target and setup readiness' - static override description = 'Read-only readiness snapshot for the selected target. For active reachability checks and diagnostics, run `beeper doctor`.' - async run(): Promise { - const { flags } = await this.parse(Status) - const target = await resolveTarget({ target: flags.target, baseURL: flags['base-url'] }) - await printData({ target, readiness: await evaluateReadiness({ baseURL: target.baseURL, target: target.id }) }, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/targets/add/desktop.ts b/packages/cli/src/commands/targets/add/desktop.ts deleted file mode 100644 index 2c1732d5..00000000 --- a/packages/cli/src/commands/targets/add/desktop.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Args, Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../../lib/command.js' -import { createProfileTarget, readTarget, updateConfig } from '../../../lib/targets.js' -import { printDryRun, printSuccess } from '../../../lib/output.js' -import { SERVER_ENVIRONMENTS, normalizeServerEnv } from '../../../lib/server-env.js' - -export default class TargetsAddDesktop extends BeeperCommand { - static override summary = 'Add a managed Beeper Desktop target' - static override args = { name: Args.string({ required: false, description: 'Target name (default: "desktop")' }) } - static override flags = { - port: Flags.integer({ description: 'TCP port the managed Desktop will expose its API on' }), - default: Flags.boolean({ default: false, description: 'Set this target as the default after creation' }), - 'server-env': Flags.string({ default: 'prod', description: 'Server environment: local, dev, staging, or prod', options: [...SERVER_ENVIRONMENTS], parse: async value => normalizeServerEnv(value) }), - } - async run(): Promise { - const { args, flags } = await this.parse(TargetsAddDesktop) - ensureWritable(flags) - const id = args.name ?? 'desktop' - if (await readTarget(id)) throw new Error(`Target "${id}" already exists.`) - if (flags['dry-run']) { - await printDryRun('targets.add.desktop', { id, type: 'desktop', serverEnv: flags['server-env'], port: flags.port, default: flags.default }, flags.json ? 'json' : 'human') - return - } - const target = await createProfileTarget('desktop', id, { serverEnv: flags['server-env'], port: flags.port }) - if (flags.default) await updateConfig(config => ({ ...config, defaultTarget: target.id })) - await printSuccess({ message: `Added target: ${target.id}`, detail: target.baseURL, data: target }, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/targets/add/remote.ts b/packages/cli/src/commands/targets/add/remote.ts deleted file mode 100644 index 6c21ddcb..00000000 --- a/packages/cli/src/commands/targets/add/remote.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Args, Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../../lib/command.js' -import { readTarget, updateConfig, writeTarget, type Target } from '../../../lib/targets.js' -import { printDryRun, printSuccess } from '../../../lib/output.js' - -export default class TargetsAddRemote extends BeeperCommand { - static override summary = 'Add a remote Beeper Desktop or Server target' - static override args = { - name: Args.string({ required: true, description: 'Local name for the target' }), - url: Args.string({ required: true, description: 'Base URL of the remote Desktop or Server API' }), - } - static override flags = { - default: Flags.boolean({ default: false, description: 'Set this target as the default after creation' }), - } - async run(): Promise { - const { args, flags } = await this.parse(TargetsAddRemote) - ensureWritable(flags) - if (await readTarget(args.name)) throw new Error(`Target "${args.name}" already exists.`) - const target: Target = { id: args.name, name: args.name, type: 'remote', baseURL: args.url, managed: false } - if (flags['dry-run']) { - await printDryRun('targets.add.remote', { target, default: flags.default }, flags.json ? 'json' : 'human') - return - } - await writeTarget(target) - if (flags.default) await updateConfig(config => ({ ...config, defaultTarget: target.id })) - await printSuccess({ message: `Added target: ${target.id}`, detail: target.baseURL, data: target }, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/targets/add/server.ts b/packages/cli/src/commands/targets/add/server.ts deleted file mode 100644 index ae3f4a3e..00000000 --- a/packages/cli/src/commands/targets/add/server.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Args, Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../../lib/command.js' -import { createProfileTarget, readTarget, updateConfig } from '../../../lib/targets.js' -import { printDryRun, printSuccess } from '../../../lib/output.js' -import { SERVER_ENVIRONMENTS, normalizeServerEnv } from '../../../lib/server-env.js' - -export default class TargetsAddServer extends BeeperCommand { - static override summary = 'Add a managed Beeper Server target' - static override args = { name: Args.string({ required: false, description: 'Target name (default: "server")' }) } - static override flags = { - port: Flags.integer({ description: 'TCP port the managed Server will expose its API on' }), - default: Flags.boolean({ default: false, description: 'Set this target as the default after creation' }), - 'server-env': Flags.string({ default: 'prod', description: 'Server environment: local, dev, staging, or prod', options: [...SERVER_ENVIRONMENTS], parse: async value => normalizeServerEnv(value) }), - } - async run(): Promise { - const { args, flags } = await this.parse(TargetsAddServer) - ensureWritable(flags) - const id = args.name ?? 'server' - if (await readTarget(id)) throw new Error(`Target "${id}" already exists.`) - if (flags['dry-run']) { - await printDryRun('targets.add.server', { id, type: 'server', serverEnv: flags['server-env'], port: flags.port, default: flags.default }, flags.json ? 'json' : 'human') - return - } - const target = await createProfileTarget('server', id, { serverEnv: flags['server-env'], port: flags.port }) - if (flags.default) await updateConfig(config => ({ ...config, defaultTarget: target.id })) - await printSuccess({ message: `Added target: ${target.id}`, detail: target.baseURL, data: target }, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/targets/disable.ts b/packages/cli/src/commands/targets/disable.ts deleted file mode 100644 index b2afabc0..00000000 --- a/packages/cli/src/commands/targets/disable.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Args } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { resolveTarget } from '../../lib/targets.js' -import { assertServerProfile, disableProfile } from '../../lib/profiles.js' -import { printDryRun, printSuccess } from '../../lib/output.js' - -export default class TargetsDisable extends BeeperCommand { - static override summary = 'Disable a local Beeper Server target at login' - static override args = { name: Args.string({ required: false, description: 'Target name. Defaults to the selected target.' }) } - async run(): Promise { - const { args, flags } = await this.parse(TargetsDisable) - ensureWritable(flags) - const target = await resolveTarget({ target: args.name ?? flags.target, baseURL: flags['base-url'] }) - if (!target) throw new Error(`Unknown Beeper target "${args.name}".`) - assertServerProfile(target) - if (flags['dry-run']) { - await printDryRun('targets.disable', { target }, flags.json ? 'json' : 'human') - return - } - const path = await disableProfile(target) - await printSuccess({ message: `Disabled target at login: ${target.id}`, detail: path, data: { target, path } }, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/targets/enable.ts b/packages/cli/src/commands/targets/enable.ts deleted file mode 100644 index 89ea1e68..00000000 --- a/packages/cli/src/commands/targets/enable.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Args } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { resolveTarget } from '../../lib/targets.js' -import { assertServerProfile, enableProfile } from '../../lib/profiles.js' -import { printDryRun, printSuccess } from '../../lib/output.js' - -export default class TargetsEnable extends BeeperCommand { - static override summary = 'Enable a local Beeper Server target at login' - static override args = { name: Args.string({ required: false, description: 'Target name. Defaults to the selected target.' }) } - async run(): Promise { - const { args, flags } = await this.parse(TargetsEnable) - ensureWritable(flags) - const target = await resolveTarget({ target: args.name ?? flags.target, baseURL: flags['base-url'] }) - if (!target) throw new Error(`Unknown Beeper target "${args.name}".`) - assertServerProfile(target) - if (flags['dry-run']) { - await printDryRun('targets.enable', { target }, flags.json ? 'json' : 'human') - return - } - const path = await enableProfile(target) - await printSuccess({ message: `Enabled target at login: ${target.id}`, detail: path, data: { target, path } }, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/targets/list.ts b/packages/cli/src/commands/targets/list.ts deleted file mode 100644 index 9b05baa2..00000000 --- a/packages/cli/src/commands/targets/list.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { BeeperCommand } from '../../lib/command.js' -import { builtInDesktopTargetID, listTargets, readConfig, type Target } from '../../lib/targets.js' -import { targetLiveStatus } from '../../lib/target-status.js' -import { printData } from '../../lib/output.js' - -export default class TargetsList extends BeeperCommand { - static override summary = 'List configured Beeper targets' - async run(): Promise { - const { flags } = await this.parse(TargetsList) - const config = await readConfig() - const targets = await listTargets() - const rows = targets.length ? targets : [{ id: builtInDesktopTargetID, type: 'desktop' as const, name: 'Beeper Desktop', baseURL: 'http://127.0.0.1:23373', managed: false }] - await printData(await Promise.all(rows.map(async target => ({ default: config.defaultTarget ? config.defaultTarget === target.id : target.id === builtInDesktopTargetID, id: target.id, type: target.type, name: target.name ?? target.id, managed: target.managed, baseURL: target.baseURL, runtime: target.runtime, ...(await targetLiveStatus(target as Target)) }))), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/targets/logs.ts b/packages/cli/src/commands/targets/logs.ts deleted file mode 100644 index 2844eb8b..00000000 --- a/packages/cli/src/commands/targets/logs.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Args, Flags } from '@oclif/core' -import { readdir, readFile, stat } from 'node:fs/promises' -import { join } from 'node:path' -import { BeeperCommand } from '../../lib/command.js' -import { customTargetID, resolveTarget } from '../../lib/targets.js' -import { desktopLogDir, profileErrorLogPath, profileLogPath } from '../../lib/profiles.js' - -export default class TargetsLogs extends BeeperCommand { - static override summary = 'Print logs for a local Beeper Desktop or Server install' - static override args = { name: Args.string({ required: false, description: 'Target name. Defaults to the selected target.' }) } - static override flags = { - lines: Flags.integer({ default: 200, description: 'Lines to print from each log file' }), - files: Flags.integer({ default: 5, description: 'Desktop log files to print, newest first' }), - all: Flags.boolean({ default: false, description: 'Print all matching log files instead of only recent files' }), - } - async run(): Promise { - const { args, flags } = await this.parse(TargetsLogs) - const target = await resolveTarget({ target: args.name ?? flags.target, baseURL: flags['base-url'] }) - if (!target) throw new Error(`Unknown Beeper target "${args.name}".`) - if (target.type === 'remote' || target.id === customTargetID) throw new Error(`Target "${target.id}" is remote and has no local logs.`) - if (target.type === 'server') { - if (!target.managed) throw new Error(`Target "${target.id}" is not a local Beeper Server install.`) - await printLogFile(profileLogPath(target.id), flags.lines) - await printLogFile(profileErrorLogPath(target.id), flags.lines) - return - } - const files = await listLogFiles(desktopLogDir(target.managed ? target : undefined)) - const selected = flags.all ? files : files.slice(0, flags.files) - for (const file of selected) { - await printLogFile(file, flags.lines) - } - } -} - -async function listLogFiles(dir: string): Promise { - const entries = await readdir(dir, { withFileTypes: true }).catch(() => []) - const files = await Promise.all(entries.map(async entry => { - const path = join(dir, entry.name) - if (entry.isDirectory()) return listLogFiles(path) - if (entry.isFile() && entry.name.endsWith('.log')) return [path] - return [] - })) - const paths = files.flat() - const stats = await Promise.all(paths.map(async path => ({ path, mtimeMs: (await stat(path)).mtimeMs }))) - return stats.sort((a, b) => b.mtimeMs - a.mtimeMs).map(item => item.path) -} - -async function printLogFile(path: string, lines: number): Promise { - const content = await readFile(path, 'utf8').catch(() => '') - if (!content) return - process.stdout.write(`\n==> ${path} <==\n`) - process.stdout.write(tailLines(content, lines)) -} - -function tailLines(content: string, lines: number): string { - if (lines <= 0) return content - const parts = content.split('\n') - const tail = parts.slice(Math.max(0, parts.length - lines - 1)).join('\n') - return tail.endsWith('\n') ? tail : `${tail}\n` -} diff --git a/packages/cli/src/commands/targets/remove.ts b/packages/cli/src/commands/targets/remove.ts deleted file mode 100644 index 988e25ad..00000000 --- a/packages/cli/src/commands/targets/remove.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Args } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { removeTarget } from '../../lib/targets.js' -import { printDryRun, printSuccess } from '../../lib/output.js' - -export default class TargetsRemove extends BeeperCommand { - static override summary = 'Remove a target' - static override args = { name: Args.string({ required: true, description: 'Target name' }) } - async run(): Promise { - const { args, flags } = await this.parse(TargetsRemove) - ensureWritable(flags) - if (flags['dry-run']) { - await printDryRun('targets.remove', { id: args.name }, flags.json ? 'json' : 'human') - return - } - await removeTarget(args.name) - await printSuccess({ message: `Removed target: ${args.name}`, data: { id: args.name } }, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/targets/restart.ts b/packages/cli/src/commands/targets/restart.ts deleted file mode 100644 index 30966768..00000000 --- a/packages/cli/src/commands/targets/restart.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Args } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { resolveTarget } from '../../lib/targets.js' -import { assertServerProfile, startProfile, stopProfile } from '../../lib/profiles.js' -import { printDryRun, printSuccess } from '../../lib/output.js' - -export default class TargetsRestart extends BeeperCommand { - static override summary = 'Restart a local Beeper Server target' - static override args = { name: Args.string({ required: false, description: 'Target name. Defaults to the selected target.' }) } - async run(): Promise { - const { args, flags } = await this.parse(TargetsRestart) - ensureWritable(flags) - const target = await resolveTarget({ target: args.name ?? flags.target, baseURL: flags['base-url'] }) - if (!target) throw new Error(`Unknown Beeper target "${args.name}".`) - assertServerProfile(target) - if (flags['dry-run']) { - await printDryRun('targets.restart', { target }, flags.json ? 'json' : 'human') - return - } - await stopProfile(target).catch(() => undefined) - const result = await startProfile(target) - await printSuccess({ message: `Restarted target: ${target.id}`, detail: target.baseURL, data: { target, result } }, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/targets/show.ts b/packages/cli/src/commands/targets/show.ts deleted file mode 100644 index 892672c4..00000000 --- a/packages/cli/src/commands/targets/show.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Args } from '@oclif/core' -import { BeeperCommand } from '../../lib/command.js' -import { resolveTarget } from '../../lib/targets.js' -import { printData } from '../../lib/output.js' - -export default class TargetsShow extends BeeperCommand { - static override summary = 'Show target details' - static override args = { name: Args.string({ required: false, description: 'Target name. Defaults to the selected target.' }) } - async run(): Promise { - const { args, flags } = await this.parse(TargetsShow) - const target = await resolveTarget({ target: args.name ?? flags.target, baseURL: flags['base-url'] }) - if (!target) throw new Error(`Unknown Beeper target "${args.name}".`) - await printData(target, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/targets/start.ts b/packages/cli/src/commands/targets/start.ts deleted file mode 100644 index b659aaaf..00000000 --- a/packages/cli/src/commands/targets/start.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Args } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { customTargetID, resolveTarget } from '../../lib/targets.js' -import { launchDesktopApp, startProfile } from '../../lib/profiles.js' -import { printDryRun, printSuccess } from '../../lib/output.js' - -export default class TargetsStart extends BeeperCommand { - static override summary = 'Start a local Server target or open Beeper Desktop' - static override args = { name: Args.string({ required: false, description: 'Target name. Defaults to the selected target.' }) } - async run(): Promise { - const { args, flags } = await this.parse(TargetsStart) - ensureWritable(flags) - const target = await resolveTarget({ target: args.name ?? flags.target, baseURL: flags['base-url'] }) - if (!target) throw new Error(`Unknown Beeper target "${args.name}".`) - if (target.type === 'desktop' && target.id !== customTargetID) { - if (flags['dry-run']) { - await printDryRun('targets.start', { target, launchDesktop: true }, flags.json ? 'json' : 'human') - return - } - const result = await launchDesktopApp(target.managed ? target : undefined) - await printSuccess({ message: 'Opened Beeper Desktop', detail: target.baseURL, data: { target, result } }, flags.json ? 'json' : 'human') - return - } - - if (!target.managed || target.type !== 'server') { - throw new Error(`Target "${target.id}" is not a local Beeper Server install.`) - } - - if (flags['dry-run']) { - await printDryRun('targets.start', { target, startProfile: true }, flags.json ? 'json' : 'human') - return - } - const result = await startProfile(target) - await printSuccess({ message: `Started target: ${target.id}`, detail: target.baseURL, data: { target, result } }, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/targets/status.ts b/packages/cli/src/commands/targets/status.ts deleted file mode 100644 index a8cf970d..00000000 --- a/packages/cli/src/commands/targets/status.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Args } from '@oclif/core' -import { BeeperCommand } from '../../lib/command.js' -import { resolveTarget } from '../../lib/targets.js' -import { targetLiveStatus } from '../../lib/target-status.js' -import { printData } from '../../lib/output.js' - -export default class TargetsStatus extends BeeperCommand { - static override summary = 'Check endpoint and process reachability for a target' - static override args = { name: Args.string({ required: false, description: 'Target name. Defaults to the selected target.' }) } - async run(): Promise { - const { args, flags } = await this.parse(TargetsStatus) - const target = await resolveTarget({ target: args.name ?? flags.target, baseURL: flags['base-url'] }) - if (!target) throw new Error(`Unknown Beeper target "${args.name}".`) - const status = await targetLiveStatus(target) - await printData({ target, ...status }, flags.json ? 'json' : 'human') - if (!status.reachable) process.exitCode = 1 - } -} diff --git a/packages/cli/src/commands/targets/stop.ts b/packages/cli/src/commands/targets/stop.ts deleted file mode 100644 index 31d96a95..00000000 --- a/packages/cli/src/commands/targets/stop.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Args } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { resolveTarget } from '../../lib/targets.js' -import { assertServerProfile, stopProfile } from '../../lib/profiles.js' -import { printDryRun, printSuccess } from '../../lib/output.js' - -export default class TargetsStop extends BeeperCommand { - static override summary = 'Stop a local Beeper Server target' - static override args = { name: Args.string({ required: false, description: 'Target name. Defaults to the selected target.' }) } - async run(): Promise { - const { args, flags } = await this.parse(TargetsStop) - ensureWritable(flags) - const target = await resolveTarget({ target: args.name ?? flags.target, baseURL: flags['base-url'] }) - if (!target) throw new Error(`Unknown Beeper target "${args.name}".`) - assertServerProfile(target) - if (flags['dry-run']) { - await printDryRun('targets.stop', { target }, flags.json ? 'json' : 'human') - return - } - await stopProfile(target) - await printSuccess({ message: `Stopped target: ${target.id}`, data: { target } }, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/targets/use.ts b/packages/cli/src/commands/targets/use.ts deleted file mode 100644 index 3dbeca6b..00000000 --- a/packages/cli/src/commands/targets/use.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Args } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { readTarget, updateConfig } from '../../lib/targets.js' -import { printDryRun, printSuccess } from '../../lib/output.js' - -export default class TargetsUse extends BeeperCommand { - static override summary = 'Set the default target' - static override args = { name: Args.string({ required: true, description: 'Target name' }) } - async run(): Promise { - const { args, flags } = await this.parse(TargetsUse) - ensureWritable(flags) - const target = await readTarget(args.name) - if (!target) throw new Error(`Unknown Beeper target "${args.name}". Run \`beeper targets list\`.`) - if (flags['dry-run']) { - await printDryRun('targets.use', { defaultTarget: target.id, target }, flags.json ? 'json' : 'human') - return - } - await updateConfig(config => ({ ...config, defaultTarget: target.id })) - await printSuccess({ message: `Using target: ${target.id}`, detail: target.baseURL, data: target }, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/update.ts b/packages/cli/src/commands/update.ts deleted file mode 100644 index f14f9f61..00000000 --- a/packages/cli/src/commands/update.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../lib/command.js' -import { - checkInstallationUpdate, - readInstallations, - updateServerInstallation, - type Installation, -} from '../lib/installations.js' -import { profileStatus, startProfile, stopProfile } from '../lib/profiles.js' -import { listTargets } from '../lib/targets.js' -import { pathSetupHint } from '../lib/env.js' -import { printData, printDryRun } from '../lib/output.js' -import pkg from '../../package.json' with { type: 'json' } - -export default class Update extends BeeperCommand { - static override summary = 'Check and install Beeper updates' - static override flags = { - cli: Flags.boolean({ default: false, description: 'Check the Beeper CLI package' }), - desktop: Flags.boolean({ default: false, description: 'Check the CLI-owned Desktop install' }), - server: Flags.boolean({ default: false, description: 'Check the CLI-owned Server install' }), - check: Flags.boolean({ default: false, description: 'Only check for updates; do not install' }), - } - - async run(): Promise { - const { flags } = await this.parse(Update) - if (!flags.check && !flags['dry-run']) ensureWritable(flags) - const selected = flags.cli || flags.desktop || flags.server - if (flags['dry-run'] && !flags.check) { - await printDryRun('update', { cli: !selected || flags.cli, desktop: !selected || flags.desktop, server: !selected || flags.server }, flags.json ? 'json' : 'human') - return - } - const installations = await readInstallations() - const results: Array> = [] - - if (!selected || flags.cli) { - results.push({ kind: 'cli', ...(await checkCLI()) }) - } - - if ((!selected || flags.desktop) && installations.desktop) { - results.push({ kind: 'desktop', ...(await checkDesktop(installations.desktop)) }) - } else if ((!selected || flags.desktop) && !installations.desktop) { - results.push({ kind: 'desktop', installed: false, action: 'Run: beeper install desktop' }) - } - - if ((!selected || flags.server) && installations.server) { - const check = await checkInstallationUpdate(installations.server) - if (check.available && !flags.check) { - const runningProfiles = await runningServerProfiles() - const updated = await updateServerInstallation(installations.server) - const restartedProfiles = [] - for (const profile of runningProfiles) { - await stopProfile(profile).catch(() => undefined) - await startProfile(profile) - restartedProfiles.push(profile.id) - } - results.push({ kind: 'server', updated: true, previousVersion: installations.server.version, currentVersion: updated.version, path: updated.path, restartedProfiles, hint: pathSetupHint() }) - } else { - results.push({ kind: 'server', ...check }) - } - } else if ((!selected || flags.server) && !installations.server) { - results.push({ kind: 'server', installed: false, action: 'Run: beeper install server' }) - } - - await printData(results, flags.json ? 'json' : 'human') - } -} - -async function runningServerProfiles(): Promise>> { - const profiles = (await listTargets()).filter(target => target.managed && target.type === 'server') - const running = [] - for (const profile of profiles) { - const status = await profileStatus(profile) - if (status.running) running.push(profile) - } - return running -} - -async function checkDesktop(installation: Installation): Promise> { - const check = await checkInstallationUpdate(installation) - return { - ...check, - action: 'Update Beeper Desktop in the app.', - } -} - -async function checkCLI(): Promise> { - const currentVersion = pkg.version - const installMethod = detectCLIInstallMethod() - try { - const response = await fetch('https://api.github.com/repos/beeper/cli/releases/latest', { - headers: { accept: 'application/vnd.github+json', 'user-agent': 'beeper-cli' }, - signal: AbortSignal.timeout(5000), - }) - if (!response.ok) throw new Error(`GitHub releases returned ${response.status}`) - const latest = await response.json() as { tag_name?: string } - const latestVersion = latest.tag_name?.replace(/^v/, '') - const available = !!latestVersion && latestVersion !== currentVersion - return { - currentVersion, - latestVersion, - installMethod: installMethod.kind, - available, - action: available ? upgradeAction(installMethod) : 'beeper-cli is up to date.', - } - } catch (error) { - return { - currentVersion, - installMethod: installMethod.kind, - available: false, - action: `Could not check GitHub releases for beeper-cli updates: ${(error as Error).message}`, - } - } -} - -type CLIInstallMethod = - | { kind: 'brew' } - | { kind: 'npm-global' } - | { kind: 'git'; path: string } - | { kind: 'unknown'; path: string } - -function detectCLIInstallMethod(): CLIInstallMethod { - const path = decodeURI(new URL(import.meta.url).pathname) - if (/\/(Cellar|homebrew|linuxbrew)\//.test(path)) return { kind: 'brew' } - // bun/npm/yarn global installs end up under `/lib/node_modules/...` or `/global/...` - if (/\/(lib\/node_modules|global\/(\d+\.\d+\.\d+\/)?node_modules)\//.test(path)) return { kind: 'npm-global' } - if (/\/(\.packs|packages\/cli\/dist|packages\/cli\/src)\//.test(path)) return { kind: 'git', path } - return { kind: 'unknown', path } -} - -function upgradeAction(method: CLIInstallMethod): string { - switch (method.kind) { - case 'brew': - return 'Update with: brew upgrade beeper/tap/cli' - case 'npm-global': - return 'Update with: npm install -g beeper-cli@latest' - case 'git': - return `Update with: git -C ${method.path.split('/packages/')[0]} pull && bun run --filter @beeper/cli build` - default: - return 'Update with: brew upgrade beeper/tap/cli OR npm install -g beeper-cli@latest' - } -} diff --git a/packages/cli/src/commands/verify.ts b/packages/cli/src/commands/verify.ts deleted file mode 100644 index a8e3dd78..00000000 --- a/packages/cli/src/commands/verify.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../lib/command.js' -import { driveVerification } from '../lib/app-state.js' -import { printData, printDryRun } from '../lib/output.js' -export default class AuthVerify extends BeeperCommand { - static override summary = 'Finish setup verification or verify another device' - static override flags = { - user: Flags.string({ description: 'User ID to verify against (defaults to your own account)' }), - } - async run(): Promise { - const { flags } = await this.parse(AuthVerify) - ensureWritable(flags) - if (flags['dry-run']) { - await printDryRun('verify', { baseURL: flags['base-url'], target: flags.target, userID: flags.user, yes: flags.yes }, flags.json ? 'json' : 'human') - return - } - await printData(await driveVerification({ baseURL: flags['base-url'], target: flags.target, userID: flags.user, yes: flags.yes }), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/verify/approve.ts b/packages/cli/src/commands/verify/approve.ts deleted file mode 100644 index 115937fa..00000000 --- a/packages/cli/src/commands/verify/approve.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' - -export default class AuthVerifyApprove extends BeeperCommand { - static override summary = 'Approve a pending device verification request' - static override flags = { - id: Flags.string({ description: 'Verification request ID. Defaults to the active request.' }), - } - - async run(): Promise { - const { flags } = await this.parse(AuthVerifyApprove) - if (flags['dry-run']) { - await printDryRun('verify.approve', { id: flags.id ?? 'active' }, flags.json ? 'json' : 'human') - return - } - - ensureWritable(flags) - const client = await createClient(flags) - await printData(await client.app.verifications.accept(flags.id ?? 'active'), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/verify/cancel.ts b/packages/cli/src/commands/verify/cancel.ts deleted file mode 100644 index a2c50f02..00000000 --- a/packages/cli/src/commands/verify/cancel.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -export default class AuthVerifyCancel extends BeeperCommand { - static override summary = 'Cancel an in-progress device verification' - static override flags = { - id: Flags.string({ description: 'Verification request ID. Defaults to the active request.' }), - } - async run(): Promise { - const { flags } = await this.parse(AuthVerifyCancel) - ensureWritable(flags) - if (flags['dry-run']) { - await printDryRun('verify.cancel', { id: flags.id ?? 'active' }, flags.json ? 'json' : 'human') - return - } - const client = await createClient(flags) - await printData(await client.app.verifications.cancel(flags.id ?? 'active', {}), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/verify/list.ts b/packages/cli/src/commands/verify/list.ts deleted file mode 100644 index 3e5724cc..00000000 --- a/packages/cli/src/commands/verify/list.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { BeeperCommand } from '../../lib/command.js' -import { getAppState } from '../../lib/app-state.js' -import { printData } from '../../lib/output.js' -export default class AuthVerifyList extends BeeperCommand { - static override summary = 'List active verification work' - async run(): Promise { - const { flags } = await this.parse(AuthVerifyList) - const state = await getAppState({ baseURL: flags['base-url'], target: flags.target }) - await printData(state.verification ? [state.verification] : [], flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/verify/qr-confirm.ts b/packages/cli/src/commands/verify/qr-confirm.ts deleted file mode 100644 index 662f4e63..00000000 --- a/packages/cli/src/commands/verify/qr-confirm.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -export default class AuthVerifyQrConfirm extends BeeperCommand { - static override summary = 'Confirm that the other device scanned your QR code' - static override flags = { - id: Flags.string({ description: 'Verification request ID. Defaults to the active request.' }), - } - async run(): Promise { - const { flags } = await this.parse(AuthVerifyQrConfirm) - ensureWritable(flags) - const client = await createClient(flags) - if (flags['dry-run']) { - await printDryRun('verify.qr-confirm', { id: flags.id ?? 'active' }, flags.json ? 'json' : 'human') - return - } - await printData(await client.app.verifications.qr.confirmScanned(flags.id ?? 'active'), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/verify/qr-scan.ts b/packages/cli/src/commands/verify/qr-scan.ts deleted file mode 100644 index 553b57c8..00000000 --- a/packages/cli/src/commands/verify/qr-scan.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -export default class AuthVerifyQrScan extends BeeperCommand { - static override summary = 'Submit a scanned QR-code verification payload' - static override flags = { - id: Flags.string({ description: 'Verification request ID. Defaults to the active request.' }), - payload: Flags.string({ required: true, description: 'Raw QR-code data scanned from the other device' }), - } - async run(): Promise { - const { flags } = await this.parse(AuthVerifyQrScan) - ensureWritable(flags) - const client = await createClient(flags) - if (flags['dry-run']) { - await printDryRun('verify.qr-scan', { id: flags.id ?? 'active', payload: flags.payload }, flags.json ? 'json' : 'human') - return - } - await printData(await client.app.verifications.qr.scan({ data: flags.payload }), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/verify/recovery-key.ts b/packages/cli/src/commands/verify/recovery-key.ts deleted file mode 100644 index b2ef5e3d..00000000 --- a/packages/cli/src/commands/verify/recovery-key.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -export default class AuthVerifyRecoveryKey extends BeeperCommand { - static override summary = 'Unlock encrypted messages with a recovery key' - static override flags = { - key: Flags.string({ description: 'Recovery key string', required: true }), - } - async run(): Promise { - const { flags } = await this.parse(AuthVerifyRecoveryKey) - ensureWritable(flags) - const client = await createClient(flags) - if (flags['dry-run']) { - await printDryRun('verify.recovery-key', { keyProvided: true }, flags.json ? 'json' : 'human') - return - } - await printData(await client.app.login.verification.recoveryKey.verify({ recoveryKey: flags.key }), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/verify/reset-recovery-key.ts b/packages/cli/src/commands/verify/reset-recovery-key.ts deleted file mode 100644 index bc891026..00000000 --- a/packages/cli/src/commands/verify/reset-recovery-key.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -import { promptYesNoDefaultYes } from '../../lib/app-api.js' - -export default class AuthVerifyResetRecoveryKey extends BeeperCommand { - static override summary = 'Create a new encrypted-messages recovery key' - - async run(): Promise { - const { flags } = await this.parse(AuthVerifyResetRecoveryKey) - ensureWritable(flags) - const client = await createClient(flags) - if (flags['dry-run']) { - await printDryRun('verify.reset-recovery-key', { confirmWithYes: flags.yes }, flags.json ? 'json' : 'human') - return - } - const reset = await client.app.login.verification.recoveryKey.reset.create({}) - - if ((flags.json || !process.stdin.isTTY) && !flags.yes) { - throw new Error('Resetting the recovery key requires --yes in non-interactive mode so the new key can be confirmed.') - } - - if (!flags.yes) { - process.stderr.write(`New recovery key:\n${reset.recoveryKey}\n`) - if (!await promptYesNoDefaultYes('I saved this recovery key. Use it for this account?')) throw new Error('Recovery key reset cancelled.') - } - - const confirmed = await client.app.login.verification.recoveryKey.reset.confirm({ recoveryKey: reset.recoveryKey }) - - await printData({ recoveryKey: reset.recoveryKey, session: confirmed.session }, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/verify/sas-confirm.ts b/packages/cli/src/commands/verify/sas-confirm.ts deleted file mode 100644 index 472b1298..00000000 --- a/packages/cli/src/commands/verify/sas-confirm.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -export default class AuthVerifySasConfirm extends BeeperCommand { - static override summary = 'Confirm matching emoji verification' - static override flags = { - id: Flags.string({ description: 'Verification request ID. Defaults to the active request.' }), - } - async run(): Promise { - const { flags } = await this.parse(AuthVerifySasConfirm) - ensureWritable(flags) - const client = await createClient(flags) - if (flags['dry-run']) { - await printDryRun('verify.sas-confirm', { id: flags.id ?? 'active' }, flags.json ? 'json' : 'human') - return - } - await printData(await client.app.verifications.sas.confirm(flags.id ?? 'active'), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/verify/sas.ts b/packages/cli/src/commands/verify/sas.ts deleted file mode 100644 index f116f6c6..00000000 --- a/packages/cli/src/commands/verify/sas.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -export default class AuthVerifySas extends BeeperCommand { - static override summary = 'Start emoji verification' - static override flags = { - id: Flags.string({ description: 'Verification request ID. Defaults to the active request.' }), - } - async run(): Promise { - const { flags } = await this.parse(AuthVerifySas) - ensureWritable(flags) - const client = await createClient(flags) - if (flags['dry-run']) { - await printDryRun('verify.sas', { id: flags.id ?? 'active' }, flags.json ? 'json' : 'human') - return - } - await printData(await client.app.verifications.sas.start(flags.id ?? 'active'), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/verify/show.ts b/packages/cli/src/commands/verify/show.ts deleted file mode 100644 index 3fc639bb..00000000 --- a/packages/cli/src/commands/verify/show.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { BeeperCommand } from '../../lib/command.js' -import { getAppState } from '../../lib/app-state.js' -import { printData } from '../../lib/output.js' -export default class AuthVerifyShow extends BeeperCommand { - static override summary = 'Show the current active verification request' - async run(): Promise { - const { flags } = await this.parse(AuthVerifyShow) - await printData((await getAppState({ baseURL: flags['base-url'], target: flags.target })).verification ?? null, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/verify/start.ts b/packages/cli/src/commands/verify/start.ts deleted file mode 100644 index 66594609..00000000 --- a/packages/cli/src/commands/verify/start.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand, ensureWritable } from '../../lib/command.js' -import { createClient } from '../../lib/client.js' -import { printData, printDryRun } from '../../lib/output.js' -export default class AuthVerifyStart extends BeeperCommand { - static override summary = 'Start a device verification request' - static override flags = { - user: Flags.string({ description: 'User ID to verify with (defaults to your own account)' }), - } - async run(): Promise { - const { flags } = await this.parse(AuthVerifyStart) - ensureWritable(flags) - const client = await createClient(flags) - if (flags['dry-run']) { - await printDryRun('verify.start', { userID: flags.user }, flags.json ? 'json' : 'human') - return - } - await printData(await client.app.verifications.create({ userID: flags.user }), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/verify/status.ts b/packages/cli/src/commands/verify/status.ts deleted file mode 100644 index 06c642e6..00000000 --- a/packages/cli/src/commands/verify/status.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { BeeperCommand } from '../../lib/command.js' -import { evaluateReadiness } from '../../lib/app-state.js' -import { printData } from '../../lib/output.js' -export default class AuthVerifyStatus extends BeeperCommand { - static override summary = 'Show encryption and device-verification readiness' - async run(): Promise { - const { flags } = await this.parse(AuthVerifyStatus) - await printData(await evaluateReadiness({ baseURL: flags['base-url'], target: flags.target }), flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/version.ts b/packages/cli/src/commands/version.ts deleted file mode 100644 index bfb6efd5..00000000 --- a/packages/cli/src/commands/version.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { readFile } from 'node:fs/promises' -import { fileURLToPath } from 'node:url' -import { dirname, join } from 'node:path' -import { BeeperCommand } from '../lib/command.js' -import { printData } from '../lib/output.js' -export default class Version extends BeeperCommand { - static override summary = 'Print CLI version' - async run(): Promise { - const { flags } = await this.parse(Version) - const root = dirname(dirname(fileURLToPath(import.meta.url))) - const pkg = JSON.parse(await readFile(join(root, '../package.json'), 'utf8')) - await printData({ name: pkg.name, version: pkg.version }, flags.json ? 'json' : 'human') - } -} diff --git a/packages/cli/src/commands/watch.ts b/packages/cli/src/commands/watch.ts deleted file mode 100644 index e585a010..00000000 --- a/packages/cli/src/commands/watch.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { createHmac } from 'node:crypto' -import { Flags } from '@oclif/core' -import WebSocket from 'ws' -import { BeeperCommand, writeEvent } from '../lib/command.js' -import { requireToken } from '../lib/client.js' -import { getBaseURL } from '../lib/targets.js' -import { isMachineReadableOutput, startStream } from '../lib/output.js' - -type WebhookConfig = { url: string; secret?: string; queue: Array<{ body: string; signature?: string }>; inflight: number; max: number } -export type EventFilter = { include?: Set; exclude?: Set } - -export default class Watch extends BeeperCommand { - static override summary = 'Stream Desktop API WebSocket events' - static override flags = { - chat: Flags.string({ char: 'c', multiple: true, description: 'Chat ID to subscribe to. Defaults to all chats.' }), - json: Flags.boolean({ default: false, description: 'Print raw JSON, one event per line' }), - 'include-type': Flags.string({ multiple: true, options: ['chat.upserted', 'chat.deleted', 'message.upserted', 'message.deleted', 'message.stream'], description: 'Only forward events of these types. Repeat for multiple.' }), - 'exclude-type': Flags.string({ multiple: true, options: ['chat.upserted', 'chat.deleted', 'message.upserted', 'message.deleted', 'message.stream'], description: 'Drop events of these types. Repeat for multiple.' }), - webhook: Flags.string({ description: 'Forward each event to this URL as a POST request (best-effort, fire-and-forget)' }), - 'webhook-secret': Flags.string({ description: 'HMAC-SHA256 secret. Signs payloads with X-Beeper-Signature: sha256=' }), - 'webhook-queue': Flags.integer({ default: 64, description: 'Maximum pending webhook deliveries before dropping events' }), - } - - async run(): Promise { - const { flags } = await this.parse(Watch) - if (flags['webhook-secret'] && !flags.webhook) throw new Error('--webhook-secret requires --webhook URL') - if (flags['include-type']?.length && flags['exclude-type']?.length) throw new Error('Use either --include-type or --exclude-type, not both.') - const filter: EventFilter = { - include: flags['include-type']?.length ? new Set(flags['include-type']) : undefined, - exclude: flags['exclude-type']?.length ? new Set(flags['exclude-type']) : undefined, - } - const token = await requireToken() - const baseURL = await getBaseURL(flags['base-url']) - const info = await fetch(new URL('/v1/info', baseURL)) - if (!info.ok) throw new Error(`Failed to fetch /v1/info: HTTP ${info.status}`) - const metadata = await info.json() as { endpoints?: { ws_events?: string } } - const endpoint = metadata.endpoints?.ws_events || '/v1/ws' - const url = new URL(endpoint, baseURL) - url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:' - - const subscribed = flags.chat?.length ? flags.chat : ['*'] - const ws = new WebSocket(url, { headers: { Authorization: `Bearer ${token}` } }) - const webhook: WebhookConfig | undefined = flags.webhook - ? { url: flags.webhook, secret: flags['webhook-secret'], queue: [], inflight: 0, max: flags['webhook-queue'] } - : undefined - - if (flags.json || isMachineReadableOutput('human')) { - await this.runJSON(ws, subscribed, flags.events, webhook, filter) - return - } - await this.runHuman(ws, subscribed, baseURL, flags.events, webhook, filter) - } - - private async runJSON(ws: WebSocket, subscribed: string[], events: boolean, webhook?: WebhookConfig, filter?: EventFilter): Promise { - ws.addEventListener('open', () => { - if (events) writeEvent('watch.open', { subscribed }) - ws.send(JSON.stringify({ type: 'subscriptions.set', chatIDs: subscribed })) - }) - ws.addEventListener('message', event => { - const data = typeof event.data === 'string' ? event.data : event.data.toString() - if (!passesFilter(data, filter)) return - if (events) writeEvent('watch.message') - process.stdout.write(`${data}\n`) - if (webhook) forwardWebhook(webhook, data, events) - }) - ws.addEventListener('error', () => { - if (events) writeEvent('watch.error', { message: 'WebSocket connection failed' }) - this.error('WebSocket connection failed', { exit: 1 }) - }) - ws.addEventListener('close', event => { - if (events) writeEvent('watch.close', { code: event.code, reason: event.reason }) - if (event.code !== 1000) this.error(`WebSocket closed: ${event.code} ${event.reason}`, { exit: 1 }) - }) - await new Promise(resolve => { - process.once('SIGINT', () => { ws.close(1000); resolve() }) - ws.addEventListener('close', () => resolve()) - }) - } - - private async runHuman(ws: WebSocket, subscribed: string[], baseURL: string, events: boolean, webhook?: WebhookConfig, filter?: EventFilter): Promise { - const stream = await startStream({ baseURL, subscribed }) - let closed = false - - const finish = async (): Promise => { - if (closed) return - closed = true - try { ws.close(1000) } catch { /* ignore */ } - await stream.close() - } - - ws.addEventListener('open', () => { - if (events) writeEvent('watch.open', { subscribed }) - stream.setConnected(true) - ws.send(JSON.stringify({ type: 'subscriptions.set', chatIDs: subscribed })) - }) - ws.addEventListener('message', event => { - const data = typeof event.data === 'string' ? event.data : event.data.toString() - if (!passesFilter(data, filter)) return - if (events) writeEvent('watch.message') - if (webhook) forwardWebhook(webhook, data, events) - try { - const parsed = JSON.parse(data) as Record - stream.push({ - type: typeof parsed.type === 'string' ? parsed.type : 'event', - chatID: typeof parsed.chatID === 'string' ? parsed.chatID : undefined, - messageID: typeof parsed.messageID === 'string' ? parsed.messageID : undefined, - ts: typeof parsed.timestamp === 'string' ? parsed.timestamp : new Date().toISOString(), - }) - } catch { - stream.push({ type: 'raw', ts: new Date().toISOString() }) - } - }) - ws.addEventListener('error', () => { - if (events) writeEvent('watch.error', { message: 'WebSocket connection failed' }) - stream.setConnected(false) - stream.setStatus('connection error') - }) - ws.addEventListener('close', event => { - if (events) writeEvent('watch.close', { code: event.code, reason: event.reason }) - stream.setConnected(false) - if (event.code !== 1000) stream.setStatus(`closed ${event.code}${event.reason ? ` ${event.reason}` : ''}`) - void finish() - }) - process.once('SIGINT', () => { void finish() }) - - await stream.done - } -} - -export function passesFilter(body: string, filter?: EventFilter): boolean { - if (!filter || (!filter.include && !filter.exclude)) return true - let type: string | undefined - try { - const parsed = JSON.parse(body) as { type?: unknown } - if (typeof parsed.type === 'string') type = parsed.type - } catch { - return true - } - if (!type) return true - if (filter.include && !filter.include.has(type)) return false - if (filter.exclude && filter.exclude.has(type)) return false - return true -} - -function forwardWebhook(webhook: WebhookConfig, body: string, events: boolean): void { - if (webhook.inflight + webhook.queue.length >= webhook.max) { - if (events) writeEvent('watch.webhook_drop', { reason: 'queue_full', size: webhook.queue.length }) - process.stderr.write(`warning: webhook queue full (${webhook.max}); dropped event\n`) - return - } - const signature = webhook.secret - ? `sha256=${createHmac('sha256', webhook.secret).update(body).digest('hex')}` - : undefined - webhook.queue.push({ body, signature }) - void drainWebhook(webhook, events) -} - -async function drainWebhook(webhook: WebhookConfig, events: boolean): Promise { - while (webhook.queue.length > 0) { - const item = webhook.queue.shift()! - webhook.inflight++ - try { - const headers: Record = { 'content-type': 'application/json' } - if (item.signature) headers['x-beeper-signature'] = item.signature - const response = await fetch(webhook.url, { method: 'POST', headers, body: item.body, signal: AbortSignal.timeout(10_000) }) - if (!response.ok) { - if (events) writeEvent('watch.webhook_error', { status: response.status }) - process.stderr.write(`warning: webhook POST ${webhook.url} returned ${response.status}\n`) - } - } catch (error) { - if (events) writeEvent('watch.webhook_error', { message: (error as Error).message }) - process.stderr.write(`warning: webhook POST failed: ${(error as Error).message}\n`) - } finally { - webhook.inflight-- - } - } -} diff --git a/packages/cli/src/lib/account-login.ts b/packages/cli/src/lib/account-login.ts index 1572d2ff..e7571067 100644 --- a/packages/cli/src/lib/account-login.ts +++ b/packages/cli/src/lib/account-login.ts @@ -1,18 +1,20 @@ -import { createInterface } from 'node:readline/promises' import { execFileSync } from 'node:child_process' import { stdin as input, stderr as output } from 'node:process' +import { setTimeout as sleep } from 'node:timers/promises' import QRCode from 'qrcode' import type { LoginSession } from '@beeper/desktop-api/resources/bridges.js' import type { BeeperDesktop } from '@beeper/desktop-api' +import { promptText } from './prompts.js' -export type AccountLoginStep = LoginSession +type AccountLoginStep = LoginSession -export type AccountLoginOptions = { +type AccountLoginOptions = { cookies?: Record fields?: Record nonInteractive?: boolean webview?: boolean webviewBackend?: 'auto' | 'chrome' | 'webkit' + webViewConstructor?: WebViewConstructor webviewTimeoutMs?: number } @@ -78,11 +80,11 @@ export async function runGuidedAccountLogin(client: BeeperDesktop, bridgeID: str continue } - throw new Error(`Missing required field ${field.id}. Pass --field ${field.id}=... or run without --non-interactive.`) + throw new Error(`Missing required field ${field.id}. Pass --field ${field.id}=... or run without --no-input.`) } const fallback = field.initialValue ? ` [${field.initialValue}]` : '' - const value = await promptText(`${field.label ?? field.id}${fallback}: `) + const value = await promptText(`${field.label ?? field.id}${fallback}: `, output) fields[field.id] = value || field.initialValue || '' } session = await client.bridges.loginSessions.steps.submit(step.stepID, { bridgeID, loginSessionID: session.loginSessionID, type: 'user_input', fields }) @@ -111,7 +113,7 @@ export async function runGuidedAccountLogin(client: BeeperDesktop, bridgeID: str continue } - if (options.nonInteractive) throw new Error(`Missing required cookie ${id}. Pass --cookie ${id}=... or run without --non-interactive.`) + if (options.nonInteractive) throw new Error(`Missing required cookie ${id}. Pass --cookie ${id}=... or run without --no-input.`) fields[id] = await promptSecret(`${id}: `) } session = await client.bridges.loginSessions.steps.submit(step.stepID, { bridgeID, loginSessionID: session.loginSessionID, type: 'cookies', fields, source: usedWebView ? 'webview' : 'api' }) @@ -166,15 +168,10 @@ type WebViewConstructor = new (options?: Record) => { } const EXTRACT_JS_KEY = '__BEEP_BEEP_AUTH_RESULTS__' -let webViewConstructorOverride: WebViewConstructor | undefined - -export function setWebViewConstructorForTest(constructor: WebViewConstructor | undefined): void { - webViewConstructorOverride = constructor -} async function collectCookieFieldsWithWebView(step: CookieLoginStep, options: AccountLoginOptions): Promise> { const BunRuntime = (globalThis as { Bun?: { WebView?: WebViewConstructor } }).Bun - const WebView = webViewConstructorOverride ?? BunRuntime?.WebView + const WebView = options.webViewConstructor ?? BunRuntime?.WebView if (!WebView) throw new Error('Bun.WebView is not available in this Bun runtime.') const backend = options.webviewBackend && options.webviewBackend !== 'auto' ? options.webviewBackend : undefined @@ -341,19 +338,12 @@ async function collectSpecialFields(view: InstanceType, fiel function normalizeCookieFields(fields: CookieLoginStep['fields']): NormalizedCookieField[] { return fields.map(field => { const rich = field as CookieField - const sources = rich.sources?.length ? rich.sources : legacySourcesForField(rich) + const sources = rich.sources ?? [] const required = rich.required ?? true return { ...rich, sources, required } }) } -function legacySourcesForField(field: CookieField): CookieFieldSource[] { - const name = field.name ?? field.id - if (field.type === 'header') return [{ type: 'request_header', name }] - if (field.type === 'local_storage') return [{ type: 'local_storage', name }] - return [{ type: 'cookie', name }] -} - function matchesCookieDomain(actual: string, expected: string): boolean { const normalizedActual = actual.replace(/^\./, '').toLowerCase() const normalizedExpected = expected.replace(/^\./, '').toLowerCase() @@ -378,24 +368,11 @@ function stringRecord(value: unknown): Record { return out } -async function sleep(ms: number): Promise { - await new Promise(resolve => setTimeout(resolve, ms)) -} - -async function promptText(label: string): Promise { - const rl = createInterface({ input, output }) - try { - return (await rl.question(label)).trim() - } finally { - rl.close() - } -} - async function promptSecret(label: string): Promise { - if (!input.isTTY) return promptText(label) + if (!input.isTTY) return promptText(label, output) try { execFileSync('stty', ['-echo'], { stdio: ['inherit', 'ignore', 'ignore'] }) - return await promptText(label) + return await promptText(label, output) } finally { execFileSync('stty', ['echo'], { stdio: ['inherit', 'ignore', 'ignore'] }) output.write('\n') diff --git a/packages/cli/src/lib/api-values.ts b/packages/cli/src/lib/api-values.ts new file mode 100644 index 00000000..e414a507 --- /dev/null +++ b/packages/cli/src/lib/api-values.ts @@ -0,0 +1,11 @@ +export type APIRecord = Record + +export function apiRecord(value: unknown): APIRecord { + return value && typeof value === 'object' && !Array.isArray(value) ? value as APIRecord : {} +} + +export function apiItems(value: unknown): APIRecord[] { + if (Array.isArray(value)) return value.map(apiRecord) + const items = apiRecord(value).items + return Array.isArray(items) ? items.map(apiRecord) : [] +} diff --git a/packages/cli/src/lib/app-api.ts b/packages/cli/src/lib/app-api.ts index ffd7a9fc..864fd7fd 100644 --- a/packages/cli/src/lib/app-api.ts +++ b/packages/cli/src/lib/app-api.ts @@ -1,28 +1,19 @@ -import { createInterface } from 'node:readline/promises' -import { stdin as input, stdout as output } from 'node:process' -import { getAccessToken, resolveTarget, updateTargetCache } from './targets.js' -import type { LoginRegisterResponse, LoginResponseResponse } from '@beeper/desktop-api/resources/app/login' -import type { ResetCreateResponse } from '@beeper/desktop-api/resources/app/login/verification/recovery-key/reset' - -export type AppLoginSuccess = LoginResponseResponse.Success | LoginRegisterResponse -export type AppRegistrationRequired = LoginResponseResponse.RegistrationRequired -export type AppLoginOutput = LoginResponseResponse | LoginRegisterResponse -export type AppRecoveryCodeResetBeginResponse = ResetCreateResponse -export type AppRequestMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' +import { resolveTarget } from './targets.js' export async function appRequest( - method: AppRequestMethod, + method: string, path: string, options: { baseURL?: string; body?: Record; token?: string | false; target?: string } = {}, ): Promise { const target = await resolveTarget({ target: options.target, baseURL: options.baseURL }) - const baseURL = target.baseURL - const token = options.token === false ? undefined : options.token ?? await getAccessToken(target) + const token = options.token === false + ? undefined + : options.token ?? process.env.BEEPER_ACCESS_TOKEN ?? target.auth?.accessToken const headers: Record = {} if (token) headers.authorization = `Bearer ${token}` if (options.body) headers['content-type'] = 'application/json' - const response = await fetch(new URL(path, baseURL), { + const response = await fetch(new URL(path, target.baseURL), { method, headers, body: options.body ? JSON.stringify(options.body) : undefined, @@ -30,33 +21,5 @@ export async function appRequest( if (!response.ok) throw new Error(`${method} ${path} failed: ${response.status} ${await response.text()}`) if (response.status === 204) return undefined as T const text = await response.text() - const data = (text ? JSON.parse(text) : {}) as T - if (method === 'GET' && path === '/v1/app/setup') { - await updateTargetCache(target, { baseURL, appState: data }).catch(() => undefined) - } - return data -} - -export async function promptText(label: string): Promise { - const rl = createInterface({ input, output }) - try { - const value = await rl.question(label) - return value.trim() - } finally { - rl.close() - } -} - -export async function promptYesNo(label: string): Promise { - const value = (await promptText(`${label} [y/N] `)).toLowerCase() - return value === 'y' || value === 'yes' -} - -export async function promptYesNoDefaultYes(label: string): Promise { - const value = (await promptText(`${label} [Y/n] `)).toLowerCase() - return value === '' || value === 'y' || value === 'yes' -} - -export function isRegistrationRequired(output: AppLoginOutput): output is AppRegistrationRequired { - return 'registrationRequired' in output && output.registrationRequired === true + return (text ? JSON.parse(text) : {}) as T } diff --git a/packages/cli/src/lib/app-state.ts b/packages/cli/src/lib/app-state.ts index e5ac6c68..b155a03b 100644 --- a/packages/cli/src/lib/app-state.ts +++ b/packages/cli/src/lib/app-state.ts @@ -1,10 +1,11 @@ import type { AppSessionResponse } from '@beeper/desktop-api/resources/app' import type { QrConfirmScannedResponse, SASConfirmResponse, SASStartResponse, VerificationAcceptResponse, VerificationCreateResponse } from '@beeper/desktop-api/resources/app/verifications' -import { appRequest, promptYesNo } from './app-api.js' +import { appRequest } from './app-api.js' +import { promptConfirm } from './prompts.js' -export type AppState = AppSessionResponse +type AppState = AppSessionResponse -export type ReadinessState = AppState['state'] +type ReadinessState = AppState['state'] | 'no-target' | 'target-unreachable' | 'login-in-progress' @@ -25,20 +26,19 @@ export type Readiness = { message?: string } -export function nextAppStep(state: AppState, targetID?: string): string | undefined { +function nextAppStep(state: AppState, targetID?: string): string | undefined { const appState = state.state as ReadinessState - const target = targetID && targetID !== 'desktop' ? ` -t ${targetID}` : '' + const target = targetID && targetID !== 'desktop' ? ` --target ${targetID}` : '' if (appState === 'ready') return undefined - if (appState === 'needs-login') return `Run: beeper setup${target}` - if (appState === 'needs-verification') return `Run: beeper verify${target}` - if (appState === 'needs-secrets' || appState === 'needs-recovery-key') return `Run: beeper verify recovery-key${target}` - if (appState === 'needs-cross-signing-setup') return `Run: beeper verify reset-recovery-key${target}` + if (appState === 'needs-login' || appState === 'needs-verification' || appState === 'verification-in-progress') return `Run: beeper setup${target}` + if (appState === 'needs-secrets' || appState === 'needs-recovery-key') return `Finish recovery in Beeper, then run: beeper setup${target}` + if (appState === 'needs-cross-signing-setup') return `Finish cross-signing setup in Beeper, then run: beeper setup${target}` return `Waiting for app state: ${appState}` } export async function evaluateReadiness(options: { baseURL?: string; target?: string; token?: string | false } = {}): Promise { try { - const app = await getAppState(options) + const app = await appRequest('GET', '/v1/app/setup', options) const state = normalizeReadinessState(app) return { state, @@ -49,18 +49,14 @@ export async function evaluateReadiness(options: { baseURL?: string; target?: st } catch (error) { return { state: 'target-unreachable', - actions: ['targets status', 'targets start', 'doctor'], + actions: ['status', 'targets runtime start', 'setup'], message: error instanceof Error ? error.message : String(error), } } } -export async function getAppState(options: { baseURL?: string; target?: string; token?: string | false } = {}): Promise { - return appRequest('GET', '/v1/app/setup', options) -} - -export async function driveVerification(options: { baseURL?: string; target?: string; userID?: string; yes?: boolean } = {}): Promise { - let state = await getAppState(options) +export async function driveVerification(options: { baseURL?: string; force?: boolean; target?: string; userID?: string } = {}): Promise { + let state = await appRequest('GET', '/v1/app/setup', options) if (state.state === 'ready') return state if (state.state === 'needs-login') throw new Error('Target is not signed in. Run `beeper setup` after signing in to Beeper Desktop.') @@ -89,9 +85,9 @@ export async function driveVerification(options: { baseURL?: string; target?: st if (actions.has('sas.confirm') && id) { const sas = state.verification?.sas - if (!options.yes) { + if (!options.force) { process.stdout.write(`Verify that this matches on the other device:\n${sas?.emojis ?? sas?.decimals ?? '(no SAS data)'}\n`) - if (!await promptYesNo('Do they match?')) throw new Error('Verification cancelled.') + if (!await promptConfirm('Do they match?')) throw new Error('Verification cancelled.') } state = (await appRequest('POST', `/v1/app/setup/verifications/${encodeURIComponent(id)}/sas/confirm`, options)).session continue @@ -128,28 +124,9 @@ function normalizeReadinessState(app: AppState): ReadinessState { } function actionsForState(state: ReadinessState): string[] { - switch (state) { - case 'no-target': - return ['targets add desktop', 'targets add remote'] - case 'target-unreachable': - return ['targets status', 'targets start', 'doctor'] - case 'needs-login': - case 'login-in-progress': - return ['setup', 'auth status'] - case 'needs-cross-signing-setup': - return ['verify reset-recovery-key'] - case 'needs-verification': - case 'verification-in-progress': - return ['verify', 'verify list', 'verify sas', 'verify qr-scan'] - case 'needs-recovery-key': - case 'needs-secrets': - return ['verify recovery-key'] - case 'needs-first-sync': - case 'initializing': - return ['setup', 'status'] - case 'ready': - return ['chats list', 'messages list', 'send text'] - case 'error': - return ['doctor', 'setup'] - } + if (state === 'ready') return ['chats list', 'messages list', 'send text'] + if (state === 'no-target') return ['setup', 'targets add'] + if (state === 'target-unreachable') return ['status', 'targets runtime start', 'setup'] + if (state === 'error') return ['status', 'setup'] + return ['setup', 'status'] } diff --git a/packages/cli/src/lib/argv.ts b/packages/cli/src/lib/argv.ts deleted file mode 100644 index 31515cf5..00000000 --- a/packages/cli/src/lib/argv.ts +++ /dev/null @@ -1,47 +0,0 @@ -export function splitCommandLine(input: string): string[] { - const tokens: string[] = [] - let current = '' - let tokenStarted = false - let quote: '"' | "'" | undefined - let escaped = false - - for (const char of input) { - if (escaped) { - current += char - tokenStarted = true - escaped = false - continue - } - - if (char === '\\' && quote !== "'") { - escaped = true - continue - } - - if ((char === '"' || char === "'") && (!quote || quote === char)) { - if (!quote) tokenStarted = true - quote = quote ? undefined : char - continue - } - - if (!quote && /\s/.test(char)) { - if (tokenStarted) { - tokens.push(current) - current = '' - tokenStarted = false - } - continue - } - - current += char - tokenStarted = true - } - - if (escaped) { - current += '\\' - tokenStarted = true - } - if (quote) throw new Error(`Unclosed ${quote} quote`) - if (tokenStarted) tokens.push(current) - return tokens -} diff --git a/packages/cli/src/lib/bridges/catalog.ts b/packages/cli/src/lib/bridges/catalog.ts deleted file mode 100644 index 562d7ff8..00000000 --- a/packages/cli/src/lib/bridges/catalog.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { constants as fsConstants } from 'node:fs' -import { access, readFile, readdir } from 'node:fs/promises' -import { basename, delimiter, join } from 'node:path' -import { beeperDir } from '../targets.js' -import { - generatedBridgeIPSuffix, - generatedOfficialBridges, - generatedSupportedBridges, - generatedTemplates, - generatedWebsocketBridges, - type GeneratedOfficialBridge, -} from './generated.js' - -export type BridgeCatalog = { - bridgeIPSuffix: Record - officialBridges: GeneratedOfficialBridge[] - supportedBridges: string[] - templates: Record - websocketBridges: Record -} - -type CatalogPlugin = Partial> - -export async function loadBridgeCatalog(): Promise { - const catalog: BridgeCatalog = { - bridgeIPSuffix: { ...generatedBridgeIPSuffix }, - officialBridges: [...generatedOfficialBridges], - supportedBridges: [...generatedSupportedBridges], - templates: { ...generatedTemplates }, - websocketBridges: { ...generatedWebsocketBridges }, - } - - for (const dir of pluginTemplateDirs()) await loadTemplateDir(catalog, dir) - for (const path of pluginCatalogPaths()) await loadCatalogPlugin(catalog, path) - catalog.supportedBridges = Object.keys(catalog.templates).map(file => basename(file, '.tpl.yaml')).sort() - return catalog -} - -export function templateName(bridgeType: string): string { - return `${bridgeType}.tpl.yaml` -} - -export function isSupported(catalog: BridgeCatalog, bridgeType: string): boolean { - return Boolean(catalog.templates[templateName(bridgeType)]) -} - -function pluginTemplateDirs(): string[] { - const dirs = [join(beeperDir(), 'bridges', 'templates')] - const envDirs = process.env.BEEPER_BRIDGE_TEMPLATE_DIR?.split(delimiter).filter(Boolean) ?? [] - return [...dirs, ...envDirs] -} - -function pluginCatalogPaths(): string[] { - const paths = [join(beeperDir(), 'bridges', 'bridges.json')] - const envPaths = process.env.BEEPER_BRIDGE_CATALOG?.split(delimiter).filter(Boolean) ?? [] - return [...paths, ...envPaths] -} - -async function loadTemplateDir(catalog: BridgeCatalog, dir: string): Promise { - if (!await exists(dir)) return - const files = (await readdir(dir)).filter(file => file.endsWith('.tpl.yaml')) - for (const file of files) catalog.templates[file] = await readFile(join(dir, file), 'utf8') -} - -async function loadCatalogPlugin(catalog: BridgeCatalog, path: string): Promise { - if (!await exists(path)) return - const plugin = JSON.parse(await readFile(path, 'utf8')) as CatalogPlugin - if (plugin.bridgeIPSuffix) Object.assign(catalog.bridgeIPSuffix, plugin.bridgeIPSuffix) - if (plugin.websocketBridges) Object.assign(catalog.websocketBridges, plugin.websocketBridges) - if (plugin.templates) Object.assign(catalog.templates, plugin.templates) - if (plugin.officialBridges) { - const byType = new Map(catalog.officialBridges.map(item => [item.typeName, item])) - for (const item of plugin.officialBridges) byType.set(item.typeName, item) - catalog.officialBridges = [...byType.values()] - } -} - -async function exists(path: string): Promise { - try { - await access(path, fsConstants.F_OK) - return true - } catch { - return false - } -} diff --git a/packages/cli/src/lib/bridges/command.ts b/packages/cli/src/lib/bridges/command.ts deleted file mode 100644 index 9e88a6e7..00000000 --- a/packages/cli/src/lib/bridges/command.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Flags } from '@oclif/core' -import { BeeperCommand } from '../command.js' - -export abstract class BridgeCommand extends BeeperCommand { - static override baseFlags = { - ...BeeperCommand.baseFlags, - target: Flags.string({ description: 'Named Beeper target to use for this command' }), - } -} diff --git a/packages/cli/src/lib/bridges/generated.ts b/packages/cli/src/lib/bridges/generated.ts deleted file mode 100644 index 6c25e6fe..00000000 --- a/packages/cli/src/lib/bridges/generated.ts +++ /dev/null @@ -1,178 +0,0 @@ -// Generated by scripts/sync-bridge-manager-config.ts from bridge-manager. Do not edit by hand. - -export type GeneratedOfficialBridge = { typeName: string; names: string[] } - -export const generatedTemplates: Record = { - "bluesky.tpl.yaml": "# Network-specific config options\nnetwork:\n # Displayname template for Bluesky users. Available variables:\n # .DisplayName - displayname set by the user. Not required, may be empty.\n # .Handle - username (domain) of the user. Always present.\n # .DID - internal user ID starting with `did:`. Always present.\n displayname_template: {{ `\"{{or .DisplayName .Handle}}\"` }}\n\n{{ setfield . \"CommandPrefix\" \"!bsky\" -}}\n{{ setfield . \"DatabaseFileName\" \"mautrix-bluesky\" -}}\n{{ setfield . \"BridgeTypeName\" \"Bluesky\" -}}\n{{ setfield . \"BridgeTypeIcon\" \"mxc://maunium.net/ezAjjDxhiJWGEohmhkpfeHYf\" -}}\n{{ setfield . \"DefaultPickleKey\" \"go.mau.fi/mautrix-bluesky\" -}}\n{{ template \"bridgev2.tpl.yaml\" . }}\n", - "bridgev2.tpl.yaml": "# Config options that affect the central bridge module.\nbridge:\n {{ if .CommandPrefix -}}\n # The prefix for commands. Only required in non-management rooms.\n command_prefix: '{{ .CommandPrefix }}'\n {{ end -}}\n # Should the bridge create a space for each login containing the rooms that account is in?\n personal_filtering_spaces: true\n # Whether the bridge should set names and avatars explicitly for DM portals.\n # This is only necessary when using clients that don't support MSC4171.\n private_chat_portal_meta: false\n # Should events be handled asynchronously within portal rooms?\n # If true, events may end up being out of order, but slow events won't block other ones.\n # This is not yet safe to use.\n async_events: false\n # Should every user have their own portals rather than sharing them?\n # By default, users who are in the same group on the remote network will be\n # in the same Matrix room bridged to that group. If this is set to true,\n # every user will get their own Matrix room instead.\n split_portals: true\n # Should the bridge resend `m.bridge` events to all portals on startup?\n resend_bridge_info: false\n # Should `m.bridge` events be sent without a state key?\n # By default, the bridge uses a unique key that won't conflict with other bridges.\n no_bridge_info_state_key: false\n # Should bridge connection status be sent to the management room as `m.notice` events?\n # These contain the same data that can be posted to an external HTTP server using homeserver -> status_endpoint.\n # Allowed values: none, errors, all\n bridge_status_notices: none\n # How long after an unknown error should the bridge attempt a full reconnect?\n # Must be at least 1 minute. The bridge will add an extra ±20% jitter to this value.\n unknown_error_auto_reconnect: null\n\n # Should leaving Matrix rooms be bridged as leaving groups on the remote network?\n bridge_matrix_leave: false\n # Should `m.notice` messages be bridged?\n bridge_notices: false\n # Should room tags only be synced when creating the portal? Tags mean things like favorite/pin and archive/low priority.\n # Tags currently can't be synced back to the remote network, so a continuous sync means tagging from Matrix will be undone.\n tag_only_on_create: true\n # List of tags to allow bridging. If empty, no tags will be bridged.\n only_bridge_tags: [m.favourite, m.lowpriority]\n # Should room mute status only be synced when creating the portal?\n # Like tags, mutes can't currently be synced back to the remote network.\n mute_only_on_create: true\n # Should the bridge check the db to ensure that incoming events haven't been handled before\n deduplicate_matrix_messages: false\n # Should cross-room reply metadata be bridged?\n # Most Matrix clients don't support this and servers may reject such messages too.\n cross_room_replies: true\n\n # What should be done to portal rooms when a user logs out or is logged out?\n # Permitted values:\n # nothing - Do nothing, let the user stay in the portals\n # kick - Remove the user from the portal rooms, but don't delete them\n # unbridge - Remove all ghosts in the room and disassociate it from the remote chat\n # delete - Remove all ghosts and users from the room (i.e. delete it)\n cleanup_on_logout:\n # Should cleanup on logout be enabled at all?\n enabled: true\n # Settings for manual logouts (explicitly initiated by the Matrix user)\n manual:\n # Action for private portals which will never be shared with other Matrix users.\n private: delete\n # Action for portals with a relay user configured.\n relayed: delete\n # Action for portals which may be shared, but don't currently have any other Matrix users.\n shared_no_users: delete\n # Action for portals which have other logged-in Matrix users.\n shared_has_users: delete\n # Settings for credentials being invalidated (initiated by the remote network, possibly through user action).\n # Keys have the same meanings as in the manual section.\n bad_credentials:\n private: nothing\n relayed: nothing\n shared_no_users: nothing\n shared_has_users: nothing\n\n # Settings for relay mode\n relay:\n # Whether relay mode should be allowed. If allowed, the set-relay command can be used to turn any\n # authenticated user into a relaybot for that chat.\n enabled: false\n # Should only admins be allowed to set themselves as relay users?\n admin_only: true\n # List of user login IDs which anyone can set as a relay, as long as the relay user is in the room.\n default_relays: []\n\n # Permissions for using the bridge.\n # Permitted values:\n # relay - Talk through the relaybot (if enabled), no access otherwise\n # commands - Access to use commands in the bridge, but not login.\n # user - Access to use the bridge with puppeting.\n # admin - Full access, user level with some additional administration tools.\n # Permitted keys:\n # * - All Matrix users\n # domain - All users on that homeserver\n # mxid - Specific user\n permissions:\n \"{{ .UserID }}\": admin\n\n# Config for the bridge's database.\ndatabase:\n # The database type. \"sqlite3-fk-wal\" and \"postgres\" are supported.\n type: sqlite3-fk-wal\n # The database URI.\n # SQLite: A raw file path is supported, but `file:?_txlock=immediate` is recommended.\n # https://github.com/mattn/go-sqlite3#connection-string\n # Postgres: Connection string. For example, postgres://user:password@host/database?sslmode=disable\n # To connect via Unix socket, use something like postgres:///dbname?host=/var/run/postgresql\n uri: file:{{.DatabasePrefix}}{{or .DatabaseFileName .BridgeName}}.db?_txlock=immediate\n # Maximum number of connections.\n max_open_conns: 5\n max_idle_conns: 2\n # Maximum connection idle time and lifetime before they're closed. Disabled if null.\n # Parsed with https://pkg.go.dev/time#ParseDuration\n max_conn_idle_time: null\n max_conn_lifetime: null\n\n# Homeserver details.\nhomeserver:\n # The address that this appservice can use to connect to the homeserver.\n # Local addresses without HTTPS are generally recommended when the bridge is running on the same machine,\n # but https also works if they run on different machines.\n address: {{ .HungryAddress }}\n # The domain of the homeserver (also known as server_name, used for MXIDs, etc).\n domain: beeper.local\n\n # What software is the homeserver running?\n # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use \"standard\" here.\n software: hungry\n # The URL to push real-time bridge status to.\n # If set, the bridge will make POST requests to this URL whenever a user's remote network connection state changes.\n # The bridge will use the appservice as_token to authorize requests.\n status_endpoint: null\n # Endpoint for reporting per-message status.\n # If set, the bridge will make POST requests to this URL when processing a message from Matrix.\n # It will make one request when receiving the message (step BRIDGE), one after decrypting if applicable\n # (step DECRYPTED) and one after sending to the remote network (step REMOTE). Errors will also be reported.\n # The bridge will use the appservice as_token to authorize requests.\n message_send_checkpoint_endpoint: null\n # Does the homeserver support https://github.com/matrix-org/matrix-spec-proposals/pull/2246?\n async_media: true\n\n # Should the bridge use a websocket for connecting to the homeserver?\n # The server side is currently not documented anywhere and is only implemented by mautrix-wsproxy,\n # mautrix-asmux (deprecated), and hungryserv (proprietary).\n websocket: {{ .Websocket }}\n # How often should the websocket be pinged? Pinging will be disabled if this is zero.\n ping_interval_seconds: 180\n\n# Application service host/registration related details.\n# Changing these values requires regeneration of the registration.\nappservice:\n # The address that the homeserver can use to connect to this appservice.\n address: irrelevant\n # A public address that external services can use to reach this appservice.\n # This value doesn't affect the registration file.\n public_address:\n\n # The hostname and port where this appservice should listen.\n # For Docker, you generally have to change the hostname to 0.0.0.0.\n hostname: 0.0.0.0\n port: 4000\n\n # The unique ID of this appservice.\n id: {{ .AppserviceID }}\n # Appservice bot details.\n bot:\n # Username of the appservice bot.\n username: {{ .BridgeName }}bot\n # Display name and avatar for bot. Set to \"remove\" to remove display name/avatar, leave empty\n # to leave display name/avatar as-is.\n {{ if .BridgeTypeName -}}\n displayname: {{ .BridgeTypeName }} bridge bot\n {{- end }}\n {{ if .BridgeTypeIcon -}}\n avatar: {{ .BridgeTypeIcon }}\n {{- end }}\n\n # Whether to receive ephemeral events via appservice transactions.\n ephemeral_events: true\n # Should incoming events be handled asynchronously?\n # This may be necessary for large public instances with lots of messages going through.\n # However, messages will not be guaranteed to be bridged in the same order they were sent in.\n async_transactions: false\n\n # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify.\n as_token: {{ .ASToken }}\n hs_token: {{ .HSToken }}\n\n # Localpart template of MXIDs for remote users.\n username_template: {{ .BridgeName }}_{{ \"{{.}}\" }}\n\n# Config options that affect the Matrix connector of the bridge.\nmatrix:\n # Whether the bridge should send the message status as a custom com.beeper.message_send_status event.\n message_status_events: true\n # Whether the bridge should send a read receipt after successfully bridging a message.\n delivery_receipts: false\n # Whether the bridge should send error notices via m.notice events when a message fails to bridge.\n message_error_notices: false\n # Whether the bridge should update the m.direct account data event when double puppeting is enabled.\n sync_direct_chat_list: false\n # Whether created rooms should have federation enabled. If false, created portal rooms\n # will never be federated. Changing this option requires recreating rooms.\n federate_rooms: false\n # The threshold as bytes after which the bridge should roundtrip uploads via the disk\n # rather than keeping the whole file in memory.\n upload_file_threshold: 5242880\n\n# Settings for provisioning API\nprovisioning:\n # Prefix for the provisioning API paths.\n prefix: /_matrix/provision\n # Shared secret for authentication. If set to \"generate\" or null, a random secret will be generated,\n # or if set to \"disable\", the provisioning API will be disabled.\n shared_secret: {{ .ProvisioningSecret }}\n # Whether to allow provisioning API requests to be authed using Matrix access tokens.\n # This follows the same rules as double puppeting to determine which server to contact to check the token,\n # which means that by default, it only works for users on the same server as the bridge.\n allow_matrix_auth: true\n # Enable debug API at /debug with provisioning authentication.\n debug_endpoints: true\n\n# Some networks require publicly accessible media download links (e.g. for user avatars when using Discord webhooks).\n# These settings control whether the bridge will provide such public media access.\npublic_media:\n # Should public media be enabled at all?\n # The public_address field under the appservice section MUST be set when enabling public media.\n enabled: false\n # A key for signing public media URLs.\n # If set to \"generate\", a random key will be generated.\n signing_key: {{ .ProvisioningSecret }}\n # Number of seconds that public media URLs are valid for.\n # If set to 0, URLs will never expire.\n expiry: 0\n # Length of hash to use for public media URLs. Must be between 0 and 32.\n hash_length: 32\n\n# Settings for converting remote media to custom mxc:// URIs instead of reuploading.\n# More details can be found at https://docs.mau.fi/bridges/go/discord/direct-media.html\ndirect_media:\n # Should custom mxc:// URIs be used instead of reuploading media?\n enabled: false\n # The server name to use for the custom mxc:// URIs.\n # This server name will effectively be a real Matrix server, it just won't implement anything other than media.\n # You must either set up .well-known delegation from this domain to the bridge, or proxy the domain directly to the bridge.\n server_name: discord-media.example.com\n # Optionally a custom .well-known response. This defaults to `server_name:443`\n well_known_response:\n # Optionally specify a custom prefix for the media ID part of the MXC URI.\n media_id_prefix:\n # If the remote network supports media downloads over HTTP, then the bridge will use MSC3860/MSC3916\n # media download redirects if the requester supports it. Optionally, you can force redirects\n # and not allow proxying at all by setting this to false.\n # This option does nothing if the remote network does not support media downloads over HTTP.\n allow_proxy: true\n # Matrix server signing key to make the federation tester pass, same format as synapse's .signing.key file.\n # This key is also used to sign the mxc:// URIs to ensure only the bridge can generate them.\n server_key: ed25519 AAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n\n# Settings for backfilling messages.\n# Note that the exact way settings are applied depends on the network connector.\n# See https://docs.mau.fi/bridges/general/backfill.html for more details.\nbackfill:\n # Whether to do backfilling at all.\n enabled: true\n # Maximum number of messages to backfill in empty rooms.\n max_initial_messages: {{ or .MaxInitialMessages 50 }}\n # Maximum number of missed messages to backfill after bridge restarts.\n max_catchup_messages: 500\n # If a backfilled chat is older than this number of hours,\n # mark it as read even if it's unread on the remote network.\n unread_hours_threshold: 720\n # Settings for backfilling threads within other backfills.\n threads:\n # Maximum number of messages to backfill in a new thread.\n max_initial_messages: 50\n # Settings for the backwards backfill queue. This only applies when connecting to\n # Beeper as standard Matrix servers don't support inserting messages into history.\n queue:\n # Should the backfill queue be enabled?\n enabled: true\n # Number of messages to backfill in one batch.\n batch_size: {{ or .MaxBackwardMessages 50 }}\n # Delay between batches in seconds.\n batch_delay: 20\n # Maximum number of batches to backfill per portal.\n # If set to -1, all available messages will be backfilled.\n max_batches: 0\n # Optional network-specific overrides for max batches.\n # Interpretation of this field depends on the network connector.\n max_batches_override:\n channel: 10\n supergroup: 10\n dm: -1\n group_dm: -1\n\n# Settings for enabling double puppeting\ndouble_puppet:\n # Servers to always allow double puppeting from.\n # This is only for other servers and should NOT contain the server the bridge is on.\n servers:\n {{ .BeeperDomain }}: {{ .HungryAddress }}\n # Whether to allow client API URL discovery for other servers. When using this option,\n # users on other servers can use double puppeting even if their server URLs aren't\n # explicitly added to the servers map above.\n allow_discovery: false\n # Shared secrets for automatic double puppeting.\n # See https://docs.mau.fi/bridges/general/double-puppeting.html for instructions.\n secrets:\n {{ .BeeperDomain }}: \"as_token:{{ .ASToken }}\"\n\n# End-to-bridge encryption support options.\n#\n# See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html for more info.\nencryption:\n # Whether to enable encryption at all. If false, the bridge will not function in encrypted rooms.\n allow: true\n # Whether to force-enable encryption in all bridged rooms.\n default: true\n # Whether to require all messages to be encrypted and drop any unencrypted messages.\n require: true\n # Whether to use MSC2409/MSC3202 instead of /sync long polling for receiving encryption-related data.\n # This option is not yet compatible with standard Matrix servers like Synapse and should not be used.\n appservice: true\n # Whether to use MSC4190 instead of appservice login to create the bridge bot device.\n # Requires the homeserver to support MSC4190 and the device masquerading parts of MSC3202.\n # Only relevant when using end-to-bridge encryption, required when using encryption with next-gen auth (MSC3861).\n # Changing this option requires updating the appservice registration file.\n msc4190: false\n # Should the bridge bot generate a recovery key and cross-signing keys and verify itself?\n # Note that without the latest version of MSC4190, this will fail if you reset the bridge database.\n # The generated recovery key will be saved in the kv_store table under `recovery_key`.\n self_sign: false\n # Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled.\n # You must use a client that supports requesting keys from other users to use this feature.\n allow_key_sharing: true\n # Pickle key for encrypting encryption keys in the bridge database.\n # If set to generate, a random key will be generated.\n pickle_key: {{ or .Params.pickle_key .DefaultPickleKey \"bbctl\" }}\n # Options for deleting megolm sessions from the bridge.\n delete_keys:\n # Beeper-specific: delete outbound sessions when hungryserv confirms\n # that the user has uploaded the key to key backup.\n delete_outbound_on_ack: true\n # Don't store outbound sessions in the inbound table.\n dont_store_outbound: false\n # Ratchet megolm sessions forward after decrypting messages.\n ratchet_on_decrypt: true\n # Delete fully used keys (index >= max_messages) after decrypting messages.\n delete_fully_used_on_decrypt: true\n # Delete previous megolm sessions from same device when receiving a new one.\n delete_prev_on_new_session: true\n # Delete megolm sessions received from a device when the device is deleted.\n delete_on_device_delete: true\n # Periodically delete megolm sessions when 2x max_age has passed since receiving the session.\n periodically_delete_expired: true\n # Delete inbound megolm sessions that don't have the received_at field used for\n # automatic ratcheting and expired session deletion. This is meant as a migration\n # to delete old keys prior to the bridge update.\n delete_outdated_inbound: false\n # What level of device verification should be required from users?\n #\n # Valid levels:\n # unverified - Send keys to all device in the room.\n # cross-signed-untrusted - Require valid cross-signing, but trust all cross-signing keys.\n # cross-signed-tofu - Require valid cross-signing, trust cross-signing keys on first use (and reject changes).\n # cross-signed-verified - Require valid cross-signing, plus a valid user signature from the bridge bot.\n # Note that creating user signatures from the bridge bot is not currently possible.\n # verified - Require manual per-device verification\n # (currently only possible by modifying the `trust` column in the `crypto_device` database table).\n verification_levels:\n # Minimum level for which the bridge should send keys to when bridging messages from the remote network to Matrix.\n receive: cross-signed-tofu\n # Minimum level that the bridge should accept for incoming Matrix messages.\n send: cross-signed-tofu\n # Minimum level that the bridge should require for accepting key requests.\n share: cross-signed-tofu\n # Options for Megolm room key rotation. These options allow you to configure the m.room.encryption event content.\n # See https://spec.matrix.org/v1.10/client-server-api/#mroomencryption for more information about that event.\n rotation:\n # Enable custom Megolm room key rotation settings. Note that these\n # settings will only apply to rooms created after this option is set.\n enable_custom: true\n # The maximum number of milliseconds a session should be used\n # before changing it. The Matrix spec recommends 604800000 (a week)\n # as the default.\n milliseconds: 2592000000\n # The maximum number of messages that should be sent with a given a\n # session before changing it. The Matrix spec recommends 100 as the\n # default.\n messages: 10000\n # Disable rotating keys when a user's devices change?\n # You should not enable this option unless you understand all the implications.\n disable_device_change_key_rotation: true\n\n# Logging config. See https://github.com/tulir/zeroconfig for details.\nlogging:\n min_level: debug\n writers:\n - type: stdout\n format: pretty-colored\n - type: file\n format: json\n filename: ./logs/bridge.log\n max_size: 100\n max_backups: 10\n compress: false\n", - "discord.tpl.yaml": "# Homeserver details.\nhomeserver:\n # The address that this appservice can use to connect to the homeserver.\n address: {{ .HungryAddress }}\n # Publicly accessible base URL for media, used for avatars in relay mode.\n # If not set, the connection address above will be used.\n public_address: https://matrix.{{ .BeeperDomain }}\n # The domain of the homeserver (also known as server_name, used for MXIDs, etc).\n domain: beeper.local\n\n # What software is the homeserver running?\n # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use \"standard\" here.\n software: hungry\n # The URL to push real-time bridge status to.\n # If set, the bridge will make POST requests to this URL whenever a user's discord connection state changes.\n # The bridge will use the appservice as_token to authorize requests.\n status_endpoint: null\n # Endpoint for reporting per-message status.\n message_send_checkpoint_endpoint: null\n # Does the homeserver support https://github.com/matrix-org/matrix-spec-proposals/pull/2246?\n async_media: true\n\n # Should the bridge use a websocket for connecting to the homeserver?\n # The server side is currently not documented anywhere and is only implemented by mautrix-wsproxy,\n # mautrix-asmux (deprecated), and hungryserv (proprietary).\n websocket: {{ .Websocket }}\n # How often should the websocket be pinged? Pinging will be disabled if this is zero.\n ping_interval_seconds: 180\n\n# Application service host/registration related details.\n# Changing these values requires regeneration of the registration.\nappservice:\n # The address that the homeserver can use to connect to this appservice.\n address: null\n\n # The hostname and port where this appservice should listen.\n hostname: {{ if .Websocket }}null{{ else }}{{ .ListenAddr }}{{ end }}\n port: {{ if .Websocket }}null{{ else }}{{ .ListenPort }}{{ end }}\n\n # Database config.\n database:\n # The database type. \"sqlite3-fk-wal\" and \"postgres\" are supported.\n type: sqlite3-fk-wal\n # The database URI.\n # SQLite: A raw file path is supported, but `file:?_txlock=immediate` is recommended.\n # https://github.com/mattn/go-sqlite3#connection-string\n # Postgres: Connection string. For example, postgres://user:password@host/database?sslmode=disable\n # To connect via Unix socket, use something like postgres:///dbname?host=/var/run/postgresql\n uri: file:{{.DatabasePrefix}}mautrix-discord.db?_txlock=immediate\n # Maximum number of connections. Mostly relevant for Postgres.\n max_open_conns: 5\n max_idle_conns: 2\n # Maximum connection idle time and lifetime before they're closed. Disabled if null.\n # Parsed with https://pkg.go.dev/time#ParseDuration\n max_conn_idle_time: null\n max_conn_lifetime: null\n\n # The unique ID of this appservice.\n id: {{ .AppserviceID }}\n # Appservice bot details.\n bot:\n # Username of the appservice bot.\n username: {{ .BridgeName }}bot\n # Display name and avatar for bot. Set to \"remove\" to remove display name/avatar, leave empty\n # to leave display name/avatar as-is.\n displayname: Discord bridge bot\n avatar: mxc://maunium.net/nIdEykemnwdisvHbpxflpDlC\n\n # Whether or not to receive ephemeral events via appservice transactions.\n # Requires MSC2409 support (i.e. Synapse 1.22+).\n ephemeral_events: true\n\n # Should incoming events be handled asynchronously?\n # This may be necessary for large public instances with lots of messages going through.\n # However, messages will not be guaranteed to be bridged in the same order they were sent in.\n async_transactions: false\n\n # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify.\n as_token: {{ .ASToken }}\n hs_token: {{ .HSToken }}\n\n# Bridge config\nbridge:\n # Localpart template of MXIDs for Discord users.\n # {{.}} is replaced with the internal ID of the Discord user.\n username_template: {{ .BridgeName }}_{{ \"{{.}}\" }}\n # Displayname template for Discord users. This is also used as the room name in DMs if private_chat_portal_meta is enabled.\n # Available variables:\n # .ID - Internal user ID\n # .Username - Legacy display/username on Discord\n # .GlobalName - New displayname on Discord\n # .Discriminator - The 4 numbers after the name on Discord\n # .Bot - Whether the user is a bot\n # .System - Whether the user is an official system user\n # .Webhook - Whether the user is a webhook and is not an application\n # .Application - Whether the user is an application\n displayname_template: {{ `\"{{if .Webhook}}Webhook{{else}}{{or .GlobalName .Username}}{{end}}\"` }}\n # Displayname template for Discord channels (bridged as rooms, or spaces when type=4).\n # Available variables:\n # .Name - Channel name, or user displayname (pre-formatted with displayname_template) in DMs.\n # .ParentName - Parent channel name (used for categories).\n # .GuildName - Guild name.\n # .NSFW - Whether the channel is marked as NSFW.\n # .Type - Channel type (see values at https://github.com/bwmarrin/discordgo/blob/v0.25.0/structs.go#L251-L267)\n channel_name_template: {{ `\"{{if or (eq .Type 3) (eq .Type 4)}}{{.Name}}{{else}}#{{.Name}}{{end}}\"` }}\n # Displayname template for Discord guilds (bridged as spaces).\n # Available variables:\n # .Name - Guild name\n guild_name_template: {{ `\"{{.Name}}\"` }}\n # Whether to explicitly set the avatar and room name for private chat portal rooms.\n # If set to `default`, this will be enabled in encrypted rooms and disabled in unencrypted rooms.\n # If set to `always`, all DM rooms will have explicit names and avatars set.\n # If set to `never`, DM rooms will never have names and avatars set.\n private_chat_portal_meta: never\n\n portal_message_buffer: 128\n\n # Number of private channel portals to create on bridge startup.\n # Other portals will be created when receiving messages.\n startup_private_channel_create_limit: 5\n # Should the bridge send a read receipt from the bridge bot when a message has been sent to Discord?\n delivery_receipts: false\n # Whether the bridge should send the message status as a custom com.beeper.message_send_status event.\n message_status_events: true\n # Whether the bridge should send error notices via m.notice events when a message fails to bridge.\n message_error_notices: false\n # Should the bridge use space-restricted join rules instead of invite-only for guild rooms?\n # This can avoid unnecessary invite events in guild rooms when members are synced in.\n restricted_rooms: false\n # Should the bridge automatically join the user to threads on Discord when the thread is opened on Matrix?\n # This only works with clients that support thread read receipts (MSC3771 added in Matrix v1.4).\n autojoin_thread_on_open: true\n # Should inline fields in Discord embeds be bridged as HTML tables to Matrix?\n # Tables aren't supported in all clients, but are the only way to emulate the Discord inline field UI.\n embed_fields_as_tables: true\n # Should guild channels be muted when the portal is created? This only meant for single-user instances,\n # it won't mute it for all users if there are multiple Matrix users in the same Discord guild.\n mute_channels_on_create: true\n # Should the bridge update the m.direct account data event when double puppeting is enabled.\n # Note that updating the m.direct event is not atomic (except with mautrix-asmux)\n # and is therefore prone to race conditions.\n sync_direct_chat_list: false\n # Set this to true to tell the bridge to re-send m.bridge events to all rooms on the next run.\n # This field will automatically be changed back to false after it, except if the config file is not writable.\n resend_bridge_info: false\n # Should incoming custom emoji reactions be bridged as mxc:// URIs?\n # If set to false, custom emoji reactions will be bridged as the shortcode instead, and the image won't be available.\n custom_emoji_reactions: true\n # Should the bridge attempt to completely delete portal rooms when a channel is deleted on Discord?\n # If true, the bridge will try to kick Matrix users from the room. Otherwise, the bridge only makes ghosts leave.\n delete_portal_on_channel_delete: true\n # Should the bridge delete all portal rooms when you leave a guild on Discord?\n # This only applies if the guild has no other Matrix users on this bridge instance.\n delete_guild_on_leave: true\n # Whether or not created rooms should have federation enabled.\n # If false, created portal rooms will never be federated.\n federate_rooms: false\n # Prefix messages from webhooks with the profile info? This can be used along with a custom displayname_template\n # to better handle webhooks that change their name all the time (like ones used by bridges).\n prefix_webhook_messages: true\n # Bridge webhook avatars?\n enable_webhook_avatars: false\n # Should the bridge upload media to the Discord CDN directly before sending the message when using a user token,\n # like the official client does? The other option is sending the media in the message send request as a form part\n # (which is always used by bots and webhooks).\n use_discord_cdn_upload: true\n # Should mxc uris copied from Discord be cached?\n # This can be `never` to never cache, `unencrypted` to only cache unencrypted mxc uris, or `always` to cache everything.\n # If you have a media repo that generates non-unique mxc uris, you should set this to never.\n cache_media: unencrypted\n # Patterns for converting Discord media to custom mxc:// URIs instead of reuploading.\n # Each of the patterns can be set to null to disable custom URIs for that type of media.\n # More details can be found at https://docs.mau.fi/bridges/go/discord/direct-media.html\n media_patterns:\n # Should custom mxc:// URIs be used instead of reuploading media?\n enabled: false\n # Pattern for normal message attachments.\n attachments: {{ `mxc://discord-media.mau.dev/attachments|{{.ChannelID}}|{{.AttachmentID}}|{{.FileName}}` }}\n # Pattern for custom emojis.\n emojis: {{ `mxc://discord-media.mau.dev/emojis|{{.ID}}.{{.Ext}}` }}\n # Pattern for stickers. Note that animated lottie stickers will not be converted if this is enabled.\n stickers: {{ `mxc://discord-media.mau.dev/stickers|{{.ID}}.{{.Ext}}` }}\n # Pattern for static user avatars.\n avatars: {{ `mxc://discord-media.mau.dev/avatars|{{.UserID}}|{{.AvatarID}}.{{.Ext}}` }}\n # Settings for converting animated stickers.\n animated_sticker:\n # Format to which animated stickers should be converted.\n # disable - No conversion, send as-is (lottie JSON)\n # png - converts to non-animated png (fastest)\n # gif - converts to animated gif\n # webm - converts to webm video, requires ffmpeg executable with vp9 codec and webm container support\n # webp - converts to animated webp, requires ffmpeg executable with webp codec/container support\n target: webp\n # Arguments for converter. All converters take width and height.\n args:\n width: 320\n height: 320\n fps: 25 # only for webm, webp and gif (2, 5, 10, 20 or 25 recommended)\n # Servers to always allow double puppeting from\n double_puppet_server_map:\n {{ .BeeperDomain }}: {{ .HungryAddress }}\n # Allow using double puppeting from any server with a valid client .well-known file.\n double_puppet_allow_discovery: false\n # Shared secrets for https://github.com/devture/matrix-synapse-shared-secret-auth\n #\n # If set, double puppeting will be enabled automatically for local users\n # instead of users having to find an access token and run `login-matrix`\n # manually.\n login_shared_secret_map:\n {{ .BeeperDomain }}: \"as_token:{{ .ASToken }}\"\n\n # The prefix for commands. Only required in non-management rooms.\n command_prefix: '!discord'\n # Messages sent upon joining a management room.\n # Markdown is supported. The defaults are listed below.\n management_room_text:\n # Sent when joining a room.\n welcome: \"Hello, I'm a Discord bridge bot.\"\n # Sent when joining a management room and the user is already logged in.\n welcome_connected: \"Use `help` for help.\"\n # Sent when joining a management room and the user is not logged in.\n welcome_unconnected: \"Use `help` for help or `login` to log in.\"\n # Optional extra text sent when joining a management room.\n additional_help: \"\"\n\n # Settings for backfilling messages.\n backfill:\n # Limits for forward backfilling.\n forward_limits:\n # Initial backfill (when creating portal). 0 means backfill is disabled.\n # A special unlimited value is not supported, you must set a limit. Initial backfill will\n # fetch all messages first before backfilling anything, so high limits can take a lot of time.\n initial:\n dm: 50\n channel: 0\n thread: 0\n # Missed message backfill (on startup).\n # 0 means backfill is disabled, -1 means fetch all messages since last bridged message.\n # When using unlimited backfill (-1), messages are backfilled as they are fetched.\n # With limits, all messages up to the limit are fetched first and backfilled afterwards.\n missed:\n dm: -1\n channel: 50\n thread: 0\n # Maximum members in a guild to enable backfilling. Set to -1 to disable limit.\n # This can be used as a rough heuristic to disable backfilling in channels that are too active.\n # Currently only applies to missed message backfill.\n max_guild_members: 500\n\n # End-to-bridge encryption support options.\n #\n # See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html for more info.\n encryption:\n # Allow encryption, work in group chat rooms with e2ee enabled\n allow: true\n # Default to encryption, force-enable encryption in all portals the bridge creates\n # This will cause the bridge bot to be in private chats for the encryption to work properly.\n default: true\n # Whether to use MSC2409/MSC3202 instead of /sync long polling for receiving encryption-related data.\n appservice: true\n # Require encryption, drop any unencrypted messages.\n require: true\n # Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled.\n # You must use a client that supports requesting keys from other users to use this feature.\n allow_key_sharing: true\n # Should users mentions be in the event wire content to enable the server to send push notifications?\n plaintext_mentions: true\n # Options for deleting megolm sessions from the bridge.\n delete_keys:\n # Beeper-specific: delete outbound sessions when hungryserv confirms\n # that the user has uploaded the key to key backup.\n delete_outbound_on_ack: true\n # Don't store outbound sessions in the inbound table.\n dont_store_outbound: false\n # Ratchet megolm sessions forward after decrypting messages.\n ratchet_on_decrypt: true\n # Delete fully used keys (index >= max_messages) after decrypting messages.\n delete_fully_used_on_decrypt: true\n # Delete previous megolm sessions from same device when receiving a new one.\n delete_prev_on_new_session: true\n # Delete megolm sessions received from a device when the device is deleted.\n delete_on_device_delete: true\n # Periodically delete megolm sessions when 2x max_age has passed since receiving the session.\n periodically_delete_expired: true\n # Delete inbound megolm sessions that don't have the received_at field used for\n # automatic ratcheting and expired session deletion. This is meant as a migration\n # to delete old keys prior to the bridge update.\n delete_outdated_inbound: false\n # What level of device verification should be required from users?\n #\n # Valid levels:\n # unverified - Send keys to all device in the room.\n # cross-signed-untrusted - Require valid cross-signing, but trust all cross-signing keys.\n # cross-signed-tofu - Require valid cross-signing, trust cross-signing keys on first use (and reject changes).\n # cross-signed-verified - Require valid cross-signing, plus a valid user signature from the bridge bot.\n # Note that creating user signatures from the bridge bot is not currently possible.\n # verified - Require manual per-device verification\n # (currently only possible by modifying the `trust` column in the `crypto_device` database table).\n verification_levels:\n # Minimum level for which the bridge should send keys to when bridging messages from WhatsApp to Matrix.\n receive: cross-signed-tofu\n # Minimum level that the bridge should accept for incoming Matrix messages.\n send: cross-signed-tofu\n # Minimum level that the bridge should require for accepting key requests.\n share: cross-signed-tofu\n # Options for Megolm room key rotation. These options allow you to\n # configure the m.room.encryption event content. See:\n # https://spec.matrix.org/v1.3/client-server-api/#mroomencryption for\n # more information about that event.\n rotation:\n # Enable custom Megolm room key rotation settings. Note that these\n # settings will only apply to rooms created after this option is\n # set.\n enable_custom: true\n # The maximum number of milliseconds a session should be used\n # before changing it. The Matrix spec recommends 604800000 (a week)\n # as the default.\n milliseconds: 2592000000\n # The maximum number of messages that should be sent with a given a\n # session before changing it. The Matrix spec recommends 100 as the\n # default.\n messages: 10000\n\n # Disable rotating keys when a user's devices change?\n # You should not enable this option unless you understand all the implications.\n disable_device_change_key_rotation: true\n\n # Settings for provisioning API\n provisioning:\n # Prefix for the provisioning API paths.\n prefix: /_matrix/provision\n # Shared secret for authentication. If set to \"generate\", a random secret will be generated,\n # or if set to \"disable\", the provisioning API will be disabled.\n shared_secret: {{ .ProvisioningSecret }}\n\n # Permissions for using the bridge.\n # Permitted values:\n # relay - Talk through the relaybot (if enabled), no access otherwise\n # user - Access to use the bridge to chat with a Discord account.\n # admin - User level and some additional administration tools\n # Permitted keys:\n # * - All Matrix users\n # domain - All users on that homeserver\n # mxid - Specific user\n permissions:\n \"{{ .UserID }}\": admin\n\n# Logging config. See https://github.com/tulir/zeroconfig for details.\nlogging:\n min_level: debug\n writers:\n - type: stdout\n format: pretty-colored\n - type: file\n format: json\n filename: ./logs/mautrix-discord.log\n max_size: 100\n max_backups: 10\n compress: true\n", - "gmessages.tpl.yaml": "# Network-specific config options\nnetwork:\n # Displayname template for SMS users.\n displayname_template: {{ `\"{{or .FullName .PhoneNumber}}\"` }}\n # Settings for how the bridge appears to the phone.\n device_meta:\n # OS name to tell the phone. This is the name that shows up in the paired devices list.\n os: Beeper (self-hosted)\n # Browser type to tell the phone. This decides which icon is shown.\n # Valid types: OTHER, CHROME, FIREFOX, SAFARI, OPERA, IE, EDGE\n browser: OTHER\n # Device type to tell the phone. This also affects the icon, as well as how many sessions are allowed simultaneously.\n # One web, two tablets and one PWA should be able to connect at the same time.\n # Valid types: WEB, TABLET, PWA\n type: TABLET\n # Should the bridge aggressively set itself as the active device if the user opens Google Messages in a browser?\n # If this is disabled, the user must manually use the `set-active` command to reactivate the bridge.\n aggressive_reconnect: true\n # Number of chats to sync when connecting to Google Messages.\n initial_chat_sync_count: 25\n\n{{ setfield . \"CommandPrefix\" \"!gm\" -}}\n{{ setfield . \"DatabaseFileName\" \"mautrix-gmessages\" -}}\n{{ setfield . \"BridgeTypeName\" \"Google Messages\" -}}\n{{ setfield . \"BridgeTypeIcon\" \"mxc://maunium.net/yGOdcrJcwqARZqdzbfuxfhzb\" -}}\n{{ setfield . \"DefaultPickleKey\" \"go.mau.fi/mautrix-gmessages\" -}}\n{{ template \"bridgev2.tpl.yaml\" . }}\n", - "googlechat.tpl.yaml": "# Homeserver details\nhomeserver:\n # The address that this appservice can use to connect to the homeserver.\n address: {{ .HungryAddress }}\n # The domain of the homeserver (for MXIDs, etc).\n domain: beeper.local\n # Whether or not to verify the SSL certificate of the homeserver.\n # Only applies if address starts with https://\n verify_ssl: true\n # What software is the homeserver running?\n # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use \"standard\" here.\n software: hungry\n # Number of retries for all HTTP requests if the homeserver isn't reachable.\n http_retry_count: 4\n # The URL to push real-time bridge status to.\n # If set, the bridge will make POST requests to this URL whenever a user's Google Chat connection state changes.\n # The bridge will use the appservice as_token to authorize requests.\n status_endpoint: null\n # Endpoint for reporting per-message status.\n message_send_checkpoint_endpoint: null\n # Whether asynchronous uploads via MSC2246 should be enabled for media.\n # Requires a media repo that supports MSC2246.\n async_media: true\n\n# Application service host/registration related details\n# Changing these values requires regeneration of the registration.\nappservice:\n # The address that the homeserver can use to connect to this appservice.\n address: \"http://{{ .ListenAddr }}:{{ .ListenPort }}\"\n\n # The hostname and port where this appservice should listen.\n hostname: {{ .ListenAddr }}\n port: {{ .ListenPort }}\n # The maximum body size of appservice API requests (from the homeserver) in mebibytes\n # Usually 1 is enough, but on high-traffic bridges you might need to increase this to avoid 413s\n max_body_size: 1\n\n # The full URI to the database. SQLite and Postgres are supported.\n # Format examples:\n # SQLite: sqlite:filename.db\n # Postgres: postgres://username:password@hostname/dbname\n database: sqlite:{{.DatabasePrefix}}mautrix-googlechat.db\n # Additional arguments for asyncpg.create_pool() or sqlite3.connect()\n # https://magicstack.github.io/asyncpg/current/api/index.html#asyncpg.pool.create_pool\n # https://docs.python.org/3/library/sqlite3.html#sqlite3.connect\n # For sqlite, min_size is used as the connection thread pool size and max_size is ignored.\n # Additionally, SQLite supports init_commands as an array of SQL queries to run on connect (e.g. to set PRAGMAs).\n database_opts:\n min_size: 1\n max_size: 1\n\n # The unique ID of this appservice.\n id: {{ .AppserviceID }}\n # Username of the appservice bot.\n bot_username: {{ .BridgeName}}bot\n # Display name and avatar for bot. Set to \"remove\" to remove display name/avatar, leave empty\n # to leave display name/avatar as-is.\n bot_displayname: Google Chat bridge bot\n bot_avatar: mxc://maunium.net/BDIWAQcbpPGASPUUBuEGWXnQ\n\n # Whether or not to receive ephemeral events via appservice transactions.\n # Requires MSC2409 support (i.e. Synapse 1.22+).\n # You should disable bridge -> sync_with_custom_puppets when this is enabled.\n ephemeral_events: true\n\n # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify.\n as_token: {{ .ASToken }}\n hs_token: {{ .HSToken }}\n\n# Prometheus telemetry config. Requires prometheus-client to be installed.\nmetrics:\n enabled: false\n listen_port: 8000\n\n# Manhole config.\nmanhole:\n # Whether or not opening the manhole is allowed.\n enabled: false\n # The path for the unix socket.\n path: /var/tmp/mautrix-googlechat.manhole\n # The list of UIDs who can be added to the whitelist.\n # If empty, any UIDs can be specified in the open-manhole command.\n whitelist:\n - 0\n\n# Bridge config\nbridge:\n # Localpart template of MXIDs for Google Chat users.\n # {userid} is replaced with the user ID of the Google Chat user.\n username_template: \"{{ .BridgeName }}_{userid}\"\n # Displayname template for Google Chat users.\n # {full_name}, {first_name}, {last_name} and {email} are replaced with names.\n displayname_template: \"{full_name}\"\n\n # The prefix for commands. Only required in non-management rooms.\n command_prefix: \"!gc\"\n\n # Number of chats to sync (and create portals for) on startup/login.\n # Set 0 to disable automatic syncing.\n initial_chat_sync: 10\n # Whether or not the Google Chat users of logged in Matrix users should be\n # invited to private chats when the user sends a message from another client.\n invite_own_puppet_to_pm: false\n # Whether or not to use /sync to get presence, read receipts and typing notifications\n # when double puppeting is enabled\n sync_with_custom_puppets: false\n # Whether or not to update the m.direct account data event when double puppeting is enabled.\n # Note that updating the m.direct event is not atomic (except with mautrix-asmux)\n # and is therefore prone to race conditions.\n sync_direct_chat_list: false\n # Servers to always allow double puppeting from\n double_puppet_server_map:\n {{ .BeeperDomain }}: {{ .HungryAddress }}\n # Allow using double puppeting from any server with a valid client .well-known file.\n double_puppet_allow_discovery: false\n # Shared secret for https://github.com/devture/matrix-synapse-shared-secret-auth\n #\n # If set, custom puppets will be enabled automatically for local users\n # instead of users having to find an access token and run `login-matrix`\n # manually.\n # If using this for other servers than the bridge's server,\n # you must also set the URL in the double_puppet_server_map.\n login_shared_secret_map:\n {{ .BeeperDomain }}: \"as_token:{{ .ASToken }}\"\n # Whether or not to update avatars when syncing all contacts at startup.\n update_avatar_initial_sync: true\n # End-to-bridge encryption support options.\n #\n # See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html for more info.\n encryption:\n # Allow encryption, work in group chat rooms with e2ee enabled\n allow: true\n # Default to encryption, force-enable encryption in all portals the bridge creates\n # This will cause the bridge bot to be in private chats for the encryption to work properly.\n default: true\n # Whether to use MSC2409/MSC3202 instead of /sync long polling for receiving encryption-related data.\n appservice: true\n # Require encryption, drop any unencrypted messages.\n require: true\n # Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled.\n # You must use a client that supports requesting keys from other users to use this feature.\n allow_key_sharing: true\n # Options for deleting megolm sessions from the bridge.\n delete_keys:\n # Beeper-specific: delete outbound sessions when hungryserv confirms\n # that the user has uploaded the key to key backup.\n delete_outbound_on_ack: true\n # Don't store outbound sessions in the inbound table.\n dont_store_outbound: false\n # Ratchet megolm sessions forward after decrypting messages.\n ratchet_on_decrypt: true\n # Delete fully used keys (index >= max_messages) after decrypting messages.\n delete_fully_used_on_decrypt: true\n # Delete previous megolm sessions from same device when receiving a new one.\n delete_prev_on_new_session: true\n # Delete megolm sessions received from a device when the device is deleted.\n delete_on_device_delete: true\n # Periodically delete megolm sessions when 2x max_age has passed since receiving the session.\n periodically_delete_expired: true\n # Delete inbound megolm sessions that don't have the received_at field used for\n # automatic ratcheting and expired session deletion. This is meant as a migration\n # to delete old keys prior to the bridge update.\n delete_outdated_inbound: true\n # What level of device verification should be required from users?\n #\n # Valid levels:\n # unverified - Send keys to all device in the room.\n # cross-signed-untrusted - Require valid cross-signing, but trust all cross-signing keys.\n # cross-signed-tofu - Require valid cross-signing, trust cross-signing keys on first use (and reject changes).\n # cross-signed-verified - Require valid cross-signing, plus a valid user signature from the bridge bot.\n # Note that creating user signatures from the bridge bot is not currently possible.\n # verified - Require manual per-device verification\n # (currently only possible by modifying the `trust` column in the `crypto_device` database table).\n verification_levels:\n # Minimum level for which the bridge should send keys to when bridging messages from Telegram to Matrix.\n receive: cross-signed-tofu\n # Minimum level that the bridge should accept for incoming Matrix messages.\n send: cross-signed-tofu\n # Minimum level that the bridge should require for accepting key requests.\n share: cross-signed-tofu\n # Options for Megolm room key rotation. These options allow you to\n # configure the m.room.encryption event content. See:\n # https://spec.matrix.org/v1.3/client-server-api/#mroomencryption for\n # more information about that event.\n rotation:\n # Enable custom Megolm room key rotation settings. Note that these\n # settings will only apply to rooms created after this option is\n # set.\n enable_custom: true\n # The maximum number of milliseconds a session should be used\n # before changing it. The Matrix spec recommends 604800000 (a week)\n # as the default.\n milliseconds: 2592000000\n # The maximum number of messages that should be sent with a given a\n # session before changing it. The Matrix spec recommends 100 as the\n # default.\n messages: 10000\n\n # Disable rotating keys when a user's devices change?\n # You should not enable this option unless you understand all the implications.\n disable_device_change_key_rotation: true\n\n # Whether or not the bridge should send a read receipt from the bridge bot when a message has\n # been sent to Google Chat.\n delivery_receipts: false\n # Whether or not delivery errors should be reported as messages in the Matrix room.\n delivery_error_reports: false\n # Whether the bridge should send the message status as a custom com.beeper.message_send_status event.\n message_status_events: true\n # Whether or not created rooms should have federation enabled.\n # If false, created portal rooms will never be federated.\n federate_rooms: false\n # Settings for backfilling messages from Google Chat.\n backfill:\n # Whether or not the Google Chat users of logged in Matrix users should be\n # invited to private chats when backfilling history from Google Chat. This is\n # usually needed to prevent rate limits and to allow timestamp massaging.\n invite_own_puppet: false\n # Number of threads to backfill in threaded spaces in initial backfill.\n initial_thread_limit: 0\n # Number of replies to backfill in each thread in initial backfill.\n initial_thread_reply_limit: 500\n # Number of messages to backfill in non-threaded spaces and DMs in initial backfill.\n initial_nonthread_limit: 1\n # Number of events to backfill in catchup backfill.\n missed_event_limit: 200\n # How many events to request from Google Chat at once in catchup backfill?\n missed_event_page_size: 100\n # If using double puppeting, should notifications be disabled\n # while the initial backfill is in progress?\n disable_notifications: true\n\n # Set this to true to tell the bridge to re-send m.bridge events to all rooms on the next run.\n # This field will automatically be changed back to false after it,\n # except if the config file is not writable.\n resend_bridge_info: false\n # Whether or not unimportant bridge notices should be sent to the bridge notice room.\n unimportant_bridge_notices: false\n # Whether or not bridge notices should be disabled entirely.\n disable_bridge_notices: true\n # Whether to explicitly set the avatar and room name for private chat portal rooms.\n # If set to `default`, this will be enabled in encrypted rooms and disabled in unencrypted rooms.\n # If set to `always`, all DM rooms will have explicit names and avatars set.\n # If set to `never`, DM rooms will never have names and avatars set.\n private_chat_portal_meta: never\n\n provisioning:\n # Internal prefix in the appservice web server for the login endpoints.\n prefix: /_matrix/provision\n # Shared secret for integration managers such as mautrix-manager.\n # If set to \"generate\", a random string will be generated on the next startup.\n # If null, integration manager access to the API will not be possible.\n shared_secret: {{ .ProvisioningSecret }}\n\n # Permissions for using the bridge.\n # Permitted values:\n # user - Use the bridge with puppeting.\n # admin - Use and administrate the bridge.\n # Permitted keys:\n # * - All Matrix users\n # domain - All users on that homeserver\n # mxid - Specific user\n permissions:\n \"{{ .UserID }}\": \"admin\"\n\n# Python logging configuration.\n#\n# See section 16.7.2 of the Python documentation for more info:\n# https://docs.python.org/3.6/library/logging.config.html#configuration-dictionary-schema\nlogging:\n version: 1\n formatters:\n colored:\n (): mautrix_googlechat.util.ColorFormatter\n format: \"[%(asctime)s] [%(levelname)s@%(name)s] %(message)s\"\n normal:\n format: \"[%(asctime)s] [%(levelname)s@%(name)s] %(message)s\"\n handlers:\n file:\n class: logging.handlers.RotatingFileHandler\n formatter: normal\n filename: ./logs/mautrix-googlechat.log\n maxBytes: 10485760\n backupCount: 10\n console:\n class: logging.StreamHandler\n formatter: colored\n loggers:\n mau:\n level: DEBUG\n maugclib:\n level: INFO\n aiohttp:\n level: INFO\n root:\n level: DEBUG\n handlers: [file, console]\n", - "gvoice.tpl.yaml": "# Network-specific config options\nnetwork:\n # Displayname template for SMS users. Available variables:\n # .Name - same as phone number in most cases\n # .Contact.Name - name from contact list\n # .Contact.FirstName - first name from contact list\n # .PhoneNumber\n displayname_template: {{ `\"{{ or .Contact.Name .Name }}\"` }}\n\n{{ setfield . \"CommandPrefix\" \"!gv\" -}}\n{{ setfield . \"DatabaseFileName\" \"mautrix-gvoice\" -}}\n{{ setfield . \"BridgeTypeName\" \"Google Voice\" -}}\n{{ setfield . \"BridgeTypeIcon\" \"mxc://maunium.net/VOPtYGBzHLRfPTEzGgNMpeKo\" -}}\n{{ setfield . \"DefaultPickleKey\" \"go.mau.fi/mautrix-gvoice\" -}}\n{{ setfield . \"MaxInitialMessages\" 10 -}}\n{{ setfield . \"MaxBackwardMessages\" 100 -}}\n{{ template \"bridgev2.tpl.yaml\" . }}\n", - "heisenbridge.tpl.yaml": "id: {{ .AppserviceID }}\nurl: {{ if .Websocket }}websocket{{ else }}http://{{ .ListenAddr }}:{{ .ListenPort }}{{ end }}\nas_token: {{ .ASToken }}\nhs_token: {{ .HSToken }}\nsender_localpart: {{ .BridgeName }}bot\nnamespaces:\n users:\n - regex: '@{{ .BridgeName }}_.+:beeper\\.local'\n exclusive: true\npush_ephemeral: true\nheisenbridge:\n media_url: https://matrix.{{ .BeeperDomain }}\n displayname: Heisenbridge\n", - "imessage.tpl.yaml": "# Homeserver details.\nhomeserver:\n # The address that this appservice can use to connect to the homeserver.\n address: {{ .HungryAddress }}\n # The address to mautrix-wsproxy (which should usually be next to the homeserver behind a reverse proxy).\n # Only the /_matrix/client/unstable/fi.mau.as_sync websocket endpoint is used on this address.\n #\n # Set to null to disable using the websocket. When not using the websocket, make sure hostname and port are set in the appservice section.\n websocket_proxy: {{ if .Websocket }}{{ replace .HungryAddress \"https\" \"wss\" }}{{ else }}null{{ end }}\n # How often should the websocket be pinged? Pinging will be disabled if this is zero.\n ping_interval_seconds: 180\n # The domain of the homeserver (also known as server_name, used for MXIDs, etc).\n domain: beeper.local\n\n # What software is the homeserver running?\n # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use \"standard\" here.\n software: hungry\n # Does the homeserver support https://github.com/matrix-org/matrix-spec-proposals/pull/2246?\n async_media: true\n\n# Application service host/registration related details.\n# Changing these values requires regeneration of the registration.\nappservice:\n # The hostname and port where this appservice should listen.\n # The default method of deploying mautrix-imessage is using a websocket proxy, so it doesn't need a http server\n # To use a http server instead of a websocket, set websocket_proxy to null in the homeserver section,\n # and set the port below to a real port.\n hostname: {{ if .Websocket }}null{{ else }}{{ .ListenAddr }}{{ end }}\n port: {{ if .Websocket }}null{{ else }}{{ .ListenPort }}{{ end }}\n # Optional TLS certificates to listen for https instead of http connections.\n tls_key: null\n tls_cert: null\n\n # Database config.\n database:\n # The database type. Only \"sqlite3-fk-wal\" is supported.\n type: sqlite3-fk-wal\n # SQLite database path. A raw file path is supported, but `file:?_txlock=immediate` is recommended.\n uri: file:{{.DatabasePrefix}}mautrix-imessage.db?_txlock=immediate\n\n # The unique ID of this appservice.\n id: {{ .AppserviceID }}\n # Appservice bot details.\n bot:\n # Username of the appservice bot.\n username: {{ .BridgeName }}bot\n # Display name and avatar for bot. Set to \"remove\" to remove display name/avatar, leave empty\n # to leave display name/avatar as-is.\n displayname: iMessage bridge bot\n avatar: mxc://maunium.net/tManJEpANASZvDVzvRvhILdX\n\n # Whether or not to receive ephemeral events via appservice transactions.\n # Requires MSC2409 support (i.e. Synapse 1.22+).\n # You should disable bridge -> sync_with_custom_puppets when this is enabled.\n ephemeral_events: true\n\n # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify.\n as_token: {{ .ASToken }}\n hs_token: {{ .HSToken }}\n\n# iMessage connection config\nimessage:\n # Available platforms:\n # * mac: Standard Mac connector, requires full disk access and will ask for AppleScript and contacts permission.\n # * ios: Jailbreak iOS connector when using with Brooklyn.\n # * android: Equivalent to ios, but for use with the Android SMS wrapper app.\n # * mac-nosip: Mac without SIP connector, runs Barcelona as a subprocess.\n platform: {{ .Params.imessage_platform }}\n # Path to the Barcelona executable for the mac-nosip connector\n imessage_rest_path: \"{{ or .Params.barcelona_path \"darwin-barcelona-mautrix\" }}\"\n # Additional arguments to pass to the mac-nosip connector\n imessage_rest_args: []\n # The mode for fetching contacts in the no-SIP connector.\n # The default mode is `ipc` which will ask Barcelona. However, recent versions of Barcelona have removed contact support.\n # You can specify `mac` to use Contacts.framework directly instead of through Barcelona.\n # You can also specify `disable` to not try to use contacts at all.\n contacts_mode: mac\n # Whether to log the contents of IPC payloads\n log_ipc_payloads: false\n # For the no-SIP connector, hackily set the user account locale before starting Barcelona.\n hacky_set_locale: null\n # A list of environment variables to add for the Barcelona process (as NAME=value strings)\n environment: []\n # Path to unix socket for Barcelona communication.\n unix_socket: mautrix-imessage.sock\n # Interval to ping Barcelona at. The process will exit if Barcelona doesn't respond in time.\n ping_interval_seconds: 15\n\n bluebubbles_url: {{ .Params.bluebubbles_url }}\n bluebubbles_password: {{ .Params.bluebubbles_password }}\n\n# Segment settings for collecting some debug data.\nsegment:\n key: null\n user_id: null\n\nhacky_startup_test:\n identifier: null\n message: null\n response_message: null\n key: null\n echo_mode: false\n\n# Bridge config\nbridge:\n # The user of the bridge.\n user: \"{{ .UserID }}\"\n\n # Localpart template of MXIDs for iMessage users.\n # {{ \"{{.}}\" }} is replaced with the phone number or email of the iMessage user.\n username_template: {{ .BridgeName }}_{{ \"{{.}}\" }}\n # Displayname template for iMessage users.\n # {{ \"{{.}}\" }} is replaced with the contact list name (if available) or username (phone number or email) of the iMessage user.\n displayname_template: \"{{ \"{{.}}\" }}\"\n # Should the bridge create a space and add bridged rooms to it?\n personal_filtering_spaces: true\n\n # Whether or not the bridge should send a read receipt from the bridge bot when a message has been\n # sent to iMessage.\n delivery_receipts: false\n # Whether or not the bridge should send the message status as a custom\n # com.beeper.message_send_status event.\n message_status_events: true\n # Whether or not the bridge should send error notices via m.notice events\n # when a message fails to bridge.\n send_error_notices: false\n # The maximum number of seconds between the message arriving at the\n # homeserver and the bridge attempting to send the message. This can help\n # prevent messages from being bridged a long time after arriving at the\n # homeserver which could cause confusion in the chat history on the remote\n # network. Set to 0 to disable.\n max_handle_seconds: 60\n # Device ID to include in m.bridge data, read by client-integrated Android SMS.\n # Not relevant for standalone bridges nor iMessage.\n device_id: null\n # Whether or not to sync with custom puppets to receive EDUs that are not normally sent to appservices.\n sync_with_custom_puppets: false\n # Whether or not to update the m.direct account data event when double puppeting is enabled.\n # Note that updating the m.direct event is not atomic (except with mautrix-asmux)\n # and is therefore prone to race conditions.\n sync_direct_chat_list: false\n # Shared secret for https://github.com/devture/matrix-synapse-shared-secret-auth\n #\n # If set, double puppeting will be enabled automatically instead of the user\n # having to find an access token and run `login-matrix` manually.\n login_shared_secret: appservice\n # Homeserver URL for the double puppet. If null, will use the URL set in homeserver -> address\n double_puppet_server_url: null\n # Backfill settings\n backfill:\n # Should backfilling be enabled at all?\n enable: true\n # Maximum number of messages to backfill for new portal rooms.\n initial_limit: 100\n # Maximum age of chats to sync in days.\n initial_sync_max_age: 7\n # If a backfilled chat is older than this number of hours, mark it as read even if it's unread on iMessage.\n # Set to -1 to let any chat be unread.\n unread_hours_threshold: 720\n # Use MSC2716 for backfilling?\n #\n # This requires a server with MSC2716 support, which is currently an experimental feature in Synapse.\n # It can be enabled by setting experimental_features -> msc2716_enabled to true in homeserver.yaml.\n msc2716: true\n # Whether or not the bridge should periodically resync chat and contact info.\n periodic_sync: true\n # Should the bridge look through joined rooms to find existing portals if the database has none?\n # This can be used to recover from bridge database loss.\n find_portals_if_db_empty: false\n # Media viewer settings. See https://gitlab.com/beeper/media-viewer for more info.\n # Used to send media viewer links instead of full files for attachments that are too big for MMS.\n media_viewer:\n # The address to the media viewer. If null, media viewer links will not be used.\n url: https://media.beeper.com\n # The homeserver domain to pass to the media viewer to use for downloading media.\n # If null, will use the server name configured in the homeserver section.\n homeserver: {{ .BeeperDomain }}\n # The minimum number of bytes in a file before the bridge switches to using the media viewer when sending MMS.\n # Note that for unencrypted files, this will use a direct link to the homeserver rather than the media viewer.\n sms_min_size: 409600\n # Same as above, but for iMessages.\n imessage_min_size: 52428800\n # Template text when inserting media viewer URLs.\n # %s is replaced with the actual URL.\n template: \"Full size attachment: %s\"\n # Should we convert heif images to jpeg before re-uploading? This increases\n # compatibility, but adds generation loss (reduces quality).\n convert_heif: false\n # Should we convert tiff images to jpeg before re-uploading? This increases\n # compatibility, but adds generation loss (reduces quality).\n convert_tiff: true\n # Modern Apple devices tend to use h265 encoding for video, which is a licensed standard and therefore not\n # supported by most major browsers. If enabled, all video attachments will be converted according to the\n # ffmpeg args.\n convert_video:\n enabled: false\n # Convert to h264 format (supported by all major browsers) at decent quality while retaining original\n # audio. Modify these args to do whatever encoding/quality you want.\n ffmpeg_args: [\"-c:v\", \"libx264\", \"-preset\", \"faster\", \"-crf\", \"22\", \"-c:a\", \"copy\"]\n extension: \"mp4\"\n mime_type: \"video/mp4\"\n # The prefix for commands.\n command_prefix: \"!im\"\n # Should we rewrite the sender in a DM to match the chat GUID?\n # This is helpful when the sender ID shifts depending on the device they use, since\n # the bridge is unable to add participants to the chat post-creation.\n force_uniform_dm_senders: true\n # Should SMS chats always be in the same room as iMessage chats with the same phone number?\n disable_sms_portals: false\n # iMessage has weird IDs for group chats, so getting all messages in the same MMS group chat into the same Matrix room\n # may require rerouting some messages based on the fake ReplyToGUID that iMessage adds.\n reroute_mms_group_replies: false\n # Whether or not created rooms should have federation enabled.\n # If false, created portal rooms will never be federated.\n federate_rooms: false\n # Send captions in the same message as images using MSC2530?\n # This is currently not supported in most clients.\n caption_in_message: true\n # Should the bridge explicitly set the avatar and room name for private chat portal rooms?\n # This is implicitly enabled in encrypted rooms.\n private_chat_portal_meta: never\n\n # End-to-bridge encryption support options.\n # See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html\n encryption:\n # Allow encryption, work in group chat rooms with e2ee enabled\n allow: true\n # Default to encryption, force-enable encryption in all portals the bridge creates\n # This will cause the bridge bot to be in private chats for the encryption to work properly.\n default: true\n # Whether or not to use MSC2409/MSC3202 instead of /sync long polling for receiving encryption-related data.\n appservice: true\n # Require encryption, drop any unencrypted messages.\n require: true\n # Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled.\n # You must use a client that supports requesting keys from other users to use this feature.\n allow_key_sharing: true\n # Options for deleting megolm sessions from the bridge.\n delete_keys:\n # Beeper-specific: delete outbound sessions when hungryserv confirms\n # that the user has uploaded the key to key backup.\n delete_outbound_on_ack: true\n # Don't store outbound sessions in the inbound table.\n dont_store_outbound: false\n # Ratchet megolm sessions forward after decrypting messages.\n ratchet_on_decrypt: true\n # Delete fully used keys (index >= max_messages) after decrypting messages.\n delete_fully_used_on_decrypt: true\n # Delete previous megolm sessions from same device when receiving a new one.\n delete_prev_on_new_session: true\n # Delete megolm sessions received from a device when the device is deleted.\n delete_on_device_delete: true\n # Periodically delete megolm sessions when 2x max_age has passed since receiving the session.\n periodically_delete_expired: true\n # What level of device verification should be required from users?\n #\n # Valid levels:\n # unverified - Send keys to all device in the room.\n # cross-signed-untrusted - Require valid cross-signing, but trust all cross-signing keys.\n # cross-signed-tofu - Require valid cross-signing, trust cross-signing keys on first use (and reject changes).\n # cross-signed-verified - Require valid cross-signing, plus a valid user signature from the bridge bot.\n # Note that creating user signatures from the bridge bot is not currently possible.\n # verified - Require manual per-device verification\n # (currently only possible by modifying the `trust` column in the `crypto_device` database table).\n verification_levels:\n # Minimum level for which the bridge should send keys to when bridging messages from WhatsApp to Matrix.\n receive: cross-signed-tofu\n # Minimum level that the bridge should accept for incoming Matrix messages.\n send: cross-signed-tofu\n # Minimum level that the bridge should require for accepting key requests.\n share: cross-signed-tofu\n # Options for Megolm room key rotation. These options allow you to\n # configure the m.room.encryption event content. See:\n # https://spec.matrix.org/v1.3/client-server-api/#mroomencryption for\n # more information about that event.\n rotation:\n # Enable custom Megolm room key rotation settings. Note that these\n # settings will only apply to rooms created after this option is\n # set.\n enable_custom: true\n # The maximum number of milliseconds a session should be used\n # before changing it. The Matrix spec recommends 604800000 (a week)\n # as the default.\n milliseconds: 2592000000\n # The maximum number of messages that should be sent with a given a\n # session before changing it. The Matrix spec recommends 100 as the\n # default.\n messages: 10000\n\n # Disable rotating keys when a user's devices change?\n # You should not enable this option unless you understand all the implications.\n disable_device_change_key_rotation: true\n\n # Settings for relay mode\n relay:\n # Whether relay mode should be allowed.\n enabled: false\n # A list of user IDs and server names who are allowed to be relayed through this bridge. Use * to allow everyone.\n whitelist: []\n\n# Logging config. See https://github.com/tulir/zeroconfig for details.\nlogging:\n min_level: debug\n writers:\n - type: stdout\n format: pretty-colored\n - type: file\n format: json\n filename: ./logs/mautrix-imessage.log\n max_size: 100\n max_backups: 10\n compress: false\n\n# This may be used by external config managers. mautrix-imessage does not read it, but will carry it across configuration migrations.\nrevision: 0\n", - "imessagego.tpl.yaml": "# Homeserver details.\nhomeserver:\n # The address that this appservice can use to connect to the homeserver.\n address: {{ .HungryAddress }}\n # The domain of the homeserver (also known as server_name, used for MXIDs, etc).\n domain: beeper.local\n\n # What software is the homeserver running?\n # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use \"standard\" here.\n software: hungry\n # The URL to push real-time bridge status to.\n # If set, the bridge will make POST requests to this URL whenever a user's discord connection state changes.\n # The bridge will use the appservice as_token to authorize requests.\n status_endpoint: null\n # Endpoint for reporting per-message status.\n message_send_checkpoint_endpoint: null\n # Does the homeserver support https://github.com/matrix-org/matrix-spec-proposals/pull/2246?\n async_media: true\n\n # Should the bridge use a websocket for connecting to the homeserver?\n # The server side is currently not documented anywhere and is only implemented by mautrix-wsproxy,\n # mautrix-asmux (deprecated), and hungryserv (proprietary).\n websocket: {{ .Websocket }}\n # How often should the websocket be pinged? Pinging will be disabled if this is zero.\n ping_interval_seconds: 180\n\n# Application service host/registration related details.\n# Changing these values requires regeneration of the registration.\nappservice:\n # The address that the homeserver can use to connect to this appservice.\n address: null\n\n # The hostname and port where this appservice should listen.\n hostname: {{ if .Websocket }}null{{ else }}{{ .ListenAddr }}{{ end }}\n port: {{ if .Websocket }}null{{ else }}{{ .ListenPort }}{{ end }}\n\n # Database config.\n database:\n # The database type. Only \"sqlite3-fk-wal\" is supported.\n type: sqlite3-fk-wal\n # SQLite database path. A raw file path is supported, but `file:?_txlock=immediate` is recommended.\n uri: file:{{.DatabasePrefix}}beeper-imessage.db?_txlock=immediate\n\n # The unique ID of this appservice.\n id: {{ .AppserviceID }}\n # Appservice bot details.\n bot:\n # Username of the appservice bot.\n username: {{ .BridgeName }}bot\n # Display name and avatar for bot. Set to \"remove\" to remove display name/avatar, leave empty\n # to leave display name/avatar as-is.\n displayname: iMessage bridge bot\n avatar: mxc://maunium.net/tManJEpANASZvDVzvRvhILdX\n\n # Whether or not to receive ephemeral events via appservice transactions.\n # Requires MSC2409 support (i.e. Synapse 1.22+).\n # You should disable bridge -> sync_with_custom_puppets when this is enabled.\n ephemeral_events: true\n\n # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify.\n as_token: {{ .ASToken }}\n hs_token: {{ .HSToken }}\n\n# Segment-compatible analytics endpoint for tracking some events, like provisioning API login and encryption errors.\nanalytics:\n # Hostname of the tracking server. The path is hardcoded to /v1/track\n host: api.segment.io\n # API key to send with tracking requests. Tracking is disabled if this is null.\n token: null\n # Optional user ID for tracking events. If null, defaults to using Matrix user ID.\n user_id: null\n\nimessage:\n device_name: {{ or .Params.device_name \"Beeper (self-hosted)\" }}\n\n# Bridge config\nbridge:\n # Localpart template of MXIDs for iMessage users.\n username_template: {{ .BridgeName }}_{{ \"{{.}}\" }}\n # Displayname template for iMessage users.\n displayname_template: \"{{ \"{{.}}\" }}\"\n\n # A URL to fetch validation data from. Use this option or the nac_plist option\n nac_validation_data_url: {{ or .Params.nac_url \"https://registration-relay.beeper.com\" }}\n # Optional auth token to use when fetching validation data. If null, defaults to passing the as_token.\n nac_validation_data_token: {{ .Params.nac_token }}\n nac_validation_is_relay: true\n\n # Servers to always allow double puppeting from\n double_puppet_server_map:\n {{ .BeeperDomain }}: {{ .HungryAddress }}\n # Allow using double puppeting from any server with a valid client .well-known file.\n double_puppet_allow_discovery: false\n # Shared secrets for https://github.com/devture/matrix-synapse-shared-secret-auth\n #\n # If set, double puppeting will be enabled automatically for local users\n # instead of users having to find an access token and run `login-matrix`\n # manually.\n login_shared_secret_map:\n {{ .BeeperDomain }}: \"as_token:{{ .ASToken }}\"\n\n # Should the bridge create a space and add bridged rooms to it?\n personal_filtering_spaces: true\n # Whether or not the bridge should send a read receipt from the bridge bot when a message has been\n # sent to iMessage.\n delivery_receipts: false\n # Whether or not the bridge should send the message status as a custom\n # com.beeper.message_send_status event.\n message_status_events: true\n # Whether or not the bridge should send error notices via m.notice events\n # when a message fails to bridge.\n send_error_notices: false\n # Enable notices about various things in the bridge management room?\n enable_bridge_notices: true\n # Enable less important notices (sent with m.notice) in the bridge management room?\n unimportant_bridge_notices: true\n # The maximum number of seconds between the message arriving at the\n # homeserver and the bridge attempting to send the message. This can help\n # prevent messages from being bridged a long time after arriving at the\n # homeserver which could cause confusion in the chat history on the remote\n # network. Set to 0 to disable.\n max_handle_seconds: 0\n # Should we convert heif images to jpeg before re-uploading? This increases\n # compatibility, but adds generation loss (reduces quality).\n convert_heif: false\n # Should we convert tiff images to jpeg before re-uploading? This increases\n # compatibility, but adds generation loss (reduces quality).\n convert_tiff: true\n # Modern Apple devices tend to use h265 encoding for video, which is a licensed standard and therefore not\n # supported by most major browsers. If enabled, all video attachments will be converted according to the\n # ffmpeg args.\n convert_mov: true\n # The prefix for commands.\n command_prefix: \"!im\"\n # Whether or not created rooms should have federation enabled.\n # If false, created portal rooms will never be federated.\n federate_rooms: false\n # Whether to explicitly set the avatar and room name for private chat portal rooms.\n # If set to `default`, this will be enabled in encrypted rooms and disabled in unencrypted rooms.\n # If set to `always`, all DM rooms will have explicit names and avatars set.\n # If set to `never`, DM rooms will never have names and avatars set.\n private_chat_portal_meta: never\n # Should iMessage reply threads be mapped to Matrix threads? If false, iMessage reply threads will be bridged\n # as replies to the previous message in the thread.\n matrix_threads: false\n\n # End-to-bridge encryption support options.\n # See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html\n encryption:\n # Allow encryption, work in group chat rooms with e2ee enabled\n allow: true\n # Default to encryption, force-enable encryption in all portals the bridge creates\n # This will cause the bridge bot to be in private chats for the encryption to work properly.\n default: true\n # Whether or not to use MSC2409/MSC3202 instead of /sync long polling for receiving encryption-related data.\n appservice: true\n # Require encryption, drop any unencrypted messages.\n require: true\n # Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled.\n # You must use a client that supports requesting keys from other users to use this feature.\n allow_key_sharing: true\n # Options for deleting megolm sessions from the bridge.\n delete_keys:\n # Beeper-specific: delete outbound sessions when hungryserv confirms\n # that the user has uploaded the key to key backup.\n delete_outbound_on_ack: true\n # Don't store outbound sessions in the inbound table.\n dont_store_outbound: false\n # Ratchet megolm sessions forward after decrypting messages.\n ratchet_on_decrypt: true\n # Delete fully used keys (index >= max_messages) after decrypting messages.\n delete_fully_used_on_decrypt: true\n # Delete previous megolm sessions from same device when receiving a new one.\n delete_prev_on_new_session: true\n # Delete megolm sessions received from a device when the device is deleted.\n delete_on_device_delete: true\n # Periodically delete megolm sessions when 2x max_age has passed since receiving the session.\n periodically_delete_expired: true\n # What level of device verification should be required from users?\n #\n # Valid levels:\n # unverified - Send keys to all device in the room.\n # cross-signed-untrusted - Require valid cross-signing, but trust all cross-signing keys.\n # cross-signed-tofu - Require valid cross-signing, trust cross-signing keys on first use (and reject changes).\n # cross-signed-verified - Require valid cross-signing, plus a valid user signature from the bridge bot.\n # Note that creating user signatures from the bridge bot is not currently possible.\n # verified - Require manual per-device verification\n # (currently only possible by modifying the `trust` column in the `crypto_device` database table).\n verification_levels:\n # Minimum level for which the bridge should send keys to when bridging messages from iMessage to Matrix.\n receive: cross-signed-tofu\n # Minimum level that the bridge should accept for incoming Matrix messages.\n send: cross-signed-tofu\n # Minimum level that the bridge should require for accepting key requests.\n share: cross-signed-tofu\n # Options for Megolm room key rotation. These options allow you to\n # configure the m.room.encryption event content. See:\n # https://spec.matrix.org/v1.3/client-server-api/#mroomencryption for\n # more information about that event.\n rotation:\n # Enable custom Megolm room key rotation settings. Note that these\n # settings will only apply to rooms created after this option is\n # set.\n enable_custom: true\n # The maximum number of milliseconds a session should be used\n # before changing it. The Matrix spec recommends 604800000 (a week)\n # as the default.\n milliseconds: 2592000000\n # The maximum number of messages that should be sent with a given a\n # session before changing it. The Matrix spec recommends 100 as the\n # default.\n messages: 10000\n\n # Disable rotating keys when a user's devices change?\n # You should not enable this option unless you understand all the implications.\n disable_device_change_key_rotation: true\n\n # Settings for provisioning API\n provisioning:\n # Prefix for the provisioning API paths.\n prefix: /_matrix/provision\n # Shared secret for authentication. If set to \"generate\", a random secret will be generated,\n # or if set to \"disable\", the provisioning API will be disabled.\n shared_secret: {{ .ProvisioningSecret }}\n\n # Permissions for using the bridge.\n # Permitted values:\n # user - Access to use the bridge to chat with a WhatsApp account.\n # admin - User level and some additional administration tools\n # Permitted keys:\n # * - All Matrix users\n # domain - All users on that homeserver\n # mxid - Specific user\n permissions:\n \"{{ .UserID }}\": admin\n\n# Logging config. See https://github.com/tulir/zeroconfig for details.\nlogging:\n min_level: debug\n writers:\n - type: stdout\n format: pretty-colored\n - type: file\n format: json\n filename: ./logs/beeper-imessage.log\n max_size: 100\n max_backups: 10\n compress: false\n", - "linkedin.tpl.yaml": "# Network-specific config options\nnetwork:\n # Displayname template for LinkedIn users.\n # .FirstName is replaced with the first name\n # .LastName is replaced with the last name\n # .Organization is replaced with the organization name\n displayname_template: {{ `\"{{ with .Organization }}{{ . }}{{ else }}{{ .FirstName }} {{ .LastName }}{{ end }}\"` }}\n\n sync:\n # Number of most recently active dialogs to check when syncing chats.\n # Set to 0 to remove limit.\n update_limit: 0\n # Number of most recently active dialogs to create portals for when syncing\n # chats.\n # Set to 0 to remove limit.\n create_limit: 10\n\n{{ setfield . \"CommandPrefix\" \"!linkedin\" -}}\n{{ setfield . \"DatabaseFileName\" \"mautrix-linkedin\" -}}\n{{ setfield . \"BridgeTypeName\" \"LinkedIn\" -}}\n{{ setfield . \"BridgeTypeIcon\" \"mxc://nevarro.space/cwsWnmeMpWSMZLUNblJHaIvP\" -}}\n{{ setfield . \"DefaultPickleKey\" \"mautrix.bridge.e2ee\" -}}\n{{ template \"bridgev2.tpl.yaml\" . }}\n", - "meta.tpl.yaml": "# Network-specific config options\nnetwork:\n # Which service is this bridge for? Available options:\n # * unset - allow users to pick any service when logging in (except facebook-tor)\n # * facebook - connect to FB Messenger via facebook.com\n # * facebook-tor - connect to FB Messenger via facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd.onion\n # (note: does not currently proxy media downloads)\n # * messenger - connect to FB Messenger via messenger.com (can be used with the facebook side deactivated)\n # * messenger-lite - connect to FB Messenger via Messenger iOS API\n # * instagram - connect to Instagram DMs via instagram.com\n #\n # Remember to change the appservice id, bot profile info, bridge username_template and management_room_text too.\n mode: {{ .Params.meta_platform }}\n # Should users be allowed to pick messenger.com login when mode is set to `facebook`?\n allow_messenger_com_on_fb: true\n # When in Instagram mode, should the bridge connect to WhatsApp servers for encrypted chats?\n # In FB/Messenger mode encryption is always enabled, this option only affects Instagram mode.\n ig_e2ee: false\n # Displayname template for FB/IG users. Available variables:\n # .DisplayName - The display name set by the user.\n # .Username - The username set by the user.\n # .ID - The internal user ID of the user.\n displayname_template: {{ `'{{or .DisplayName .Username \"Unknown user\"}}'` }}\n # Static proxy address (HTTP or SOCKS5) for connecting to Meta.\n proxy:\n # HTTP endpoint to request new proxy address from, for dynamically assigned proxies.\n # The endpoint must return a JSON body with a string field called proxy_url.\n get_proxy_from:\n # Minimum interval between full reconnects in seconds, default is 1 hour\n min_full_reconnect_interval_seconds: 3600\n # Interval to force refresh the connection (full reconnect), default is 20 hours. Set 0 to disable force refreshes.\n force_refresh_interval_seconds: 72000\n # Should connection state be cached to allow quicker restarts?\n cache_connection_state: false\n # Disable fetching XMA media (reels, stories, etc) when backfilling.\n disable_xma_backfill: true\n # Disable fetching XMA media entirely.\n disable_xma_always: false\n\n{{ setfield . \"DatabaseFileName\" \"mautrix-meta\" -}}\n{{ setfield . \"DefaultPickleKey\" \"mautrix.bridge.e2ee\" -}}\n{{ if eq .Params.meta_platform \"facebook\" \"facebook-tor\" \"messenger\" \"messenger-lite\" -}}\n {{ setfield . \"CommandPrefix\" \"!fb\" -}}\n {{ setfield . \"BridgeTypeName\" \"Facebook\" -}}\n {{ setfield . \"BridgeTypeIcon\" \"mxc://maunium.net/ygtkteZsXnGJLJHRchUwYWak\" -}}\n{{ else if eq .Params.meta_platform \"instagram\" -}}\n {{ setfield . \"CommandPrefix\" \"!ig\" -}}\n {{ setfield . \"BridgeTypeName\" \"Instagram\" -}}\n {{ setfield . \"BridgeTypeIcon\" \"mxc://maunium.net/JxjlbZUlCPULEeHZSwleUXQv\" -}}\n{{ else -}}\n {{ setfield . \"CommandPrefix\" \"!meta\" -}}\n {{ setfield . \"BridgeTypeName\" \"Meta\" -}}\n {{ setfield . \"BridgeTypeIcon\" \"mxc://maunium.net/DxpVrwwzPUwaUSazpsjXgcKB\" -}}\n{{ end -}}\n{{ template \"bridgev2.tpl.yaml\" . }}\n", - "signal.tpl.yaml": "# Network-specific config options\nnetwork:\n # Displayname template for Signal users.\n displayname_template: {{ `'{{or .Nickname .ContactName .ProfileName .PhoneNumber \"Unknown user\" }}'` }}\n # Should avatars from the user's contact list be used? This is not safe on multi-user instances.\n use_contact_avatars: true\n # Should the bridge sync ghost user info even if profile fetching fails? This is not safe on multi-user instances.\n use_outdated_profiles: true\n # Should the Signal user's phone number be included in the room topic in private chat portal rooms?\n number_in_topic: true\n # Default device name that shows up in the Signal app.\n device_name: {{ or .Params.device_name \"Beeper (self-hosted)\" }}\n # Avatar image for the Note to Self room.\n note_to_self_avatar: mxc://maunium.net/REBIVrqjZwmaWpssCZpBlmlL\n # Format for generating URLs from location messages for sending to Signal.\n # Google Maps: 'https://www.google.com/maps/place/%[1]s,%[2]s'\n # OpenStreetMap: 'https://www.openstreetmap.org/?mlat=%[1]s&mlon=%[2]s'\n location_format: 'https://www.google.com/maps/place/%[1]s,%[2]s'\n # Should view-once messages disappear shortly after sending a read receipt on Matrix?\n disappear_view_once: true\n\n{{ setfield . \"CommandPrefix\" \"!signal\" -}}\n{{ setfield . \"DatabaseFileName\" \"mautrix-signal\" -}}\n{{ setfield . \"BridgeTypeName\" \"Signal\" -}}\n{{ setfield . \"BridgeTypeIcon\" \"mxc://maunium.net/wPJgTQbZOtpBFmDNkiNEMDUp\" -}}\n{{ setfield . \"DefaultPickleKey\" \"mautrix.bridge.e2ee\" -}}\n{{ template \"bridgev2.tpl.yaml\" . }}\n", - "slack.tpl.yaml": "network:\n # Displayname template for Slack users. Available variables:\n # .Name - The username of the user\n # .ID - The internal ID of the user\n # .IsBot - Whether the user is a bot\n # .Profile.DisplayName - The username or real name of the user (depending on settings)\n # Variables only available for users (not bots):\n # .TeamID - The internal ID of the workspace the user is in\n # .TZ - The timezone region of the user (e.g. Europe/London)\n # .TZLabel - The label of the timezone of the user (e.g. Greenwich Mean Time)\n # .TZOffset - The UTC offset of the timezone of the user (e.g. 0)\n # .Profile.RealName - The real name of the user\n # .Profile.FirstName - The first name of the user\n # .Profile.LastName - The last name of the user\n # .Profile.Title - The job title of the user\n # .Profile.Pronouns - The pronouns of the user\n # .Profile.Email - The email address of the user\n # .Profile.Phone - The formatted phone number of the user\n displayname_template: '{{ `{{or .Profile.DisplayName .Profile.RealName .Name}}{{if .IsBot}} (bot){{end}}` }}'\n # Channel name template for Slack channels (all types). Available variables:\n # .Name - The name of the channel\n # .TeamName - The name of the team the channel is in\n # .TeamDomain - The Slack subdomain of the team the channel is in\n # .ID - The internal ID of the channel\n # .IsNoteToSelf - Whether the channel is a DM with yourself\n # .IsGeneral - Whether the channel is the #general channel\n # .IsChannel - Whether the channel is a channel (rather than a DM)\n # .IsPrivate - Whether the channel is private\n # .IsIM - Whether the channel is a one-to-one DM\n # .IsMpIM - Whether the channel is a group DM\n # .IsShared - Whether the channel is shared with another workspace.\n # .IsExtShared - Whether the channel is shared with an external organization.\n # .IsOrgShared - Whether the channel is shared with an organization in the same enterprise grid.\n channel_name_template: '{{ `{{if or .IsNoteToSelf (and (not .IsIM) (not .IsMpIM))}}{{if and .IsChannel (not .IsPrivate)}}#{{end}}{{.Name}}{{if .IsNoteToSelf}} (you){{end}}{{end}}` }}'\n # Displayname template for Slack workspaces. Available variables:\n # .Name - The name of the team\n # .Domain - The Slack subdomain of the team\n # .ID - The internal ID of the team\n team_name_template: '{{ `{{.Name}}` }}'\n # Should incoming custom emoji reactions be bridged as mxc:// URIs?\n # If set to false, custom emoji reactions will be bridged as the shortcode instead, and the image won't be available.\n custom_emoji_reactions: true\n # Should channels and group DMs have the workspace icon as the Matrix room avatar?\n workspace_avatar_in_rooms: false\n # Number of participants to sync in channels (doesn't affect group DMs)\n participant_sync_count: 5\n # Should channel participants only be synced when creating the room?\n # If you want participants to always be accurately synced, set participant_sync_count to a high value and this to false.\n participant_sync_only_on_create: true\n # Should channel portals be muted by default?\n mute_channels_by_default: true\n # Options for backfilling messages from Slack.\n backfill:\n # Number of conversations to fetch from Slack when syncing workspace.\n # This option applies even if message backfill is disabled below.\n # If set to -1, all chats in the client.boot response will be bridged, and nothing will be fetched separately.\n conversation_count: -1\n\n{{ setfield . \"CommandPrefix\" \"!slack\" -}}\n{{ setfield . \"DatabaseFileName\" \"mautrix-slack\" -}}\n{{ setfield . \"BridgeTypeName\" \"Slack\" -}}\n{{ setfield . \"BridgeTypeIcon\" \"mxc://maunium.net/pVtzLmChZejGxLqmXtQjFxem\" -}}\n{{ template \"bridgev2.tpl.yaml\" . }}\n", - "telegram.tpl.yaml": "# Network-specific config options\nnetwork:\n # Get your own API keys at https://my.telegram.org/apps\n api_id: {{ .Params.api_id }}\n api_hash: {{ .Params.api_hash }}\n\n # Device info shown in the Telegram device list.\n device_info:\n device_model: {{ or .Params.device_name \"Beeper (self-hosted)\" }}\n system_version:\n app_version: auto\n lang_code: en\n system_lang_code: en\n\n # Settings for converting animated stickers.\n animated_sticker:\n # Format to which animated stickers should be converted.\n #\n # disable - no conversion, send as-is (gzipped lottie)\n # png - converts to non-animated png (fastest),\n # gif - converts to animated gif\n # webm - converts to webm video, requires ffmpeg executable with vp9 codec\n # and webm container support\n # webp - converts to animated webp, requires ffmpeg executable with webp\n # codec/container support\n target: webp\n # Should video stickers be converted to the specified format as well?\n convert_from_webm: false\n # Arguments for converter. All converters take width and height.\n args:\n width: 256\n height: 256\n fps: 25 # only for webm, webp and gif (2, 5, 10, 20 or 25 recommended)\n\n # Maximum number of pixels in an image before sending to Telegram as a\n # document. Defaults to 4096x4096 = 16777216.\n image_as_file_pixels: 16777216\n\n # Should view-once messages be disabled entirely?\n disable_view_once: false\n # Should disappearing messages be disabled entirely?\n disable_disappearing: false\n\n # Settings for syncing the member list for portals.\n member_list:\n # Maximum number of members to sync per portal when starting up. Other\n # members will be synced when they send messages. The maximum is 10000,\n # after which the Telegram server will not send any more members.\n #\n # -1 means no limit (which means it's limited to 10000 by the server)\n max_initial_sync: 20\n # Whether or not to sync the member list in broadcast channels. If\n # disabled, members will still be synced when they send messages.\n #\n # If no channel admins have logged into the bridge, the bridge won't be\n # able to sync the member list regardless of this setting.\n sync_broadcast_channels: false\n # Whether or not to skip deleted members when syncing members.\n skip_deleted: true\n # Maximum number of participants in chats to bridge. Only applies when the\n # portal is being created. If there are more members when trying to create a\n # room, the room creation will be cancelled.\n #\n # -1 means no limit (which means all chats can be bridged)\n max_member_count: 10000\n\n # Settings for pings to the Telegram server.\n ping:\n # The interval (in seconds) between pings.\n interval_seconds: 30\n # The timeout (in seconds) for a single ping.\n timeout_seconds: 10\n\n sync:\n # Number of most recently active dialogs to check when syncing chats.\n # Set to 0 to remove limit.\n update_limit: 0\n # Number of most recently active dialogs to create portals for when syncing\n # chats.\n # Set to 0 to remove limit.\n create_limit: 15\n # Whether or not to sync and create portals for direct chats at startup.\n direct_chats: true\n\n # Should the bridge send all unicode reactions as custom emoji reactions to\n # Telegram? By default, the bridge only uses custom emojis for unicode emojis\n # that aren't allowed in reactions.\n always_custom_emoji_reaction: false\n\n # The avatar to use for the Telegram Saved Messages chat\n saved_message_avatar: mxc://maunium.net/XhhfHoPejeneOngMyBbtyWDk\n\n # Create a new room and tombstone the old one when upgrading rooms\n always_tombstone_on_supergroup_migration: false\n\n{{ setfield . \"CommandPrefix\" \"!tg\" -}}\n{{ setfield . \"DatabaseFileName\" \"mautrix-telegram\" -}}\n{{ setfield . \"BridgeTypeName\" \"Telegram\" -}}\n{{ setfield . \"BridgeTypeIcon\" \"mxc://maunium.net/tJCRmUyJDsgRNgqhOgoiHWbX\" -}}\n{{ setfield . \"DefaultPickleKey\" \"mautrix.bridge.e2ee\" -}}\n{{ template \"bridgev2.tpl.yaml\" . }}\n", - "twitter.tpl.yaml": "# Network-specific config options\nnetwork:\n # Displayname template for Twitter users.\n # .DisplayName is replaced with the display name of the Twitter user.\n # .Username is replaced with the username of the Twitter user.\n displayname_template: {{ `\"{{ .DisplayName }}\"` }}\n\n # Maximum number of conversations to sync on startup\n conversation_sync_limit: 20\n\n # Should the bridge cache sessions instead of resyncing chats on every restart?\n cache_session: true\n\n # Should the bridge use \"X\" instead of \"Twitter\" in certain places,\n # such as the management room welcome message and MSC2346 bridge info?\n x: false\n\n{{ setfield . \"CommandPrefix\" \"!tw\" -}}\n{{ setfield . \"DatabaseFileName\" \"mautrix-twitter\" -}}\n{{ setfield . \"BridgeTypeName\" \"Twitter\" -}}\n{{ setfield . \"BridgeTypeIcon\" \"mxc://maunium.net/HVHcnusJkQcpVcsVGZRELLCn\" -}}\n{{ setfield . \"DefaultPickleKey\" \"mautrix.bridge.e2ee\" -}}\n{{ template \"bridgev2.tpl.yaml\" . }}\n", - "whatsapp.tpl.yaml": "# Network-specific config options\nnetwork:\n # Device name that's shown in the \"WhatsApp Web\" section in the mobile app.\n os_name: Beeper (self-hosted)\n # Browser name that determines the logo shown in the mobile app.\n # Must be \"unknown\" for a generic icon or a valid browser name if you want a specific icon.\n # List of valid browser names: https://github.com/tulir/whatsmeow/blob/efc632c008604016ddde63bfcfca8de4e5304da9/binary/proto/def.proto#L43-L64\n browser_name: unknown\n\n # Proxy to use for all WhatsApp connections.\n proxy: null\n # Alternative to proxy: an HTTP endpoint that returns the proxy URL to use for WhatsApp connections.\n get_proxy_url: null\n # Whether the proxy options should only apply to the login websocket and not to authenticated connections.\n proxy_only_login: false\n\n # Displayname template for WhatsApp users.\n # .PushName - nickname set by the WhatsApp user\n # .BusinessName - validated WhatsApp business name\n # .Phone - phone number (international format)\n # .FullName - Name you set in the contacts list\n displayname_template: {{ `'{{or .FullName .BusinessName .PushName .Phone .RedactedPhone \"Unknown user\"}}'` }}\n\n # Should incoming calls send a message to the Matrix room?\n call_start_notices: true\n # Should another user's cryptographic identity changing send a message to Matrix?\n identity_change_notices: false\n # Send the presence as \"available\" to whatsapp when users start typing on a portal.\n # This works as a workaround for homeservers that do not support presence, and allows\n # users to see when the whatsapp user on the other side is typing during a conversation.\n send_presence_on_typing: false\n # Should WhatsApp status messages be bridged into a Matrix room?\n # Disabling this won't affect already created status broadcast rooms.\n enable_status_broadcast: true\n # Should sending WhatsApp status messages be allowed?\n # This can cause issues if the user has lots of contacts, so it's disabled by default.\n disable_status_broadcast_send: true\n # Should the status broadcast room be muted and moved into low priority by default?\n # This is only applied when creating the room, the user can unmute it later.\n mute_status_broadcast: true\n # Tag to apply to the status broadcast room.\n status_broadcast_tag: m.lowpriority\n # Should the bridge use thumbnails from WhatsApp?\n # They're disabled by default due to very low resolution.\n whatsapp_thumbnail: false\n # Should the bridge detect URLs in outgoing messages, ask the homeserver to generate a preview,\n # and send it to WhatsApp? URL previews can always be sent using the `com.beeper.linkpreviews`\n # key in the event content even if this is disabled.\n url_previews: false\n # Should the bridge always send \"active\" delivery receipts (two gray ticks on WhatsApp)\n # even if the user isn't marked as online (e.g. when presence bridging isn't enabled)?\n #\n # By default, the bridge acts like WhatsApp web, which only sends active delivery\n # receipts when it's in the foreground.\n force_active_delivery_receipts: false\n # Settings for converting animated stickers.\n animated_sticker:\n # Format to which animated stickers should be converted.\n # disable - No conversion, just unzip and send raw lottie JSON\n # png - converts to non-animated png (fastest)\n # gif - converts to animated gif\n # webm - converts to webm video, requires ffmpeg executable with vp9 codec and webm container support\n # webp - converts to animated webp, requires ffmpeg executable with webp codec/container support\n target: webp\n # Arguments for converter. All converters take width and height.\n args:\n width: 320\n height: 320\n fps: 25 # only for webm, webp and gif (2, 5, 10, 20 or 25 recommended)\n\n # Settings for handling history sync payloads.\n history_sync:\n # How many conversations should the bridge create after login?\n # If -1, all conversations received from history sync will be bridged.\n # Other conversations will be backfilled on demand when receiving a message.\n max_initial_conversations: -1\n # Should the bridge request a full sync from the phone when logging in?\n # This bumps the size of history syncs from 3 months to 1 year.\n request_full_sync: true\n # Configuration parameters that are sent to the phone along with the request full sync flag.\n # By default, (when the values are null or 0), the config isn't sent at all.\n full_sync_config:\n # Number of days of history to request.\n # The limit seems to be around 3 years, but using higher values doesn't break.\n days_limit: 1825\n # This is presumably the maximum size of the transferred history sync blob, which may affect what the phone includes in the blob.\n size_mb_limit: 512\n # This is presumably the local storage quota, which may affect what the phone includes in the history sync blob.\n storage_quota_mb: 16384\n # Settings for media requests. If the media expired, then it will not be on the WA servers.\n # Media can always be requested by reacting with the ♻️ (recycle) emoji.\n # These settings determine if the media requests should be done automatically during or after backfill.\n media_requests:\n # Should the expired media be automatically requested from the server as part of the backfill process?\n auto_request_media: true\n # Whether to request the media immediately after the media message is backfilled (\"immediate\")\n # or at a specific time of the day (\"local_time\").\n request_method: immediate\n # If request_method is \"local_time\", what time should the requests be sent (in minutes after midnight)?\n request_local_time: 120\n # Maximum number of media request responses to handle in parallel per user.\n max_async_handle: 2\n\n{{ setfield . \"CommandPrefix\" \"!wa\" -}}\n{{ setfield . \"DatabaseFileName\" \"mautrix-whatsapp\" -}}\n{{ setfield . \"BridgeTypeName\" \"WhatsApp\" -}}\n{{ setfield . \"BridgeTypeIcon\" \"mxc://maunium.net/NeXNQarUbrlYBiPCpprYsRqr\" -}}\n{{ setfield . \"DefaultPickleKey\" \"maunium.net/go/mautrix-whatsapp\" -}}\n{{ template \"bridgev2.tpl.yaml\" . }}\n" -} as const - -export const generatedSupportedBridges = [ - "bluesky", - "bridgev2", - "discord", - "gmessages", - "googlechat", - "gvoice", - "heisenbridge", - "imessage", - "imessagego", - "linkedin", - "meta", - "signal", - "slack", - "telegram", - "twitter", - "whatsapp" -] as const - -export const generatedOfficialBridges: GeneratedOfficialBridge[] = [ - { - "typeName": "discord", - "names": [ - "discord" - ] - }, - { - "typeName": "meta", - "names": [ - "meta", - "instagram", - "facebook" - ] - }, - { - "typeName": "googlechat", - "names": [ - "googlechat", - "gchat" - ] - }, - { - "typeName": "imessagego", - "names": [ - "imessagego" - ] - }, - { - "typeName": "imessage", - "names": [ - "imessage" - ] - }, - { - "typeName": "linkedin", - "names": [ - "linkedin" - ] - }, - { - "typeName": "signal", - "names": [ - "signal" - ] - }, - { - "typeName": "slack", - "names": [ - "slack" - ] - }, - { - "typeName": "telegram", - "names": [ - "telegram" - ] - }, - { - "typeName": "twitter", - "names": [ - "twitter" - ] - }, - { - "typeName": "whatsapp", - "names": [ - "whatsapp" - ] - }, - { - "typeName": "heisenbridge", - "names": [ - "irc", - "heisenbridge" - ] - }, - { - "typeName": "gmessages", - "names": [ - "gmessages", - "googlemessages", - "rcs", - "sms" - ] - }, - { - "typeName": "gvoice", - "names": [ - "gvoice", - "googlevoice" - ] - }, - { - "typeName": "bluesky", - "names": [ - "bluesky", - "bsky" - ] - } -] - -export const generatedWebsocketBridges: Record = { - "discord": true, - "slack": true, - "whatsapp": true, - "gmessages": true, - "gvoice": true, - "heisenbridge": true, - "imessage": true, - "imessagego": true, - "signal": true, - "bridgev2": true, - "meta": true, - "twitter": true, - "bluesky": true, - "linkedin": true, - "telegram": true -} - -export const generatedBridgeIPSuffix: Record = { - "telegram": "17", - "whatsapp": "18", - "meta": "19", - "googlechat": "20", - "twitter": "27", - "signal": "28", - "discord": "34", - "slack": "35", - "gmessages": "36", - "imessagego": "37", - "gvoice": "38", - "bluesky": "40", - "linkedin": "41" -} - diff --git a/packages/cli/src/lib/bridges/go-template.ts b/packages/cli/src/lib/bridges/go-template.ts deleted file mode 100644 index 06e687b3..00000000 --- a/packages/cli/src/lib/bridges/go-template.ts +++ /dev/null @@ -1,183 +0,0 @@ -import type { BridgeCatalog } from './catalog.js' - -type Context = Record - -export function renderBridgeTemplate(catalog: BridgeCatalog, name: string, params: Context): string { - const template = catalog.templates[name] - if (template === undefined) throw new Error(`Unknown bridge template ${name}`) - return renderTemplate(catalog, name, template, params) -} - -function renderTemplate(catalog: BridgeCatalog, name: string, template: string, params: Context): string { - let out = '' - const tokens = tokenize(template) - const stack: Array<{ active: boolean; matched: boolean; parentActive: boolean }> = [] - const isActive = () => stack.every(item => item.active) - - for (const token of tokens) { - if (token.type === 'text') { - if (isActive()) out += token.value - continue - } - - const action = token.value.trim() - if (action.startsWith('if ')) { - const parentActive = isActive() - const condition = Boolean(evalExpr(action.slice(3).trim(), params)) - stack.push({ active: parentActive && condition, matched: condition, parentActive }) - } else if (action.startsWith('else if ')) { - const current = stack.at(-1) - if (!current) throw new Error(`Unexpected {{ else if }} in ${name}`) - const condition = !current.matched && Boolean(evalExpr(action.slice(8).trim(), params)) - current.active = current.parentActive && condition - current.matched = current.matched || condition - } else if (action === 'else') { - const current = stack.at(-1) - if (!current) throw new Error(`Unexpected {{ else }} in ${name}`) - current.active = current.parentActive && !current.matched - current.matched = true - } else if (action === 'end') { - if (!stack.pop()) throw new Error(`Unexpected {{ end }} in ${name}`) - } else if (isActive()) { - out += String(evalExpr(action, params, catalog) ?? '') - } - } - if (stack.length) throw new Error(`Unclosed {{ if }} block in ${name}`) - return out -} - -function tokenize(template: string): Array<{ type: 'action' | 'text'; value: string }> { - const tokens: Array<{ type: 'action' | 'text'; value: string }> = [] - let index = 0 - while (index < template.length) { - const start = template.indexOf('{{', index) - if (start === -1) { - tokens.push({ type: 'text', value: template.slice(index) }) - break - } - let text = template.slice(index, start) - const trimLeft = template[start + 2] === '-' - if (trimLeft) text = text.replace(/[ \t]*$/, '') - tokens.push({ type: 'text', value: text }) - const end = findActionEnd(template, start + 2) - if (end === -1) throw new Error('Unclosed template action') - const trimRight = template[end - 1] === '-' - const actionStart = start + (trimLeft ? 3 : 2) - const actionEnd = end - (trimRight ? 1 : 0) - tokens.push({ type: 'action', value: template.slice(actionStart, actionEnd) }) - index = end + 2 - if (trimRight) { - const match = template.slice(index).match(/^[ \t]*(?:\r?\n)?/) - index += match?.[0].length ?? 0 - } - } - return tokens -} - -function findActionEnd(template: string, start: number): number { - let quote: string | undefined - for (let i = start; i < template.length - 1; i++) { - const ch = template[i]! - if (quote) { - if (ch === quote) quote = undefined - } else if (ch === '"' || ch === '\'' || ch === '`') { - quote = ch - } else if (ch === '}' && template[i + 1] === '}') { - return i - } - } - return -1 -} - -function evalExpr(expr: string, ctx: Context, catalog?: BridgeCatalog): unknown { - const parts = splitArgs(expr) - if (!parts.length) return '' - const [head, ...rest] = parts - switch (head) { - case 'or': - for (const part of rest) { - const value = evalExpr(part!, ctx, catalog) - if (truthy(value)) return value - } - return '' - case 'replace': { - const [inputValue = '', search = '', replacement = ''] = rest.map(part => String(evalExpr(part!, ctx, catalog) ?? '')) - return inputValue.split(search).join(replacement) - } - case 'setfield': { - const [, field, valueExpr] = rest - if (!field || !valueExpr) return '' - ctx[stripQuotes(field)] = evalExpr(valueExpr, ctx, catalog) - return '' - } - case 'eq': - return rest.length >= 2 && evalExpr(rest[0]!, ctx, catalog) === evalExpr(rest[1]!, ctx, catalog) - case 'template': { - if (!catalog) throw new Error('template function requires a catalog') - const [templateExpr] = rest - const templateName = String(evalExpr(templateExpr!, ctx, catalog)) - return renderBridgeTemplate(catalog, templateName, ctx) - } - default: - if (parts.length > 1) throw new Error(`Unsupported template expression: ${expr}`) - return evalAtom(head!, ctx) - } -} - -function evalAtom(atom: string, ctx: Context): unknown { - atom = atom.trim() - if (atom === '.') return ctx - if (atom === 'true') return true - if (atom === 'false') return false - if (/^\d+$/.test(atom)) return Number(atom) - if (isQuoted(atom)) return stripQuotes(atom) - if (atom.startsWith('.')) return lookupPath(ctx, atom.slice(1).split('.').filter(Boolean)) - return atom -} - -function lookupPath(value: unknown, path: string[]): unknown { - let current = value - for (const part of path) { - if (!current || typeof current !== 'object') return '' - current = (current as Record)[part] - } - return current ?? '' -} - -function splitArgs(expr: string): string[] { - const out: string[] = [] - let current = '' - let quote: string | undefined - for (let i = 0; i < expr.length; i++) { - const ch = expr[i]! - if (quote) { - current += ch - if (ch === quote) quote = undefined - } else if (ch === '"' || ch === '\'' || ch === '`') { - quote = ch - current += ch - } else if (/\s/.test(ch)) { - if (current) { - out.push(current) - current = '' - } - } else { - current += ch - } - } - if (current) out.push(current) - return out -} - -function truthy(value: unknown): boolean { - return Boolean(value) -} - -function isQuoted(value: string): boolean { - return value.length >= 2 && ['"', '\'', '`'].includes(value[0]!) && value.at(-1) === value[0] -} - -function stripQuotes(value: string): string { - if (!isQuoted(value)) return value - return value.slice(1, -1) -} diff --git a/packages/cli/src/lib/bridges/manager.ts b/packages/cli/src/lib/bridges/manager.ts deleted file mode 100644 index 51907268..00000000 --- a/packages/cli/src/lib/bridges/manager.ts +++ /dev/null @@ -1,761 +0,0 @@ -import { createDecipheriv } from 'node:crypto' -import { createInterface } from 'node:readline/promises' -import { stdin as input, stdout as output } from 'node:process' -import { constants as fsConstants } from 'node:fs' -import { access, chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises' -import { createWriteStream } from 'node:fs' -import { homedir, platform } from 'node:os' -import { basename, dirname, join } from 'node:path' -import { spawn } from 'node:child_process' -import { once } from 'node:events' -import { pipeline } from 'node:stream/promises' -import { Readable } from 'node:stream' -import { clearTargetAuth, resolveTarget, saveTargetAuth, type Target } from '../targets.js' -import { authRequired } from '../errors.js' -import { isNoInput } from '../command.js' -import { isSupported, loadBridgeCatalog, templateName, type BridgeCatalog } from './catalog.js' -import { renderBridgeTemplate } from './go-template.js' - -export type BridgeEnv = { - accessToken: string - catalog: BridgeCatalog - domain: string - envName: string - target: Target -} - -export type BridgeTargetEnv = { - domain: string - envName: string - target: Target -} - -export type BridgeFlags = { - env?: string - 'base-url'?: string - target?: string -} - -export type WhoamiResponse = { - user: { - asmuxData?: { login_token?: string } - bridges?: Record - hungryserv?: WhoamiBridge - } - userInfo: { - isAdmin?: boolean - isFree?: boolean - username: string - channel?: string - createdAt?: string - email?: string - fullName?: string - bridgeClusterId?: string - supportRoomId?: string - } -} - -export type WhoamiBridge = { - version?: string - remoteState?: Record - bridgeState?: { - bridgeType?: string - isSelfHosted?: boolean - stateEvent?: string - } -} - -export type AppserviceRegistration = { - id: string - url?: string - as_token: string - hs_token: string - sender_localpart: string - namespaces?: { - users?: Array<{ exclusive: boolean; regex: string }> - aliases?: Array<{ exclusive: boolean; regex: string }> - rooms?: Array<{ exclusive: boolean; regex: string }> - } - protocols?: string[] - rate_limited?: boolean - receive_ephemeral?: boolean - 'de.sorunome.msc2409.push_ephemeral'?: boolean - 'org.matrix.msc3202'?: boolean - 'io.element.msc4190'?: boolean -} - -export type RegisterJSON = { - registration: AppserviceRegistration - homeserver_url: string - homeserver_domain: string - your_user_id: string -} - -export type GeneratedBridgeConfig = RegisterJSON & { - bridgeType: string - config?: string -} - -export async function prepareBridgeEnv(flags: BridgeFlags): Promise { - const targetEnv = await prepareBridgeTargetEnv(flags) - const { target } = targetEnv - const accessToken = process.env.BEEPER_ACCESS_TOKEN || target.auth?.accessToken - if (!accessToken) throw authRequired('beeper bridges requires the selected target to have a Matrix access token.') - if (!/^(syt|bat)_/.test(accessToken)) { - throw authRequired('beeper bridges requires a Matrix/Beeper access token (syt_ or bat_), not a Desktop API token.') - } - return { ...targetEnv, accessToken, catalog: await loadBridgeCatalog() } -} - -export async function prepareBridgeTargetEnv(flags: BridgeFlags): Promise { - const target = await resolveTarget({ target: flags.target, baseURL: flags['base-url'] }) - const envName = flags.env || process.env.BEEPER_BRIDGE_ENV || target.serverEnv || 'prod' - return { - domain: resolveDomain(envName), - envName, - target, - } -} - -export async function loginWithEmail(env: BridgeTargetEnv, email: string, codeProvider: (requestID: string) => Promise): Promise<{ accessToken: string; userID: string; whoami: WhoamiResponse | undefined }> { - const start = await beeperPublicAPI<{ request: string }>(env.domain, 'POST', '/user/login', {}) - await beeperPublicAPI(env.domain, 'POST', '/user/login/email', { - request: start.request, - email, - appType: 'bbctl', - onlyExistingAccounts: true, - }) - for (;;) { - const code = await codeProvider(start.request) - try { - const response = await beeperPublicAPI<{ token: string; whoami?: WhoamiResponse }>(env.domain, 'POST', '/user/login/response', { - request: start.request, - response: code, - appType: 'bbctl', - onlyExistingAccounts: true, - }) - const login = await matrixLogin(env.domain, { - type: 'org.matrix.login.jwt', - token: response.token, - initial_device_display_name: 'github.com/beeper/bridge-manager', - }) - await saveTargetAuth(env.target, { accessToken: login.access_token, source: 'manual', tokenType: 'Bearer' }) - return { accessToken: login.access_token, userID: login.user_id, whoami: response.whoami } - } catch (error) { - if (!String((error as Error).message).includes('invalid login code')) throw error - process.stderr.write(`${(error as Error).message}\n`) - } - } -} - -export async function loginWithPassword(env: BridgeTargetEnv, username: string, password: string): Promise<{ accessToken: string; userID: string }> { - const login = await matrixLogin(env.domain, { - type: 'm.login.password', - identifier: { type: 'm.id.user', user: username }, - password, - initial_device_display_name: 'github.com/beeper/bridge-manager', - }) - await saveTargetAuth(env.target, { accessToken: login.access_token, source: 'manual', tokenType: 'Bearer' }) - return { accessToken: login.access_token, userID: login.user_id } -} - -export async function logoutBridgeTarget(env: BridgeEnv, force: boolean): Promise { - try { - await matrixAPI(env.domain, env.accessToken, 'POST', '/_matrix/client/v3/logout', {}) - } catch (error) { - if (!force) throw new Error(`error logging out: ${(error as Error).message}`) - } - if (env.target.auth?.accessToken) { - await clearTargetAuth(env.target) - } else if (process.env.BEEPER_ACCESS_TOKEN) { - throw new Error('Logged out on the server, but BEEPER_ACCESS_TOKEN cannot be cleared from this process.') - } -} - -export async function whoami(env: BridgeEnv): Promise { - return beeperAPI(env, 'GET', '/whoami') as Promise -} - -export async function registerBridge( - env: BridgeEnv, - bridge: string, - options: { address?: string; bridgeType?: string; force?: boolean; get?: boolean; noState?: boolean }, -): Promise { - const info = await whoami(env) - const username = info.userInfo.username - const bridgeInfo = info.user.bridges?.[bridge] - if (bridgeInfo && !bridgeInfo.bridgeState?.isSelfHosted && !options.force) { - throw new Error(`Your ${bridge} bridge is not self-hosted.`) - } - - const req = options.address - ? { address: options.address, push: true, self_hosted: true } - : { push: false, self_hosted: true } - if (options.get && options.address) throw new Error("You can't use --get with --address") - - const registration = await hungryAPI( - env, - username, - options.get ? 'GET' : 'PUT', - `/_matrix/asmux/mxauth/appservice/${encodeURIComponent(username)}/${encodeURIComponent(bridge)}`, - options.get ? undefined : req, - ) - registration.namespaces = registration.namespaces ?? {} - if (registration.namespaces.users?.length) registration.namespaces.users = registration.namespaces.users.slice(0, 1) - - const state = (options.bridgeType && options.bridgeType !== 'heisenbridge') || ['androidsms', 'imessagecloud', 'imessage'].includes(bridge) - ? 'STARTING' - : 'RUNNING' - if (!options.noState) { - await postBridgeState(env, username, bridge, registration.as_token, { - stateEvent: state, - reason: 'SELF_HOST_REGISTERED', - isSelfHosted: true, - bridgeType: options.bridgeType || undefined, - }) - } - - return { - registration, - homeserver_url: hungryURL(env.domain, username), - homeserver_domain: 'beeper.local', - your_user_id: `@${username}:${env.domain}`, - } -} - -export async function deleteBridge(env: BridgeEnv, bridge: string): Promise { - await beeperAPI(env, 'DELETE', `/bridge/${encodeURIComponent(bridge)}`) -} - -export async function generateBridgeConfig( - env: BridgeEnv, - bridge: string, - options: { force?: boolean; noState?: boolean; params?: string[]; type?: string }, -): Promise { - validateBridgeName(bridge, Boolean(options.force)) - const info = await whoami(env) - const existingType = info.user.bridges?.[bridge]?.bridgeState?.bridgeType - const bridgeType = existingType || await guessOrAskBridgeType(env.catalog, bridge, options.type) - const extraParams = parseParams(options.params) - const cliParams = new Set(Object.keys(extraParams)) - await applyBridgeParamDefaults(bridge, bridgeType, extraParams) - const addedParams = Object.entries(extraParams).filter(([key]) => !cliParams.has(key)) - if (addedParams.length && !isNoInput()) { - process.stderr.write(`To run without specifying parameters interactively, add ${addedParams.map(([key, value]) => `--param '${key}=${value}'`).join(' ')} next time\n`) - } - - const reg = await registerBridge(env, bridge, { bridgeType, force: options.force, noState: options.noState }) - const websocket = Boolean(env.catalog.websocketBridges[bridgeType]) - let listenAddr = '' - let listenPort = 0 - if (!websocket) { - const proxy = getBridgeWebsocketProxyConfig(env.catalog, bridge, bridgeType) - listenAddr = proxy.listenAddr - listenPort = proxy.listenPort - reg.registration.url = proxy.url - } - - const databasePrefix = process.env.BEEPER_BRIDGE_DATABASE_DIR ? join(process.env.BEEPER_BRIDGE_DATABASE_DIR, `${bridge}-`) : '' - const params = { - HungryAddress: reg.homeserver_url, - BeeperDomain: env.domain, - Websocket: websocket, - ListenAddr: listenAddr, - ListenPort: listenPort, - AppserviceID: reg.registration.id, - ASToken: reg.registration.as_token, - HSToken: reg.registration.hs_token, - BridgeName: bridge, - Username: localpart(reg.your_user_id), - UserID: reg.your_user_id, - ProvisioningSecret: info.user.asmuxData?.login_token ?? '', - DatabasePrefix: databasePrefix, - Params: extraParams, - } - - return { - ...reg, - bridgeType, - config: renderBridgeTemplate(env.catalog, templateName(bridgeType), params), - } -} - -export function validateBridgeName(bridge: string, force = false): void { - if (!/^[a-z0-9-]{1,32}$/.test(bridge)) throw new Error('Invalid bridge name. Names must consist of 1-32 lowercase ASCII letters, digits and -.') - if (!bridge.startsWith('sh-')) { - if (!force) throw new Error('Self-hosted bridge names should start with sh-') - process.stderr.write('Self-hosted bridge names should start with sh-\n') - } -} - -export function validateBridgeID(bridge: string): void { - if (!/^[a-z0-9-]{1,32}$/.test(bridge)) throw new Error('Invalid bridge name') -} - -export async function outputFile(name: string, data: string, outputPath: string): Promise { - if (outputPath === '-') { - process.stderr.write(`${name} file:\n`) - process.stdout.write(`${data.trimEnd()}\n`) - return - } - await writeFile(outputPath, data, { mode: 0o600 }) - process.stderr.write(`Wrote ${name.toLowerCase()} file to ${outputPath}\n`) -} - -export async function writeRegistrationJSON(output: RegisterJSON): Promise { - process.stdout.write(`${JSON.stringify(output, null, 2)}\n`) -} - -export function registrationToYAML(registration: AppserviceRegistration): string { - return yamlValue({ - id: registration.id, - url: registration.url ?? '', - as_token: registration.as_token, - hs_token: registration.hs_token, - sender_localpart: registration.sender_localpart, - rate_limited: registration.rate_limited, - namespaces: registration.namespaces ?? {}, - protocols: registration.protocols, - 'de.sorunome.msc2409.push_ephemeral': registration['de.sorunome.msc2409.push_ephemeral'], - receive_ephemeral: registration.receive_ephemeral, - 'org.matrix.msc3202': registration['org.matrix.msc3202'], - 'io.element.msc4190': registration['io.element.msc4190'], - }) -} - -export function parseRegistration(data: string): AppserviceRegistration { - const trimmed = data.trim() - if (trimmed.startsWith('{')) return JSON.parse(trimmed) as AppserviceRegistration - const reg: Record = {} - const lines = data.split(/\r?\n/) - let section: string | undefined - let namespace: string | undefined - for (const line of lines) { - if (!line.trim() || line.trimStart().startsWith('#')) continue - const indent = line.match(/^ */)?.[0].length ?? 0 - const trimmedLine = line.trim() - if (indent === 0) { - const [key, ...rest] = trimmedLine.split(':') - section = key - namespace = undefined - if (rest.join(':').trim()) reg[key!] = parseScalar(rest.join(':').trim()) - else if (key === 'namespaces') reg.namespaces = {} - } else if (section === 'namespaces' && indent === 2) { - namespace = trimmedLine.slice(0, -1) - ;((reg.namespaces as Record)[namespace] = []) - } else if (section === 'namespaces' && namespace && trimmedLine.startsWith('- ')) { - const keyValue = trimmedLine.slice(2) - const [key, ...rest] = keyValue.split(':') - ;((reg.namespaces as Record)[namespace] ??= []).push({ [key!]: parseScalar(rest.join(':').trim()) }) - } else if (section === 'namespaces' && namespace && indent >= 6) { - const list = (reg.namespaces as Record>>)[namespace] - const item = list?.at(-1) - const [key, ...rest] = trimmedLine.split(':') - if (item && key) item[key] = parseScalar(rest.join(':').trim()) - } - } - return reg as AppserviceRegistration -} - -export function bridgeDataDir(envName: string): string { - const dataHome = process.env.BBCTL_DATA_HOME - || (platform() === 'win32' || platform() === 'darwin' ? userConfigDir() : process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share')) - return join(dataHome, 'bbctl', envName) -} - -export function getBridgeWebsocketProxyConfig(catalog: BridgeCatalog, bridgeName: string, bridgeType: string): { listenAddr: string; listenPort: number; url: string } { - const suffix = catalog.bridgeIPSuffix[bridgeType] || '1' - const listenAddr = platform() === 'darwin' ? '127.0.0.1' : `127.29.3.${suffix}` - const listenPort = 30_000 + (crc32(Buffer.from(bridgeName)) % 30_000) - return { listenAddr, listenPort, url: `http://${listenAddr}:${listenPort}` } -} - -export async function updateGoBridge(binaryPath: string, bridgeType: string, noUpdate: boolean): Promise { - await mkdir(dirname(binaryPath), { recursive: true, mode: 0o700 }) - let currentCommit = '' - if (await exists(binaryPath)) { - try { - const version = JSON.parse(await runCapture(binaryPath, ['--version-json'], dirname(binaryPath))) - currentCommit = version.Commit ?? version.commit ?? '' - } catch (error) { - process.stderr.write(`Failed to get current bridge version: ${(error as Error).message} - reinstalling\n`) - } - } - await downloadMautrixBridgeBinary(bridgeType, binaryPath, noUpdate, currentCommit) -} - -export async function compileGoBridge(buildDir: string, binaryPath: string, bridgeType: string, noUpdate: boolean): Promise { - await mkdir(dirname(buildDir), { recursive: true, mode: 0o700 }) - if (!await exists(buildDir)) { - const repo = bridgeType === 'imessagego' ? 'https://github.com/beeper/imessage.git' : `https://github.com/mautrix/${bridgeType}.git` - process.stderr.write(`Cloning ${repo} to ${buildDir}\n`) - await runCommand('git', ['clone', repo, buildDir], dirname(buildDir)) - } else { - if (await exists(binaryPath)) { - try { - await runCapture(binaryPath, ['--version-json'], buildDir) - if (noUpdate) { - process.stderr.write('Not updating bridge because --no-update was specified\n') - return - } - } catch (error) { - process.stderr.write(`Failed to get current bridge version: ${(error as Error).message} - reinstalling\n`) - } - } - process.stderr.write(`Pulling ${buildDir}\n`) - await runCommand('git', ['pull'], buildDir) - } - process.stderr.write('Compiling bridge with ./build.sh\n') - await runCommand('./build.sh', [], buildDir) -} - -export async function setupPythonVenv(bridgeDir: string, bridgeType: string, localDev: boolean): Promise { - let installPackage: string - let localRequirements = ['-r', 'requirements.txt'] - if (bridgeType === 'heisenbridge') installPackage = 'heisenbridge' - else if (bridgeType === 'googlechat') { - installPackage = 'mautrix-googlechat[all]' - localRequirements = [...localRequirements, '-r', 'optional-requirements.txt'] - } else { - throw new Error(`unknown python bridge type ${bridgeType}`) - } - const venvPath = join(bridgeDir, localDev ? '.venv' : 'venv') - const venvArgs = ['-m', 'venv', ...(process.env.SYSTEM_SITE_PACKAGES === 'true' ? ['--system-site-packages'] : []), venvPath] - process.stderr.write(`Creating Python virtualenv at ${venvPath}\n`) - await runCommand('python3', venvArgs, bridgeDir) - const packages = localDev ? localRequirements : [installPackage] - process.stderr.write(`Installing ${packages.join(' ')} into virtualenv\n`) - await runCommand(join(venvPath, 'bin', 'pip3'), ['install', '--upgrade', ...packages], bridgeDir) - return venvPath -} - -export async function runCommand(command: string, args: string[], cwd: string): Promise { - const child = spawn(command, args, { cwd, stdio: 'inherit' }) - const [code, signal] = await once(child, 'exit') as [number | null, NodeJS.Signals | null] - if (code !== 0) throw new Error(`${command} exited with ${signal ?? code}`) -} - -async function downloadMautrixBridgeBinary(bridge: string, binaryPath: string, noUpdate: boolean, currentCommit: string): Promise { - const repo = `mautrix/${bridge}` - const ref = bridge === 'imessage' ? 'master' : 'main' - const job = getJobFromBridge(bridge) - const fileName = basename(binaryPath) - process.stderr.write(`${currentCommit ? 'Checking for updates to' : 'Finding latest version of'} ${fileName} from mau.dev\n`) - const build = await getLastBuild('mau.dev', repo, ref, job) - if (build.commit === currentCommit) { - process.stderr.write(`${fileName} is up to date (${currentCommit.slice(0, 8)})\n`) - return - } - if (currentCommit && noUpdate) { - process.stderr.write(`${fileName} is out of date, latest commit is ${build.commit.slice(0, 8)}\n`) - return - } - const artifactURL = `https://mau.dev${build.jobURL}/artifacts/raw/${fileName}` - await downloadFile(artifactURL, binaryPath) - if (platform() === 'darwin' && needsLibolmDylib(bridge)) { - const libolmPath = join(dirname(binaryPath), 'libolm.3.dylib') - if (!await exists(libolmPath)) await downloadFile(`https://mau.dev${build.jobURL}/artifacts/raw/libolm.3.dylib`, libolmPath) - } - process.stderr.write(`Successfully installed ${fileName} commit ${build.commit.slice(0, 8)}\n`) -} - -async function getLastBuild(domain: string, repo: string, ref: string, job: string): Promise<{ commit: string; jobURL: string }> { - const query = `query($repo: ID!, $ref: String!, $job: String!) { - project(fullPath: $repo) { - pipelines(status: SUCCESS, ref: $ref, first: 1) { - nodes { sha job(name: $job) { webPath } } - } - } -}` - const response = await fetch(`https://${domain}/api/graphql`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', 'User-Agent': 'beeper-cli bridge-manager-ts' }, - body: JSON.stringify({ query, variables: { repo, ref, job } }), - }) - if (!response.ok) throw new Error(`GitLab GraphQL returned HTTP ${response.status}: ${await response.text()}`) - const json = await response.json() as { data?: { project?: { pipelines?: { nodes?: Array<{ sha: string; job?: { webPath?: string } }> } } } } - const node = json.data?.project?.pipelines?.nodes?.[0] - if (!node?.sha || !node.job?.webPath) throw new Error('did not get pipeline info in response') - return { commit: node.sha, jobURL: node.job.webPath } -} - -function getJobFromBridge(bridge: string): string { - const osAndArch = `${platform()}/${process.arch}` - if (osAndArch === 'linux/x64') return 'build amd64' - if (osAndArch === 'linux/arm64') return 'build arm64' - if (osAndArch === 'linux/arm') { - if (bridge === 'signal') throw new Error('mautrix-signal binaries for 32-bit arm are not built in the CI') - return 'build arm' - } - if (osAndArch === 'darwin/arm64') return bridge === 'imessage' ? 'build universal' : 'build macos arm64' - if (bridge === 'imessage') return 'build universal' - throw new Error(`binaries for ${osAndArch} are not built in the CI`) -} - -async function downloadFile(url: string, path: string): Promise { - await mkdir(dirname(path), { recursive: true, mode: 0o700 }) - const response = await fetch(url) - if (!response.ok || !response.body) throw new Error(`failed to download ${url}: HTTP ${response.status}`) - const tmp = join(dirname(path), `tmp-${basename(path)}-${Date.now()}`) - await pipeline(Readable.fromWeb(response.body as unknown as import('node:stream/web').ReadableStream), createWriteStream(tmp)) - await chmod(tmp, 0o755) - await rm(path, { force: true }) - await import('node:fs/promises').then(fs => fs.rename(tmp, path)) -} - -function needsLibolmDylib(bridge: string): boolean { - return ['imessage', 'whatsapp', 'discord', 'slack', 'gmessages', 'gvoice', 'signal', 'imessagego', 'meta', 'twitter', 'bluesky', 'linkedin', 'telegram'].includes(bridge) -} - -async function beeperAPI(env: BridgeEnv, method: string, path: string, body?: unknown): Promise { - return requestJSON(`https://api.${env.domain}${path}`, method, env.accessToken, body) -} - -async function beeperPublicAPI(domain: string, method: string, path: string, body?: unknown): Promise { - return requestJSON(`https://api.${domain}${path}`, method, 'BEEPER-PRIVATE-API-PLEASE-DONT-USE', body) as Promise -} - -async function hungryAPI(env: BridgeEnv, username: string, method: string, path: string, body?: unknown): Promise { - return requestJSON(`${hungryURL(env.domain, username)}${path}`, method, env.accessToken, body) as Promise -} - -async function postBridgeState(env: BridgeEnv, username: string, bridge: string, asToken: string, body: Record): Promise { - await requestJSON(`https://api.${env.domain}/bridgebox/${encodeURIComponent(username)}/bridge/${encodeURIComponent(bridge)}/bridge_state`, 'POST', asToken, body) -} - -async function matrixLogin(domain: string, body: Record): Promise<{ access_token: string; user_id: string }> { - return requestJSON(`https://matrix.${domain}/_matrix/client/v3/login`, 'POST', '', body) as Promise<{ access_token: string; user_id: string }> -} - -async function matrixAPI(domain: string, token: string, method: string, path: string, body?: unknown): Promise { - return requestJSON(`https://matrix.${domain}${path}`, method, token, body) -} - -async function requestJSON(url: string, method: string, token: string, body?: unknown): Promise { - const response = await fetch(url, { - method, - headers: { - ...(token ? { Authorization: `Bearer ${token}` } : {}), - 'Content-Type': 'application/json', - 'User-Agent': 'beeper-cli bridge-manager-ts', - }, - body: body === undefined ? undefined : JSON.stringify(body), - }) - if (!response.ok) { - const text = await response.text() - let message = text - try { - const parsed = JSON.parse(text) as { error?: string; errcode?: string } - message = parsed.error || parsed.errcode || text - } catch {} - throw new Error(`server returned error (HTTP ${response.status}): ${message}`) - } - if (response.status === 204) return undefined - return response.json() -} - -function resolveDomain(value: string): string { - const envs: Record = { - prod: 'beeper.com', - production: 'beeper.com', - staging: 'beeper-staging.com', - dev: 'beeper-dev.com', - local: 'beeper.localtest.me', - } - return envs[value] ?? value -} - -async function guessOrAskBridgeType(catalog: BridgeCatalog, bridge: string, bridgeType?: string): Promise { - if (!bridgeType) { - outer: - for (const item of catalog.officialBridges) { - for (const name of item.names) { - if (bridge.includes(name)) { - bridgeType = item.typeName - break outer - } - } - } - } - if (bridgeType && isSupported(catalog, bridgeType)) return bridgeType - process.stderr.write(`Unsupported bridge type ${bridgeType ?? ''}\n`) - if (isNoInput() || !process.stdin.isTTY) throw new Error('Pass --type with one of: ' + catalog.supportedBridges.join(', ')) - return promptSelect('Select bridge type:', catalog.supportedBridges) -} - -async function applyBridgeParamDefaults(bridgeName: string, bridgeType: string, params: Record): Promise { - if (bridgeType === 'telegram') { - params.api_id ??= '26417019' - params.api_hash ??= decryptTelegramAPIHash() - } else if (bridgeType === 'meta') { - let metaPlatform = params.meta_platform - if (!metaPlatform) { - if (bridgeName.includes('facebook-tor') || bridgeName.includes('facebooktor')) metaPlatform = 'facebook-tor' - else if (bridgeName.includes('facebook')) metaPlatform = 'facebook' - else if (bridgeName.includes('messenger')) metaPlatform = 'messenger' - else if (bridgeName.includes('instagram')) metaPlatform = 'instagram' - else metaPlatform = '' - params.meta_platform = metaPlatform - } - if (metaPlatform && !['instagram', 'facebook', 'facebook-tor', 'messenger', 'messenger-lite'].includes(metaPlatform)) { - throw new Error('Invalid Meta platform specified') - } - if (metaPlatform === 'facebook-tor' && !params.proxy) params.proxy = await promptInput('Enter Tor proxy address', 'socks5://localhost:1080') - } else if (bridgeType === 'imessagego') { - if (!params.nac_token) params.nac_token = await promptInput('Enter iMessage registration code') - } else if (bridgeType === 'imessage') { - await applyIMessageParams(params) - } -} - -async function applyIMessageParams(params: Record): Promise { - if (platform() !== 'darwin' && !params.imessage_platform) params.imessage_platform = 'bluebubbles' - if (!params.imessage_platform) { - params.imessage_platform = await promptSelect('Select iMessage connector:', ['mac', 'mac-nosip', 'bluebubbles']) - } - if (params.imessage_platform === 'mac-nosip' && !params.barcelona_path) { - params.barcelona_path = await promptInput('Enter Barcelona executable path:', 'darwin-barcelona-mautrix') - } - if (params.imessage_platform === 'bluebubbles') { - if (!params.bluebubbles_url) params.bluebubbles_url = await promptInput('Enter BlueBubbles API address:') - if (!params.bluebubbles_password) params.bluebubbles_password = await promptInput('Enter BlueBubbles password:') - } -} - -function parseParams(values: string[] | undefined): Record { - const params: Record = {} - for (const item of values ?? []) { - const index = item.indexOf('=') - if (index <= 0) throw new Error(`Invalid param ${item}`) - params[item.slice(0, index).toLowerCase()] = item.slice(index + 1) - } - return params -} - -async function promptInput(message: string, defaultValue = ''): Promise { - if (isNoInput() || !process.stdin.isTTY) { - if (defaultValue) return defaultValue - throw new Error(`${message} is required. Pass it with --param.`) - } - const rl = createInterface({ input, output }) - try { - const suffix = defaultValue ? ` (${defaultValue})` : '' - return (await rl.question(`${message}${suffix}: `)).trim() || defaultValue - } finally { - rl.close() - } -} - -async function promptSelect(message: string, options: string[]): Promise { - const rl = createInterface({ input, output }) - try { - process.stdout.write(`${message}\n`) - options.forEach((option, index) => process.stdout.write(` ${index + 1}. ${option}\n`)) - for (;;) { - const answer = (await rl.question('Select: ')).trim() - const selected = Number.parseInt(answer, 10) - if (Number.isInteger(selected) && selected >= 1 && selected <= options.length) return options[selected - 1]! - if (options.includes(answer)) return answer - process.stdout.write('Choose one of the listed options.\n') - } - } finally { - rl.close() - } -} - -function decryptTelegramAPIHash(): string { - const key = Buffer.from('qDP2pQ1LogRjxUYrFUDjDw', 'base64url') - const data = Buffer.from('B9VMuZeZlFk0pkbLcfSDDQ', 'base64url') - const decipher = createDecipheriv('aes-128-ecb', key, null) - decipher.setAutoPadding(false) - return Buffer.concat([decipher.update(data), decipher.final()]).toString('hex') -} - -function localpart(userID: string): string { - return userID.startsWith('@') ? userID.slice(1).split(':')[0] ?? userID : userID -} - -export function hungryURL(domain: string, username: string): string { - return `https://matrix.${domain}/_hungryserv/${encodeURIComponent(username)}` -} - -function yamlValue(value: unknown, indent = 0): string { - if (Array.isArray(value)) { - return value.map(item => `${' '.repeat(indent)}- ${yamlInline(item, indent + 2)}`).join('') - } - if (!value || typeof value !== 'object') return `${yamlScalar(value)}\n` - let out = '' - for (const [key, child] of Object.entries(value as Record)) { - if (child === undefined) continue - if (child && typeof child === 'object') out += `${' '.repeat(indent)}${key}:\n${yamlValue(child, indent + 2)}` - else out += `${' '.repeat(indent)}${key}: ${yamlScalar(child)}\n` - } - return out -} - -function yamlInline(value: unknown, indent: number): string { - if (!value || typeof value !== 'object' || Array.isArray(value)) return yamlScalar(value) + '\n' - const entries = Object.entries(value as Record) - const [first, ...rest] = entries - if (!first) return '{}\n' - return `${first[0]}: ${yamlScalar(first[1])}\n${rest.map(([key, child]) => `${' '.repeat(indent)}${key}: ${yamlScalar(child)}\n`).join('')}` -} - -function yamlScalar(value: unknown): string { - if (typeof value === 'boolean') return value ? 'true' : 'false' - if (typeof value === 'number') return String(value) - if (value === null || value === undefined) return '' - const text = String(value) - if (!text || /[:\n{}[\],&*#?|\-<>=!%@`]/.test(text) || /^\s|\s$/.test(text)) return JSON.stringify(text) - return text -} - -function parseScalar(value: string): unknown { - if (value === 'true') return true - if (value === 'false') return false - if (/^-?\d+$/.test(value)) return Number(value) - if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) return value.slice(1, -1) - return value -} - -async function runCapture(command: string, args: string[], cwd: string): Promise { - const child = spawn(command, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] }) - let stdout = '' - let stderr = '' - child.stdout.on('data', chunk => stdout += String(chunk)) - child.stderr.on('data', chunk => stderr += String(chunk)) - const [code, signal] = await once(child, 'exit') as [number | null, NodeJS.Signals | null] - if (code !== 0) throw new Error(stderr.trim() || `${command} exited with ${signal ?? code}`) - return stdout -} - -async function exists(path: string): Promise { - try { - await access(path, fsConstants.F_OK) - return true - } catch { - return false - } -} - -function userConfigDir(): string { - if (platform() === 'darwin') return join(homedir(), 'Library', 'Application Support') - if (platform() === 'win32') return process.env.APPDATA || join(homedir(), 'AppData', 'Roaming') - return process.env.XDG_CONFIG_HOME || join(homedir(), '.config') -} - -const crcTable = new Uint32Array(256).map((_, index) => { - let c = index - for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1 - return c >>> 0 -}) - -function crc32(buffer: Buffer): number { - let crc = 0xffffffff - for (const byte of buffer) crc = crcTable[(crc ^ byte) & 0xff]! ^ (crc >>> 8) - return (crc ^ 0xffffffff) >>> 0 -} diff --git a/packages/cli/src/lib/bridges/websocket-proxy.ts b/packages/cli/src/lib/bridges/websocket-proxy.ts deleted file mode 100644 index f676a0cd..00000000 --- a/packages/cli/src/lib/bridges/websocket-proxy.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { readFile } from 'node:fs/promises' -import { setTimeout as delay } from 'node:timers/promises' -import WebSocket from 'ws' -import { parseRegistration, type AppserviceRegistration } from './manager.js' - -type WebsocketMessage = { - command?: string - data?: unknown - id?: number - status?: string - txn_id?: string - [key: string]: unknown -} - -const defaultReconnectBackoff = 2_000 -const maxReconnectBackoff = 120_000 -const reconnectBackoffReset = 300_000 - -export async function proxyAppserviceWebsocket(options: { homeserverURL: string; registrationPath: string }): Promise { - const registration = parseRegistration(await readFile(options.registrationPath, 'utf8')) - if (!registration.url || registration.url === 'websocket') { - throw new Error('You must change the `url` field in the registration file to point at the local appservice HTTP server (e.g. `http://localhost:8080`)') - } - if (!registration.url.startsWith('http://') && !registration.url.startsWith('https://')) { - throw new Error('`url` field in registration must start with http:// or https://') - } - const controller = new AbortController() - process.once('SIGINT', () => controller.abort()) - process.once('SIGTERM', () => controller.abort()) - await runProxyLoop(controller.signal, options.homeserverURL, registration) -} - -export async function runProxyLoop(signal: AbortSignal, homeserverURL: string, registration: AppserviceRegistration): Promise { - let reconnectBackoff = defaultReconnectBackoff - let lastDisconnect = Date.now() - while (!signal.aborted) { - try { - await runSingleProxy(signal, homeserverURL, registration) - return - } catch (error) { - if (signal.aborted) return - if (String((error as Error).message).includes('conn_replaced')) return - process.stderr.write(`Error in appservice websocket: ${(error as Error).message}\n`) - } - const now = Date.now() - reconnectBackoff = lastDisconnect + reconnectBackoffReset < now ? defaultReconnectBackoff : Math.min(maxReconnectBackoff, reconnectBackoff * 2) - lastDisconnect = now - process.stderr.write(`Websocket disconnected, reconnecting in ${Math.round(reconnectBackoff / 1000)}s\n`) - await delay(reconnectBackoff, undefined, { signal }).catch(() => undefined) - } -} - -async function runSingleProxy(signal: AbortSignal, homeserverURL: string, registration: AppserviceRegistration): Promise { - const wsURL = new URL('/_matrix/client/unstable/fi.mau.as_sync', homeserverURL) - wsURL.protocol = wsURL.protocol === 'http:' ? 'ws:' : 'wss:' - const ws = new WebSocket(wsURL, { - headers: { - Authorization: `Bearer ${registration.as_token}`, - 'User-Agent': 'beeper-cli bridge-manager-ts', - 'X-Mautrix-Process-ID': String(process.pid), - 'X-Mautrix-Websocket-Version': '3', - }, - }) - signal.addEventListener('abort', () => ws.close()) - await new Promise((resolve, reject) => { - ws.once('open', () => { - send(ws, { command: 'bridge_status', data: { stateEvent: 'UNCONFIGURED' } }) - keepalive(signal, ws).catch(error => process.stderr.write(`Websocket ping returned error: ${(error as Error).message}\n`)) - resolve() - }) - ws.once('error', reject) - }) - await new Promise((resolve, reject) => { - ws.on('message', data => { - handleMessage(ws, registration, JSON.parse(String(data))).catch(error => { - process.stderr.write(`Failed to handle websocket message: ${(error as Error).message}\n`) - }) - }) - ws.once('close', (code, reason) => { - if (code === 4001 || String(reason).includes('conn_replaced')) reject(new Error('conn_replaced')) - else resolve() - }) - ws.once('error', reject) - }) -} - -async function handleMessage(ws: WebSocket, registration: AppserviceRegistration, msg: WebsocketMessage): Promise { - if (!msg.command || msg.command === 'transaction') { - const txnID = String(msg.txn_id ?? '') - const url = new URL(`/_matrix/app/v1/transactions/${encodeURIComponent(txnID)}`, registration.url) - const response = await fetch(url, { - method: 'PUT', - headers: { Authorization: `Bearer ${registration.hs_token}`, 'Content-Type': 'application/json' }, - body: JSON.stringify(msg), - }) - if (!response.ok) throw new Error(`transaction proxy returned HTTP ${response.status}: ${await response.text()}`) - sendResponse(ws, msg, true, { txn_id: txnID }) - } else if (msg.command === 'http_proxy') { - const req = msg.data as { body?: unknown; headers?: Record; path?: string; query?: string } - const url = new URL(req.path || '/', registration.url) - url.search = req.query || '' - const response = await fetch(url, { - method: 'PUT', - headers: req.headers as HeadersInit, - body: bodyFromProxyRequest(req.body), - }) - const bytes = Buffer.from(await response.arrayBuffer()) - const text = bytes.toString('utf8') - const body = isJSON(text) ? JSON.parse(text) : bytes.toString('base64url') - const headers: Record = {} - response.headers.forEach((value, key) => { - headers[key] = value - }) - sendResponse(ws, msg, true, { status: response.status, headers, body }) - } else if (msg.command === 'connect' || msg.command === 'response' || msg.command === 'error') { - return - } else { - sendResponse(ws, msg, false, { message: 'unknown request type' }) - } -} - -async function keepalive(signal: AbortSignal, ws: WebSocket): Promise { - while (!signal.aborted && ws.readyState === WebSocket.OPEN) { - await delay(180_000, undefined, { signal }).catch(() => undefined) - if (signal.aborted || ws.readyState !== WebSocket.OPEN) return - send(ws, { command: 'ping', data: { timestamp: Date.now() } }) - } -} - -function sendResponse(ws: WebSocket, msg: WebsocketMessage, ok: boolean, data: unknown): void { - if (!msg.id || msg.command === 'response' || msg.command === 'error') return - send(ws, { id: msg.id, command: ok ? 'response' : 'error', data }) -} - -function send(ws: WebSocket, msg: unknown): void { - if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg)) -} - -function bodyFromProxyRequest(body: unknown): BodyInit | undefined { - if (body === undefined || body === null) return undefined - if (typeof body === 'string') return body - return JSON.stringify(body) -} - -function isJSON(text: string): boolean { - try { - JSON.parse(text) - return true - } catch { - return false - } -} diff --git a/packages/cli/src/lib/client.ts b/packages/cli/src/lib/client.ts deleted file mode 100644 index a7f674bc..00000000 --- a/packages/cli/src/lib/client.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { BeeperDesktop } from '@beeper/desktop-api' -import { resolveTarget } from './targets.js' -import { ensureDesktopToken } from './desktop-auth.js' - -export async function createClient(flags: { baseURL?: string; 'base-url'?: string; target?: string; debug?: boolean } = {}) { - const target = await resolveTarget({ target: flags.target, baseURL: flags.baseURL || flags['base-url'] }) - const accessToken = process.env.BEEPER_ACCESS_TOKEN - || target.auth?.accessToken - || await ensureDesktopToken({ baseURL: target.baseURL, scan: target.id === 'desktop' }) - return new BeeperDesktop({ - accessToken, - baseURL: target.baseURL, - logLevel: flags.debug ? 'debug' : 'warn', - }) -} - -export async function requireToken(options: { baseURL?: string; target?: string; scan?: boolean } = {}): Promise { - const target = await resolveTarget({ target: options.target, baseURL: options.baseURL }) - const token = process.env.BEEPER_ACCESS_TOKEN || target.auth?.accessToken - if (token) return token - return ensureDesktopToken({ baseURL: target.baseURL, scan: options.scan }) -} diff --git a/packages/cli-plugin-cloudflare/src/lib/cloudflared.ts b/packages/cli/src/lib/cloudflare-tunnel.ts similarity index 69% rename from packages/cli-plugin-cloudflare/src/lib/cloudflared.ts rename to packages/cli/src/lib/cloudflare-tunnel.ts index bada2c6a..18c538b5 100644 --- a/packages/cli-plugin-cloudflare/src/lib/cloudflared.ts +++ b/packages/cli/src/lib/cloudflare-tunnel.ts @@ -1,3 +1,4 @@ +import { spawn, execFileSync, type ChildProcess } from 'node:child_process' import { createWriteStream } from 'node:fs' import { access, chmod, mkdir, rename, rm } from 'node:fs/promises' import { arch, platform } from 'node:os' @@ -6,35 +7,17 @@ import { Readable } from 'node:stream' import { pipeline } from 'node:stream/promises' import type { ReadableStream } from 'node:stream/web' import { fileURLToPath } from 'node:url' -import { execFileSync, spawn, type ChildProcess } from 'node:child_process' -export const currentCloudflaredVersion = '2024.8.2' -const repo = `https://github.com/cloudflare/cloudflared/releases/download/${currentCloudflaredVersion}/` +const currentCloudflaredVersion = '2024.8.2' +const downloadBaseURL = `https://github.com/cloudflare/cloudflared/releases/download/${currentCloudflaredVersion}/` const downloads: Record> = { - linux: { - arm64: 'cloudflared-linux-arm64', - arm: 'cloudflared-linux-arm', - x64: 'cloudflared-linux-amd64', - ia32: 'cloudflared-linux-386', - }, - darwin: { - arm64: 'cloudflared-darwin-arm64.tgz', - x64: 'cloudflared-darwin-amd64.tgz', - }, - win32: { - arm64: 'cloudflared-windows-amd64.exe', - ia32: 'cloudflared-windows-386.exe', - x64: 'cloudflared-windows-amd64.exe', - }, + darwin: { arm64: 'cloudflared-darwin-arm64.tgz', x64: 'cloudflared-darwin-amd64.tgz' }, + linux: { arm: 'cloudflared-linux-arm', arm64: 'cloudflared-linux-arm64', ia32: 'cloudflared-linux-386', x64: 'cloudflared-linux-amd64' }, + win32: { arm64: 'cloudflared-windows-amd64.exe', ia32: 'cloudflared-windows-386.exe', x64: 'cloudflared-windows-amd64.exe' }, } -export type TunnelStatus = - | { status: 'starting' } - | { status: 'connected'; url: string } - | { status: 'error'; message: string; tryMessage?: string } - -export type StartTunnelOptions = { +type StartTunnelOptions = { cloudflaredPath?: string debug?: boolean install?: boolean @@ -44,6 +27,7 @@ export type StartTunnelOptions = { } export type StartedTunnel = { + cloudflaredPath: string done: Promise<{ code: number | null; signal: NodeJS.Signals | null }> process: ChildProcess stop: () => void @@ -51,31 +35,39 @@ export type StartedTunnel = { url: string } -export function defaultCloudflaredPath(): string { - return join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', platform() === 'win32' ? 'cloudflared.exe' : 'cloudflared') +function cloudflaredPath(explicit?: string): string { + return explicit ?? process.env.BEEPER_CLOUDFLARED_PATH ?? join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', platform() === 'win32' ? 'cloudflared.exe' : 'cloudflared') } -export function cloudflaredPath(explicit?: string): string { - return explicit ?? process.env.BEEPER_CLOUDFLARED_PATH ?? defaultCloudflaredPath() -} +export async function startCloudflareTunnel(options: StartTunnelOptions): Promise { + const bin = await ensureCloudflared(options) + const retries = options.retries ?? 5 + let lastError: Error | undefined -export async function ensureCloudflared(options: { cloudflaredPath?: string; debug?: boolean; install?: boolean } = {}): Promise { - const target = cloudflaredPath(options.cloudflaredPath) - if (isTruthy(process.env.BEEPER_IGNORE_CLOUDFLARED)) { - if (options.debug) process.stderr.write('Skipping cloudflared installation because BEEPER_IGNORE_CLOUDFLARED is set.\n') - return target + for (let attempt = 0; attempt <= retries; attempt += 1) { + try { + return await runCloudflared(bin, options) + } catch (error) { + lastError = error as Error + if (attempt >= retries) break + if (options.debug) process.stderr.write(`cloudflared crashed before connecting; retrying (${attempt + 1}/${retries})\n`) + await new Promise(resolve => setTimeout(resolve, 1000)) + } } - if (await isUsableCloudflared(target)) return target - if (!options.install) { - throw new Error(`cloudflared not found at ${target}. Install it or rerun with --install.\n${whatToTry()}`) - } + throw new Error(`Could not start Cloudflare Tunnel: max retries reached.${lastError ? `\n${lastError.message}` : ''}\n${whatToTry()}`) +} +async function ensureCloudflared(options: { cloudflaredPath?: string; debug?: boolean; install?: boolean }): Promise { + const target = cloudflaredPath(options.cloudflaredPath) + if (truthy(process.env.BEEPER_IGNORE_CLOUDFLARED)) return target + if (await isUsableCloudflared(target)) return target + if (!options.install) throw new Error(`cloudflared not found at ${target}. Install it or rerun with --install.\n${whatToTry()}`) await installCloudflared(target) return target } -export async function installCloudflared(target = defaultCloudflaredPath()): Promise { +async function installCloudflared(target: string): Promise { const url = downloadURL() await mkdir(dirname(target), { recursive: true }) const temporary = url.endsWith('.tgz') ? `${target}.tgz` : `${target}.download` @@ -92,34 +84,8 @@ export async function installCloudflared(target = defaultCloudflaredPath()): Pro if (platform() !== 'win32') await chmod(target, 0o755) } -export async function startCloudflareTunnel(options: StartTunnelOptions): Promise { - const bin = await ensureCloudflared(options) - const retries = options.retries ?? 5 - let attempt = 0 - let lastError: Error | undefined - - while (attempt <= retries) { - try { - const started = await runCloudflared(bin, options) - return started - } catch (error) { - lastError = error as Error - attempt += 1 - if (attempt > retries) throw error - if (options.debug) process.stderr.write(`cloudflared crashed before connecting; retrying (${attempt}/${retries})\n`) - await new Promise(resolve => { - setTimeout(resolve, 1000) - }) - } - } - - throw new Error(`Could not start Cloudflare Tunnel: max retries reached.${lastError ? `\n${lastError.message}` : ''}\n${whatToTry()}`) -} - async function runCloudflared(bin: string, options: StartTunnelOptions): Promise { - const child = spawn(bin, ['tunnel', '--url', options.url, '--no-autoupdate'], { - stdio: ['ignore', 'pipe', 'pipe'], - }) + const child = spawn(bin, ['tunnel', '--url', options.url, '--no-autoupdate'], { stdio: ['ignore', 'pipe', 'pipe'] }) const errors: string[] = [] let connected = false let publicURL: string | undefined @@ -141,7 +107,7 @@ async function runCloudflared(bin: string, options: StartTunnelOptions): Promise const chunk = data.toString() if (options.debug) process.stderr.write(chunk) publicURL ??= findTunnelURL(chunk) - connected ||= hasConnection(chunk) + connected ||= /(INF Registered tunnel connection|INF Connection)/.test(chunk) const error = findKnownError(chunk) if (error) errors.push(error) @@ -149,6 +115,7 @@ async function runCloudflared(bin: string, options: StartTunnelOptions): Promise resolved = true cleanup() resolve({ + cloudflaredPath: bin, done, process: child, stop() { @@ -187,11 +154,9 @@ async function isUsableCloudflared(path: string): Promise { } function downloadURL(system = platform(), cpu = arch()): string { - const platformDownloads = downloads[system] - if (!platformDownloads) throw new Error(`Unsupported system platform: ${system}`) - const file = platformDownloads[cpu] - if (!file) throw new Error(`Unsupported system architecture: ${cpu}`) - return repo + file + const file = downloads[system]?.[cpu] + if (!file) throw new Error(`Unsupported system platform or architecture: ${system}/${cpu}`) + return downloadBaseURL + file } async function downloadFile(url: string, to: string): Promise { @@ -204,10 +169,6 @@ export function findTunnelURL(data: string, domain = cloudflaredDomain()): strin return data.match(new RegExp(`https:\\/\\/[^\\s]+\\.${escapeRegExp(domain)}`))?.[0] } -function hasConnection(data: string): boolean { - return /(INF Registered tunnel connection|INF Connection)/.test(data) -} - export function findKnownError(data: string): string | undefined { const knownErrors = [ /failed to request quick Tunnel/i, @@ -219,15 +180,7 @@ export function findKnownError(data: string): string | undefined { /ERR Failed to create new quic connection error/i, ] if (!knownErrors.some(error => error.test(data))) return undefined - return `Could not start Cloudflare Tunnel: ${cleanCloudflareLog(data)}` -} - -function cleanCloudflareLog(input: string): string { - return input.replace(/^[0-9TZ:-]+ (ERR )?/g, '').replace(/connIndex.*/g, '').trim() -} - -function lastTunnelError(errors: string[]): string | undefined { - return [...new Set(errors)].slice(-5).join('\n') || undefined + return `Could not start Cloudflare Tunnel: ${data.replace(/^[0-9TZ:-]+ (ERR )?/g, '').replace(/connIndex.*/g, '').trim()}` } export function cloudflaredDomain(): string { @@ -243,14 +196,6 @@ export function whatToTry(): string { ].join(' ') } -function escapeRegExp(value: string): string { - return value.replace(/[|\\{}()[\]^$+*?.]/g, match => `\\${match}`) -} - -function isTruthy(value: string | undefined): boolean { - return ['1', 'on', 'true', 'yes'].includes(String(value ?? '').toLowerCase()) -} - export function versionIsGreaterThan(versionA: string, versionB: string): boolean { const [majorA = 0, minorA = 0, patchA = 0] = versionA.split('.').map(Number) const [majorB = 0, minorB = 0, patchB = 0] = versionB.split('.').map(Number) @@ -258,3 +203,15 @@ export function versionIsGreaterThan(versionA: string, versionB: string): boolea if (minorA !== minorB) return minorA > minorB return patchA > patchB } + +function lastTunnelError(errors: string[]): string | undefined { + return [...new Set(errors)].slice(-5).join('\n') || undefined +} + +function escapeRegExp(value: string): string { + return value.replace(/[|\\{}()[\]^$+*?.]/g, match => `\\${match}`) +} + +function truthy(value: string | undefined): boolean { + return ['1', 'on', 'true', 'yes'].includes(String(value ?? '').toLowerCase()) +} diff --git a/packages/cli/src/lib/command-metadata.ts b/packages/cli/src/lib/command-metadata.ts deleted file mode 100644 index 214abf19..00000000 --- a/packages/cli/src/lib/command-metadata.ts +++ /dev/null @@ -1,55 +0,0 @@ -export type CommandMetadata = { - mutates: boolean - requiresAuth: boolean - selectors: string[] - output: 'data' | 'list' | 'stream' | 'success' | 'send-result' | 'manual' | 'schema' - related: string[] -} - -const mutatingRoots = new Set(['export', 'install', 'presence', 'send', 'setup', 'update']) -const mutatingVerbs = new Set([ - 'add', 'approve', 'archive', 'avatar', 'cancel', 'delete', 'description', 'disable', 'disappear', 'download', - 'draft', 'edit', 'enable', 'export', 'focus', 'logout', 'mark-read', 'mark-unread', 'mute', 'notify-anyway', - 'pin', 'post', 'priority', 'qr-confirm', 'qr-scan', 'recovery-key', 'remind', 'remove', 'rename', 'reset', - 'reset-recovery-key', 'response', 'restart', 'sas', 'sas-confirm', 'set', 'start', 'stop', 'unarchive', - 'unmute', 'unpin', 'unremind', 'use', -]) -const localOnly = new Set(['completion', 'config', 'docs', 'man', 'schema', 'version']) - -export function metadataForCommand(command: string): CommandMetadata { - const parts = command.split(' ') - const root = parts[0] ?? '' - const mutates = command === 'verify' || command === 'api request' || mutatingRoots.has(root) || parts.some(part => mutatingVerbs.has(part ?? '')) - const requiresAuth = !localOnly.has(root) && command !== 'targets list' && !command.startsWith('targets add') && !command.startsWith('install ') - const selectors = [ - command.includes('chats ') || command.includes('messages ') || command.startsWith('send ') || command === 'presence' || command.startsWith('resolve chat') ? 'chat' : undefined, - command.includes('accounts ') || command.includes('contacts ') || command === 'chats start' || command.startsWith('resolve account') || command.startsWith('resolve contact') ? 'account' : undefined, - command.includes('targets ') || command === 'status' || command === 'doctor' || command.startsWith('auth ') || command.startsWith('verify') || command.startsWith('resolve target') ? 'target' : undefined, - command.startsWith('bridges ') || command === 'accounts add' || command.startsWith('resolve bridge') ? 'bridge' : undefined, - command.includes('messages ') || command.startsWith('send react') || command.startsWith('send unreact') ? 'message' : undefined, - ].filter(Boolean) as string[] - const output = command === 'schema' ? 'schema' - : command.startsWith('send ') ? 'send-result' - : command === 'watch' || command === 'rpc' ? 'stream' - : command === 'man' ? 'manual' - : command.endsWith('list') || command.includes('search') || command === 'bridges list' || command.startsWith('resolve ') ? 'list' - : mutates ? 'success' - : 'data' - const related = relatedForCommand(command) - return { mutates, requiresAuth, selectors, output, related } -} - -function relatedForCommand(command: string): string[] { - if (command.startsWith('send ')) return ['messages list', 'watch'] - if (command.startsWith('messages ')) return ['chats list', 'send text'] - if (command.startsWith('chats ')) return ['messages list', 'send text'] - if (command.startsWith('bridges ')) return ['accounts add', 'accounts list'] - if (command.startsWith('accounts ')) return ['bridges list', 'chats list'] - if (command.startsWith('targets ')) return ['status', 'doctor'] - if (command.startsWith('resolve ')) return ['chats search', 'accounts list', 'targets list', 'bridges list'] - if (command === 'status') return ['doctor', 'setup'] - if (command === 'doctor') return ['status', 'setup'] - if (command === 'schema') return ['man'] - if (command.startsWith('verify')) return ['setup', 'status'] - return [] -} diff --git a/packages/cli/src/lib/command.ts b/packages/cli/src/lib/command.ts deleted file mode 100644 index ac7faa13..00000000 --- a/packages/cli/src/lib/command.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { Command, Flags } from '@oclif/core' -import { BugError, CLIError, ExitCodes } from './errors.js' - -export abstract class BeeperCommand extends Command { - static override baseFlags = { - 'base-url': Flags.string({ description: 'Beeper Desktop API base URL (overrides --target)' }), - target: Flags.string({ char: 't', description: 'Named Beeper target to use for this command' }), - debug: Flags.boolean({ default: false, description: 'Print SDK debug logging on stderr' }), - 'dry-run': Flags.boolean({ default: false, description: 'Do not make changes; print intended actions when supported' }), - events: Flags.boolean({ default: false, description: 'Emit NDJSON lifecycle events on stderr (long-running commands)' }), - force: Flags.boolean({ char: 'f', default: false, description: 'Skip confirmations for destructive commands' }), - format: Flags.string({ options: ['json', 'jsonl', 'table', 'text', 'ids'], description: 'Output format. Defaults to json for agents/non-TTY, table for TTY.' }), - full: Flags.boolean({ default: false, description: 'Disable text-output truncation; print full IDs and bodies' }), - json: Flags.boolean({ default: false, description: 'Alias for --format json' }), - 'no-input': Flags.boolean({ default: false, description: 'Never prompt; fail instead (useful for agents and CI)' }), - quiet: Flags.boolean({ char: 'q', default: false, description: 'Suppress spinners and success lines (errors still print). Honored with or without --json.' }), - 'read-only': Flags.boolean({ default: false, description: 'Reject commands that would modify Beeper or local CLI state (or set BEEPER_READONLY=1)' }), - 'results-only': Flags.boolean({ default: false, description: 'In JSON mode, emit only the primary result instead of the envelope' }), - select: Flags.string({ description: 'In JSON/JSONL mode, project comma-separated fields; dot paths supported' }), - timeout: Flags.string({ description: 'Maximum time to wait, such as 30s, 2m, or 1h' }), - yes: Flags.boolean({ char: 'y', default: false, description: 'Alias for --force' }), - } - - public override async init(): Promise { - await super.init() - if (this.argv.includes('--quiet') || this.argv.includes('-q')) { - process.env.BEEPER_QUIET = '1' - } - - const format = outputFormatFromArgv(this.argv) - if (format) { - process.env.BEEPER_OUTPUT_FORMAT = format - } else if (this.argv.includes('--json')) { - process.env.BEEPER_OUTPUT_FORMAT = 'json' - } else if (process.env.BEEPER_AGENT === '1' || !process.stdout.isTTY) { - process.env.BEEPER_OUTPUT_FORMAT = 'json' - } - - const select = stringFlagFromArgv(this.argv, '--select') - if (select) process.env.BEEPER_OUTPUT_SELECT = select - if (this.argv.includes('--results-only')) process.env.BEEPER_OUTPUT_RESULTS_ONLY = '1' - if (this.argv.includes('--no-input') || process.env.BEEPER_AGENT === '1') process.env.BEEPER_NO_INPUT = '1' - if (this.argv.includes('--force') || this.argv.includes('-f') || this.argv.includes('--yes') || this.argv.includes('-y')) process.env.BEEPER_FORCE = '1' - } - - protected override async catch(error: Error & { exitCode?: number }): Promise { - const message = error.message || String(error) - const inferredCode = error instanceof CLIError ? error.exitCode : inferExitCode(message) - const code = inferredCode ?? error.exitCode ?? ExitCodes.Generic - process.exitCode = process.exitCode ?? code - const tryMessage = error instanceof CLIError ? error.tryMessage : undefined - const isBug = error instanceof BugError || (!(error instanceof CLIError) && inferredCode === undefined) - - if (this.argv.includes('--events')) { - writeEvent('error', { message, exitCode: code, kind: isBug ? 'bug' : 'abort', tryMessage }) - return - } - - if (isMachineOutput(this.argv)) { - const errorCodeValue = error instanceof CLIError && error.code ? error.code : errorCode(code, isBug) - const data = error instanceof CLIError ? error.data : undefined - process.stderr.write(`${JSON.stringify({ ok: false, data: data ?? null, error: { code: errorCodeValue, message, exitCode: code, kind: isBug ? 'bug' : 'abort', hint: tryMessage } })}\n`) - return - } - - if (isBug) { - process.stderr.write(formatBugPanel(error, this.config.version)) - return - } - - if (tryMessage) process.stderr.write(`${message}\n hint: ${tryMessage}\n`) - else return super.catch(error) - } -} - -function inferExitCode(message: string): number | undefined { - if (/\b401\b|unauthorized|invalid token|auth(?:entication)? required/i.test(message)) return ExitCodes.AuthRequired - if (/\b404\b|not\s+found|unknown .*target|no .*matches/i.test(message)) return ExitCodes.NotFound - if (/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|fetch failed|not reachable|not ready/i.test(message)) return ExitCodes.NotReady - if (/\busage\b|\binvalid (?:argument|option|flag|value|input)\b|\bmust provide\b|\brequired (?:flag|argument|option|value)\b|\bunknown flag\b|\bparse error\b/i.test(message)) return ExitCodes.Usage - return undefined -} - -function formatBugPanel(error: Error, version: string): string { - const bar = '─'.repeat(60) - const stack = error.stack?.split('\n').slice(0, 8).join('\n') ?? error.message - return [ - '', - `┌─ unexpected error ${bar.slice(20)}`, - `│ ${error.message}`, - '│', - ...stack.split('\n').map(line => `│ ${line}`), - '│', - `│ beeper-cli ${version} — please report at`, - '│ https://github.com/beeper/desktop-api-cli/issues', - `└${'─'.repeat(60)}`, - '', - ].join('\n') -} - -export function ensureWritable(flags: { 'read-only'?: boolean }): void { - const env = process.env.BEEPER_READONLY - const readOnly = flags['read-only'] || ['1', 'on', 'true', 'yes'].includes(String(env ?? '').toLowerCase()) - if (readOnly) throw new CLIError('read-only mode: command would modify Beeper or local CLI state', ExitCodes.Usage) -} - -export function writeEvent(event: string, data: Record = {}): void { - process.stderr.write(`${JSON.stringify({ event, data, ts: Date.now() })}\n`) -} - -export function isQuiet(): boolean { - return process.env.BEEPER_QUIET === '1' -} - -export function isNoInput(): boolean { - return process.env.BEEPER_NO_INPUT === '1' -} - -export function isForce(flags?: { force?: boolean; yes?: boolean }): boolean { - return Boolean(flags?.force || flags?.yes || process.env.BEEPER_FORCE === '1') -} - -function outputFormatFromArgv(argv: string[]): string | undefined { - return stringFlagFromArgv(argv, '--format') -} - -function stringFlagFromArgv(argv: string[], name: string): string | undefined { - for (let i = 0; i < argv.length; i++) { - const arg = argv[i] - if (arg === name) return argv[i + 1] - if (arg?.startsWith(`${name}=`)) return arg.slice(name.length + 1) - } - - return undefined -} - -function isMachineOutput(argv: string[]): boolean { - const format = outputFormatFromArgv(argv) ?? process.env.BEEPER_OUTPUT_FORMAT - return argv.includes('--json') || format === 'json' || format === 'jsonl' -} - -function errorCode(code: number, isBug: boolean): string { - if (isBug) return 'internal_error' - switch (code) { - case ExitCodes.Ambiguous: { - return 'ambiguous_selector' - } - - case ExitCodes.AuthRequired: { - return 'auth_required' - } - - case ExitCodes.CommandNotFound: { - return 'command_not_found' - } - - case ExitCodes.NotFound: { - return 'not_found' - } - - case ExitCodes.NotReady: { - return 'not_ready' - } - - case ExitCodes.Usage: { - return 'usage_error' - } - - default: { - return 'runtime_error' - } - } -} diff --git a/packages/cli/src/lib/copy.ts b/packages/cli/src/lib/copy.ts deleted file mode 100644 index 3e9366c2..00000000 --- a/packages/cli/src/lib/copy.ts +++ /dev/null @@ -1,66 +0,0 @@ -export const apiCopy = { - accounts: { - list: 'List chat accounts connected to this Beeper Client API server, including bridge, network, user identity, and connection status.', - }, - assets: { - download: 'Download a file from an mxc:// or localmxc:// URL to the device running the Beeper Client API and return the local file URL.', - upload: 'Upload a file to a temporary location using multipart/form-data. Returns an uploadID that can be referenced when sending a message or creating a draft attachment.', - }, - chats: { - archive: 'Archive or unarchive a chat. Set archived=true to move it to Archive, or archived=false to move it back to the inbox.', - create: 'Create a direct or group chat from participant IDs. Returns the created chat.', - list: 'List all chats sorted by last activity (most recent first). Combines all accounts into a single paginated list.', - markRead: 'Mark a chat as read, optionally through a specific message ID.', - markUnread: 'Mark a chat as unread, optionally from a specific message ID.', - notifyAnyway: 'Send a notification despite the recipient focus state when the network supports it. Currently intended for iMessage on macOS; unsupported networks return an error.', - retrieve: 'Retrieve chat details, including metadata, participants, and the latest message.', - search: 'Search chats by title, network, or participant names.', - start: 'Resolve a user/contact and open a direct chat. Reuses and returns an existing direct chat when one is found. Available in Beeper v4.2.808+.', - }, - contacts: { - list: 'List merged contacts for a specific account with cursor-based pagination.', - search: 'Search contacts on a specific account using merged account contacts, network search, and exact identifier lookup.', - }, - messages: { - delete: 'Delete a message by final message ID. Pending message IDs are not accepted because messages cannot be deleted while sending.', - list: 'List all messages in a chat with cursor-based pagination. Sorted by timestamp.', - retrieve: 'Retrieve a message by final message ID, pendingMessageID, or Matrix event ID. chatID may be a Beeper chat ID or a local chat ID.', - search: 'Search messages across chats.', - send: 'Send a text message to a specific chat. Supports replying to existing messages. Returns a pending message ID.', - update: 'Edit the text content of an existing message. Messages with attachments cannot be edited.', - }, - reactions: { - add: 'Add a reaction to an existing message.', - delete: 'Remove the reaction added by the authenticated user from an existing message.', - }, - reminders: { - create: 'Set a reminder for a chat at a specific time.', - delete: 'Clear an existing reminder from a chat.', - }, -} as const - -export const sdkParamCopy = { - attachmentFile: 'The file to upload (max 500 MB).', - chatID: 'Chat selector. Prefer the numeric local chat ID shown by chats list, or use the full Beeper/Matrix chat ID.', - fileName: 'Original filename. Defaults to the uploaded file name if omitted', - forEveryone: 'True to request deletion for everyone when the network supports it; false to delete only for the authenticated user when supported.', - messageID: 'Message ID.', - mimeType: 'MIME type. Auto-detected from magic bytes if omitted', - reactionKey: 'Reaction key to add (emoji, shortcode, or custom emoji key)', - remindAt: 'Timestamp when the reminder should trigger.', - replyToMessageID: 'Provide a message ID to send this as a reply to an existing message', - searchQuery: 'User-typed search text. Literal word matching (non-semantic).', - text: 'Draft text. Plain text and Markdown are converted to Matrix HTML with the same rules used by send and edit.', -} as const - -export const cliCopy = { - args: { - accountSelector: 'Account ID, network, bridge, or account user', - chatSelector: `${sdkParamCopy.chatID} Also accepts exact chat titles or search text for interactive use.`, - }, - flags: { - baseURL: 'Beeper Desktop API base URL', - json: 'Print JSON', - pick: 'Pick the Nth chat when the input is ambiguous', - }, -} as const diff --git a/packages/cli/src/lib/desktop-auth.ts b/packages/cli/src/lib/desktop-auth.ts index f8209679..aa8a1626 100644 --- a/packages/cli/src/lib/desktop-auth.ts +++ b/packages/cli/src/lib/desktop-auth.ts @@ -1,8 +1,8 @@ -import { readConfig } from './targets.js' -import { authRequired, notReady } from './errors.js' -import { loginWithPKCE } from './oauth.js' +import { AbortError, ExitCodes } from './errors.js' +import { loginWithPKCE, type TokenResponse } from './oauth.js' +import { defaultDesktopBaseURL, defaultDesktopPort, type AuthSource, type StoredAuth } from './targets.js' -export type DesktopAppStatus = { +type DesktopAppStatus = { state?: string } @@ -11,12 +11,10 @@ type DesktopProbe = { status?: DesktopAppStatus } -const defaultPort = 23_373 -const scanPorts = Array.from({ length: 20 }, (_, index) => defaultPort + index) +const scanPorts = Array.from({ length: 20 }, (_, index) => defaultDesktopPort + index) export async function findLocalDesktop(options: { baseURL?: string; scan?: boolean; timeoutMs?: number } = {}): Promise { - const config = await readConfig() - const preferred = options.baseURL ?? config.baseURL ?? 'http://127.0.0.1:23373' + const preferred = options.baseURL ?? defaultDesktopBaseURL const candidates = candidateBaseURLs(preferred, options.scan ?? true) const timeoutMs = options.timeoutMs ?? 500 @@ -34,34 +32,43 @@ export async function findLocalDesktop(options: { baseURL?: string; scan?: boole } catch { /* fall through */ } } - throw notReady(`Could not find a running Beeper Desktop API on ${candidates.join(', ')}.`) + throw new AbortError(`Could not find a running Beeper Desktop API on ${candidates.join(', ')}.`, ExitCodes.NotReady) } -export async function ensureDesktopToken(options: { +type AuthorizedTargetToken = TokenResponse & { clientID: string } + +export async function authorizeTarget(options: { baseURL?: string clientName?: string openBrowser?: boolean - save?: boolean scan?: boolean scope?: string -} = {}): Promise { +} = {}): Promise { const desktop = await findLocalDesktop({ baseURL: options.baseURL, scan: options.scan }) if (desktop.status?.state === 'needs-login') { - throw authRequired('Beeper Desktop is not signed in. Open Beeper Desktop and sign in, then rerun this command.') + throw new AbortError('Beeper Desktop is not signed in. Open Beeper Desktop and sign in, then rerun this command.', ExitCodes.AuthRequired) } - const token = await loginWithPKCE({ + return loginWithPKCE({ baseURL: desktop.baseURL, clientName: options.clientName ?? 'Beeper CLI', openBrowser: options.openBrowser ?? true, - save: options.save ?? true, scope: options.scope ?? 'read write', - source: 'desktop-oauth', }) - return token.access_token } -export async function getDesktopAppStatus(baseURL: string): Promise { +export function authFromToken(token: AuthorizedTargetToken, source: AuthSource): StoredAuth { + return { + accessToken: token.access_token, + clientID: token.clientID, + expiresAt: token.expires_in ? new Date(Date.now() + token.expires_in * 1000).toISOString() : undefined, + scope: token.scope, + source, + tokenType: token.token_type, + } +} + +async function getDesktopAppStatus(baseURL: string): Promise { const response = await fetchWithTimeout(new URL('/v1/app/setup', baseURL), {}, 2_000) if (response.status === 401 || response.status === 403 || response.status === 404) return undefined if (!response.ok) throw new Error(`GET /v1/app/setup failed: ${response.status} ${await response.text()}`) diff --git a/packages/cli/src/lib/did-you-mean.ts b/packages/cli/src/lib/did-you-mean.ts deleted file mode 100644 index b89b9c2d..00000000 --- a/packages/cli/src/lib/did-you-mean.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { createInterface, Interface } from 'node:readline' -import { CLIError, ExitCodes } from './errors.js' - -export type Suggestion = { value: T; label: string; distance: number } - -export function levenshtein(a: string, b: string): number { - if (a === b) return 0 - if (!a.length) return b.length - if (!b.length) return a.length - const matrix: number[][] = Array.from({ length: a.length + 1 }, (_, i) => [i, ...new Array(b.length).fill(0)]) - for (let j = 1; j <= b.length; j++) matrix[0]![j] = j - for (let i = 1; i <= a.length; i++) { - for (let j = 1; j <= b.length; j++) { - const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1 - matrix[i]![j] = Math.min( - matrix[i - 1]![j]! + 1, - matrix[i]![j - 1]! + 1, - matrix[i - 1]![j - 1]! + cost, - ) - } - } - return matrix[a.length]![b.length]! -} - -export function rankSuggestions(query: string, items: T[], labelOf: (item: T) => string | undefined, max = 3): Suggestion[] { - const q = query.trim().toLowerCase() - const scored: Suggestion[] = [] - for (const item of items) { - const label = labelOf(item) - if (!label) continue - const l = label.toLowerCase() - const dist = Math.min( - levenshtein(q, l), - l.includes(q) ? Math.max(0, l.length - q.length) : Infinity, - ) - if (Number.isFinite(dist)) scored.push({ value: item, label, distance: dist }) - } - scored.sort((a, b) => a.distance - b.distance || a.label.length - b.label.length) - const cutoff = Math.max(3, Math.ceil(q.length * 0.6)) - return scored.filter(s => s.distance <= cutoff).slice(0, max) -} - -export async function confirmSuggestion(prompt: string, options: { timeoutMs?: number; assumeYes?: boolean } = {}): Promise { - if (options.assumeYes) return true - if (!process.stdin.isTTY || !process.stderr.isTTY) return false - const rl: Interface = createInterface({ input: process.stdin, output: process.stderr }) - return new Promise(resolve => { - let resolved = false - const finish = (value: boolean): void => { - if (resolved) return - resolved = true - rl.close() - resolve(value) - } - const timer = options.timeoutMs ? setTimeout(() => finish(true), options.timeoutMs) : undefined - rl.question(`${prompt} [Y/n] `, answer => { - if (timer) clearTimeout(timer) - const a = answer.trim().toLowerCase() - finish(a === '' || a === 'y' || a === 'yes') - }) - }) -} - -export function declineWithExit127(message: string): never { - throw new CLIError(message, ExitCodes.CommandNotFound) -} diff --git a/packages/cli/src/lib/env.ts b/packages/cli/src/lib/env.ts deleted file mode 100644 index 06d1f65b..00000000 --- a/packages/cli/src/lib/env.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { delimiter } from 'node:path' -import { mkdir, readFile, writeFile } from 'node:fs/promises' -import { dirname, join } from 'node:path' -import { homedir } from 'node:os' -import { binDir } from './installations.js' - -export type ShellName = 'sh' | 'fish' | 'powershell' - -export function isBeeperBinOnPath(pathValue = process.env.PATH ?? ''): boolean { - return pathValue.split(delimiter).includes(binDir()) -} - -export function pathSetup(shell: ShellName): string { - const dir = binDir() - if (shell === 'fish') return `fish_add_path ${fishQuote(dir)}` - if (shell === 'powershell') return `$env:Path = ${powershellQuote(`${dir};`)} + $env:Path` - return `export PATH=${shQuote(dir)}:$PATH` -} - -export async function installPathSetup(shell: ShellName = detectShell()): Promise<{ path: string; line: string; changed: boolean }> { - if (shell === 'powershell') throw new Error('PowerShell PATH persistence is not supported yet. Run: beeper env --shell powershell') - const path = shellConfigPath(shell) - const line = pathSetup(shell) - const current = await readFile(path, 'utf8').catch(error => { - if ((error as NodeJS.ErrnoException).code === 'ENOENT') return '' - throw error - }) - if (current.includes(binDir())) return { path, line, changed: false } - await mkdir(dirname(path), { recursive: true }) - await writeFile(path, `${current}${current.endsWith('\n') || current.length === 0 ? '' : '\n'}${line}\n`, 'utf8') - return { path, line, changed: true } -} - -export function pathSetupHint(): string | undefined { - if (isBeeperBinOnPath()) return undefined - return `Add ${binDir()} to PATH: eval "$(beeper env)"` -} - -function shQuote(value: string): string { - return `'${value.replaceAll("'", "'\\''")}'` -} - -function fishQuote(value: string): string { - return shQuote(value) -} - -function powershellQuote(value: string): string { - return `'${value.replaceAll("'", "''")}'` -} - -function detectShell(): ShellName { - return (process.env.SHELL ?? '').includes('fish') ? 'fish' : 'sh' -} - -function shellConfigPath(shell: ShellName): string { - if (shell === 'fish') return join(homedir(), '.config', 'fish', 'config.fish') - return join(homedir(), (process.env.SHELL ?? '').includes('zsh') ? '.zshrc' : '.bashrc') -} diff --git a/packages/cli/src/lib/errors.ts b/packages/cli/src/lib/errors.ts index 894c93a4..a337279d 100644 --- a/packages/cli/src/lib/errors.ts +++ b/packages/cli/src/lib/errors.ts @@ -1,16 +1,14 @@ /** * Beeper CLI exit codes: - * 0 success * 1 generic runtime error * 2 usage error (parsing, missing required flag/arg, invalid combination) * 3 auth required (no stored token; user must authenticate) * 4 target/account not ready (target reachable but not signed-in or not verified) * 5 not found (selector matched nothing) * 6 ambiguous selector (multiple matches; use exact ID or --pick) - * 127 user declined a did-you-mean suggestion (POSIX "command not found" semantics) + * 127 user declined a selector suggestion (POSIX "command not found" semantics) */ export const ExitCodes = { - Success: 0, Generic: 1, Usage: 2, AuthRequired: 3, @@ -20,19 +18,17 @@ export const ExitCodes = { CommandNotFound: 127, } as const -export type ExitCode = typeof ExitCodes[keyof typeof ExitCodes] +type ExitCode = typeof ExitCodes[keyof typeof ExitCodes] export class CLIError extends Error { readonly exitCode: ExitCode readonly tryMessage?: string readonly code?: string - readonly data?: Record - constructor(message: string, exitCode: ExitCode, tryMessage?: string, options: { code?: string; data?: Record } = {}) { + constructor(message: string, exitCode: ExitCode, tryMessage?: string, code?: string) { super(message) this.exitCode = exitCode this.tryMessage = tryMessage - this.code = options.code - this.data = options.data + this.code = code this.name = 'CLIError' } } @@ -42,25 +38,8 @@ export class CLIError extends Error { * Renders as a single-line red message. Do not include a stack trace. */ export class AbortError extends CLIError { - constructor(message: string, exitCode: ExitCode = ExitCodes.Generic, tryMessage?: string, options: { code?: string; data?: Record } = {}) { - super(message, exitCode, tryMessage, options) + constructor(message: string, exitCode: ExitCode = ExitCodes.Generic, tryMessage?: string, code?: string) { + super(message, exitCode, tryMessage, code) this.name = 'AbortError' } } - -/** - * Unexpected internal failure that should be reported. Renders as a boxed panel with - * the stack and a "report this" hint. Always exits with ExitCodes.Generic. - */ -export class BugError extends CLIError { - constructor(message: string, tryMessage?: string) { - super(message, ExitCodes.Generic, tryMessage, { code: 'internal_error' }) - this.name = 'BugError' - } -} - -export const usageError = (message: string) => new AbortError(message, ExitCodes.Usage) -export const authRequired = (message: string) => new AbortError(message, ExitCodes.AuthRequired) -export const notReady = (message: string) => new AbortError(message, ExitCodes.NotReady) -export const notFound = (message: string, data?: Record) => new AbortError(message, ExitCodes.NotFound, undefined, { code: 'not_found', data }) -export const ambiguous = (message: string, data?: Record) => new AbortError(message, ExitCodes.Ambiguous, 'Pass an exact ID or --pick N.', { code: 'ambiguous_selector', data }) diff --git a/packages/cli/src/lib/export/index.ts b/packages/cli/src/lib/export.ts similarity index 95% rename from packages/cli/src/lib/export/index.ts rename to packages/cli/src/lib/export.ts index dca90d82..8dfbea22 100644 --- a/packages/cli/src/lib/export/index.ts +++ b/packages/cli/src/lib/export.ts @@ -5,18 +5,18 @@ import { basename, dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import type { Chat } from '@beeper/desktop-api/resources/chats/chats' import type { Attachment, Message } from '@beeper/desktop-api/resources/shared' +import { apiItems } from './api-values.js' -export type ExportOptions = { +type ExportOptions = { accountIDs?: string[] chatIDs?: string[] downloadAttachments: boolean - events?: boolean force: boolean limitChats?: number limitMessages?: number maxParticipants?: number + onProgress?: (message: string) => void outDir: string - quiet: boolean } type ExportState = { @@ -64,7 +64,7 @@ export async function exportBeeperData(client: any, options: ExportOptions): Pro const startedAt = state.createdAt progress(options, `Export directory: ${options.outDir}`) - const accounts = accountItems(await client.accounts.list()) + const accounts = apiItems(await client.accounts.list()) await writeJSONAtomic(join(options.outDir, 'accounts.json'), accounts) progress(options, `Accounts: ${accounts.length}`) @@ -238,7 +238,7 @@ async function exportChatMessages( } existing.sort((a, b) => String(a.sortKey || a.timestamp).localeCompare(String(b.sortKey || b.timestamp))) - const allAttachments = await readAttachmentsManifest(chatDir) + const allAttachments = await readJSONL(join(chatDir, 'attachments', 'attachments.jsonl')) return { attachmentCount, attachments: allAttachments, messages: existing } } @@ -331,12 +331,7 @@ async function writeResponseBody(response: Response, path: string): Promise() - for (const attachment of attachments) { - const list = byMessage.get(attachment.messageID) ?? [] - list.push(attachment) - byMessage.set(attachment.messageID, list) - } + const byMessage = attachmentsByMessage(attachments) const lines = [ `# ${escapeMarkdown(chat.title || chat.id)}`, @@ -366,12 +361,7 @@ function renderMarkdown(chat: Chat, messages: Message[], attachments: Attachment } function renderHTML(chat: Chat, messages: Message[], attachments: AttachmentExport[]): string { - const byMessage = new Map() - for (const attachment of attachments) { - const list = byMessage.get(attachment.messageID) ?? [] - list.push(attachment) - byMessage.set(attachment.messageID, list) - } + const byMessage = attachmentsByMessage(attachments) const rows = messages.map(message => { const sender = message.senderName || message.senderID || 'Unknown sender' @@ -427,8 +417,14 @@ ${indent(rows, 6)} ` } -async function readAttachmentsManifest(chatDir: string): Promise { - return readJSONL(join(chatDir, 'attachments', 'attachments.jsonl')) +function attachmentsByMessage(attachments: AttachmentExport[]): Map { + const byMessage = new Map() + for (const attachment of attachments) { + const list = byMessage.get(attachment.messageID) ?? [] + list.push(attachment) + byMessage.set(attachment.messageID, list) + } + return byMessage } async function readJSONL(path: string): Promise { @@ -480,11 +476,6 @@ async function exists(path: string): Promise { } } -function accountItems(accounts: unknown): unknown[] { - if (Array.isArray(accounts)) return accounts - return (accounts as { items?: unknown[] }).items ?? [] -} - function safeSegment(value: string): string { const normalized = value.replace(/[^a-zA-Z0-9._-]+/g, '_').replace(/^_+|_+$/g, '') return normalized.slice(0, 120) || 'item' @@ -550,6 +541,5 @@ function indent(value: string, spaces: number): string { } function progress(options: ExportOptions, message: string): void { - if (options.events) process.stderr.write(`${JSON.stringify({ event: 'export.progress', data: { message }, ts: new Date().toISOString() })}\n`) - if (!options.quiet) process.stderr.write(`${message}\n`) + options.onProgress?.(message) } diff --git a/packages/cli/src/lib/ink/components.tsx b/packages/cli/src/lib/ink/components.tsx deleted file mode 100644 index de288d11..00000000 --- a/packages/cli/src/lib/ink/components.tsx +++ /dev/null @@ -1,948 +0,0 @@ -import React from 'react' -import { Box, Text } from 'ink' -import { bridgeColor, glyphs, senderColor, theme } from './theme.js' - -// OSC 8 hyperlink — modern terminals (iTerm, Ghostty, WezTerm, VS Code, etc.) -// render this as clickable; everything else ignores the escapes and shows the -// label text once. -const supportsHyperlinks = process.stdout.isTTY && process.env.TERM !== 'dumb' -const OSC8_START = ']8;;' -const OSC8_END = ']8;;' -const BEL = '' -const Hyperlink: React.FC<{ url: string; children?: React.ReactNode }> = ({ url, children }) => { - if (!supportsHyperlinks) return <>{children ?? url} - return {OSC8_START}{url}{BEL}{children ?? url}{OSC8_END} -} - -import { - attachmentLabel, - chatPreview, - compact, - formatBytes, - formatDuration, - formatTime, - isArchived, - isLowPriority, - isMuted, - isPinned, - messageText, - participantsSummary, - type RecordValue, - shortID, - stringValue, -} from './format.js' - -// ─── primitives ──────────────────────────────────────────────────────────────── - -export const Rail: React.FC<{ color: string }> = ({ color }) => ( - {glyphs.rail} -) - -export const Hairline: React.FC<{ width?: number }> = ({ width = 60 }) => ( - {glyphs.hairline.repeat(width)} -) - -export const Meta: React.FC<{ children: React.ReactNode }> = ({ children }) => ( - {children} -) - -export const Dim: React.FC<{ children: React.ReactNode }> = ({ children }) => ( - {children} -) - -export const KV: React.FC<{ label: string; value: React.ReactNode; tone?: 'normal' | 'muted' | 'dim'; width?: number }> = - ({ label, value, tone = 'normal', width = 12 }) => { - const valueColor = tone === 'dim' ? theme.subtle : tone === 'muted' ? theme.muted : theme.text - return ( - - {label.padEnd(width)} - {value} - - ) - } - -export const Pill: React.FC<{ color: string; children: React.ReactNode; muted?: boolean }> = ({ color, children, muted }) => ( - - {children} - -) - -export type Suggestion = { command: string; hint?: string } - -export const Suggestions: React.FC<{ suggestions?: Suggestion[]; label?: string }> = ({ suggestions, label = 'Try' }) => { - if (!suggestions?.length) return null - return ( - - {label} - {suggestions.map(s => ( - - {glyphs.arrow} - {s.command} - {s.hint && — {s.hint}} - - ))} - - ) -} - -// ─── empty / success / failure ──────────────────────────────────────────────── - -export const EmptyState: React.FC<{ title: string; subtitle?: string; suggestions?: Suggestion[] }> = ({ title, subtitle, suggestions }) => ( - - - - - {title} - - {subtitle && ( - - {subtitle} - - )} - - -) - -export const SuccessLine: React.FC<{ message: string; detail?: string; entity?: React.ReactNode }> = ({ message, detail, entity }) => ( - - - {glyphs.check} - - {message} - {detail && {detail}} - - {entity && ( - - {entity} - - )} - -) - -export const FailureLine: React.FC<{ message: string; detail?: string }> = ({ message, detail }) => ( - - {glyphs.cross} - - {message} - {detail && {detail}} - -) - -export const SectionHeader: React.FC<{ label: string; count?: number }> = ({ label, count }) => ( - - {label.toUpperCase()} - {count != null && {count}} - -) - -// ─── domain rows ─────────────────────────────────────────────────────────────── - -type RowProps = { item: T } - -function chatRailColor(chat: RecordValue, unread: number, mentions: number): string { - if (mentions > 0) return theme.mention - if (unread > 0) return theme.primary - if (isPinned(chat)) return theme.warn - const tint = bridgeColor(stringValue(chat.network)) - return tint ?? theme.subtle -} - -export const ChatRow: React.FC> = ({ item: chat }) => { - const unread = Number(chat.unreadCount ?? 0) - const mentions = Number(chat.unreadMentionsCount ?? 0) - const muted = isMuted(chat) - const pinned = isPinned(chat) - const archived = isArchived(chat) - const lowPriority = isLowPriority(chat) - const preview = chatPreview(chat) - const lastActivity = formatTime(chat.lastActivity) - const network = stringValue(chat.network) - const tint = bridgeColor(network) - const railColor = chatRailColor(chat, unread, mentions) - - const titleNode = ( - 0} color={muted ? theme.muted : theme.text}>{String(chat.title)} - ) - - return ( - - - - - {titleNode} - {network && {network.toLowerCase()}} - {pinned && {glyphs.pin}} - {muted && {glyphs.mute}} - {archived && {glyphs.archive}} - {lowPriority && {glyphs.lowPriority}} - - {mentions > 0 && ( - {glyphs.mention}{mentions} - )} - {unread > 0 && ( - - {' '}{unread}{mentions > 0 ? '' : ` unread`} - - )} - {lastActivity && ( - {lastActivity} - )} - - {preview && ( - - {preview.kind === 'draft' ? ( - - draft - {preview.text ? ` ${preview.text}` : ''} - - ) : ( - - {preview.sender ? {preview.sender} : null} - {preview.text} - - )} - - )} - - {chat.localChatID || chat.rowID ? ( - <> - local - {String(chat.localChatID ?? chat.rowID)} - id - - ) : null} - {String(chat.id)} - - - ) -} - -export const ChatDetail: React.FC> = ({ item: chat }) => { - const unread = Number(chat.unreadCount ?? 0) - const mentions = Number(chat.unreadMentionsCount ?? 0) - const participants = chat.participants && typeof chat.participants === 'object' ? chat.participants as RecordValue : undefined - const items = Array.isArray(participants?.items) ? participants!.items as RecordValue[] : [] - const network = stringValue(chat.network) - const tint = bridgeColor(network) - - return ( - - - - - {String(chat.title)} - {network && {network.toLowerCase()}} - {isPinned(chat) && {glyphs.pin} pinned} - {isMuted(chat) && {glyphs.mute} muted} - {isArchived(chat) && {glyphs.archive} archived} - {isLowPriority(chat) && {glyphs.lowPriority} low-priority} - - {chat.type ? : null} - {chat.lastActivity ? : null} - {unread > 0 && 0 ? ` (${mentions} @)` : ''}`} />} - {participants && ( - - )} - {items.length > 0 && ( - - PARTICIPANTS - {items.slice(0, 20).map((p, i) => ( - - {glyphs.bullet} - - {stringValue(p.fullName) ?? stringValue(p.username) ?? shortID(String(p.id))} - {p.isSelf ? you : null} - - ))} - {items.length > 20 && … {items.length - 20} more} - - )} - - id - {String(chat.id)} - - {chat.localChatID || chat.rowID ? ( - - local - {String(chat.localChatID ?? chat.rowID)} - - ) : null} - {chat.accountID ? ( - - acct - {String(chat.accountID)} - - ) : null} - - ) -} - -export const MessageRow: React.FC> = ({ item: message }) => { - const mine = Boolean(message.isSender) - const senderID = stringValue(message.senderID) - const sender = stringValue(message.senderName) ?? (senderID ? shortID(senderID) : 'unknown') - const text = messageText(message) - const timestamp = formatTime(message.timestamp) - const status = typeof message.sendStatus === 'object' && message.sendStatus ? message.sendStatus as RecordValue : undefined - const showFailure = status?.status && status.status !== 'SUCCESS' - const attachments = Array.isArray(message.attachments) ? message.attachments : [] - const reactions = Array.isArray(message.reactions) ? message.reactions : [] - const railColor = mine ? theme.mine : senderColor(senderID) - - return ( - - - - - {mine ? 'you' : sender} - {message.type != null && message.type !== 'TEXT' ? ( - {String(message.type).toLowerCase()} - ) : null} - {message.isUnread ? ( - {glyphs.unread} unread - ) : null} - {message.isDeleted ? ( - deleted - ) : null} - {message.editedTimestamp ? ( - {glyphs.edited} edited - ) : null} - {attachments.length > 0 && ( - {glyphs.attachment} {attachmentLabel(attachments)} - )} - {reactions.length > 0 && ( - {glyphs.reaction}{reactions.length} - )} - - {timestamp && {timestamp}} - - {text && ( - - {text} - - )} - {showFailure ? ( - - {glyphs.cross} {String(status?.status)} - {status?.message ? {String(status.message)} : null} - - ) : null} - - {String(message.id)} - {message.chatID ? in {String(message.chatID)} : null} - - - ) -} - -export const UserRow: React.FC> = ({ item: user }) => { - const title = stringValue(user.fullName) - ?? stringValue(user.username) - ?? stringValue(user.email) - ?? stringValue(user.phoneNumber) - ?? String(user.id) - const handles = compact([ - user.username ? `@${user.username}` : undefined, - user.email ? String(user.email) : undefined, - user.phoneNumber ? String(user.phoneNumber) : undefined, - ]) - const rail = user.isSelf ? theme.mine : senderColor(stringValue(user.id)) - - return ( - - - - - {title} - {user.isSelf ? you : null} - {user.cannotMessage ? cannot message : null} - - {handles.length > 0 ? ( - - {handles.join(' ')} - - ) : null} - - {String(user.id)} - {user.accountID ? on {String(user.accountID)} : null} - - - ) -} - -export const AccountRow: React.FC> = ({ item: account }) => { - const id = account.accountID ?? account.id - const bridge = account.bridge && typeof account.bridge === 'object' ? account.bridge as RecordValue : undefined - const bridgeLabel = bridge - ? compact([stringValue(bridge.id), stringValue(bridge.provider), stringValue(bridge.type)]).join(' ') - : stringValue(account.bridge) - const title = stringValue(account.displayName) - ?? stringValue(account.name) - ?? stringValue(account.network) - ?? stringValue(bridge?.id) - ?? String(id) - const network = stringValue(account.network) ?? stringValue(bridge?.type) - const tint = bridgeColor(network) - const state = stringValue(account.state) - const stateLow = state?.toLowerCase() ?? '' - const connected = stateLow.includes('online') || stateLow.includes('connect') - const errored = stateLow.includes('error') || stateLow.includes('fail') - const stateTone = connected ? theme.mine : errored ? theme.danger : theme.warnAlt - const handles = compact([ - account.username ? `@${account.username}` : undefined, - stringValue(account.userID), - ]) - - return ( - - - - - {title} - {network && {network.toLowerCase()}} - {state && {connected ? glyphs.dot : errored ? glyphs.cross : glyphs.ring} {stateLow}} - - {handles.length > 0 && ( - - {handles.join(' ')} - - )} - - {String(id)} - {bridgeLabel ? bridge {bridgeLabel} : null} - - - ) -} - -export const BridgeRow: React.FC> = ({ item: bridge }) => { - const id = String(bridge.id) - const provider = stringValue(bridge.provider) - const type = stringValue(bridge.type) - const network = stringValue(bridge.network) ?? type - const title = stringValue(bridge.displayName) ?? id - const status = stringValue(bridge.status) - const statusText = stringValue(bridge.statusText) - const accounts = Array.isArray(bridge.accounts) ? bridge.accounts as RecordValue[] : [] - const available = status === 'available' - const connected = status === 'connected' || accounts.length > 0 - const rail = available ? theme.mine : connected ? theme.primary : theme.warn - const flows = Array.isArray(bridge.loginFlows) ? bridge.loginFlows as RecordValue[] : [] - - return ( - - - - - {title} - {network && {network.toLowerCase()}} - {provider && {provider}} - {status && {status.replaceAll('_', ' ')}} - - {statusText ? ( - - {statusText} - - ) : null} - - id - {id} - {typeof bridge.activeAccountCount === 'number' ? accounts {String(bridge.activeAccountCount)} : null} - {typeof bridge.supportsMultipleAccounts === 'boolean' ? ( - {bridge.supportsMultipleAccounts ? 'multiple allowed' : 'single account'} - ) : null} - {flows.length > 0 ? flows {flows.length} : null} - - {flows.length > 0 ? ( - - {flows.slice(0, 5).map(flow => { - const flowID = String(flow.id ?? flow.type ?? 'flow') - const name = stringValue(flow.name) - const description = stringValue(flow.description) - return ( - - {glyphs.bullet} {flowID}{name ? ` ${name}` : ''}{description ? ` - ${description}` : ''} - - ) - })} - {flows.length > 5 ? {flows.length - 5} more flows : null} - - ) : null} - {accounts.length > 0 ? ( - - {accounts.slice(0, 3).map(account => ( - - {glyphs.bullet} {String(account.accountID ?? account.id)} - {account.status ? ` ${String(account.status).replaceAll('_', ' ')}` : ''} - - ))} - {accounts.length > 3 ? {accounts.length - 3} more : null} - - ) : null} - {available ? ( - - beeper accounts add {id} - - ) : null} - - ) -} - -export const SendResultCard: React.FC<{ result: RecordValue }> = ({ result }) => { - const message = result.message && typeof result.message === 'object' ? result.message as RecordValue : undefined - const sendStatus = message?.sendStatus && typeof message.sendStatus === 'object' ? message.sendStatus as RecordValue : undefined - const state = stringValue(result.state) - const finalID = stringValue(message?.id) - const status = stringValue(sendStatus?.status) - const failed = status?.startsWith('FAIL') - const sent = status === 'SUCCESS' - return ( - - - - - Message send - - {failed ? ( - {glyphs.cross} failed - ) : sent ? ( - {glyphs.check} sent - ) : state === 'resolved' ? ( - {glyphs.check} resolved - ) : ( - {glyphs.ring} accepted by Desktop - )} - - - {result.pendingMessageID ? : null} - {finalID ? : null} - {status ? : null} - {sendStatus?.message ? : null} - {result.hint ? ( - - {String(result.hint)} - - ) : null} - - ) -} - -export const AssetRow: React.FC> = ({ item: asset }) => { - const title = stringValue(asset.fileName) - ?? stringValue(asset.uploadID) - ?? stringValue(asset.srcURL) - ?? 'asset' - const mime = stringValue(asset.mimeType) - const meta = compact([ - typeof asset.fileSize === 'number' ? formatBytes(asset.fileSize) : undefined, - asset.width && asset.height ? `${String(asset.width)}×${String(asset.height)}` : undefined, - typeof asset.duration === 'number' ? formatDuration(asset.duration) : undefined, - ]) - - return ( - - - - - {title} - {mime && {mime}} - {meta.length > 0 && {meta.join(' ')}} - - {asset.srcURL ? ( - - {String(asset.srcURL)} - - ) : null} - {asset.uploadID ? ( - - upload {String(asset.uploadID)} - - ) : null} - {asset.error ? ( - - {glyphs.cross} {String(asset.error)} - - ) : null} - - ) -} - -// ─── system / cards ──────────────────────────────────────────────────────────── - -export const InfoCard: React.FC<{ info: RecordValue }> = ({ info }) => { - const version = stringValue(info.version) - const platform = stringValue(info.platform) - const user = info.user && typeof info.user === 'object' ? info.user as RecordValue : undefined - const userName = user ? (stringValue(user.fullName) ?? stringValue(user.username) ?? stringValue(user.id)) : undefined - const endpoints = info.endpoints && typeof info.endpoints === 'object' ? info.endpoints as RecordValue : undefined - - return ( - - - - - Beeper Desktop - {version && v{version}} - {platform && {platform}} - - {userName && } - {endpoints && Object.entries(endpoints).map(([key, value]) => - typeof value === 'string' ? ( - - {key.padEnd(12)} - - {value} - - - ) : null, - )} - - ) -} - -export const DoctorCard: React.FC<{ checks: Array<{ ok: boolean; name: string; detail?: string }>; ok: boolean }> = ({ checks, ok }) => { - const longest = Math.max(0, ...checks.map(c => c.name.length)) - return ( - - - - - Doctor - - {ok ? ( - {glyphs.check} healthy - ) : ( - {glyphs.cross} attention needed - )} - - - {checks.map(check => ( - - {check.ok ? glyphs.check : glyphs.cross} - - {check.name.padEnd(longest + 2)} - {check.detail && {check.detail}} - - ))} - - {!ok && ( - - )} - - ) -} - -export const AuthStatusCard: React.FC<{ auth: RecordValue }> = ({ auth }) => { - const ok = Boolean(auth.authenticated) - const expires = auth.expiresAt ? formatTime(auth.expiresAt) ?? String(auth.expiresAt) : undefined - return ( - - - - - Authentication - - {ok ? ( - {glyphs.check} signed in - ) : ( - {glyphs.ring} signed out - )} - - {String(auth.baseURL)} - } /> - - {auth.clientID ? : null} - {auth.scope ? : null} - {expires ? : null} - {!ok && ( - - )} - - ) -} - -export const ReadinessCard: React.FC<{ data: RecordValue }> = ({ data }) => { - const target = data.target && typeof data.target === 'object' ? data.target as RecordValue : undefined - const readiness = data.readiness && typeof data.readiness === 'object' ? data.readiness as RecordValue : data - const state = stringValue(readiness.state) ?? 'unknown' - const ready = state === 'ready' - const actions = Array.isArray(readiness.actions) ? readiness.actions.map(String) : [] - const message = stringValue(readiness.message) - return ( - - - - - {ready ? 'Ready' : 'Not ready'} - {state.replaceAll('-', ' ')} - - {target ? ( - <> - - {target.baseURL ? {String(target.baseURL)}} /> : null} - - ) : null} - {message ? : null} - {actions.length > 0 && !ready ? ( - ({ command: `beeper ${command}` }))} /> - ) : null} - - ) -} - -export const UserInfoCard: React.FC<{ user: RecordValue }> = ({ user }) => { - const name = stringValue(user.name) - ?? stringValue(user.preferred_username) - ?? stringValue(user.email) - ?? String(user.sub) - return ( - - - - - {name} - you - - {user.email ? : null} - {user.preferred_username ? : null} - {user.sub ? : null} - - ) -} - -// ─── auth flow / login wizard ────────────────────────────────────────────────── - -export const AuthCodeCard: React.FC<{ url: string; code?: string; hint?: string }> = ({ url, code, hint }) => ( - - - - - Sign in to Beeper - - {hint && ( - - {hint} - - )} - - {url} - - {code && ( - - code - {code} - - )} - -) - -export const AuthSignedIn: React.FC<{ as: string; detail?: string; saved?: boolean }> = ({ as, detail, saved }) => ( - - - {glyphs.check} - - Signed in - as - {as} - - {detail && {detail}} - {saved === false && token not saved (--no-save)} - -) - -// ─── config / commands manifest ──────────────────────────────────────────────── - -function maskToken(value: string): string { - if (value.length <= 12) return '••••' - return `${value.slice(0, 6)}…${value.slice(-4)}` -} - -function renderConfigValue(key: string, value: unknown): React.ReactNode { - if (value == null) return - if (typeof value !== 'object') { - if (/token|secret|key/i.test(key) && typeof value === 'string') { - return {maskToken(value)} - } - return {String(value)} - } - const record = value as RecordValue - const inner = Object.entries(record).filter(([, v]) => v != null) - if (!inner.length) return {'{}'} - const width = Math.max(...inner.map(([k]) => k.length)) + 2 - return ( - - {inner.map(([k, v]) => ( - - {k.padEnd(width)} - {/^(accesstoken|token|secret|key)$/i.test(k) && typeof v === 'string' - ? {maskToken(v)} - : {typeof v === 'object' ? JSON.stringify(v) : String(v)}} - - ))} - - ) -} - -export const ConfigView: React.FC<{ data: RecordValue }> = ({ data }) => { - const entries = Object.entries(data).filter(([, v]) => v != null) - if (!entries.length) { - return ', hint: 'override the API endpoint' }, - ]} /> - } - const width = Math.max(...entries.map(([k]) => k.length)) + 2 - return ( - - - - - Config - - {entries.map(([key, value]) => ( - - - {key.padEnd(width)} - {typeof value !== 'object' || value == null ? renderConfigValue(key, value) : null} - - {typeof value === 'object' && value != null && ( - - {renderConfigValue(key, value)} - - )} - - ))} - - ) -} - -type ManifestItem = { command: string; description: string; group?: string } - -export const CommandsView: React.FC<{ items: ManifestItem[]; title?: string; intro?: string[] }> = ({ items, title = 'Commands', intro }) => { - const groups = new Map() - for (const item of items) { - const g = item.group ?? 'Common' - if (!groups.has(g)) groups.set(g, []) - groups.get(g)!.push(item) - } - // Hard cap so descriptions stay aligned. Anything longer drops its description onto the next indented line. - const NAME_WIDTH = 32 - return ( - - - - - {title} - - {intro?.map((line, i) => ( - - {line} - - ))} - {[...groups.entries()].map(([group, list]) => ( - - {group.toUpperCase()} - {list.map(item => { - const fits = item.command.length <= NAME_WIDTH - return fits ? ( - - {item.command.padEnd(NAME_WIDTH + 2)} - {item.description} - - ) : ( - - {item.command} - {item.description} - - ) - })} - - ))} - - ) -} - -// ─── stream feed (used by `watch`) ───────────────────────────────────────────── - -export type StreamEvent = { type: string; chatID?: string; messageID?: string; ts?: string } - -const eventTone: Record = { - 'message.new': theme.primary, - 'message.send': theme.mine, - 'message.edit': theme.warn, - 'message.delete': theme.danger, - 'chat.update': theme.cyan, - 'chat.read': theme.subtle, - 'chat.typing': theme.magenta, - 'reaction.add': theme.magenta, - 'reaction.remove': theme.subtle, -} - -export const StreamEventLine: React.FC<{ event: StreamEvent; index: number }> = ({ event, index }) => { - const color = eventTone[event.type] ?? theme.primary - const time = event.ts ? formatTime(event.ts) : undefined - return ( - - {String(index).padStart(4)} - {glyphs.dot} - - {event.type} - {event.chatID && in {event.chatID}} - {event.messageID && msg {event.messageID}} - {time && {time}} - - ) -} - -export const StreamHeader: React.FC<{ subscribed: string[]; baseURL: string; connected: boolean }> = ({ subscribed, baseURL, connected }) => { - const label = subscribed.length === 1 && subscribed[0] === '*' ? 'all chats' : `${subscribed.length} chat${subscribed.length === 1 ? '' : 's'}` - return ( - - - - - Watching events - {label} - {!connected && connecting…} - - - {baseURL} · press ⌃C to stop - - - ) -} - -// ─── generic fallback ───────────────────────────────────────────────────────── - -export const GenericRow: React.FC> = ({ item }) => { - const title = item.title ?? item.displayName ?? item.name ?? item.id ?? item.messageID - const scalarEntries = Object.entries(item).filter(([key, value]) => { - if (value == null) return false - if (key === 'title' || key === 'displayName' || key === 'name') return false - if (typeof value === 'object') return false - return true - }) - const width = scalarEntries.length ? Math.max(...scalarEntries.map(([k]) => k.length)) + 2 : 0 - return ( - - {title != null && ( - - - - {String(title)} - - )} - {scalarEntries.map(([key, value]) => ( - - {key.padEnd(width)} - {String(value)} - - ))} - - ) -} diff --git a/packages/cli/src/lib/ink/format.ts b/packages/cli/src/lib/ink/format.ts deleted file mode 100644 index 64102c80..00000000 --- a/packages/cli/src/lib/ink/format.ts +++ /dev/null @@ -1,121 +0,0 @@ -export type RecordValue = Record - -export function stringValue(value: unknown): string | undefined { - return typeof value === 'string' && value.trim() ? value : undefined -} - -export function compact(values: unknown[]): string[] { - return values.filter((value): value is string => typeof value === 'string' && value.length > 0) -} - -export function formatTime(value: unknown): string | undefined { - if (typeof value !== 'string' && typeof value !== 'number') return undefined - const date = new Date(value) - if (Number.isNaN(date.valueOf())) return String(value) - const now = Date.now() - const diffMs = now - date.valueOf() - const abs = Math.abs(diffMs) - const suffix = diffMs >= 0 ? 'ago' : 'from now' - if (abs < 60_000) return 'just now' - if (abs < 3_600_000) return `${Math.round(abs / 60_000)}m ${suffix}` - if (abs < 86_400_000) return `${Math.round(abs / 3_600_000)}h ${suffix}` - if (abs < 604_800_000) return `${Math.round(abs / 86_400_000)}d ${suffix}` - return date.toLocaleDateString(undefined, { - month: 'short', - day: 'numeric', - year: date.getFullYear() === new Date().getFullYear() ? undefined : 'numeric', - }) -} - -export function formatBytes(bytes: number): string { - if (bytes < 1024) return `${bytes} B` - if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` - return `${(bytes / 1024 / 1024).toFixed(1)} MB` -} - -export function formatDuration(ms: number): string { - const seconds = Math.round(ms / 1000) - if (seconds < 60) return `${seconds}s` - const minutes = Math.floor(seconds / 60) - const remainder = seconds % 60 - return `${minutes}m ${remainder}s` -} - -export function shortID(value: string): string { - const local = value.split(':')[0] - return local?.replace(/^@/, '') || value -} - -export function truncate(value: string, max: number): string { - return value.length <= max ? value : `${value.slice(0, max - 1)}…` -} - -export function cleanText(value: unknown): string | undefined { - const raw = stringValue(value) - if (!raw) return undefined - const text = raw - .replace(/<[^>]*>/g, ' ') - .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') - .replace(/[*_`~>#]/g, '') - .replace(/\s+/g, ' ') - .trim() - return text ? truncate(text, 240) : undefined -} - -export function attachmentLabel(attachments: unknown[]): string { - const labels = attachments.map(item => { - if (!item || typeof item !== 'object') return 'attachment' - const attachment = item as RecordValue - return stringValue(attachment.fileName) ?? stringValue(attachment.type) ?? 'attachment' - }) - return labels.length === 1 ? labels[0]! : `${labels.length} attachments` -} - -export function messageText(message: RecordValue): string | undefined { - if (message.isDeleted) return 'deleted message' - const text = cleanText(message.text) - if (text) return text - if (Array.isArray(message.attachments) && message.attachments.length > 0) return attachmentLabel(message.attachments) - if (Array.isArray(message.links) && message.links.length > 0) return `${message.links.length} link${message.links.length === 1 ? '' : 's'}` - return undefined -} - -export function chatPreview(chat: RecordValue): { kind: 'draft' | 'message'; sender?: string; text: string } | undefined { - if (chat.draft && typeof chat.draft === 'object') { - const draft = chat.draft as RecordValue - const text = cleanText(draft.text) ?? '' - return { kind: 'draft', text } - } - const lastMessage = (chat.latestMessage ?? chat.lastMessage) as RecordValue | undefined - if (lastMessage && typeof lastMessage === 'object') { - const sender = stringValue(lastMessage.senderName) ?? (lastMessage.isSender ? 'you' : undefined) - const text = messageText(lastMessage) ?? '' - if (!text && !sender) return undefined - return { kind: 'message', sender, text } - } - return undefined -} - -export function participantsSummary(participants: RecordValue): string | undefined { - const total = participants.total - const items = Array.isArray(participants.items) ? participants.items : [] - if (typeof total === 'number') return `${total} participant${total === 1 ? '' : 's'}` - if (items.length) return `${items.length} participant${items.length === 1 ? '' : 's'}` - return undefined -} - -export function isPinned(chat: RecordValue): boolean { - return Boolean(chat.isPinned ?? chat.pinned) -} - -export function isMuted(chat: RecordValue): boolean { - return Boolean(chat.isMuted ?? chat.muted) -} - -export function isArchived(chat: RecordValue): boolean { - return Boolean(chat.isArchived ?? chat.archived) -} - -export function isLowPriority(chat: RecordValue): boolean { - return Boolean(chat.isLowPriority ?? chat.lowPriority) -} diff --git a/packages/cli/src/lib/ink/render.tsx b/packages/cli/src/lib/ink/render.tsx deleted file mode 100644 index 007c629f..00000000 --- a/packages/cli/src/lib/ink/render.tsx +++ /dev/null @@ -1,338 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react' -import { Box, render as inkRender, Static, Text, useApp, useInput } from 'ink' -import Spinner from 'ink-spinner' -import { - AccountRow, - AssetRow, - AuthStatusCard, - BridgeRow, - ChatDetail, - ChatRow, - CommandsView, - ConfigView, - DoctorCard, - EmptyState, - FailureLine, - GenericRow, - InfoCard, - MessageRow, - ReadinessCard, - SectionHeader, - SendResultCard, - type StreamEvent, - StreamEventLine, - StreamHeader, - type Suggestion, - SuccessLine, - UserInfoCard, - UserRow, -} from './components.js' -import type { RecordValue } from './format.js' -import { theme } from './theme.js' - -const App: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const { exit } = useApp() - useEffect(() => { - setTimeout(() => exit(), 0) - }, [exit]) - return <>{children} -} - -async function renderOnce(node: React.ReactNode): Promise { - const instance = inkRender({node}, { exitOnCtrlC: false, patchConsole: false }) - await instance.waitUntilExit().catch(() => undefined) -} - -type Kind = - | 'chat' | 'chatDetail' | 'message' | 'user' | 'account' | 'bridge' | 'asset' - | 'info' | 'doctor' | 'auth' | 'oauth' | 'search' - | 'commandManifest' | 'config' | 'readiness' | 'sendResult' - | 'generic' - -function detectKind(record: RecordValue): Kind { - if (typeof record.id === 'string' && typeof record.accountID === 'string' && typeof record.title === 'string' && typeof record.unreadCount === 'number') { - if (record.participants && typeof record.participants === 'object') return 'chatDetail' - return 'chat' - } - if (typeof record.id === 'string' && typeof record.chatID === 'string' && typeof record.senderID === 'string' && typeof record.timestamp === 'string') return 'message' - if (typeof record.chatID === 'string' && typeof record.pendingMessageID === 'string' && (record.accepted === true || typeof record.state === 'string')) return 'sendResult' - if (typeof record.id === 'string' && (typeof record.fullName === 'string' || typeof record.username === 'string' || typeof record.email === 'string' || typeof record.phoneNumber === 'string')) return 'user' - if (typeof record.id === 'string' && typeof record.provider === 'string' && typeof record.status === 'string' && (typeof record.displayName === 'string' || Array.isArray(record.accounts))) return 'bridge' - if (typeof (record.accountID ?? record.id) === 'string' && (typeof record.network === 'string' || typeof record.bridge === 'string' || typeof record.displayName === 'string')) return 'account' - if (typeof record.uploadID === 'string' || typeof record.srcURL === 'string') return 'asset' - if (typeof record.version === 'string' && typeof record.endpoints === 'object') return 'info' - if (typeof record.ok === 'boolean' && Array.isArray(record.checks)) return 'doctor' - if (typeof record.ok === 'boolean' && record.checks && typeof record.checks === 'object') return 'doctor' - if (record.readiness && typeof record.readiness === 'object') return 'readiness' - if (typeof record.state === 'string' && Array.isArray(record.actions)) return 'readiness' - if (typeof record.authenticated === 'boolean' && typeof record.baseURL === 'string') return 'auth' - if (typeof record.sub === 'string' && (typeof record.email === 'string' || typeof record.name === 'string' || typeof record.preferred_username === 'string')) return 'oauth' - if (Array.isArray(record.chats) && Array.isArray(record.messages)) return 'search' - return 'generic' -} - -function isManifestList(items: unknown[]): items is Array<{ command: string; description: string; group?: string }> { - return items.every(item => - item != null - && typeof item === 'object' - && typeof (item as Record).command === 'string' - && typeof (item as Record).description === 'string', - ) -} - -function rowFor(kind: Kind, item: RecordValue, key: number): React.ReactNode { - switch (kind) { - case 'chat': return - case 'chatDetail': return - case 'message': return - case 'user': return - case 'account': return - case 'bridge': return - case 'asset': return - default: return - } -} - -export async function renderList(items: RecordValue[], empty?: { title: string; subtitle?: string; suggestions?: Suggestion[] }): Promise { - if (!items.length) { - if (empty) await renderOnce() - return - } - if (isManifestList(items)) { - await renderOnce() - return - } - const kind = detectKind(items[0]!) - await renderOnce( - - {items.map((item, index) => rowFor(kind === 'chatDetail' ? 'chat' : kind, item, index))} - , - ) -} - -export async function renderValue(value: unknown): Promise { - if (Array.isArray(value)) { - await renderList(value as RecordValue[]) - return - } - if (!value || typeof value !== 'object') { - if (value === undefined) return - process.stdout.write(`${String(value)}\n`) - return - } - const record = value as RecordValue - const kind = detectKind(record) - switch (kind) { - case 'info': - await renderOnce() - return - case 'doctor': { - const checks = Array.isArray(record.checks) - ? record.checks as Array<{ ok: boolean; name: string; detail?: string }> - : record.checks && typeof record.checks === 'object' - ? Object.entries(record.checks as Record).map(([name, value]) => { - const detail = typeof value === 'object' && value ? JSON.stringify(value) : String(value) - const ok = name === 'readiness' - ? (value as RecordValue)?.state === 'ready' - : typeof (value as RecordValue)?.reachable === 'boolean' - ? Boolean((value as RecordValue).reachable) - : Boolean(value) - return { ok, name, detail } - }) - : [] - await renderOnce() - return - } - case 'auth': - await renderOnce() - return - case 'oauth': - await renderOnce() - return - case 'search': { - const chats = Array.isArray(record.chats) ? record.chats as RecordValue[] : [] - const messages = Array.isArray(record.messages) ? record.messages as RecordValue[] : [] - if (!chats.length && !messages.length) { - await renderOnce( - "', hint: 'narrow with filters' }, - ]} - />, - ) - return - } - await renderOnce( - - {chats.length > 0 && } - {chats.map((item, index) => )} - {messages.length > 0 && } - {messages.map((item, index) => )} - , - ) - return - } - case 'readiness': - await renderOnce() - return - case 'sendResult': - await renderOnce() - return - case 'chat': - case 'chatDetail': - await renderOnce() - return - case 'message': - await renderOnce() - return - case 'user': - await renderOnce() - return - case 'account': - await renderOnce() - return - case 'bridge': - await renderOnce() - return - case 'asset': - await renderOnce() - return - default: - await renderOnce() - } -} - -export async function renderEmptyState(opts: { title: string; subtitle?: string; suggestions?: Suggestion[] }): Promise { - await renderOnce() -} - -export async function renderSuccess(opts: { message: string; detail?: string; entity?: unknown }): Promise { - let entityNode: React.ReactNode = null - if (opts.entity && typeof opts.entity === 'object') { - const record = opts.entity as RecordValue - const kind = detectKind(record) - entityNode = rowFor(kind === 'chatDetail' ? 'chat' : kind, record, 0) - } - await renderOnce() -} - -export async function renderFailure(opts: { message: string; detail?: string }): Promise { - await renderOnce() -} - -export async function renderConfig(data: RecordValue): Promise { - await renderOnce() -} - -export async function renderCommands(items: Array<{ command: string; description: string; group?: string }>, opts?: { title?: string; intro?: string[] }): Promise { - await renderOnce() -} - -// ─── streaming render (used by `watch`) ─────────────────────────────────────── - -export type StreamController = { - push(event: StreamEvent): void - setConnected(connected: boolean): void - setStatus(status: string | undefined): void - close(): Promise - done: Promise -} - -type StreamState = { - events: StreamEvent[] - connected: boolean - status: string | undefined -} - -type StreamProps = { - initialState: StreamState - baseURL: string - subscribed: string[] - bind: (api: { update: (next: StreamState) => void; exit: () => void; onInterrupt: (fn: () => void) => void }) => void -} - -const StreamView: React.FC = ({ initialState, baseURL, subscribed, bind }) => { - const [state, setState] = useState(initialState) - const { exit } = useApp() - const interruptRef = useRef<(() => void) | undefined>(undefined) - - useEffect(() => { - bind({ - update: setState, - exit: () => exit(), - onInterrupt: fn => { interruptRef.current = fn }, - }) - }, [bind, exit]) - - useInput((input, key) => { - if (key.ctrl && input === 'c') { - interruptRef.current?.() - } - }) - - return ( - - - ({ event, index: index + 1 }))}> - {({ event, index }) => } - - {!state.connected && state.status && ( - - - {state.status} - - )} - - ) -} - -export function renderStream(opts: { baseURL: string; subscribed: string[] }): StreamController { - const initial: StreamState = { events: [], connected: false, status: 'connecting' } - let current = initial - let api: { update: (next: StreamState) => void; exit: () => void; onInterrupt: (fn: () => void) => void } | undefined - const interruptHandlers: Array<() => void> = [] - - const setState = (next: StreamState): void => { - current = next - api?.update(next) - } - - const instance = inkRender( - { - api = hooks - hooks.onInterrupt(() => { - for (const fn of interruptHandlers) fn() - }) - }} - />, - { exitOnCtrlC: false, patchConsole: false }, - ) - - return { - push(event) { - setState({ ...current, events: [...current.events, event] }) - }, - setConnected(connected) { - setState({ ...current, connected, status: connected ? undefined : current.status }) - }, - setStatus(status) { - setState({ ...current, status }) - }, - async close() { - api?.exit() - await instance.waitUntilExit().catch(() => undefined) - }, - get done() { - return instance.waitUntilExit().then(() => undefined) - }, - } -} - -export type { Suggestion, StreamEvent } diff --git a/packages/cli/src/lib/ink/spinner.tsx b/packages/cli/src/lib/ink/spinner.tsx deleted file mode 100644 index 2c3beabf..00000000 --- a/packages/cli/src/lib/ink/spinner.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React, { useEffect, useState } from 'react' -import { Box, render, Text, useApp } from 'ink' -import Spinner from 'ink-spinner' -import { glyphs, theme } from './theme.js' - -type State = - | { kind: 'spinning'; label: string } - | { kind: 'succeed'; label: string } - | { kind: 'fail'; label: string } - -const externalListeners = new Map void>() - -type SpinnerLineProps = { - id: symbol - initial: State -} - -const SpinnerLine: React.FC = ({ id, initial }) => { - const [state, setState] = useState(initial) - const { exit } = useApp() - - useEffect(() => { - externalListeners.set(id, value => { - setState(value) - if (value.kind !== 'spinning') { - setTimeout(() => exit(), 0) - } - }) - return () => { externalListeners.delete(id) } - }, [id, exit]) - - if (state.kind === 'spinning') { - return ( - - - {state.label} - - ) - } - if (state.kind === 'succeed') { - return ( - - {glyphs.check} - {state.label} - - ) - } - return ( - - {glyphs.cross} - {state.label} - - ) -} - -export type SpinnerHandle = { - update(label: string): void - succeed(label?: string): Promise - fail(label?: string): Promise - stop(): Promise -} - -export function createInkSpinner(initialLabel: string, stream: NodeJS.WriteStream = process.stderr): SpinnerHandle { - const id = Symbol('spinner') - let currentLabel = initialLabel - let finished = false - - const instance = render( - , - { stdout: stream as unknown as NodeJS.WriteStream, exitOnCtrlC: false, patchConsole: false }, - ) - - const finish = (state: State): Promise => { - if (finished) return Promise.resolve() - finished = true - const listener = externalListeners.get(id) - if (listener) listener(state) - else instance.unmount() - return instance.waitUntilExit().then(() => undefined).catch(() => undefined) - } - - return { - update(label) { - if (finished) return - currentLabel = label - const listener = externalListeners.get(id) - listener?.({ kind: 'spinning', label }) - }, - succeed(label) { - return finish({ kind: 'succeed', label: label ?? currentLabel }) - }, - fail(label) { - return finish({ kind: 'fail', label: label ?? currentLabel }) - }, - stop() { - if (finished) return Promise.resolve() - finished = true - instance.unmount() - return instance.waitUntilExit().then(() => undefined).catch(() => undefined) - }, - } -} - -export async function withInkSpinner( - label: string, - fn: () => Promise, - options?: { done?: (value: T) => string | undefined; stream?: NodeJS.WriteStream }, -): Promise { - if (process.env.BEEPER_QUIET === '1') return fn() - const stream = options?.stream ?? process.stderr - const spinner = createInkSpinner(label, stream) - try { - const value = await fn() - const doneLabel = options?.done?.(value) - if (doneLabel) await spinner.succeed(doneLabel) - else await spinner.stop() - return value - } catch (error) { - await spinner.fail(label) - throw error - } -} diff --git a/packages/cli/src/lib/ink/theme.ts b/packages/cli/src/lib/ink/theme.ts deleted file mode 100644 index 31bed95c..00000000 --- a/packages/cli/src/lib/ink/theme.ts +++ /dev/null @@ -1,106 +0,0 @@ -// Beeper Desktop dark-theme palette → Ink hex strings. -// Mirrors src/renderer/scss/tokens/colors{,_dark}.scss in the desktop app. -export const theme = { - // Brand / accent - primary: '#2561fb', - primaryDim: '#1b43aa', - primaryGlow: '#5a86ff', - link: '#5cadff', - - // Text - text: '#ededed', // --color-text-neutrals (dark) - muted: '#adadad', // --color-text-neutrals-weak - subtle: '#7e7e7e', // --color-text-neutrals-subtle - hairline: '#343434', // --color-border-neutrals - - // Surface (only used when we explicitly fill — Ink defaults to the user's term bg) - surface: '#000000', - surfaceAlt: '#1c1c1c', - surfaceHover: '#232323', - - // Semantic - mine: '#4cc38a', // --color-text-success (dark) - online: '#1ec843', - warn: '#f6ce46', // --color-pin - warnAlt: '#f1a10d', - danger: '#ff6369', // --color-text-error (dark) - magenta: '#912ce1', // --color-mute (dark) - cyan: '#00c2d7', - - // Highlights - mention: '#5a86ff', - draft: '#f6ce46', -} as const - -// Per-bridge "iconBackground" tints from beeper/desktop bundled-platforms/bridges/*/info.ts. -// Used to tint the rail/badge on chat & account rows so a glance reveals the network. -const bridgeTint: Record = { - imessage: '#19BA3B', - imessagecloud: '#19BA3B', - imessagego: '#19BA3B', - androidsms: '#19BA3B', - whatsapp: '#48C95F', - telegram: '#2EA4DB', - signal: '#3542FF', - discord: '#5865F2', - linkedin: '#086CE1', - twitter: '#202124', - x: '#202124', - bluesky: '#549B57', - beeper: '#0D4FFB', - beeperai: '#0D4FFB', - ai: '#0D4FFB', - instagram: '#d833ca', - facebook: '#0d4ffb', - messenger: '#0d4ffb', - googlechat: '#1ab5a2', - googlevoice: '#0eb3ef', - googlemessages: '#19ba3b', - slack: '#9745ea', - matrix: '#0eb3ef', -} - -export function bridgeColor(network: string | undefined | null): string | undefined { - if (!network) return undefined - const key = String(network).toLowerCase().replace(/[^a-z0-9]/g, '') - return bridgeTint[key] -} - -// Group-chat sender name palette (8-color rotation) from the desktop dark theme. -const groupSenderPalette = [ - '#63c174', '#f1a10d', '#f76190', '#bf7af0', - '#00c2d7', '#f65cb6', '#849dff', '#0ac5b3', -] as const - -export function senderColor(id: string | undefined | null): string { - if (!id) return theme.text - let hash = 0 - for (let i = 0; i < id.length; i++) hash = ((hash << 5) - hash + id.charCodeAt(i)) | 0 - return groupSenderPalette[Math.abs(hash) % groupSenderPalette.length]! -} - -// Glyphs — every visual cue we use sits in this map so a single audit covers them. -export const glyphs = { - rail: '▎', // narrow left-edge bar; replaces avatars - arrow: '›', - arrowR: '→', - arrowL: '←', - check: '✓', - cross: '✗', - dot: '●', - ring: '○', - pin: '★', - mute: '◐', - archive: '◇', - reaction: '♥', - attachment: '📎', - reply: '↳', - edited: '✎', - bullet: '·', - lowPriority: '◌', - mention: '@', - draft: '✎', - unread: '●', - spinner: '◇', - hairline: '─', -} as const diff --git a/packages/cli/src/lib/installations.ts b/packages/cli/src/lib/installations.ts index 6f3a2b58..4e5a4203 100644 --- a/packages/cli/src/lib/installations.ts +++ b/packages/cli/src/lib/installations.ts @@ -7,18 +7,14 @@ import { pipeline } from 'node:stream/promises' import type { ReadableStream } from 'node:stream/web' import { execFile } from 'node:child_process' import { promisify } from 'node:util' -import { beeperDir } from './targets.js' +import { beeperDir, type ManagedTargetType } from './targets.js' import { SERVER_ENV_API_BASE_URLS, normalizeServerEnv, type ServerEnv } from './server-env.js' -export type { ServerEnv } from './server-env.js' - const execFileAsync = promisify(execFile) -export type InstallKind = 'desktop' | 'server' -export type InstallChannel = 'stable' | 'nightly' - -export type Installation = { - kind: InstallKind +type InstallChannel = 'stable' | 'nightly' +type Installation = { + kind: ManagedTargetType channel: InstallChannel serverEnv: ServerEnv bundleID: string @@ -30,28 +26,36 @@ export type Installation = { updatedAt: string } -export type Installations = Partial> +export type Installations = Partial> -export type UpdateInfo = { +type UpdateInfo = { available: boolean latestVersion?: string - currentVersion?: string - action: string - feedURL?: string } -export type FeedInfo = { +type FeedInfo = { version?: string url?: string raw: unknown } -export const installationsPath = () => join(beeperDir(), 'installations.json') -export const appsDir = () => join(beeperDir(), 'apps') -export const binDir = () => join(beeperDir(), 'bin') -export const desktopInstallDir = () => join(appsDir(), 'desktop') -export const serverInstallRoot = () => join(appsDir(), 'server') -export const serverBinPath = () => join(binDir(), 'beeper-server') +type InstallRequest = { + kind: ManagedTargetType + channel: InstallChannel + serverEnv: ServerEnv + platform: 'macos' | 'windows' | 'linux' + feedPlatform: 'darwin' | 'win32' | 'linux' + arch: 'x64' | 'arm64' + bundleID: string + apiBaseURL: string +} + +const installationsPath = () => join(beeperDir(), 'installations.json') +const appsDir = () => join(beeperDir(), 'apps') +const binDir = () => join(beeperDir(), 'bin') +const desktopInstallDir = () => join(appsDir(), 'desktop') +const serverInstallRoot = () => join(appsDir(), 'server') +const serverBinPath = () => join(binDir(), 'beeper-server') export async function readInstallations(): Promise { try { @@ -62,33 +66,24 @@ export async function readInstallations(): Promise { } } -export async function writeInstallations(installations: Installations): Promise { +async function writeInstallations(installations: Installations): Promise { await mkdir(dirname(installationsPath()), { recursive: true }) await writeFile(installationsPath(), `${JSON.stringify(installations, null, 2)}\n`, { mode: 0o600 }) } -export async function saveInstallation(installation: Installation): Promise { +async function saveInstallation(installation: Installation): Promise { const current = await readInstallations() await writeInstallations({ ...current, [installation.kind]: installation }) return installation } -export function normalizeInstallRequest(options: { - kind: InstallKind +function normalizeInstallRequest(options: { + kind: ManagedTargetType channel?: InstallChannel serverEnv?: string platform?: NodeJS.Platform arch?: string -}): { - kind: InstallKind - channel: InstallChannel - serverEnv: ServerEnv - platform: 'macos' | 'windows' | 'linux' - feedPlatform: 'darwin' | 'win32' | 'linux' - arch: 'x64' | 'arm64' - bundleID: string - apiBaseURL: string -} { +}): InstallRequest { const serverEnv = normalizeServerEnv(options.serverEnv) const channel = options.channel ?? 'stable' const platform = normalizeDownloadPlatform(options.platform ?? process.platform) @@ -107,7 +102,7 @@ export function normalizeInstallRequest(options: { } } -export function feedURLFor(options: ReturnType): string { +function feedURLFor(options: InstallRequest): string { const url = new URL('/desktop/update-feed.json', options.apiBaseURL) url.searchParams.set('bundleID', options.bundleID) url.searchParams.set('platform', options.feedPlatform) @@ -116,11 +111,11 @@ export function feedURLFor(options: ReturnType): return url.toString() } -export function downloadURLFor(options: ReturnType): string { +function downloadURLFor(options: InstallRequest): string { return `${options.apiBaseURL}/desktop/download/${options.platform}/${options.arch}/${options.channel}/${options.bundleID}` } -export async function fetchFeed(feedURL: string): Promise { +async function fetchFeed(feedURL: string): Promise { const response = await fetch(feedURL, { signal: AbortSignal.timeout(30_000) }) if (!response.ok) throw new Error(`Update feed returned ${response.status} ${response.statusText}`) const raw = await response.json() as unknown @@ -134,15 +129,9 @@ export async function fetchFeed(feedURL: string): Promise { export async function checkInstallationUpdate(installation: Installation): Promise { const feed = await fetchFeed(installation.feedURL) const latestVersion = feed.version - const available = !!latestVersion && latestVersion !== installation.version return { - available, + available: !!latestVersion && latestVersion !== installation.version, latestVersion, - currentVersion: installation.version, - action: installation.kind === 'desktop' - ? 'Update Beeper Desktop in the app.' - : available ? 'Run: beeper update --server' : 'Beeper Server is up to date.', - feedURL: installation.feedURL, } } @@ -209,11 +198,7 @@ export async function installServer(options: { channel?: InstallChannel; serverE }) } -export async function updateServerInstallation(installation: Installation): Promise { - return installServer({ channel: installation.channel, serverEnv: installation.serverEnv }) -} - -export async function downloadArtifact(url: string, destinationDir: string): Promise { +async function downloadArtifact(url: string, destinationDir: string): Promise { await mkdir(destinationDir, { recursive: true }) const response = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(120_000) }) if (!response.ok || !response.body) throw new Error(`Download returned ${response.status} ${response.statusText}`) @@ -344,7 +329,7 @@ function normalizeArch(arch: string): 'x64' | 'arm64' { throw new Error(`Unsupported architecture "${arch}".`) } -function bundleIDFor(kind: InstallKind, channel: InstallChannel): string { +function bundleIDFor(kind: ManagedTargetType, channel: InstallChannel): string { const base = kind === 'desktop' ? 'com.automattic.beeper.desktop' : 'com.automattic.beeper.server' return channel === 'nightly' ? `${base}.nightly` : base } diff --git a/packages/cli/src/lib/local-desktop.ts b/packages/cli/src/lib/local-desktop.ts index 9c3c8790..ec6bfb94 100644 --- a/packages/cli/src/lib/local-desktop.ts +++ b/packages/cli/src/lib/local-desktop.ts @@ -4,6 +4,7 @@ import { homedir } from 'node:os' import { join } from 'node:path' import { promisify } from 'node:util' import { BeeperDesktop } from '@beeper/desktop-api' +import { apiItems, apiRecord } from './api-values.js' import type { Readiness } from './app-state.js' import type { StoredAuth, Target } from './targets.js' @@ -44,18 +45,18 @@ export async function findLocalDesktopSession(target?: Target): Promise accountName(item)) .filter((name): name is string => Boolean(name)) .slice(0, 8) @@ -105,8 +104,7 @@ export async function connectedAccountSummary(target: Target, auth?: StoredAuth) export async function localConnectedAccountSummary(dataDir: string): Promise { const bridgeAccounts = await readKeyValue(dataDir, 'bridgeAccounts').catch(() => undefined) - const rows = Array.isArray(bridgeAccounts) ? bridgeAccounts : [] - const names = rows + const names = apiItems(bridgeAccounts) .map(item => accountName(item)) .filter((name): name is string => Boolean(name)) return [...new Set(names)].slice(0, 8) @@ -151,16 +149,15 @@ async function readKeyValue(dataDir: string, key: string): Promise { } function accountName(item: unknown): string | undefined { - if (!item || typeof item !== 'object') return undefined - const record = item as Record - const bridge = record.bridge && typeof record.bridge === 'object' ? record.bridge as Record : undefined - const network = record.network && typeof record.network === 'object' ? record.network as Record : undefined + const record = apiRecord(item) + const bridge = apiRecord(record.bridge) + const network = apiRecord(record.network) return stringValue(record.network) - ?? stringValue(network?.displayName) - ?? stringValue(network?.name) + ?? stringValue(network.displayName) + ?? stringValue(network.name) ?? stringValue(record.displayName) ?? stringValue(record.name) - ?? stringValue(bridge?.type) + ?? stringValue(bridge.type) ?? stringValue(record.accountID) ?? stringValue(record.id) } @@ -173,10 +170,6 @@ function booleanValue(value: unknown): boolean | undefined { return typeof value === 'boolean' ? value : undefined } -function recordValue(value: unknown): Record | undefined { - return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : undefined -} - function sqlString(value: string): string { return value.replaceAll("'", "''") } diff --git a/packages/cli/src/lib/manifest.ts b/packages/cli/src/lib/manifest.ts deleted file mode 100644 index 8ae53c47..00000000 --- a/packages/cli/src/lib/manifest.ts +++ /dev/null @@ -1,612 +0,0 @@ -export type ManifestCommand = { - command: string - description: string - examples?: string[] -} - -export const commandManifest: ManifestCommand[] = [ - { - command: 'setup', - description: 'Make the selected target ready for messaging', - examples: [ - 'beeper setup', - 'beeper setup --local', - 'beeper setup --oauth', - 'beeper setup --remote https://desktop.example.com', - 'beeper setup --desktop --install', - ], - }, - { - command: 'install desktop', - description: 'Install Beeper Desktop locally', - examples: ['beeper install desktop', 'beeper install desktop --channel nightly'], - }, - { - command: 'install server', - description: 'Install Beeper Server locally', - examples: ['beeper install server', 'beeper install server --server-env staging'], - }, - { - command: 'targets list', - description: 'List configured Beeper targets', - examples: ['beeper targets list', 'beeper targets list --json'], - }, - { - command: 'bridges list', - description: 'List bridges that can connect chat accounts', - examples: ['beeper bridges list', 'beeper bridges list --provider local --json'], - }, - { - command: 'bridges show', - description: 'Show bridge details, login flows, and connected accounts', - examples: ['beeper bridges show local-whatsapp', 'beeper bridges show telegram'], - }, - { - command: 'targets add desktop', - description: 'Add a managed Beeper Desktop target', - examples: ['beeper targets add desktop work --default'], - }, - { - command: 'targets add server', - description: 'Add a managed Beeper Server target', - examples: ['beeper targets add server prod --server-env prod --default'], - }, - { - command: 'targets add remote', - description: 'Add a remote Beeper Desktop or Server target', - examples: ['beeper targets add remote work https://desktop.example.com --default'], - }, - { - command: 'targets use', - description: 'Set the default target', - examples: ['beeper targets use work'], - }, - { - command: 'targets show', - description: 'Show target details', - examples: ['beeper targets show', 'beeper targets show work'], - }, - { - command: 'targets status', - description: 'Check endpoint and process reachability for a target', - examples: ['beeper targets status', 'beeper targets status work --json'], - }, - { - command: 'targets start', - description: 'Start a local Server target or open Beeper Desktop', - examples: ['beeper targets start work'], - }, - { - command: 'targets stop', - description: 'Stop a local Beeper Server target', - examples: ['beeper targets stop work'], - }, - { - command: 'targets restart', - description: 'Restart a local Beeper Server target', - examples: ['beeper targets restart work'], - }, - { - command: 'targets logs', - description: 'Print logs for a local Beeper Desktop or Server install', - examples: ['beeper targets logs work'], - }, - { - command: 'targets enable', - description: 'Enable a local Beeper Server target at login', - examples: ['beeper targets enable work'], - }, - { - command: 'targets disable', - description: 'Disable a local Beeper Server target at login', - examples: ['beeper targets disable work'], - }, - { - command: 'targets remove', - description: 'Remove a target', - examples: ['beeper targets remove work'], - }, - { - command: 'targets tunnel', - description: 'Expose a local Desktop API over a public Cloudflare tunnel', - examples: [ - 'beeper targets tunnel', - 'beeper targets tunnel --target work --read-only', - 'beeper targets tunnel --as work-laptop --port 23373', - ], - }, - { - command: 'auth status', - description: 'Show stored auth for the selected target', - examples: ['beeper auth status', 'beeper auth status --json'], - }, - { - command: 'auth logout', - description: 'Clear stored authentication', - examples: ['beeper auth logout'], - }, - { - command: 'auth email start', - description: 'Start email sign-in for a target', - examples: ['beeper auth email start --email you@example.com --target work --json'], - }, - { - command: 'auth email response', - description: 'Finish email sign-in with a verification code', - examples: ['beeper auth email response --setup-request-id --code --target work --json'], - }, - { - command: 'verify', - description: 'Finish setup verification or verify another device', - examples: ['beeper verify', 'beeper verify --user @alice:beeper.com'], - }, - { - command: 'verify status', - description: 'Show encryption and device-verification readiness', - examples: ['beeper verify status --json'], - }, - { - command: 'verify approve', - description: 'Approve a pending device verification request', - examples: ['beeper verify approve --id active'], - }, - { - command: 'verify recovery-key', - description: 'Unlock encrypted messages with a recovery key', - examples: ['beeper verify recovery-key --key ABCD-EFGH-IJKL'], - }, - { - command: 'verify reset-recovery-key', - description: 'Create a new encrypted-messages recovery key', - examples: ['beeper verify reset-recovery-key'], - }, - { - command: 'verify cancel', - description: 'Cancel an in-progress device verification', - examples: ['beeper verify cancel'], - }, - { - command: 'verify list', - description: 'List active verification work', - examples: ['beeper verify list'], - }, - { - command: 'verify start', - description: 'Start a device verification request', - examples: ['beeper verify start --user @alice:beeper.com'], - }, - { - command: 'verify show', - description: 'Show the current active verification request', - examples: ['beeper verify show --json'], - }, - { - command: 'verify sas', - description: 'Start emoji verification', - examples: ['beeper verify sas'], - }, - { - command: 'verify sas-confirm', - description: 'Confirm matching emoji verification', - examples: ['beeper verify sas-confirm'], - }, - { - command: 'verify qr-scan', - description: 'Submit a scanned QR-code verification payload', - examples: ['beeper verify qr-scan --payload "..."'], - }, - { - command: 'verify qr-confirm', - description: 'Confirm that the other device scanned your QR code', - examples: ['beeper verify qr-confirm'], - }, - { - command: 'accounts list', - description: 'List connected accounts', - examples: ['beeper accounts list', 'beeper accounts list --account whatsapp --json'], - }, - { - command: 'accounts add', - description: 'Connect a chat account by bridge', - examples: [ - 'beeper accounts add', - 'beeper accounts add local-whatsapp', - 'beeper accounts add discord --non-interactive --cookie sessiontoken=...', - 'beeper accounts add discord --webview --webview-backend chrome', - ], - }, - { - command: 'accounts show', - description: 'Show account details', - examples: ['beeper accounts show whatsapp-main'], - }, - { - command: 'accounts remove', - description: 'Remove an account', - examples: ['beeper accounts remove whatsapp-main'], - }, - { - command: 'accounts use', - description: 'Select a default account for account-scoped commands', - examples: ['beeper accounts use whatsapp-main'], - }, - { - command: 'chats list', - description: 'List chats', - examples: [ - 'beeper chats list', - 'beeper chats list --pinned --limit 50', - 'beeper chats list --unread --no-muted --format json', - 'beeper ls --format ids', - ], - }, - { - command: 'chats search', - description: 'Search chats', - examples: ['beeper chats search Family'], - }, - { - command: 'chats show', - description: 'Show chat details', - examples: ['beeper chats show --chat 10313', 'beeper chats show --chat \'!plUOsWkvMmJmJPVAjS:beeper.com\''], - }, - { - command: 'chats start', - description: 'Start a chat', - examples: ['beeper chats start +15551234567', 'beeper chats start @alice:beeper.com --title "Alice"'], - }, - { - command: 'chats archive', - description: 'Archive a chat', - examples: ['beeper chats archive --chat 10313'], - }, - { - command: 'chats unarchive', - description: 'Unarchive a chat', - examples: ['beeper chats unarchive --chat 10313'], - }, - { - command: 'chats pin', - description: 'Pin a chat', - examples: ['beeper chats pin --chat 10313'], - }, - { - command: 'chats unpin', - description: 'Unpin a chat', - examples: ['beeper chats unpin --chat 10313'], - }, - { - command: 'chats mute', - description: 'Mute a chat', - examples: ['beeper chats mute --chat 10313'], - }, - { - command: 'chats unmute', - description: 'Unmute a chat', - examples: ['beeper chats unmute --chat 10313'], - }, - { - command: 'chats mark-read', - description: 'Mark a chat as read', - examples: ['beeper chats mark-read --chat 10313'], - }, - { - command: 'chats mark-unread', - description: 'Mark a chat as unread', - examples: ['beeper chats mark-unread --chat 10313'], - }, - { - command: 'chats priority', - description: 'Move a chat to the Inbox or Low Priority', - examples: [ - 'beeper chats priority --chat 10313 --level inbox', - 'beeper chats priority --chat \'!plUOsWkvMmJmJPVAjS:beeper.com\' --level low', - ], - }, - { - command: 'chats notify-anyway', - description: 'Send an iMessage Notify Anyway alert', - examples: ['beeper chats notify-anyway --chat 10313'], - }, - { - command: 'chats rename', - description: 'Rename a chat', - examples: ['beeper chats rename --chat 10313 --title "Family"'], - }, - { - command: 'chats description', - description: 'Set a chat description', - examples: [ - 'beeper chats description --chat 10313 --description "Engineering chat"', - 'beeper chats description --chat 10313 --clear', - ], - }, - { - command: 'chats avatar', - description: 'Set a chat avatar', - examples: ['beeper chats avatar --chat 10313 --file ./team.png'], - }, - { - command: 'chats draft', - description: 'Set or clear a chat draft', - examples: [ - 'beeper chats draft --chat 10313 --text "on my way"', - 'beeper chats draft --chat 10313 --clear', - ], - }, - { - command: 'chats disappear', - description: 'Set disappearing-message expiry', - examples: ['beeper chats disappear --chat 10313 --seconds 86400'], - }, - { - command: 'chats remind', - description: 'Set a chat reminder', - examples: [ - 'beeper chats remind --chat 10313 --when 2026-06-01T09:00:00Z', - 'beeper chats remind --chat 10313 --when 2026-06-01T09:00:00Z --dismiss-on-message', - ], - }, - { - command: 'chats unremind', - description: 'Clear a chat reminder', - examples: ['beeper chats unremind --chat 10313'], - }, - { - command: 'chats focus', - description: 'Focus Beeper Desktop on a chat', - examples: ['beeper chats focus --chat 10313'], - }, - { - command: 'messages list', - description: 'List chat messages', - examples: [ - 'beeper messages list --chat 10313 --limit 50', - 'beeper messages list --chat 10313 --before-cursor "" --limit 100', - 'beeper messages list --chat 10313 --sender me --asc', - ], - }, - { - command: 'messages search', - description: 'Search messages across chats', - examples: [ - 'beeper messages search invoice', - 'beeper search invoice --format jsonl --select id,chatID,text', - 'beeper messages search --chat 10313 --sender me --media image', - 'beeper messages search "flight" --after 2026-01-01 --before 2026-02-01', - ], - }, - { - command: 'messages show', - description: 'Show one message', - examples: ['beeper messages show --chat 10313 --id '], - }, - { - command: 'messages context', - description: 'Show message context', - examples: ['beeper messages context --chat 10313 --id --before 5 --after 5'], - }, - { - command: 'messages edit', - description: 'Edit a message', - examples: ['beeper messages edit --chat 10313 --id --message "fixed"'], - }, - { - command: 'messages delete', - description: 'Delete a message', - examples: ['beeper messages delete --chat 10313 --id --for-everyone'], - }, - { - command: 'messages export', - description: 'Export one chat to JSON', - examples: [ - 'beeper messages export --chat 10313 --output chat.json', - 'beeper messages export --chat 8951 --after 2026-01-01T00:00:00Z --output -', - 'beeper messages export --chat \'!plUOsWkvMmJmJPVAjS:beeper.com\' --before-cursor "" --limit 500', - ], - }, - { - command: 'send text', - description: 'Send a text message', - examples: [ - 'beeper send --to 10313 --message "on my way" --dry-run --format json', - 'beeper send text --to 10313 --message "on my way"', - 'beeper send text --to 8951 --message "hi"', - 'beeper send text --to "Family" --message "hi" --pick 1', - ], - }, - { - command: 'send file', - description: 'Send a file', - examples: ['beeper send file --to 8951 --file ./photo.jpg --caption "from today"'], - }, - { - command: 'send react', - description: 'Send a reaction to a message', - examples: ['beeper send react --to 10313 --id --reaction "+1"'], - }, - { - command: 'send sticker', - description: 'Send a sticker', - examples: ['beeper send sticker --to 10313 --file ./hi.webp'], - }, - { - command: 'send unreact', - description: 'Remove a reaction from a message', - examples: ['beeper send unreact --to 10313 --id --reaction "+1"'], - }, - { - command: 'send voice', - description: 'Send a voice note', - examples: [ - 'beeper send voice --to 10313 --file ./note.ogg', - 'beeper send voice --to 10313 --file ./note.ogg --duration 12', - ], - }, - { - command: 'presence', - description: 'Send a typing (or paused) indicator to a chat', - examples: [ - 'beeper presence --chat 10313', - 'beeper presence --chat 10313 --state paused', - 'beeper presence --chat 10313 --duration 5', - ], - }, - { - command: 'contacts list', - description: 'List contacts', - examples: ['beeper contacts list --account whatsapp --query alice'], - }, - { - command: 'contacts search', - description: 'Search contacts', - examples: ['beeper contacts search alice'], - }, - { - command: 'contacts show', - description: 'Show contact details', - examples: ['beeper contacts show "Alice" --account whatsapp'], - }, - { - command: 'resolve chat', - description: 'Resolve a chat selector to concrete chat candidates', - examples: ['beeper resolve chat Family --format json', 'beeper resolve chat Family --pick 1 --results-only'], - }, - { - command: 'resolve account', - description: 'Resolve an account selector', - examples: ['beeper resolve account whatsapp --format json'], - }, - { - command: 'resolve contact', - description: 'Resolve a contact selector', - examples: ['beeper resolve contact Alice --account whatsapp --format json'], - }, - { - command: 'resolve target', - description: 'Resolve a target selector', - examples: ['beeper resolve target desktop --format json'], - }, - { - command: 'resolve bridge', - description: 'Resolve a bridge selector', - examples: ['beeper resolve bridge whatsapp --format json'], - }, - { - command: 'media download', - description: 'Download message media', - examples: [ - 'beeper media download mxc://beeper.com/abc --out ./downloads', - 'beeper media download mxc://beeper.com/abc -o - > photo.jpg', - ], - }, - { - command: 'export', - description: 'Export accounts, chats, messages, Markdown transcripts, and attachments', - examples: ['beeper export --out ./beeper-export', 'beeper export --chat 10313 --out ./chat'], - }, - { - command: 'watch', - description: 'Stream Desktop API WebSocket events', - examples: [ - 'beeper watch', - 'beeper watch --chat 10313 --json', - 'beeper watch --include-type message.upserted --include-type message.deleted', - 'beeper watch --webhook https://example.com/hook --webhook-secret "$BEEPER_WEBHOOK_SECRET"', - ], - }, - { - command: 'rpc', - description: 'Run newline-delimited JSON command RPC over stdin/stdout', - examples: ['printf \'{"id":1,"command":"chats list --json"}\\n\' | beeper rpc'], - }, - { - command: 'man', - description: 'Print the command manual', - examples: ['beeper man', 'beeper man --format json', 'beeper man --format ids'], - }, - { - command: 'schema', - description: 'Print machine-readable command/flag schema', - examples: [ - 'beeper schema', - 'beeper schema send --results-only', - 'beeper schema --select commands.path,commands.flags.name --results-only', - ], - }, - { - command: 'doctor', - description: 'Probe the target live and report diagnostics', - examples: ['beeper doctor', 'beeper doctor --json'], - }, - { - command: 'status', - description: 'Show selected target and setup readiness', - examples: ['beeper status', 'beeper status --json'], - }, - { - command: 'docs', - description: 'Open Beeper CLI docs', - examples: ['beeper docs'], - }, - { - command: 'version', - description: 'Print CLI version', - examples: ['beeper version'], - }, - { - command: 'completion', - description: 'Print shell completion setup', - examples: ['beeper completion'], - }, - { - command: 'plugins', - description: 'Manage Beeper CLI plugins', - examples: ['beeper plugins', 'beeper plugins install @beeper/cli-plugin-cloudflare'], - }, - { - command: 'plugins available', - description: 'List recommended optional Beeper CLI plugins', - examples: ['beeper plugins available', 'beeper plugins available --json'], - }, - { - command: 'update', - description: 'Check and install Beeper updates', - examples: ['beeper update --check', 'beeper update --cli', 'beeper update --server'], - }, - { - command: 'config get', - description: 'Print CLI configuration', - examples: ['beeper config get', 'beeper config get defaultTarget'], - }, - { - command: 'config set', - description: 'Set a CLI configuration value', - examples: ['beeper config set defaultTarget work'], - }, - { - command: 'config path', - description: 'Print the CLI config path', - examples: ['beeper config path'], - }, - { - command: 'config reset', - description: 'Reset CLI configuration', - examples: ['beeper config reset'], - }, - { - command: 'api get', - description: 'Call a raw Desktop API GET path', - examples: ['beeper api get /v1/info', 'beeper api get /v1/chats --json'], - }, - { - command: 'api post', - description: 'Call a raw Desktop API POST path with a JSON body', - examples: ['beeper api post /v1/chats/abc/read --body \'{"messageID":"x"}\''], - }, - { - command: 'api request', - description: 'Call a raw Desktop API path with any supported HTTP method', - examples: ['beeper api request DELETE /v1/chats/abc/messages/def/reactions --body \'{"reactionKey":"👍"}\''], - }, -] diff --git a/packages/cli/src/lib/oauth.ts b/packages/cli/src/lib/oauth.ts index 22fbf983..7ddae58c 100644 --- a/packages/cli/src/lib/oauth.ts +++ b/packages/cli/src/lib/oauth.ts @@ -1,26 +1,28 @@ +import { createHash, randomBytes } from 'node:crypto' import { createServer } from 'node:http' import { AddressInfo } from 'node:net' import { spawn } from 'node:child_process' -import { createPKCEPair, createState } from './pkce.js' -import { updateConfig, type AuthSource } from './targets.js' -export type OAuthLoginOptions = { +type OAuthLoginOptions = { baseURL: string clientName: string openBrowser: boolean - save?: boolean scope: string - source?: AuthSource timeoutMs?: number } +type PKCEPair = { + codeChallenge: string + codeVerifier: string +} + type RegisterResponse = { authorization_endpoint?: string client_id: string token_endpoint?: string } -type TokenResponse = { +export type TokenResponse = { access_token: string expires_in?: number scope?: string @@ -59,27 +61,22 @@ export async function loginWithPKCE(options: OAuthLoginOptions): Promise ({ - ...config, - baseURL: options.baseURL, - auth: { - accessToken: token.access_token, - clientID: registered.client_id, - expiresAt: token.expires_in ? new Date(Date.now() + token.expires_in * 1000).toISOString() : undefined, - scope: token.scope, - source: options.source, - tokenType: token.token_type, - }, - })) - } - return { ...token, clientID: registered.client_id } } finally { await callback.close() } } +function createPKCEPair(): PKCEPair { + const codeVerifier = randomBytes(64).toString('base64url') + const codeChallenge = createHash('sha256').update(codeVerifier).digest('base64url') + return { codeChallenge, codeVerifier } +} + +function createState(): string { + return randomBytes(24).toString('base64url') +} + async function registerClient(baseURL: string, clientName: string, redirectURI: string, scope: string): Promise { const response = await fetch(new URL('/oauth/register', baseURL), { method: 'POST', diff --git a/packages/cli/src/lib/output.ts b/packages/cli/src/lib/output.ts deleted file mode 100644 index 2f836aaf..00000000 --- a/packages/cli/src/lib/output.ts +++ /dev/null @@ -1,292 +0,0 @@ -import type { StreamController, Suggestion } from './ink/render.js' - -export type OutputFormat = 'human' | 'json' | 'jsonl' | 'table' | 'text' | 'ids' -type RecordValue = Record - -const writeJSON = (value: unknown, format: 'json' | 'jsonl'): void => { - process.stdout.write(`${JSON.stringify(value, null, format === 'json' ? 2 : 0)}\n`) -} - -const envelope = (data: unknown, meta: Record = {}) => ({ ok: true, data, error: null, meta }) - -const loadInk = () => import('./ink/render.js') - -export async function printData(value: unknown, format: OutputFormat): Promise { - format = effectiveFormat(format) - if (format === 'ids') { - printIDs(Array.isArray(value) ? value : [value]) - return - } - if (format === 'json') { - writeJSON(jsonPayload(value), 'json') - return - } - if (format === 'jsonl') { - value = projectJSON(value) - if (Array.isArray(value)) { - for (const item of value) process.stdout.write(`${JSON.stringify(item)}\n`) - return - } - process.stdout.write(`${JSON.stringify(value)}\n`) - return - } - if (format === 'text') { - printText(value) - return - } - const { renderValue } = await loadInk() - await renderValue(value) -} - -export async function printDryRun(action: string, request: Record, format: OutputFormat): Promise { - await printData({ dryRun: true, action, request }, format) -} - -export async function printList( - value: unknown[], - format: OutputFormat, - empty: { title: string; subtitle?: string; suggestions?: Suggestion[] }, -): Promise { - format = effectiveFormat(format) - if (format === 'ids') { - printIDs(value) - return - } - if (format === 'json') { - writeJSON(jsonPayload(value), 'json') - return - } - if (format === 'jsonl') { - const projected = projectJSON(value) - for (const item of Array.isArray(projected) ? projected : [projected]) process.stdout.write(`${JSON.stringify(item)}\n`) - return - } - if (format === 'text') { - printText(value) - return - } - const { renderList } = await loadInk() - await renderList(value as RecordValue[], empty) -} - -export async function collectPage(iterable: AsyncIterable, limit?: number): Promise { - if (limit !== undefined && limit <= 0) return [] - const items: T[] = [] - for await (const item of iterable) { - items.push(item) - if (limit !== undefined && items.length >= limit) break - } - return items -} - -export function printIDs(values: unknown[]): void { - for (const value of values) { - if (!value || typeof value !== 'object') continue - const record = value as Record - const id = record.localChatID ?? record.rowID ?? record.id ?? record.chatID ?? record.messageID - if (id) process.stdout.write(`${String(id)}\n`) - } -} - -export async function emptyState(opts: { title: string; subtitle?: string; suggestions?: Suggestion[] }): Promise { - const { renderEmptyState } = await loadInk() - await renderEmptyState(opts) -} - -export async function printSuccess( - opts: { message: string; detail?: string; entity?: unknown; data?: Record }, - format: OutputFormat, -): Promise { - format = effectiveFormat(format) - if (format === 'json' || format === 'jsonl') { - writeJSON(jsonPayload({ message: opts.message, detail: opts.detail, entity: opts.entity, ...opts.data }), format) - return - } - if (format === 'ids') { - printIDs([opts.entity ?? opts.data ?? {}]) - return - } - if (format === 'text') { - process.stdout.write(`${opts.message}${opts.detail ? `\t${opts.detail}` : ''}\n`) - return - } - if (process.env.BEEPER_QUIET === '1') return - const { renderSuccess } = await loadInk() - await renderSuccess(opts) -} - -export async function printFailure( - opts: { message: string; detail?: string; data?: Record }, - format: OutputFormat, -): Promise { - format = effectiveFormat(format) - if (format === 'json' || format === 'jsonl') { - writeJSON({ ok: false, data: opts.data ?? null, error: { message: opts.message, detail: opts.detail } }, format) - return - } - if (format === 'text') { - process.stdout.write(`${opts.message}${opts.detail ? `\t${opts.detail}` : ''}\n`) - return - } - if (format === 'ids') { - if (opts.data) printIDs([opts.data]) - return - } - const { renderFailure } = await loadInk() - await renderFailure(opts) -} - -export async function printConfig(data: Record, format: OutputFormat): Promise { - format = effectiveFormat(format) - if (format === 'json' || format === 'jsonl') { - writeJSON(jsonPayload(data), format) - return - } - if (format === 'ids') { - printIDs([data]) - return - } - if (format === 'text') { - printText(data) - return - } - const { renderConfig } = await loadInk() - await renderConfig(data) -} - -export async function printCommands( - items: Array<{ command: string; description: string; group?: string }>, - format: OutputFormat, - opts?: { title?: string; intro?: string[] }, -): Promise { - format = effectiveFormat(format) - if (format === 'json' || format === 'jsonl') { - writeJSON(jsonPayload(items, opts ? { title: opts.title } : {}), format) - return - } - if (format === 'ids') { - for (const item of items) process.stdout.write(`${item.command}\n`) - return - } - if (format === 'text') { - for (const item of items) process.stdout.write(`${item.command}\t${item.description}\n`) - return - } - const { renderCommands } = await loadInk() - await renderCommands(items, opts) -} - -export async function startStream(opts: { baseURL: string; subscribed: string[] }): Promise { - const { renderStream } = await loadInk() - return renderStream(opts) -} - -export type { Suggestion } from './ink/render.js' - -export function isMachineReadableOutput(format?: OutputFormat): boolean { - const effective = effectiveFormat(format ?? 'human') - return effective === 'json' || effective === 'jsonl' || effective === 'ids' || effective === 'text' -} - -function effectiveFormat(format: OutputFormat): OutputFormat { - const env = process.env.BEEPER_OUTPUT_FORMAT as OutputFormat | undefined - if (env && ['json', 'jsonl', 'table', 'text', 'ids'].includes(env)) return env === 'table' ? 'human' : env - return format === 'table' ? 'human' : format -} - -function jsonPayload(value: unknown, meta: Record = {}): unknown { - const projected = projectJSON(value) - if (process.env.BEEPER_OUTPUT_RESULTS_ONLY === '1') return unwrapPrimary(projected) - return envelope(projected, meta) -} - -function projectJSON(value: unknown): unknown { - const fields = (process.env.BEEPER_OUTPUT_SELECT ?? '') - .split(',') - .map(item => item.trim()) - .filter(Boolean) - let output = value - if (process.env.BEEPER_OUTPUT_RESULTS_ONLY === '1') output = unwrapPrimary(output) - if (!fields.length) return output - return selectFields(output, fields) -} - -function unwrapPrimary(value: unknown): unknown { - if (!value || typeof value !== 'object' || Array.isArray(value)) return value - const record = value as Record - if ('items' in record) return record.items - if ('results' in record) return record.results - if ('data' in record) return record.data - const metaKeys = new Set(['nextCursor', 'nextPageToken', 'cursor', 'hasMore', 'count', 'query']) - const keys = Object.keys(record).filter(key => !metaKeys.has(key)) - if (keys.length === 1) return record[keys[0]!] - return value -} - -function selectFields(value: unknown, fields: string[]): unknown { - if (Array.isArray(value)) return value.map(item => selectFields(item, fields)) - if (!value || typeof value !== 'object') return value - const out: Record = {} - for (const field of fields) { - const selected = selectPath(value, field.split('.')) - if (selected !== undefined) mergeSelected(out, selected) - } - return out -} - -function selectPath(value: unknown, parts: string[]): unknown { - if (!parts.length) return value - if (Array.isArray(value)) { - const items = value.map(item => selectPath(item, parts)).filter(item => item !== undefined) - return items.length ? items : undefined - } - if (!value || typeof value !== 'object') return undefined - const [part, ...rest] = parts - if (!part) return undefined - const child = (value as Record)[part] - if (child === undefined) return undefined - const selected = selectPath(child, rest) - return selected === undefined ? undefined : { [part]: selected } -} - -function mergeSelected(target: Record, selected: unknown): void { - if (!selected || typeof selected !== 'object' || Array.isArray(selected)) return - for (const [key, value] of Object.entries(selected as Record)) { - const current = target[key] - if (Array.isArray(value)) { - const currentItems = Array.isArray(current) ? current : [] - target[key] = value.map((item, index) => { - const base = currentItems[index] - if (item && typeof item === 'object' && !Array.isArray(item) && base && typeof base === 'object' && !Array.isArray(base)) { - return mergeObjects(base as Record, item as Record) - } - return item - }) - } else if (value && typeof value === 'object' && !Array.isArray(value) && current && typeof current === 'object' && !Array.isArray(current)) { - target[key] = mergeObjects(current as Record, value as Record) - } else { - target[key] = value - } - } -} - -function mergeObjects(left: Record, right: Record): Record { - const out = { ...left } - mergeSelected(out, right) - return out -} - -function printText(value: unknown): void { - if (Array.isArray(value)) { - for (const item of value) printText(item) - return - } - if (!value || typeof value !== 'object') { - if (value !== undefined) process.stdout.write(`${String(value)}\n`) - return - } - for (const [key, item] of Object.entries(value as Record)) { - if (item === null || item === undefined) continue - process.stdout.write(`${key}\t${typeof item === 'object' ? JSON.stringify(item) : String(item)}\n`) - } -} diff --git a/packages/cli/src/lib/paging.ts b/packages/cli/src/lib/paging.ts new file mode 100644 index 00000000..40f48111 --- /dev/null +++ b/packages/cli/src/lib/paging.ts @@ -0,0 +1,9 @@ +export async function collectPage(iterable: AsyncIterable, limit?: number): Promise { + if (limit !== undefined && limit <= 0) return [] + const items: T[] = [] + for await (const item of iterable) { + items.push(item) + if (limit !== undefined && items.length >= limit) break + } + return items +} diff --git a/packages/cli/src/lib/pkce.ts b/packages/cli/src/lib/pkce.ts deleted file mode 100644 index 34cac242..00000000 --- a/packages/cli/src/lib/pkce.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { createHash, randomBytes } from 'node:crypto' - -export type PKCEPair = { - codeChallenge: string - codeVerifier: string -} - -export function createPKCEPair(): PKCEPair { - const codeVerifier = randomBytes(64).toString('base64url') - const codeChallenge = createHash('sha256').update(codeVerifier).digest('base64url') - return { codeChallenge, codeVerifier } -} - -export function createState(): string { - return randomBytes(24).toString('base64url') -} diff --git a/packages/cli/src/lib/profiles.ts b/packages/cli/src/lib/profiles.ts index 38437c03..38dd0c0c 100644 --- a/packages/cli/src/lib/profiles.ts +++ b/packages/cli/src/lib/profiles.ts @@ -1,17 +1,17 @@ import { spawn } from 'node:child_process' import { execFile } from 'node:child_process' import { closeSync, openSync } from 'node:fs' -import { mkdir, readFile, rm, writeFile } from 'node:fs/promises' +import { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises' import { homedir } from 'node:os' import { join } from 'node:path' +import { setTimeout as sleep } from 'node:timers/promises' import { promisify } from 'node:util' -import { beeperDir, pathExists, type Target } from './targets.js' -import { readInstallations } from './installations.js' -import { usageError } from './errors.js' +import { beeperDir, type Target } from './targets.js' +import { readInstallations, type Installations } from './installations.js' const execFileAsync = promisify(execFile) -export type ProfileRun = { +type ProfileRun = { id: string pid: number startedAt: string @@ -19,23 +19,17 @@ export type ProfileRun = { errorLog: string } -export const profileRunDir = () => join(beeperDir(), 'run', 'profiles') -export const profileLogDir = () => join(beeperDir(), 'logs', 'profiles') -export const profileRunPath = (id: string) => join(profileRunDir(), `${id}.json`) +const profileRunDir = () => join(beeperDir(), 'run', 'profiles') +const profileLogDir = () => join(beeperDir(), 'logs', 'profiles') +const profileRunPath = (id: string) => join(profileRunDir(), `${id}.json`) export const profileLogPath = (id: string) => join(profileLogDir(), `${id}.log`) export const profileErrorLogPath = (id: string) => join(profileLogDir(), `${id}.err.log`) -export function assertProfile(target: Target): void { - if (!target.managed || !target.dataDir) throw new Error(`Target "${target.id}" is not a local profile.`) +function assertProfile(target: Target): void { + if (!target.dataDir) throw new Error(`Target "${target.id}" is not a local profile.`) } -export function assertServerProfile(target: Target): void { - if (!target.managed || !target.dataDir || target.type !== 'server') { - throw usageError(`Target "${target.id}" is not a local Beeper Server install.`) - } -} - -export function defaultDesktopDataDir(profile?: string): string { +function defaultDesktopDataDir(profile?: string): string { const appName = `BeeperTexts${profile ? `-${profile}` : ''}` if (process.platform === 'darwin') return join(homedir(), 'Library', 'Application Support', appName) if (process.platform === 'win32') return process.env.APPDATA ? join(process.env.APPDATA, appName) : join(homedir(), appName) @@ -48,13 +42,12 @@ export function desktopLogDir(target?: Target): string { export async function startProfile(target: Target): Promise { assertProfile(target) - if (target.type === 'desktop') return startDesktopProfile(target) + if (target.type === 'desktop') return launchDesktopApp(target) return startServerProfile(target) } export async function launchDesktopApp(target?: Target): Promise<{ id: string; startedAt: string }> { - const installations = await readInstallations().catch(() => ({ desktop: undefined })) - const appPath = installations.desktop?.path ?? await findDesktopAppPath() + const appPath = await findDesktopAppPath() const args = appPath ? ['-n', appPath, '--args'] : ['-n', '-a', 'Beeper', '--args'] args.push('--no-enforce-app-location') if (target?.port) args.push(`--pas-port=${target.port}`) @@ -71,8 +64,8 @@ export async function launchDesktopApp(target?: Target): Promise<{ id: string; s return { id: target?.id ?? 'desktop', startedAt: new Date().toISOString() } } -export async function findDesktopAppPath(): Promise { - const installations = await readInstallations().catch(() => ({ desktop: undefined })) +export async function findDesktopAppPath(installations?: Installations): Promise { + installations ??= await readInstallations().catch(() => ({})) if (installations.desktop?.path && await isBeeperDesktopApp(installations.desktop.path)) return installations.desktop.path if (process.platform === 'darwin') { @@ -91,13 +84,13 @@ export async function findDesktopAppPath(): Promise { join(localAppData, 'Programs', 'Beeper Nightly', 'Beeper Nightly.exe'), ] for (const path of candidates) { - if (await pathExists(path)) return path + if (await access(path).then(() => true, () => false)) return path } } if (process.platform === 'linux') { for (const path of ['/usr/bin/beeper', '/usr/local/bin/beeper']) { - if (await pathExists(path)) return path + if (await access(path).then(() => true, () => false)) return path } } @@ -105,7 +98,7 @@ export async function findDesktopAppPath(): Promise { } async function isBeeperDesktopApp(path: string): Promise { - if (!await pathExists(path)) return false + if (!await access(path).then(() => true, () => false)) return false if (process.platform !== 'darwin') return true const bundleID = await readBundleID(path) return bundleID === 'com.automattic.beeper.desktop' || bundleID === 'com.automattic.beeper.desktop.nightly' @@ -145,51 +138,7 @@ export async function stopProfile(target: Target): Promise { await rm(profileRunPath(target.id), { force: true }) } -export async function profileStatus(target: Target): Promise> { - assertProfile(target) - const run = await readRun(target.id) - const reachable = await isReachable(target) - return { - id: target.id, - type: target.type, - url: target.baseURL, - running: reachable || !!run && isRunning(run.pid), - pid: run?.pid, - startedAt: run?.startedAt, - log: run?.log, - errorLog: run?.errorLog, - } -} - -export async function enableProfile(target: Target): Promise { - assertProfile(target) - if (target.type !== 'server') throw new Error('Manage Desktop start at launch in Beeper Desktop.') - if (process.platform === 'darwin') return enableLaunchAgent(target) - if (process.platform === 'linux') return enableSystemdUnit(target) - throw new Error('Beeper Server is not available on Windows.') -} - -export async function disableProfile(target: Target): Promise { - assertProfile(target) - if (target.type !== 'server') throw new Error('Manage Desktop start at launch in Beeper Desktop.') - if (process.platform === 'darwin') { - const path = join(process.env.HOME ?? beeperDir(), 'Library', 'LaunchAgents', launchAgentName(target)) - await execFileAsync('launchctl', ['bootout', `gui/${process.getuid?.() ?? 501}`, path]).catch(() => undefined) - await execFileAsync('launchctl', ['disable', `gui/${process.getuid?.() ?? 501}/${launchAgentLabel(target)}`]).catch(() => undefined) - await rm(path, { force: true }) - return path - } - if (process.platform === 'linux') { - const path = join(process.env.HOME ?? beeperDir(), '.config', 'systemd', 'user', systemdUnitName(target)) - await execFileAsync('systemctl', ['--user', 'disable', '--now', systemdUnitName(target)]).catch(() => undefined) - await execFileAsync('systemctl', ['--user', 'daemon-reload']).catch(() => undefined) - await rm(path, { force: true }) - return path - } - throw new Error('Beeper Server is not available on Windows.') -} - -export async function readRun(id: string): Promise { +async function readRun(id: string): Promise { try { return JSON.parse(await readFile(profileRunPath(id), 'utf8')) as ProfileRun } catch (error) { @@ -198,10 +147,6 @@ export async function readRun(id: string): Promise { } } -async function startDesktopProfile(target: Target): Promise<{ id: string; startedAt: string }> { - return launchDesktopApp(target) -} - async function startServerProfile(target: Target): Promise { const current = await readRun(target.id) if (current) { @@ -260,98 +205,6 @@ function isRunning(pid: number): boolean { } } -async function writeLaunchAgent(target: Target): Promise { - const installations = await readInstallations() - const binary = process.env.BEEPER_SERVER_BIN || installations.server?.path - if (!binary) throw new Error('Beeper Server is not installed. Run: beeper install server') - const dir = join(process.env.HOME ?? beeperDir(), 'Library', 'LaunchAgents') - await mkdir(dir, { recursive: true }) - const path = join(dir, launchAgentName(target)) - await writeFile(path, launchAgentPlist(target, binary), 'utf8') - return path -} - -async function enableLaunchAgent(target: Target): Promise { - const path = await writeLaunchAgent(target) - await mkdir(profileLogDir(), { recursive: true }) - const service = `gui/${process.getuid?.() ?? 501}` - await execFileAsync('launchctl', ['bootout', service, path]).catch(() => undefined) - await execFileAsync('launchctl', ['bootstrap', service, path]) - await execFileAsync('launchctl', ['enable', `${service}/${launchAgentLabel(target)}`]) - await execFileAsync('launchctl', ['kickstart', '-k', `${service}/${launchAgentLabel(target)}`]).catch(() => undefined) - return path -} - -async function enableSystemdUnit(target: Target): Promise { - const path = await writeSystemdUnit(target) - await mkdir(profileLogDir(), { recursive: true }) - await execFileAsync('systemctl', ['--user', 'daemon-reload']) - await execFileAsync('systemctl', ['--user', 'enable', '--now', systemdUnitName(target)]) - return path -} - -async function writeSystemdUnit(target: Target): Promise { - const installations = await readInstallations() - const binary = process.env.BEEPER_SERVER_BIN || installations.server?.path - if (!binary) throw new Error('Beeper Server is not installed. Run: beeper install server') - const dir = join(process.env.HOME ?? beeperDir(), '.config', 'systemd', 'user') - await mkdir(dir, { recursive: true }) - const path = join(dir, systemdUnitName(target)) - await writeFile(path, systemdUnit(target, binary), 'utf8') - return path -} - -function launchAgentName(target: Target): string { - return `${launchAgentLabel(target)}.plist` -} - -function launchAgentLabel(target: Target): string { - return `com.beeper.cli.profile.${target.id}` -} - -function systemdUnitName(target: Target): string { - return `beeper-profile-${target.id}.service` -} - -function launchAgentPlist(target: Target, binary: string): string { - return ` - - -Label${escapeXML(launchAgentLabel(target))} -ProgramArguments${[binary, ...serverArgs(target)].map(arg => `${escapeXML(arg)}`).join('')} -EnvironmentVariablesBEEPER_SERVER_DATA_DIR${escapeXML(target.dataDir!)} -RunAtLoad -KeepAlive -StandardOutPath${escapeXML(profileLogPath(target.id))} -StandardErrorPath${escapeXML(profileErrorLogPath(target.id))} - -` -} - -function systemdUnit(target: Target, binary: string): string { - return `[Unit] -Description=Beeper profile ${target.id} - -[Service] -ExecStart=${[binary, ...serverArgs(target)].map(systemdQuote).join(' ')} -Restart=always -Environment=BEEPER_SERVER_DATA_DIR=${systemdQuote(target.dataDir!)} -StandardOutput=append:${profileLogPath(target.id)} -StandardError=append:${profileErrorLogPath(target.id)} - -[Install] -WantedBy=default.target -` -} - -function escapeXML(value: string): string { - return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>') -} - -function systemdQuote(value: string): string { - return value.includes(' ') ? `"${value.replaceAll('"', '\\"')}"` : value -} - async function isReachable(target: Target): Promise { return fetch(new URL('/v1/info', target.baseURL), { signal: AbortSignal.timeout(1000) }) .then(response => response.ok) @@ -375,7 +228,3 @@ async function waitForExit(pid: number, timeoutMs: number): Promise { } return false } - -async function sleep(ms: number): Promise { - await new Promise(resolve => setTimeout(resolve, ms)) -} diff --git a/packages/cli/src/lib/prompts.ts b/packages/cli/src/lib/prompts.ts new file mode 100644 index 00000000..d3c84d68 --- /dev/null +++ b/packages/cli/src/lib/prompts.ts @@ -0,0 +1,35 @@ +import { stdin as input, stdout as defaultOutput } from 'node:process' +import { createInterface } from 'node:readline/promises' +import type { Writable } from 'node:stream' + +export async function promptText(label: string, output: Writable = defaultOutput): Promise { + const rl = createInterface({ input, output }) + try { + return (await rl.question(label)).trim() + } finally { + rl.close() + } +} + +export async function promptConfirm(label: string, defaultYes = false, output?: Writable): Promise { + const value = (await promptText(`${label} ${defaultYes ? '[Y/n]' : '[y/N]'} `, output)).toLowerCase() + if (!value) return defaultYes + return value === 'y' || value === 'yes' +} + +export async function promptChoice( + label: string, + choices: string[], + options: { defaultValue?: string; output?: Writable } = {}, +): Promise { + if (!choices.length) throw new Error('promptChoice requires at least one choice') + const out = options.output ?? defaultOutput + for (;;) { + const answer = await promptText(label, out) + const value = answer || options.defaultValue + const index = value && /^\d+$/.test(value) ? Number.parseInt(value, 10) : 0 + if (index >= 1 && index <= choices.length) return choices[index - 1]! + if (value && choices.includes(value)) return value + out.write(`Choose one of: ${choices.join(', ')}\n`) + } +} diff --git a/packages/cli/src/lib/recommended-plugins.ts b/packages/cli/src/lib/recommended-plugins.ts deleted file mode 100644 index a924aa04..00000000 --- a/packages/cli/src/lib/recommended-plugins.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type RecommendedPlugin = { - commands: string[] - description: string - install: string - name: string -} - -export const recommendedPlugins: RecommendedPlugin[] = [ - { - commands: ['targets tunnel'], - description: 'Expose a selected Beeper target through Cloudflare Tunnel', - install: 'beeper plugins install @beeper/cli-plugin-cloudflare', - name: '@beeper/cli-plugin-cloudflare', - }, -] diff --git a/packages/cli/src/lib/resolve.ts b/packages/cli/src/lib/resolve.ts index 4414df5b..2bc6dcaf 100644 --- a/packages/cli/src/lib/resolve.ts +++ b/packages/cli/src/lib/resolve.ts @@ -1,19 +1,18 @@ +import { apiItems, apiRecord, type APIRecord } from './api-values.js' import { readConfig } from './targets.js' -import { ambiguous, notFound } from './errors.js' -import { confirmSuggestion, declineWithExit127, rankSuggestions } from './did-you-mean.js' -import { collectPage } from './output.js' +import { AbortError, CLIError, ExitCodes } from './errors.js' +import { collectPage } from './paging.js' +import { promptConfirm } from './prompts.js' -type AnyRecord = Record - -export type AccountResolutionOptions = { +type AccountResolutionOptions = { allowMultiplePerInput?: boolean applyDefault?: boolean } -export type ChatResolutionOptions = { +type ChatResolutionOptions = { accountIDs?: string[] + noInput?: boolean pick?: number - assumeYes?: boolean } export async function resolveAccountIDs( @@ -23,24 +22,22 @@ export async function resolveAccountIDs( ): Promise { let effectiveInputs = inputs if (!effectiveInputs?.length && options.applyDefault !== false) { - const config = await readConfig().catch(() => ({} as { defaultAccount?: string })) + const config = await readConfig() if (config.defaultAccount) effectiveInputs = [config.defaultAccount] } if (!effectiveInputs?.length) return undefined - const accounts = accountItems(await client.accounts.list()) + const accounts = apiItems(await client.accounts.list()) const resolved: string[] = [] for (const input of effectiveInputs) { const matches = matchAccounts(accounts, input) - if (matches.length === 0) throw notFound(`No account matches "${input}"`, { selector: input, kind: 'account' }) + if (matches.length === 0) { + throw new AbortError(`No account matches "${input}"`, ExitCodes.NotFound, undefined, 'not_found') + } if (matches.length > 1 && !options.allowMultiplePerInput) { - throw ambiguous(formatAmbiguous(`account "${input}"`, matches.map(formatAccount)), { - selector: input, - kind: 'account', - candidates: matches.map((account, index) => ({ pick: index + 1, id: String(account.accountID ?? account.id), label: formatAccount(account) })), - }) + throw new AbortError(formatAmbiguous(`account "${input}"`, matches.map(formatAccount)), ExitCodes.Ambiguous, 'Pass an exact ID or --pick N.', 'ambiguous_selector') } - resolved.push(...matches.map(account => String(account.accountID))) + resolved.push(...matches.map(accountIDOf).filter(Boolean)) } return Array.from(new Set(resolved)) @@ -48,13 +45,15 @@ export async function resolveAccountIDs( export async function resolveAccountID(client: any, input: string): Promise { const [accountID] = await resolveAccountIDs(client, [input]) ?? [] - if (!accountID) throw notFound(`No account matches "${input}"`, { selector: input, kind: 'account' }) + if (!accountID) { + throw new AbortError(`No account matches "${input}"`, ExitCodes.NotFound, undefined, 'not_found') + } return accountID } export async function listAccountIDs(client: any): Promise { - const accounts = accountItems(await client.accounts.list()) - return accounts.map(account => String(account.accountID)).filter(Boolean) + const accounts = apiItems(await client.accounts.list()) + return accounts.map(accountIDOf).filter(Boolean) } export async function resolveChatID(client: any, input: string, options: ChatResolutionOptions = {}): Promise { @@ -62,55 +61,46 @@ export async function resolveChatID(client: any, input: string, options: ChatRes const exact = await retrieveChat(client, input) if (exact) return chatInputID(exact) - const candidates = await collectPage(client.chats.search({ + const candidates = await collectPage(client.chats.search({ accountIDs: options.accountIDs, query: input, scope: 'titles', }), 10) - const normalizedInput = normalize(input) + const normalizedInput = normalizeSelector(input) const exactMatches = candidates.filter(chat => - normalize(chat.id) === normalizedInput || - normalize(chat.localChatID) === normalizedInput || - normalize(chat.title) === normalizedInput + normalizeSelector(chat.id) === normalizedInput || + normalizeSelector(chat.localChatID) === normalizedInput || + normalizeSelector(chat.title) === normalizedInput ) const matches = exactMatches.length ? exactMatches : candidates if (matches.length === 0) { const suggestion = await suggestChat(client, input, options) if (suggestion) return suggestion - throw notFound(`No chat matches "${input}"`, { selector: input, kind: 'chat' }) + throw new AbortError(`No chat matches "${input}"`, ExitCodes.NotFound, undefined, 'not_found') } if (matches.length === 1) return chatInputID(matches[0]!) if (options.pick) { const selected = matches[options.pick - 1] - if (!selected) throw notFound(`--pick ${options.pick} is outside the ${matches.length} matching chats`, { selector: input, kind: 'chat', pick: options.pick, count: matches.length }) + if (!selected) { + throw new AbortError(`--pick ${options.pick} is outside the ${matches.length} matching chats`, ExitCodes.NotFound, undefined, 'not_found') + } return chatInputID(selected) } - throw ambiguous(formatAmbiguous(`chat "${input}"`, matches.map(formatChat)), { - selector: input, - kind: 'chat', - candidates: matches.map((chat, index) => ({ - pick: index + 1, - id: String(chat.id), - localChatID: chat.localChatID ? String(chat.localChatID) : undefined, - title: chat.title ? String(chat.title) : undefined, - network: chat.network ? String(chat.network) : undefined, - label: formatChat(chat), - })), - }) + throw new AbortError(formatAmbiguous(`chat "${input}"`, matches.map(formatChat)), ExitCodes.Ambiguous, 'Pass an exact ID or --pick N.', 'ambiguous_selector') } async function suggestChat(client: any, input: string, options: ChatResolutionOptions): Promise { - if (process.env.BEEPER_NO_INPUT === '1') return undefined - let pool: AnyRecord[] + if (options.noInput) return undefined + let pool: APIRecord[] try { - pool = await collectPage(client.chats.list({ accountIDs: options.accountIDs, limit: 100 }), 100) + pool = await collectPage(client.chats.list({ accountIDs: options.accountIDs, limit: 100 }), 100) } catch { return undefined } - const ranked = rankSuggestions(input, pool, chat => chat.title as string | undefined, 3) + const ranked = rankSuggestions(input, pool, chat => typeof chat.title === 'string' ? chat.title : undefined) const top = ranked[0] if (!top) return undefined const detail = top.value.network ? ` (${top.value.network})` : '' @@ -119,42 +109,74 @@ async function suggestChat(client: any, input: string, options: ChatResolutionOp for (const alt of ranked.slice(1)) { process.stderr.write(` also: ${alt.label}${alt.value.network ? ` (${alt.value.network})` : ''}\n`) } - const ok = await confirmSuggestion('use it?', { assumeYes: options.assumeYes, timeoutMs: 10_000 }) - if (!ok) declineWithExit127(`no chat selected for "${input}"`) + const ok = process.stdin.isTTY && process.stderr.isTTY + ? await promptConfirm('use it?', true, process.stderr) + : false + if (!ok) throw new CLIError(`no chat selected for "${input}"`, ExitCodes.CommandNotFound) return chatInputID(top.value) } -function accountItems(accounts: unknown): AnyRecord[] { - if (Array.isArray(accounts)) return accounts as AnyRecord[] - return ((accounts as { items?: AnyRecord[] }).items ?? []) +type Suggestion = { value: T; label: string; distance: number } + +function rankSuggestions(query: string, items: T[], labelOf: (item: T) => string | undefined): Suggestion[] { + const q = query.trim().toLowerCase() + const scored: Suggestion[] = [] + for (const item of items) { + const label = labelOf(item) + if (!label) continue + const l = label.toLowerCase() + const distance = Math.min(levenshtein(q, l), l.includes(q) ? Math.max(0, l.length - q.length) : Infinity) + if (Number.isFinite(distance)) scored.push({ value: item, label, distance }) + } + scored.sort((a, b) => a.distance - b.distance || a.label.length - b.label.length) + const cutoff = Math.max(3, Math.ceil(q.length * 0.6)) + return scored.filter(suggestion => suggestion.distance <= cutoff).slice(0, 3) +} + +function levenshtein(a: string, b: string): number { + if (a === b) return 0 + if (!a.length) return b.length + if (!b.length) return a.length + const matrix: number[][] = Array.from({ length: a.length + 1 }, (_, i) => [i, ...new Array(b.length).fill(0)]) + for (let j = 1; j <= b.length; j++) matrix[0]![j] = j + for (let i = 1; i <= a.length; i++) { + for (let j = 1; j <= b.length; j++) { + const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1 + matrix[i]![j] = Math.min(matrix[i - 1]![j]! + 1, matrix[i]![j - 1]! + 1, matrix[i - 1]![j - 1]! + cost) + } + } + return matrix[a.length]![b.length]! } -function matchAccounts(accounts: AnyRecord[], input: string): AnyRecord[] { - const normalizedInput = normalize(input) +function matchAccounts(accounts: APIRecord[], input: string): APIRecord[] { + const normalizedInput = normalizeSelector(input) const exact = accounts.filter(account => - normalize(account.accountID) === normalizedInput || - normalize(account.network) === normalizedInput || - normalize(account.bridge?.type) === normalizedInput || - normalize(account.bridge?.id) === normalizedInput || - normalize(account.user?.id) === normalizedInput || - normalize(account.user?.username) === normalizedInput || - normalize(account.user?.displayName) === normalizedInput || - normalize(account.user?.name) === normalizedInput || - normalize(account.user?.email) === normalizedInput + accountKeys(account).some(value => normalizeSelector(value) === normalizedInput) ) if (exact.length) return exact return accounts.filter(account => - includesNormalized(account.accountID, normalizedInput) || - includesNormalized(account.network, normalizedInput) || - includesNormalized(account.bridge?.type, normalizedInput) || - includesNormalized(account.bridge?.id, normalizedInput) || - includesNormalized(account.user?.displayName, normalizedInput) || - includesNormalized(account.user?.name, normalizedInput) + accountKeys(account).some(value => normalizeSelector(value).includes(normalizedInput)) ) } -async function retrieveChat(client: any, input: string): Promise { +function accountKeys(account: APIRecord): unknown[] { + const bridge = apiRecord(account.bridge) + const user = apiRecord(account.user) + return [ + account.accountID, + account.network, + bridge.type, + bridge.id, + user.id, + user.username, + user.displayName, + user.name, + user.email, + ] +} + +async function retrieveChat(client: any, input: string): Promise { try { return await client.chats.retrieve(input, { maxParticipantCount: 0 }) } catch (error) { @@ -164,36 +186,42 @@ async function retrieveChat(client: any, input: string): Promise ` ${index + 1}. ${choice}`).join('\n')}` } -function formatAccount(account: AnyRecord): string { +function formatAccount(account: APIRecord): string { + const bridge = apiRecord(account.bridge) + const user = apiRecord(account.user) const network = account.network ? ` ${account.network}` : '' - const bridge = account.bridge?.type ? ` ${account.bridge.type}` : '' - const user = account.user?.displayName || account.user?.name || account.user?.username || account.user?.id || '' - return `${account.accountID}${network}${bridge}${user ? ` ${user}` : ''}` + const bridgeName = bridge.type ? ` ${bridge.type}` : '' + const userName = user.displayName || user.name || user.username || user.id || '' + return `${accountIDOf(account) || account.id || ''}${network}${bridgeName}${userName ? ` ${userName}` : ''}` +} + +function accountIDOf(account: APIRecord): string { + return typeof account.accountID === 'string' && account.accountID + ? account.accountID + : typeof account.id === 'string' && account.id + ? account.id + : '' } -function formatChat(chat: AnyRecord): string { +function formatChat(chat: APIRecord): string { const network = chat.network ? ` ${chat.network}` : '' const local = chat.localChatID ? ` local:${chat.localChatID}` : '' return `${chat.id}${local}${network} ${chat.title ?? ''}`.trim() } -function chatInputID(chat: AnyRecord): string { +function chatInputID(chat: APIRecord): string { return String(chat.localChatID || chat.id) } -export function userQueryFromInput(input: string): AnyRecord { +export function userQueryFromInput(input: string): APIRecord { const trimmed = input.trim() if (/^@[^:]+:.+/.test(trimmed)) return { id: trimmed, username: trimmed } if (trimmed.includes('@')) return { email: trimmed, username: trimmed } diff --git a/packages/cli/src/lib/runner.ts b/packages/cli/src/lib/runner.ts deleted file mode 100644 index 9ffc8a79..00000000 --- a/packages/cli/src/lib/runner.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { spawn } from 'node:child_process' - -export type RunResult = { - code: number | null - signal: NodeJS.Signals | null - stdout: string - stderr: string -} - -export async function runCli(args: string[], options: { inherit?: boolean } = {}): Promise { - const child = spawn(process.execPath, [process.argv[1]!, ...args], { - env: process.env, - stdio: [options.inherit ? 'inherit' : 'ignore', options.inherit ? 'inherit' : 'pipe', options.inherit ? 'inherit' : 'pipe'], - }) - - const waitForExit = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => { - child.once('error', reject) - child.once('exit', (code, signal) => resolve({ code, signal })) - }) - - if (options.inherit) { - const { code, signal } = await waitForExit - return { code, signal, stdout: '', stderr: '' } - } - - const [stdout, stderr, exit] = await Promise.all([ - streamToString(child.stdout), - streamToString(child.stderr), - waitForExit, - ]) - return { code: exit.code, signal: exit.signal, stdout, stderr } -} - -async function streamToString(stream: NodeJS.ReadableStream | null): Promise { - if (!stream) return '' - const chunks: Buffer[] = [] - for await (const chunk of stream) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))) - return Buffer.concat(chunks).toString('utf8') -} diff --git a/packages/cli/src/lib/send-message.ts b/packages/cli/src/lib/send-message.ts deleted file mode 100644 index e4722f23..00000000 --- a/packages/cli/src/lib/send-message.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { createReadStream } from 'node:fs' -import { waitForMessage } from './wait.js' - -export type AttachmentType = 'sticker' | 'voice-note' -export type SendMessageResult = { - accepted: boolean - state: 'accepted' | 'resolved' - chatID: string - pendingMessageID?: string - message?: unknown - hint?: string -} - -export async function sendMessage(client: any, options: { - chatID: string - file?: string - fileName?: string - mimeType?: string - replyTo?: string - text: string - mentions?: string[] - noPreview?: boolean - attachmentType?: AttachmentType - duration?: number - wait?: boolean - waitTimeoutMs?: number -}): Promise { - const uploaded = options.file - ? await client.assets.upload({ - file: createReadStream(options.file), - fileName: options.fileName, - mimeType: options.mimeType, - }) - : undefined - - if (options.file && !uploaded?.uploadID) throw new Error('Upload did not return an uploadID') - - const pending = await client.messages.send(options.chatID, { - attachment: uploaded?.uploadID - ? { - uploadID: uploaded.uploadID, - type: options.attachmentType, - duration: options.duration ?? uploaded.duration, - fileName: uploaded.fileName, - mimeType: options.mimeType ?? uploaded.mimeType, - size: uploaded.width && uploaded.height ? { height: uploaded.height, width: uploaded.width } : undefined, - } - : undefined, - replyToMessageID: options.replyTo, - text: options.text, - mentions: options.mentions?.length ? options.mentions : undefined, - disableLinkPreview: options.noPreview || undefined, - }) - - if (!options.wait) { - return { - ...pending, - accepted: true, - state: 'accepted', - chatID: options.chatID, - hint: 'Desktop accepted the send request. Pass --wait to wait for the final message or failure.', - } - } - const message = await waitForMessage(client, options.chatID, pending.pendingMessageID, { - timeoutMs: options.waitTimeoutMs, - }) - return { - accepted: true, - state: 'resolved', - chatID: options.chatID, - pendingMessageID: pending.pendingMessageID, - message, - } -} diff --git a/packages/cli/src/lib/server-env.ts b/packages/cli/src/lib/server-env.ts index d4a3bc0c..79782e4c 100644 --- a/packages/cli/src/lib/server-env.ts +++ b/packages/cli/src/lib/server-env.ts @@ -1,4 +1,4 @@ -export const SERVER_ENVIRONMENTS = ['local', 'dev', 'staging', 'prod'] as const +const SERVER_ENVIRONMENTS = ['local', 'dev', 'staging', 'prod'] as const export type ServerEnv = typeof SERVER_ENVIRONMENTS[number] diff --git a/packages/cli/src/lib/setup-login.ts b/packages/cli/src/lib/setup-login.ts index 1768c885..62a47721 100644 --- a/packages/cli/src/lib/setup-login.ts +++ b/packages/cli/src/lib/setup-login.ts @@ -1,14 +1,16 @@ import { BeeperDesktop } from '@beeper/desktop-api' -import { evaluateReadiness } from './app-state.js' -import { isRegistrationRequired, promptText, promptYesNoDefaultYes, type AppLoginSuccess } from './app-api.js' +import type { LoginRegisterResponse, LoginResponseResponse } from '@beeper/desktop-api/resources/app/login' +import { evaluateReadiness, type Readiness } from './app-state.js' import { connectedAccountSummary } from './local-desktop.js' -import { saveTargetAuth, writeTarget, type AuthSource, type Target } from './targets.js' +import { promptConfirm, promptText } from './prompts.js' +import { publicTarget, writeTarget, type PublicTarget, type Target } from './targets.js' + +type AppLoginSuccess = LoginResponseResponse.Success | LoginRegisterResponse export type SetupLoginResult = { accounts: string[] - authSource?: AuthSource - readiness: Awaited> - target: Omit & { auth?: { source?: AuthSource; tokenType?: 'Bearer' } } + readiness: Readiness + target: PublicTarget } export async function startEmailSetup(target: Target, email: string): Promise<{ setupRequestID: string }> { @@ -20,20 +22,20 @@ export async function startEmailSetup(target: Target, email: string): Promise<{ export async function finishEmailSetup(target: Target, options: { code: string - email?: string + force?: boolean json?: boolean setupRequestID: string username?: string - yes?: boolean }): Promise { const client = setupClient(target) let output = await client.app.login.response({ setupRequestID: options.setupRequestID, response: options.code }) - if (isRegistrationRequired(output)) { + if ('registrationRequired' in output && output.registrationRequired === true) { const nonInteractive = options.json || !process.stdin.isTTY - if (nonInteractive && !options.yes) throw new Error('Registration requires --yes to accept the Beeper terms in non-interactive setup.') - const username = options.username ?? (nonInteractive ? undefined : await promptUsername(output.usernameSuggestions)) + if (nonInteractive && !options.force) throw new Error('Registration requires --force to accept the Beeper terms in non-interactive setup.') + const fallback = output.usernameSuggestions?.[0] + const username = options.username ?? (nonInteractive ? undefined : (await promptText(`Username${fallback ? ` [${fallback}]` : ''}: `)) || fallback) if (!username) throw new Error('Registration requires --username.') - if (!options.yes && !await promptYesNoDefaultYes('Accept the Beeper terms and create this account?')) throw new Error('Registration cancelled.') + if (!options.force && !await promptConfirm('Accept the Beeper terms and create this account?', true)) throw new Error('Registration cancelled.') output = await client.app.login.register({ acceptTerms: true, leadToken: output.leadToken, @@ -44,12 +46,6 @@ export async function finishEmailSetup(target: Target, options: { return persistSetupLogin(target, output as AppLoginSuccess) } -export async function interactiveEmailSetup(target: Target, options: { email: string; json?: boolean; username?: string; yes?: boolean }): Promise { - const start = await startEmailSetup(target, options.email) - const code = await promptText('Email code: ') - return finishEmailSetup(target, { ...options, code, setupRequestID: start.setupRequestID }) -} - function setupClient(target: Target): BeeperDesktop { return new BeeperDesktop({ baseURL: target.baseURL, accessToken: 'setup-login-public-client', logLevel: 'warn' }) } @@ -57,24 +53,11 @@ function setupClient(target: Target): BeeperDesktop { async function persistSetupLogin(target: Target, data: AppLoginSuccess): Promise { const token = data.matrix?.accessToken if (!token) throw new Error('Setup did not return a Matrix access token.') - const auth = { accessToken: token, source: 'manual' as AuthSource, tokenType: 'Bearer' as const } - await writeTarget(target) - await saveTargetAuth(target, auth) + const auth = { accessToken: token, source: 'email' as const, tokenType: 'Bearer' as const } + await writeTarget({ ...target, auth }) const [readiness, accounts] = await Promise.all([ evaluateReadiness({ baseURL: target.baseURL, target: target.id, token }), connectedAccountSummary(target, auth).catch(() => []), ]) - return { accounts, authSource: auth.source, readiness, target: publicTarget({ ...target, auth }) } -} - -function publicTarget(target: Target): Omit & { auth?: { source?: AuthSource; tokenType?: 'Bearer' } } { - const { auth, ...rest } = target - return { ...rest, auth: auth ? { source: auth.source, tokenType: auth.tokenType } : undefined } -} - -async function promptUsername(suggestions: string[] | undefined): Promise { - const fallback = suggestions?.[0] - const suffix = fallback ? ` [${fallback}]` : '' - const value = await promptText(`Username${suffix}: `) - return value || fallback || '' + return { accounts, readiness, target: publicTarget({ ...target, auth }) } } diff --git a/packages/cli/src/lib/target-status.ts b/packages/cli/src/lib/target-status.ts index 0b9435f2..982fbbd6 100644 --- a/packages/cli/src/lib/target-status.ts +++ b/packages/cli/src/lib/target-status.ts @@ -1,11 +1,11 @@ -import type { Target } from './targets.js' +import type { ManagedTargetType, Target } from './targets.js' import { checkInstallationUpdate, readInstallations } from './installations.js' -export type TargetLiveStatus = { +type TargetLiveStatus = { reachable: boolean version?: string bundleID?: string - actualType?: 'desktop' | 'server' + actualType?: ManagedTargetType error?: string update?: { available: boolean @@ -14,7 +14,7 @@ export type TargetLiveStatus = { } } -export async function targetLiveStatus(target: Pick): Promise { +export async function targetLiveStatus(target: Pick): Promise { try { const response = await fetch(new URL('/v1/info', target.baseURL), { signal: AbortSignal.timeout(3000) }) if (!response.ok) return { reachable: false, error: `${response.status} ${response.statusText}` } @@ -36,8 +36,8 @@ export async function targetLiveStatus(target: Pick undefined) : undefined @@ -49,7 +49,9 @@ export async function targetLiveStatus(target: Pick, -): 'desktop' | 'server' | undefined { - if (target.type === 'server' && target.managed && info.server?.hostname === '127.0.0.1' && info.server.remote_access === false) return 'server' + target: Pick, +): ManagedTargetType | undefined { + if (target.type === 'server' && target.dataDir && info.server?.hostname === '127.0.0.1' && info.server.remote_access === false) return 'server' return typeFromBundleID(info.app?.bundle_id) } -function typeFromBundleID(bundleID?: string): 'desktop' | 'server' | undefined { +function typeFromBundleID(bundleID?: string): ManagedTargetType | undefined { if (!bundleID) return undefined if (bundleID.includes('.server')) return 'server' if (bundleID.includes('.desktop')) return 'desktop' diff --git a/packages/cli/src/lib/targets.ts b/packages/cli/src/lib/targets.ts index 8c1623ad..0c4fd1c8 100644 --- a/packages/cli/src/lib/targets.ts +++ b/packages/cli/src/lib/targets.ts @@ -1,11 +1,10 @@ -import { constants as fsConstants } from 'node:fs' -import { access, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises' +import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises' import { homedir } from 'node:os' import { dirname, join } from 'node:path' -import { notFound } from './errors.js' +import { AbortError, ExitCodes } from './errors.js' import { normalizeServerEnv } from './server-env.js' -export type AuthSource = 'desktop-db' | 'desktop-cache' | 'desktop-oauth' | 'remote-oauth' | 'manual' +export type AuthSource = 'desktop-db' | 'desktop-oauth' | 'email' | 'remote-oauth' export type StoredAuth = { accessToken: string @@ -16,55 +15,39 @@ export type StoredAuth = { tokenType: 'Bearer' } +export type ManagedTargetType = 'desktop' | 'server' + export type Target = { id: string - type: 'desktop' | 'server' | 'remote' + type: ManagedTargetType | 'remote' name?: string baseURL: string auth?: StoredAuth - managed?: boolean dataDir?: string profile?: string - runtime?: { - install?: 'desktop' | 'server' - dataDir?: string - port?: number - } serverEnv?: string port?: number } -export type ManagedTargetType = 'desktop' | 'server' +export type PublicTarget = Omit & { auth?: Pick } -export type Config = { +type Config = { defaultTarget?: string defaultAccount?: string - baseURL?: string - auth?: StoredAuth } -const defaultPort = 23_373 -const defaultBaseURL = `http://127.0.0.1:${defaultPort}` +export const defaultDesktopPort = 23_373 +export const defaultDesktopBaseURL = `http://127.0.0.1:${defaultDesktopPort}` export const builtInDesktopTargetID = 'desktop' -export const customTargetID = 'custom' +const customTargetID = 'custom' export function beeperDir(): string { return process.env.BEEPER_CLI_CONFIG_DIR ?? join(homedir(), '.beeper') } -export const configPath = () => join(beeperDir(), 'config.json') -export const cachePath = () => join(beeperDir(), 'cache.json') -export const targetsDir = () => join(beeperDir(), 'targets') -export const pluginsDir = () => join(beeperDir(), 'plugins') -export const profileDataDir = (type: ManagedTargetType, id: string) => join(beeperDir(), 'profiles', type, id) - -export async function ensureBeeperDirs(): Promise { - await Promise.all([ - mkdir(targetsDir(), { recursive: true }), - mkdir(pluginsDir(), { recursive: true }), - mkdir(join(beeperDir(), 'profiles'), { recursive: true }), - ]) -} +const configPath = () => join(beeperDir(), 'config.json') +const targetsDir = () => join(beeperDir(), 'targets') +const profileDataDir = (type: ManagedTargetType, id: string) => join(beeperDir(), 'profiles', type, id) export async function readConfig(): Promise { try { @@ -75,7 +58,7 @@ export async function readConfig(): Promise { } } -export async function writeConfig(config: Config): Promise { +async function writeConfig(config: Config): Promise { await mkdir(dirname(configPath()), { recursive: true }) await writeFile(configPath(), `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 }) } @@ -86,29 +69,7 @@ export async function updateConfig(update: (config: Config) => Config | Promise< return next } -export async function resetConfig(): Promise { - await rm(configPath(), { force: true }) -} - -export async function updateTargetCache(target: Target, data: Record): Promise { - let cache: { targets?: Record } = {} - try { - cache = JSON.parse(await readFile(cachePath(), 'utf8')) - } catch (error) { - if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error - } - await mkdir(dirname(cachePath()), { recursive: true }) - await writeFile(cachePath(), `${JSON.stringify({ - ...cache, - targets: { - ...cache.targets, - [target.id]: { ...data, updatedAt: new Date().toISOString() }, - }, - }, null, 2)}\n`, { mode: 0o600 }) -} - export async function listTargets(): Promise { - await ensureBeeperDirs() const files = await readdir(targetsDir()).catch(() => []) const targets = await Promise.all(files.filter(file => file.endsWith('.json')).map(async file => { try { @@ -130,107 +91,79 @@ export async function readTarget(id: string): Promise { } export async function writeTarget(target: Target): Promise { - await ensureBeeperDirs() + await mkdir(targetsDir(), { recursive: true }) await writeFile(targetPath(target.id), `${JSON.stringify(target, null, 2)}\n`, { mode: 0o600 }) } export async function removeTarget(id: string): Promise { await rm(targetPath(id), { force: true }) await updateConfig(config => { - const next = config.defaultTarget === id ? { ...config, defaultTarget: undefined } : config - if (id === builtInDesktopTargetID) return { ...next, auth: undefined, baseURL: undefined } - return next + return config.defaultTarget === id ? { ...config, defaultTarget: undefined } : config }) } -export async function saveTargetAuth(target: Target, auth: StoredAuth): Promise { - if (target.id === customTargetID) { - await updateConfig(config => ({ ...config, baseURL: target.baseURL, auth })) - return - } - await writeTarget({ ...target, auth }) -} - -export async function clearTargetAuth(target: Target): Promise { - if (target.id === customTargetID) { - await updateConfig(config => ({ ...config, auth: undefined })) - return - } - await writeTarget({ ...target, auth: undefined }) - if (target.id === builtInDesktopTargetID) await updateConfig(config => ({ ...config, auth: undefined })) -} - export async function resolveTarget(options: { target?: string; baseURL?: string } = {}): Promise { if (options.baseURL) return { id: customTargetID, type: 'desktop', baseURL: options.baseURL } - const envTarget = process.env.BEEPER_TARGET const config = await readConfig() - const targetID = options.target ?? envTarget ?? config.defaultTarget + const targetID = options.target ?? config.defaultTarget if (targetID) { const target = await readTarget(targetID) - if (!target && targetID === builtInDesktopTargetID) return builtInDesktopTarget(config) - if (!target) throw notFound(`Unknown Beeper target "${targetID}". Run \`beeper targets list\`.`) - return withConfigAuth(target, config) + if (!target && targetID === builtInDesktopTargetID) return builtInDesktopTarget() + if (!target) { + throw new AbortError(`Unknown Beeper target "${targetID}". Run \`beeper targets list\`.`, ExitCodes.NotFound, undefined, 'not_found') + } + return target } const targets = await listTargets() - if (targets.length === 1 && targets[0]) return withConfigAuth(targets[0], config) + if (targets.length === 1 && targets[0]) return targets[0] const desktopTarget = await readTarget(builtInDesktopTargetID) - if (desktopTarget) return withConfigAuth(desktopTarget, config) - return builtInDesktopTarget(config) + if (desktopTarget) return desktopTarget + return builtInDesktopTarget() +} + +export async function createDefaultDesktopTarget(baseURL = defaultDesktopBaseURL): Promise { + const target = builtInDesktopTarget(baseURL) + await writeTarget(target) + await updateConfig(config => ({ ...config, defaultTarget: config.defaultTarget ?? target.id })) + return target } -function builtInDesktopTarget(config: Config): Target { +function builtInDesktopTarget(baseURL = defaultDesktopBaseURL): Target { return { id: builtInDesktopTargetID, type: 'desktop', name: 'Beeper Desktop', - baseURL: process.env.BEEPER_DESKTOP_BASE_URL || config.baseURL || defaultBaseURL, - auth: config.auth, + baseURL, } } -function withConfigAuth(target: Target, config: Config): Target { - if (target.auth || target.type !== 'desktop' || !config.auth) return target - if (config.baseURL && config.baseURL !== target.baseURL) return target - return { ...target, auth: config.auth } -} - function normalizeLocalTarget(target: Target): Target { - if (!target.managed || target.type === 'remote') return target - const port = target.port ?? target.runtime?.port - if (!port) return target - return { ...target, baseURL: `http://127.0.0.1:${port}` } + if (!target.dataDir || target.type === 'remote') return target + return target.port ? { ...target, baseURL: `http://127.0.0.1:${target.port}` } : target } export async function createProfileTarget(type: ManagedTargetType, id: string, options: { serverEnv?: string; port?: number } = {}): Promise { const serverEnv = normalizeServerEnv(options.serverEnv) const port = options.port ?? await nextPort() + const dataDir = profileDataDir(type, id) const target: Target = { id, type, name: id, baseURL: `http://127.0.0.1:${port}`, - managed: true, - dataDir: profileDataDir(type, id), + dataDir, profile: id, - runtime: { - install: type, - dataDir: profileDataDir(type, id), - port, - }, serverEnv, port, } - await mkdir(target.dataDir!, { recursive: true }) + await mkdir(dataDir, { recursive: true }) await writeTarget(target) return target } -export async function getAccessToken(target?: Target): Promise { - return process.env.BEEPER_ACCESS_TOKEN || target?.auth?.accessToken || (await resolveTarget()).auth?.accessToken -} - -export async function getBaseURL(override?: string): Promise { - return (await resolveTarget({ baseURL: override })).baseURL +export function publicTarget(target: Target): PublicTarget { + const { auth, ...rest } = target + return { ...rest, auth: auth ? { source: auth.source, tokenType: auth.tokenType } : undefined } } function targetPath(id: string): string { @@ -239,17 +172,8 @@ function targetPath(id: string): string { async function nextPort(): Promise { const used = new Set((await listTargets()).map(target => target.port).filter((port): port is number => typeof port === 'number')) - for (let port = defaultPort + 1; port < defaultPort + 200; port++) { + for (let port = defaultDesktopPort + 1; port < defaultDesktopPort + 200; port++) { if (!used.has(port)) return port } throw new Error('No available default port for a new Beeper target.') } - -export async function pathExists(path: string): Promise { - try { - await access(path, fsConstants.F_OK) - return true - } catch { - return false - } -} diff --git a/packages/cli/src/lib/update-banner.ts b/packages/cli/src/lib/update-banner.ts deleted file mode 100644 index a065ba2f..00000000 --- a/packages/cli/src/lib/update-banner.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { readFile } from 'node:fs/promises' -import { join } from 'node:path' - -export type UpdateAvailability = { - current: string - latest?: string - available: boolean -} - -/** - * Read the cached dist-tag info that @oclif/plugin-warn-if-update-available writes. - * Synchronous-style: returns immediately from disk; never hits the network. - * Returns undefined when the cache is missing or unreadable — caller should treat - * that as "no banner to show". - */ -export async function readUpdateAvailability(options: { cacheDir: string; currentVersion: string; tag?: string }): Promise { - try { - const raw = await readFile(join(options.cacheDir, 'version'), 'utf8') - const parsed = JSON.parse(raw) as Record - const tag = options.tag ?? 'latest' - const latest = parsed[tag] - if (!latest) return { current: options.currentVersion, available: false } - const available = stripPrerelease(latest) !== stripPrerelease(options.currentVersion) && isGreater(latest, options.currentVersion) - return { current: options.currentVersion, latest, available } - } catch { - return undefined - } -} - -export function formatUpdateFooter(availability: UpdateAvailability | undefined): string | undefined { - if (!availability?.available || !availability.latest) return undefined - return `↑ beeper-cli ${availability.latest} available — beeper update` -} - -function stripPrerelease(v: string): string { - return v.split('-')[0] ?? v -} - -function isGreater(a: string, b: string): boolean { - const aa = stripPrerelease(a).split('.').map(n => Number(n) || 0) - const bb = stripPrerelease(b).split('.').map(n => Number(n) || 0) - for (let i = 0; i < Math.max(aa.length, bb.length); i++) { - const x = aa[i] ?? 0 - const y = bb[i] ?? 0 - if (x !== y) return x > y - } - return false -} diff --git a/packages/cli/src/lib/wait.ts b/packages/cli/src/lib/wait.ts deleted file mode 100644 index 12d0ad50..00000000 --- a/packages/cli/src/lib/wait.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { setTimeout as sleep } from 'node:timers/promises' - -export type WaitOptions = { - intervalMs?: number - timeoutMs?: number -} - -export async function waitForMessage(client: any, chatID: string, pendingMessageID: string, options: WaitOptions = {}) { - const started = Date.now() - const timeoutMs = options.timeoutMs ?? 30_000 - const intervalMs = options.intervalMs ?? 750 - let lastError: unknown - - while (Date.now() - started < timeoutMs) { - try { - return await client.messages.retrieve(pendingMessageID, { chatID }) - } catch (error) { - lastError = error - await sleep(intervalMs) - } - } - - throw new Error(`Timed out waiting for ${pendingMessageID}${lastError instanceof Error ? `: ${lastError.message}` : ''}`) -} diff --git a/packages/cli/src/plugin-sdk.ts b/packages/cli/src/plugin-sdk.ts deleted file mode 100644 index 3e36b6f7..00000000 --- a/packages/cli/src/plugin-sdk.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { BeeperDesktop } from '@beeper/desktop-api' - -export { Args, Command, Flags, ux } from '@oclif/core' -export { BeeperCommand, ensureWritable, writeEvent, isQuiet } from './lib/command.js' -export { - AbortError, - BugError, - CLIError, - ExitCodes, - ambiguous, - authRequired, - notFound, - notReady, - usageError, - type ExitCode, -} from './lib/errors.js' -export { - configPath, - getAccessToken, - getBaseURL, - readConfig, - resolveTarget, - resetConfig, - updateConfig, - writeConfig, - type Config, - type StoredAuth, - type Target, -} from './lib/targets.js' -export { createClient as createBeeperClient, requireToken } from './lib/client.js' -export { - collectPage, - emptyState, - printConfig, - printData, - printFailure, - printIDs, - printList, - printSuccess, - startStream, - type OutputFormat, - type Suggestion, -} from './lib/output.js' -export { - resolveAccountID, - resolveAccountIDs, - resolveChatID, - listAccountIDs, - userQueryFromInput, - type AccountResolutionOptions, - type ChatResolutionOptions, -} from './lib/resolve.js' -export { appRequest } from './lib/app-api.js' -export { - confirmSuggestion, - declineWithExit127, - levenshtein, - rankSuggestions, - type Suggestion as DidYouMeanSuggestion, -} from './lib/did-you-mean.js' -export { - formatUpdateFooter, - readUpdateAvailability, - type UpdateAvailability, -} from './lib/update-banner.js' - -export type BeeperClient = BeeperDesktop - -export type BeeperPluginContext = { - baseURL?: string - debug?: boolean - json?: boolean - readOnly?: boolean - quiet?: boolean -} diff --git a/packages/cli/src/types/qrcode.d.ts b/packages/cli/src/types/qrcode.d.ts deleted file mode 100644 index fcc8e975..00000000 --- a/packages/cli/src/types/qrcode.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -declare module 'qrcode' { - export type TerminalQRCodeOptions = { - errorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H' - small?: boolean - type: 'terminal' - } - - const QRCode: { - toString(data: string, options: TerminalQRCodeOptions): Promise - } - - export default QRCode -} diff --git a/packages/cli/src/types/ws.d.ts b/packages/cli/src/types/ws.d.ts deleted file mode 100644 index 4d307d4d..00000000 --- a/packages/cli/src/types/ws.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -declare module 'ws' { - import type { IncomingHttpHeaders } from 'node:http' - - class WebSocket { - static OPEN: number - constructor(url: string | URL, options?: { headers?: Record }) - readyState: number - close(code?: number, reason?: string): void - kill?: () => void - addEventListener(event: 'open', listener: () => void): void - addEventListener(event: 'message', listener: (event: { data: Buffer | string }) => void): void - addEventListener(event: 'error', listener: (event: { error?: Error }) => void): void - addEventListener(event: 'close', listener: (event: { code: number; reason: string }) => void): void - once(event: 'open', listener: () => void): this - once(event: 'error', listener: (error: Error) => void): this - once(event: 'close', listener: (code: number, reason: Buffer) => void): this - on(event: 'message', listener: (data: Buffer | string) => void): this - send(data: string): void - } - - export { WebSocket } - export default WebSocket -} diff --git a/packages/cli/test/account-login.test.ts b/packages/cli/test/account-login.test.ts index 5f652ecc..4b6eaba8 100644 --- a/packages/cli/test/account-login.test.ts +++ b/packages/cli/test/account-login.test.ts @@ -1,11 +1,8 @@ -import { afterEach, describe, expect, it, mock } from 'bun:test' -import { runGuidedAccountLogin, setWebViewConstructorForTest } from '../src/lib/account-login.js' +import { describe, expect, it, mock } from 'bun:test' +import { runGuidedAccountLogin } from '../src/lib/account-login.js' type Session = Parameters[2] - -afterEach(() => { - setWebViewConstructorForTest(undefined) -}) +type SubmitStep = (stepID: string, body: unknown) => Promise describe('runGuidedAccountLogin', () => { it('submits display steps interactively and returns the completed session', async () => { @@ -143,8 +140,6 @@ describe('runGuidedAccountLogin', () => { } } - setWebViewConstructorForTest(FakeWebView) - const cookieStep = session({ currentStep: { type: 'cookies', @@ -165,6 +160,7 @@ describe('runGuidedAccountLogin', () => { const result = await runGuidedAccountLogin(fakeClient(submit), 'googlechat', cookieStep, { nonInteractive: true, + webViewConstructor: FakeWebView, webview: true, webviewBackend: 'chrome', webviewTimeoutMs: 100, @@ -195,7 +191,7 @@ function session(overrides: Partial = {}): Session { } as Session } -function fakeClient(submit: ReturnType) { +function fakeClient(submit: SubmitStep) { return { bridges: { loginSessions: { diff --git a/packages/cli/test/cli-smoke.ts b/packages/cli/test/cli-smoke.ts index 43a736f4..75b8d8c6 100644 --- a/packages/cli/test/cli-smoke.ts +++ b/packages/cli/test/cli-smoke.ts @@ -1,15 +1,14 @@ import assert from 'node:assert/strict' import { spawnSync } from 'node:child_process' -import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs' +import { existsSync, rmSync } from 'node:fs' import { join } from 'node:path' import { fileURLToPath } from 'node:url' -import { commandManifest } from '../dist/lib/manifest.js' -import { resolveAccountID, resolveAccountIDs, resolveChatID } from '../dist/lib/resolve.js' -import { downloadURLFor, feedURLFor, normalizeInstallRequest } from '../dist/lib/installations.js' const root = fileURLToPath(new URL('..', import.meta.url)) -const configDir = '/tmp/beeper-cli-test' -const run = (...args) => spawnSync(process.execPath, ['./bin/dev.js', ...args], { +const configDir = '/tmp/beeper-cli-smoke' +rmSync(configDir, { recursive: true, force: true }) + +const run = (...args: string[]) => spawnSync('bun', ['./bin/dev.js', ...args], { cwd: root, encoding: 'utf8', env: { @@ -18,354 +17,415 @@ const run = (...args) => spawnSync(process.execPath, ['./bin/dev.js', ...args], }, }) -const ok = (...args) => { +const ok = (...args: string[]) => { const result = run(...args) assert.equal(result.status, 0, `${args.join(' ')} failed\nSTDOUT:\n${result.stdout}\nSTDERR:\n${result.stderr}`) return result.stdout } -const expectedCommands = [ - 'setup', - 'install desktop', - 'install server', - 'targets list', - 'bridges list', - 'bridges show', - 'targets add desktop', - 'targets add server', - 'targets add remote', - 'targets use', - 'targets show', - 'targets status', - 'targets start', - 'targets stop', - 'targets restart', - 'targets logs', - 'targets enable', - 'targets disable', - 'targets remove', - 'targets tunnel', - 'auth status', - 'auth logout', - 'auth email start', - 'auth email response', - 'verify', - 'verify status', - 'verify approve', - 'verify recovery-key', - 'verify reset-recovery-key', - 'verify cancel', - 'verify list', - 'verify start', - 'verify show', - 'verify sas', - 'verify sas-confirm', - 'verify qr-scan', - 'verify qr-confirm', - 'accounts list', - 'accounts add', - 'accounts show', - 'accounts remove', - 'accounts use', - 'chats list', - 'chats search', - 'chats show', - 'chats start', - 'chats archive', - 'chats unarchive', - 'chats pin', - 'chats unpin', - 'chats mute', - 'chats unmute', - 'chats mark-read', - 'chats mark-unread', - 'chats priority', - 'chats notify-anyway', - 'chats rename', - 'chats description', - 'chats avatar', - 'chats draft', - 'chats disappear', - 'chats remind', - 'chats unremind', - 'chats focus', - 'messages list', - 'messages search', - 'messages show', - 'messages context', - 'messages edit', - 'messages delete', - 'messages export', - 'send text', - 'send file', - 'send react', - 'send sticker', - 'send unreact', - 'send voice', - 'presence', - 'contacts list', - 'contacts search', - 'contacts show', - 'resolve chat', - 'resolve account', - 'resolve contact', - 'resolve target', - 'resolve bridge', - 'media download', - 'export', - 'watch', - 'rpc', - 'man', - 'schema', - 'doctor', - 'status', - 'docs', - 'version', - 'completion', - 'plugins', - 'plugins available', - 'update', - 'config get', - 'config set', - 'config path', - 'config reset', - 'api get', - 'api post', - 'api request', -] - -const internalCommands = new Set(['autocomplete']) -const commandFiles = listCommandFiles(join(root, 'src/commands')).filter(file => !internalCommands.has(fileToCommand(file))) -const commandNames = commandFiles.map(file => fileToCommand(file)).sort() -const manifestNames = commandManifest.map(item => item.command).sort() -// First-party commands shipped by a separate plugin package (not present in src/commands here). -const pluginShippedCommands = new Set(['targets tunnel']) - -assert.deepEqual(commandManifest.map(item => item.command), expectedCommands, 'command manifest must be the nuclear public surface') -assert.deepEqual(manifestNames.filter(name => !pluginShippedCommands.has(name)), commandNames, 'command manifest must match src/commands (excluding plugin-shipped commands)') -assert.equal(new Set(manifestNames).size, manifestNames.length, 'command manifest must not contain duplicates') - -const help = ok('--help') -assert.match(help, /\btargets\b/, 'help should expose targets') -assert.match(help, /\bchats\b/, 'help should expose chats') -assert.match(help, /\bmessages\b/, 'help should expose messages') -// Anchor to the column-2 command/topic listing so we don't false-positive on the word -// "commands" inside another command's summary (e.g. rpc). -assert.doesNotMatch(help, /^\s{2,}(profile|commands|llm|login|logout)\s/m, 'help must not expose deleted root/internal commands') -assert.match(help, /\bplugins\b/, 'help should expose plugin management') -assert.doesNotMatch(help, /^\s{2,}autocomplete\s/m, 'help should expose completion instead of raw autocomplete') -assert.match(help, /\bbridges\b/, 'help should expose bridges') -assert.match(help, /\bverify\b/, 'help should expose verification') -assert.doesNotMatch(help, /\bassets\b|\bapp\b/, 'help must not expose old API namespaces') - -for (const command of expectedCommands) { - // Plugin-shipped commands aren't loaded unless the plugin is installed. - if (pluginShippedCommands.has(command)) continue - ok(...command.split(' '), '--help') -} +assert.match(ok('--help'), /Usage: beeper /) +assert.match(ok('--help'), /targets add/) +assert.match(ok('--help'), /targets runtime start\s+Start a local target runtime/) +assert.match(ok('--help'), /targets runtime stop\s+Stop a local server runtime/) +assert.match(ok('--help'), /targets runtime restart\s+Restart a local server runtime/) +assert.match(ok('--help'), /targets tunnel/) +assert.match(ok('--help'), /use account\s+Select the default account/) +assert.match(ok('--help'), /use target\s+Select the default target/) +assert.match(ok('--help'), /remove account\s+Remove an account/) +assert.match(ok('--help'), /remove target\s+Remove a target/) +assert.match(ok('--help'), /auth email start\s+Start email sign-in for a target/) +assert.match(ok('--help'), /auth email response\s+Finish email sign-in for a target/) +assert.match(ok('--help'), /install desktop\s+Install Beeper Desktop locally/) +assert.match(ok('--help'), /install server\s+Install Beeper Server locally/) +assert.match(ok('--help'), /accounts list/) +assert.match(ok('--help'), /accounts add/) +assert.match(ok('--help'), /messages list/) +assert.match(ok('--help'), /chats archive\s+Archive or unarchive a chat/) +assert.match(ok('--help'), /chats disappear\s+Set a disappearing-message timer/) +assert.match(ok('--help'), /chats priority\s+Set chat priority/) +assert.match(ok('--help'), /chats focus\s+Focus a chat in Beeper/) +assert.match(ok('--help'), /chats notify-anyway\s+Notify a chat anyway/) +assert.match(ok('--help'), /messages context/) +assert.match(ok('--help'), /messages edit\s+Edit a message/) +assert.match(ok('--help'), /messages delete\s+Delete a message/) +assert.match(ok('--help'), /api request/) +assert.match(ok('--help'), /send text\s+Send a text message/) +assert.match(ok('--help'), /send file\s+Send a file message/) +assert.match(ok('--help'), /send sticker\s+Send a sticker/) +assert.match(ok('--help'), /send voice\s+Send a voice note/) +assert.match(ok('--help'), /send react\s+Send or remove a reaction/) +assert.match(ok('--help'), /send presence\s+Send a typing indicator/) +assert.match(ok('--help'), /resolve account\s+Resolve an account selector/) +assert.match(ok('--help'), /resolve bridge\s+Resolve a bridge selector/) +assert.match(ok('--help'), /resolve chat\s+Resolve a chat selector/) +assert.match(ok('--help'), /resolve contact\s+Resolve a contact selector/) +assert.match(ok('--help'), /resolve target\s+Resolve a target selector/) +assert.match(ok('--help'), /watch/) +assert.match(ok('--help'), /media download/) +assert.match(ok('--help'), /export\s+Export accounts/) +assert.match(ok('setup', '--help'), /--remote/) +assert.match(ok('targets', 'tunnel', '--help'), /--url-only/) +assert.match(ok('accounts', 'add', '--help'), /--webview-backend/) +assert.match(ok('watch', '--help'), /--include-type/) +assert.match(ok('send', 'presence', '--help'), /--state/) +assert.match(ok('media', 'download', '--help'), /--out/) +assert.match(ok('export', '--help'), /--no-attachments/) -assert.match(ok('send', 'text', '--help'), /--to/, 'send text should use --to') -assert.match(ok('send', 'text', '--help'), /--message/, 'send text should use --message') -assert.match(ok('send', 'file', '--help'), /--file/, 'send file should use --file') -assert.match(ok('send', 'file', '--help'), /--caption/, 'send file should use --caption') -assert.match(ok('messages', 'list', '--help'), /--chat/, 'messages list should use --chat') -assert.doesNotMatch(ok('chats', 'mute', '--help'), /--duration/, 'chats mute must not expose duration until API supports it') -assert.match(ok('chats', 'list', '--help'), /--account=\.\.\./, 'account filters must stay local') -assert.doesNotMatch(ok('status', '--help'), /--account/, '--account must not be global') -const setupHelp = ok('setup', '--help') -assert.match(setupHelp, /--local/, 'setup should expose local Desktop direct setup') -assert.match(setupHelp, /--oauth/, 'setup should expose OAuth setup') -assert.match(setupHelp, /--remote/, 'setup should expose remote setup shortcut') -assert.match(setupHelp, /--server/, 'setup should expose Server setup shortcut') -assert.match(setupHelp, /--desktop/, 'setup should expose Desktop setup shortcut') -assert.match(setupHelp, /--email/, 'setup should expose email setup start') -assert.doesNotMatch(setupHelp, /--code|--accept-terms/, 'setup must not accept OTP or terms flags in the first command') - -const man = JSON.parse(ok('man', '--json')) -assert.equal(man.ok, true) -assert.equal(man.error, null) -assert.deepEqual(man.data.map(item => item.command), expectedCommands) - -const availablePlugins = JSON.parse(ok('plugins', 'available', '--json')) -assert.equal(availablePlugins.ok, true) -assert.equal(availablePlugins.data[0].name, '@beeper/cli-plugin-cloudflare') -assert.equal(availablePlugins.data[0].status, 'not installed') -assert.deepEqual(availablePlugins.data[0].commands, ['targets tunnel']) -assert.match(ok('chats', 'list', '--help'), /preferred chat selectors/, 'chats list --ids should describe preferred selectors') -assert.match(ok('chats', '--help'), /preferred chat selectors/, 'chats should alias chats list') -assert.match(ok('accounts', 'chats', '--help'), /preferred chat selectors/, 'accounts chats should alias chats list') -assert.match(ok('accounts', '--help'), /List connected accounts/, 'accounts should alias accounts list') -assert.match(ok('bridges', 'list', '--help'), /connect chat accounts/, 'bridges list should expose bridge catalog') -assert.match(ok('bridges', '--help'), /connect chat accounts/, 'bridges should alias bridges list') -assert.match(ok('verify', '--help'), /device verification/, 'verify should be a root command') -assert.throws(() => ok('auth', 'verify', '--help'), /failed/, 'auth verify must not remain public') -assert.throws(() => ok('messages', 'react', '--help'), /failed/, 'messages react must not remain public') +const version = JSON.parse(ok('version', '--json')) +assert.equal(version.name, 'beeper-cli') +assert.match(version.version, /^\d+\.\d+\.\d+/) -rmSync(configDir, { recursive: true, force: true }) -let result = run('targets', 'add', 'remote', 'work', 'http://127.0.0.1:23373', '--default', '--json') -assert.equal(result.status, 0, result.stderr) -let envelope = JSON.parse(result.stdout) -assert.equal(envelope.ok, true) -assert.equal(envelope.data.id, 'work') -assert.equal(envelope.data.type, 'remote') +let result = run('version', '--json', '--plain') +assert.equal(result.status, 2) +let errorPayload = JSON.parse(result.stderr) +assert.equal(errorPayload.error.code, 'usage_error') +assert.match(errorPayload.error.message, /cannot combine --json and --plain/) -result = run('targets', 'list', '--json') -assert.equal(result.status, 0, result.stderr) -envelope = JSON.parse(result.stdout) -assert.equal(envelope.ok, true) -assert(envelope.data.some(item => item.id === 'work' && item.default)) +result = run('messages', 'list', '--limit', '12abc', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.equal(errorPayload.error.code, 'usage_error') +assert.match(errorPayload.error.message, /--limit must be an integer/) + +result = run('messages', 'list', '--limit=', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.equal(errorPayload.error.code, 'usage_error') +assert.match(errorPayload.error.message, /--limit must be an integer/) + +result = run('messages', 'list', '--limit', '1e2', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.equal(errorPayload.error.code, 'usage_error') +assert.match(errorPayload.error.message, /--limit must be an integer/) + +let payload = JSON.parse(ok('targets', 'list', '--json')) +assert.equal(payload[0].id, 'desktop') +assert.equal(existsSync(join(configDir, 'config.json')), false) +assert.equal(existsSync(join(configDir, 'targets')), false) + +payload = JSON.parse(ok('use', 'target', 'desktop', '--json')) +assert.equal(payload.defaultTarget, 'desktop') + +result = run('--safety-profile', 'readonly', 'use', 'target', 'desktop', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /blocked by safety profile "readonly"/) + +result = run('--safety-profile', 'readonly', 'targets', 'tunnel', 'desktop', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /blocked by safety profile "readonly"/) -result = run('auth', 'status', '--json') +result = run('--safety-profile', 'readonly', 'use', 'account', 'matrix', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /blocked by safety profile "readonly"/) + +result = run('--safety-profile', 'readonly', 'remove', 'target', 'desktop', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /blocked by safety profile "readonly"/) + +result = run('--safety-profile', 'readonly', 'send', 'text', '--to', 'chat', '--message', 'hello', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /blocked by safety profile "readonly"/) + +result = run('--safety-profile', 'readonly', 'chats', 'archive', '--chat', 'chat', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /blocked by safety profile "readonly"/) + +result = run('--safety-profile', 'readonly', 'accounts', 'add', 'matrix', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /blocked by safety profile "readonly"/) + +result = run('--safety-profile', 'readonly', 'send', 'presence', '--to', 'chat', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /blocked by safety profile "readonly"/) + +result = run('--safety-profile', 'readonly', 'media', 'download', 'mxc://server/file', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /blocked by safety profile "readonly"/) + +result = run('--safety-profile', 'readonly', 'export', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /blocked by safety profile "readonly"/) + +result = run('targets', 'add', 'desktop', 'http://127.0.0.1:23374', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /reserved/) + +result = run('targets', 'add', 'work', 'http://127.0.0.1:23373', '--default', '--json') assert.equal(result.status, 0, result.stderr) -envelope = JSON.parse(result.stdout) -assert.equal(envelope.ok, true) -assert.equal(envelope.data.authenticated, false) -assert.equal(envelope.data.target, 'work') - -result = run('send', 'text', '--to', 'family', '--message', 'on my way', '--read-only', '--json') -assert.notEqual(result.status, 0) -envelope = JSON.parse(result.stderr) -assert.equal(envelope.ok, false) -assert.match(envelope.error.message, /read-only mode/) - -result = run('setup', '--remote', 'http://127.0.0.1:9', '--target', 'email-remote', '--email', 'staging-user-123456@example.invalid', '--json') -assert.notEqual(result.status, 0) -envelope = JSON.parse(result.stderr) -assert.equal(envelope.ok, false) -assert.match(envelope.error.message, /auth email start/) -assert.doesNotMatch(envelope.error.message, /--code|OTP/i, 'setup must direct automation to the two-step email commands without accepting OTP itself') - -result = run('targets', 'show', 'email-remote', '--json') -assert.notEqual(result.status, 0) -envelope = JSON.parse(result.stderr) -assert.equal(envelope.ok, false) -assert.match(envelope.error.message, /Unknown Beeper target/) +payload = JSON.parse(result.stdout) +assert.equal(payload.target.id, 'work') +assert.equal(payload.target.type, 'remote') -rmSync(configDir, { recursive: true, force: true }) -const fakeServerPath = join(configDir, 'bin', 'beeper-server') -mkdirSync(join(configDir, 'bin'), { recursive: true }) -writeFileSync(fakeServerPath, '#!/bin/sh\n', { mode: 0o755 }) -writeFileSync(join(configDir, 'installations.json'), `${JSON.stringify({ - server: { - kind: 'server', - channel: 'stable', - serverEnv: 'prod', - bundleID: 'com.automattic.beeper.server', - version: 'test', - path: fakeServerPath, - feedURL: 'https://example.invalid/feed', - downloadURL: 'https://example.invalid/download', - installedAt: '2026-05-18T00:00:00.000Z', - updatedAt: '2026-05-18T00:00:00.000Z', +payload = JSON.parse(ok('use', 'target', 'work', '--json')) +assert.equal(payload.defaultTarget, 'work') + +payload = JSON.parse(ok('status', '--json')) +assert.equal(payload.auth.authenticated, false) +assert.equal(payload.target.id, 'work') + +payload = JSON.parse(ok('auth', 'logout', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'auth.logout') + +payload = JSON.parse(ok('auth', 'email', 'start', '--email', 'qa@example.invalid', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'auth.email.start') + +payload = JSON.parse(ok('auth', 'email', 'response', '--setup-request-id', 'setup-1', '--code', '123456', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'auth.email.response') + +payload = JSON.parse(ok('install', 'desktop', '--server-env', 'staging', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'install.desktop') +assert.equal(payload.request.serverEnv, 'staging') + +payload = JSON.parse(ok('install', 'server', '--server-env', 'staging', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'install.server') +assert.equal(payload.request.serverEnv, 'staging') + +payload = JSON.parse(ok('targets', 'runtime', 'start', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'targets.runtime.start') +assert.equal(payload.request.target.id, 'work') +assert.equal(payload.request.target.auth, undefined) + +payload = JSON.parse(ok('targets', 'runtime', 'stop', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'targets.runtime.stop') +assert.equal(payload.request.target.id, 'work') + +payload = JSON.parse(ok('targets', 'runtime', 'restart', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'targets.runtime.restart') +assert.equal(payload.request.target.id, 'work') + +result = run('targets', 'runtime', 'bogus', '--dry-run', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /unknown command "targets runtime bogus"/) + +payload = JSON.parse(ok('targets', 'tunnel', 'work', '--retries', '1', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'targets.tunnel') +assert.equal(payload.request.target, 'work') +assert.equal(payload.request.retries, 1) + +payload = JSON.parse(ok('remove', 'target', 'work', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'remove.target') +assert.equal(payload.request.id, 'work') + +payload = JSON.parse(ok('api', 'request', 'POST', '/v1/example', '--body', '{"ok":true}', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.request.body.ok, true) + +payload = JSON.parse(ok('api', 'request', 'GET', '/v1/example', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.request.method, 'GET') + +payload = JSON.parse(ok('send', 'voice', '--to', 'chat', '--file', './note.ogg', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'send.voice') +assert.equal(payload.request.chat, 'chat') + +payload = JSON.parse(ok('send', 'text', '--to', 'chat', '--message', 'hello', '--mention', 'user1', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'send.text') +assert.equal(payload.request.mentions[0], 'user1') + +payload = JSON.parse(ok('send', 'react', '--to', 'chat', '--id', 'm1', '--reaction', '+1', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'send.react') +assert.equal(payload.request.reactionKey, '+1') + +payload = JSON.parse(ok('chats', 'disappear', '--chat', 'chat', '--seconds', 'off', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.request.chat, 'chat') +assert.equal(payload.request.messageExpirySeconds, null) + +payload = JSON.parse(ok('chats', 'disappear', '--chat', 'chat', '--seconds', '3600', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.request.messageExpirySeconds, 3600) + +result = run('chats', 'disappear', '--chat', 'chat', '--seconds', '1e2', '--dry-run', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.equal(errorPayload.error.code, 'usage_error') +assert.match(errorPayload.error.message, /--seconds must be a positive integer or "off"/) + +payload = JSON.parse(ok('chats', 'priority', '--chat', 'chat', '--level', 'low', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.request.chat, 'chat') + +payload = JSON.parse(ok('chats', 'focus', '--chat', 'chat', '--text', 'draft', '--file', './draft.txt', '--message', 'm1', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'chats.focus') +assert.equal(payload.request.draftText, 'draft') +assert.equal(payload.request.draftAttachmentPath, './draft.txt') + +payload = JSON.parse(ok('chats', 'notify-anyway', '--chat', 'chat', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'chats.notify-anyway') + +result = run('chats', 'notify-anyway', '--dry-run', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /--chat is required/) + +payload = JSON.parse(ok('messages', 'context', '--chat', 'chat', '--id', 'm1', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.request.chat, 'chat') +assert.equal(payload.request.messageID, 'm1') + +payload = JSON.parse(ok('messages', 'edit', '--chat', 'chat', '--id', 'm1', '--message', 'edited', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'messages.edit') +assert.equal(payload.request.chat, 'chat') +assert.equal(payload.request.text, 'edited') + +payload = JSON.parse(ok('messages', 'delete', '--chat', 'chat', '--id', 'm1', '--for-everyone', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'messages.delete') +assert.equal(payload.request.forEveryone, true) +assert.equal(payload.request.messageID, 'm1') + +payload = JSON.parse(ok('export', '--chat', 'chat', '--out', '/tmp/beeper-export', '--limit-messages', '10', '--no-attachments', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'export') +assert.equal(payload.request.outDir, '/tmp/beeper-export') + +payload = JSON.parse(ok('send', 'presence', '--to', 'chat', '--duration', '1', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'send.presence') +assert.equal(payload.request.durationSeconds, 1) + +payload = JSON.parse(ok('media', 'download', 'mxc://server/file', '--out', '/tmp', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'media.download') +assert.equal(payload.request.out, '/tmp') + +payload = JSON.parse(ok('export', '--out', '/tmp/beeper-export', '--limit-chats', '1', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'export') +assert.equal(payload.request.outDir, '/tmp/beeper-export') +assert.equal(payload.request.limitChats, 1) + +payload = JSON.parse(ok('--safety-profile', 'readonly', 'resolve', 'target', 'desktop', '--json')) +assert.equal(payload.kind, 'target') +assert.equal(payload.selected.id, 'desktop') + +payload = JSON.parse(ok('targets', 'list', '--json')) +assert.equal(payload[0].id, 'work') + +const schema = JSON.parse(ok('schema', '--json')) +assert.equal(schema.schema_version, 1) +assert.equal(schema.command.type, 'application') + +const mcp = spawnSync('bun', ['./bin/dev.js', 'mcp'], { + cwd: root, + encoding: 'utf8', + env: { + ...process.env, + BEEPER_CLI_CONFIG_DIR: configDir, }, -}, null, 2)}\n`) -result = run('setup', '--json') -assert.equal(result.status, 0, result.stderr) -envelope = JSON.parse(result.stdout) -assert.equal(envelope.ok, true) -assert(envelope.data.availableActions.some(action => action.id === 'use-installed-server' && action.command === 'beeper setup --server --yes')) -assert(!envelope.data.availableActions.some(action => action.id === 'install-server'), 'setup must not offer to reinstall an already installed Server') + input: '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}\n', +}) +assert.equal(mcp.status, 0, mcp.stderr) +payload = JSON.parse(mcp.stdout) +assert.ok(payload.result.tools.some((tool: { name: string }) => tool.name === 'targets_list')) +assert.ok(payload.result.tools.some((tool: { name: string }) => tool.name === 'messages_search')) +assert.ok(payload.result.tools.some((tool: { name: string }) => tool.name === 'contacts_list')) +assert.ok(payload.result.tools.some((tool: { name: string }) => tool.name === 'api_request')) +assert.ok(payload.result.tools.some((tool: { name: string }) => tool.name === 'resolve_target')) +assert.ok(payload.result.tools.some((tool: { name: string }) => tool.name === 'resolve_chat')) +assert.ok(payload.result.tools.some((tool: { name: string }) => tool.name === 'messages_context')) -const rpcResult = spawnSync(process.execPath, ['./bin/dev.js', 'rpc'], { +const mcpInitialize = spawnSync('bun', ['./bin/dev.js', 'mcp'], { cwd: root, encoding: 'utf8', env: { ...process.env, BEEPER_CLI_CONFIG_DIR: configDir, }, - input: '{"id":1,"command":"auth status --json"}\n', + input: '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n', }) -assert.equal(rpcResult.status, 0, rpcResult.stderr) -const rpcLine = JSON.parse(rpcResult.stdout) -assert.equal(rpcLine.id, 1) -assert.equal(rpcLine.ok, true) -assert.match(rpcLine.stdout, /"ok": true/) - -const stagingServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'staging', channel: 'stable', platform: 'darwin', arch: 'arm64' }) -assert.equal(stagingServerRequest.channel, 'stable') -assert.equal(stagingServerRequest.bundleID, 'com.automattic.beeper.server') -assert.equal(feedURLFor(stagingServerRequest), 'https://api.beeper-staging.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=darwin&channel=stable&arch=arm64') -assert.equal(downloadURLFor(stagingServerRequest), 'https://api.beeper-staging.com/desktop/download/macos/arm64/stable/com.automattic.beeper.server') - -const prodServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'prod', channel: 'stable', platform: 'darwin', arch: 'arm64' }) -assert.equal(prodServerRequest.channel, 'stable') -assert.equal(prodServerRequest.bundleID, 'com.automattic.beeper.server') -assert.equal(feedURLFor(prodServerRequest), 'https://api.beeper.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=darwin&channel=stable&arch=arm64') -assert.equal(downloadURLFor(prodServerRequest), 'https://api.beeper.com/desktop/download/macos/arm64/stable/com.automattic.beeper.server') - -const productionAliasServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'production', channel: 'stable', platform: 'darwin', arch: 'arm64' }) -assert.equal(productionAliasServerRequest.serverEnv, 'prod') - -const localServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'local', channel: 'stable', platform: 'darwin', arch: 'arm64' }) -assert.equal(feedURLFor(localServerRequest), 'https://api.beeper.localtest.me/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=darwin&channel=stable&arch=arm64') -assert.equal(downloadURLFor(localServerRequest), 'https://api.beeper.localtest.me/desktop/download/macos/arm64/stable/com.automattic.beeper.server') - -const devServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'dev', channel: 'stable', platform: 'darwin', arch: 'arm64' }) -assert.equal(feedURLFor(devServerRequest), 'https://api.beeper-dev.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server&platform=darwin&channel=stable&arch=arm64') -assert.equal(downloadURLFor(devServerRequest), 'https://api.beeper-dev.com/desktop/download/macos/arm64/stable/com.automattic.beeper.server') - -const stagingNightlyServerRequest = normalizeInstallRequest({ kind: 'server', serverEnv: 'staging', channel: 'nightly', platform: 'darwin', arch: 'arm64' }) -assert.equal(stagingNightlyServerRequest.channel, 'nightly') -assert.equal(stagingNightlyServerRequest.bundleID, 'com.automattic.beeper.server.nightly') -assert.equal(feedURLFor(stagingNightlyServerRequest), 'https://api.beeper-staging.com/desktop/update-feed.json?bundleID=com.automattic.beeper.server.nightly&platform=darwin&channel=nightly&arch=arm64') -assert.equal(downloadURLFor(stagingNightlyServerRequest), 'https://api.beeper-staging.com/desktop/download/macos/arm64/nightly/com.automattic.beeper.server.nightly') - -const desktopNightlyRequest = normalizeInstallRequest({ kind: 'desktop', channel: 'nightly', platform: 'darwin', arch: 'arm64' }) -assert.equal(downloadURLFor(desktopNightlyRequest), 'https://api.beeper.com/desktop/download/macos/arm64/nightly/com.automattic.beeper.desktop.nightly') - -const fakeClient = { - accounts: { - list: async () => [ - { accountID: 'imessage-main', bridge: { id: 'local-imessage', type: 'imessage' }, network: 'iMessage', user: { displayName: 'Main' } }, - { accountID: 'telegram-main', bridge: { id: 'telegramgo', type: 'telegram' }, network: 'Telegram', user: { displayName: 'Main' } }, - ], +assert.equal(mcpInitialize.status, 0, mcpInitialize.stderr) +payload = JSON.parse(mcpInitialize.stdout) +assert.equal(payload.result.serverInfo.name, 'beeper') +assert.equal(payload.result.serverInfo.version, version.version) + +const mcpCall = spawnSync('bun', ['./bin/dev.js', 'mcp'], { + cwd: root, + encoding: 'utf8', + env: { + ...process.env, + BEEPER_CLI_CONFIG_DIR: configDir, }, - chats: { - retrieve: async id => { - if (id === '!exact:beeper.com' || id === '10313') return { id: '!family:beeper.com', localChatID: '10313', title: 'Family', network: 'iMessage' } - throw new Error('not found') - }, - search: async function* ({ query }) { - const rows = [ - { id: '!family:beeper.com', localChatID: '10313', title: 'Family', network: 'iMessage' }, - { id: '!family-work:beeper.com', localChatID: '8951', title: 'Family Work', network: 'Telegram' }, - ].filter(chat => chat.title.toLowerCase().includes(String(query).toLowerCase())) - for (const row of rows) yield row - }, + input: '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"version","arguments":{}}}\n', +}) +assert.equal(mcpCall.status, 0, mcpCall.stderr) +payload = JSON.parse(mcpCall.stdout) +const mcpVersion = JSON.parse(payload.result.content[0].text) +assert.match(mcpVersion.name, /beeper-cli/) +assert.equal(mcpVersion.version, version.version) + +const mcpEOFCall = spawnSync('bun', ['./bin/dev.js', 'mcp'], { + cwd: root, + encoding: 'utf8', + env: { + ...process.env, + BEEPER_CLI_CONFIG_DIR: configDir, }, -} + input: '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"version","arguments":{}}}', +}) +assert.equal(mcpEOFCall.status, 0, mcpEOFCall.stderr) +payload = JSON.parse(mcpEOFCall.stdout) +assert.equal(payload.id, 4) +assert.equal(JSON.parse(payload.result.content[0].text).version, version.version) -assert.equal(await resolveAccountID(fakeClient, 'imessage'), 'imessage-main') -assert.deepEqual(await resolveAccountIDs(fakeClient, ['main'], { allowMultiplePerInput: true }), ['imessage-main', 'telegram-main']) -await assert.rejects(() => resolveAccountID(fakeClient, 'main'), /Ambiguous account/) -assert.equal(await resolveChatID(fakeClient, '!exact:beeper.com'), '!exact:beeper.com') -assert.equal(await resolveChatID(fakeClient, '10313'), '10313') -assert.equal(await resolveChatID(fakeClient, 'Family Work'), '8951') -assert.equal(await resolveChatID(fakeClient, 'fam', { pick: 2 }), '8951') -await assert.rejects(() => resolveChatID(fakeClient, 'fam'), /Ambiguous chat/) -await assert.rejects(() => resolveChatID(fakeClient, 'missing'), /No chat matches/) - -function listCommandFiles(dir) { - const output = [] - for (const entry of readdirSync(dir, { withFileTypes: true })) { - // Skip private/internal files like _complete used by autocomplete. - if (entry.name.startsWith('_') || entry.name === 'autocomplete.ts') continue - const path = join(dir, entry.name) - if (entry.isDirectory()) { - output.push(...listCommandFiles(path)) - } else if (entry.isFile() && /\.(ts|tsx)$/.test(entry.name) && !entry.name.endsWith('.d.ts')) { - output.push(path) - } - } - return output -} +const mcpDryRunCall = spawnSync('bun', ['./bin/dev.js', '--dry-run', 'mcp'], { + cwd: root, + encoding: 'utf8', + env: { + ...process.env, + BEEPER_CLI_CONFIG_DIR: configDir, + }, + input: '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"messages_context","arguments":{"chat":"chat","id":"m1","after":"3","before":"4"}}}\n', +}) +assert.equal(mcpDryRunCall.status, 0, mcpDryRunCall.stderr) +payload = JSON.parse(mcpDryRunCall.stdout) +const mcpContext = JSON.parse(payload.result.content[0].text) +assert.equal(mcpContext.dry_run, true) +assert.equal(mcpContext.request.after, 3) +assert.equal(mcpContext.request.before, 4) -function fileToCommand(file) { - const relative = file.slice(join(root, 'src/commands').length + 1) - const parts = relative.replace(/\.(ts|tsx)$/, '').split('/') - return parts.map(part => part === 'index' ? undefined : part).filter(Boolean).join(' ') -} +const mcpInvalidJSON = spawnSync('bun', ['./bin/dev.js', 'mcp'], { + cwd: root, + encoding: 'utf8', + env: { + ...process.env, + BEEPER_CLI_CONFIG_DIR: configDir, + }, + input: '{bad json}\n', +}) +assert.equal(mcpInvalidJSON.status, 0, mcpInvalidJSON.stderr) +payload = JSON.parse(mcpInvalidJSON.stdout) +assert.equal(payload.jsonrpc, '2.0') +assert.equal(payload.error.code, -32000) +assert.match(payload.error.message, /JSON/) -assert(!existsSync(join(root, 'src/commands/profile')), 'profile namespace must be deleted') -assert(!existsSync(join(root, 'src/commands/target')), 'singular target namespace must be deleted') -assert(!existsSync(join(root, 'src/commands/app')), 'app/e2ee namespace must be deleted') +rmSync(configDir, { recursive: true, force: true }) diff --git a/packages/cli/test/cloudflare-tunnel.test.ts b/packages/cli/test/cloudflare-tunnel.test.ts new file mode 100644 index 00000000..7dc60be5 --- /dev/null +++ b/packages/cli/test/cloudflare-tunnel.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'bun:test' +import { cloudflaredDomain, findKnownError, findTunnelURL, versionIsGreaterThan, whatToTry } from '../src/lib/cloudflare-tunnel.js' + +describe('cloudflare tunnel helpers', () => { + it('parses cloudflared output and versions', () => { + expect(versionIsGreaterThan('2024.8.2', '2024.8.1')).toBe(true) + expect(versionIsGreaterThan('2024.8.2', '2024.8.2')).toBe(false) + expect(versionIsGreaterThan('2024.8.2', '2024.9.0')).toBe(false) + expect(findTunnelURL('INF https://example.trycloudflare.com ready')).toBe('https://example.trycloudflare.com') + expect(findTunnelURL('INF https://example.example.com ready', 'example.com')).toBe('https://example.example.com') + expect(findTunnelURL('INF https://example.example.com ready')).toBeUndefined() + expect(findKnownError('2024-01-01 ERR Failed to serve quic connection connIndex=1')).toMatch(/Could not start Cloudflare Tunnel/) + expect(whatToTry()).toMatch(/BEEPER_CLOUDFLARED_PATH/) + }) + + it('allows overriding the parsed cloudflared domain', () => { + const previous = process.env.BEEPER_CLOUDFLARED_DOMAIN + process.env.BEEPER_CLOUDFLARED_DOMAIN = 'beeper.test' + expect(cloudflaredDomain()).toBe('beeper.test') + if (previous === undefined) delete process.env.BEEPER_CLOUDFLARED_DOMAIN + else process.env.BEEPER_CLOUDFLARED_DOMAIN = previous + }) +}) diff --git a/packages/cli/test/e2e-staging.ts b/packages/cli/test/e2e-staging.ts index 5e3cd6ae..29ad0e5c 100644 --- a/packages/cli/test/e2e-staging.ts +++ b/packages/cli/test/e2e-staging.ts @@ -5,6 +5,9 @@ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import path from 'node:path' import { fileURLToPath } from 'node:url' +import { commands } from '../src/cli/commands.js' +import { apiItems } from '../src/lib/api-values.js' +import { createProfileTarget, readTarget } from '../src/lib/targets.js' const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') await loadEnvFile(process.env.BEEPER_E2E_ENV_FILE || path.join(repoRoot, '.env.e2e')) @@ -13,9 +16,10 @@ const cliBin = process.env.BEEPER_E2E_CLI_BIN || path.join(repoRoot, 'bin/dev.js const runID = process.env.BEEPER_E2E_RUN_ID || String(Date.now()) const workDir = process.env.BEEPER_E2E_WORKDIR || path.join(tmpdir(), `beeper-cli-e2e-${runID}`) const configDir = process.env.BEEPER_E2E_CONFIG_DIR || path.join(workDir, 'cli-config') +process.env.BEEPER_CLI_CONFIG_DIR = configDir const reportPath = process.env.BEEPER_E2E_REPORT || path.join(workDir, 'report.json') const emailBase = Number(process.env.BEEPER_E2E_EMAIL_BASE || (900000 + Math.floor(Math.random() * 50000))) -const otp = process.env.BEEPER_E2E_OTP?.trim() +const otp = process.env.BEEPER_E2E_OTP?.trim() || '959729' const accountCount = Number(process.env.BEEPER_E2E_ACCOUNT_COUNT || 3) const portStart = Number(process.env.BEEPER_E2E_PORT_START || 24_573) const desktopCount = Number(process.env.BEEPER_E2E_DESKTOP_TARGETS || 1) @@ -66,7 +70,7 @@ if (previousReport?.runID === runID) { } process.on('SIGINT', async () => { - report.notes.push('Interrupted. Run the cleanup phase to stop managed server targets and remove isolated state.') + report.notes.push('Interrupted. Run the cleanup phase to stop local server targets and remove isolated state.') await writeReport() process.exit(130) }) @@ -84,7 +88,6 @@ async function main() { if (hasPhase('start')) await phaseStart() if (hasPhase('login')) await phaseLogin() if (hasPhase('readiness')) await phaseReadiness() - if (hasPhase('verify')) await phaseVerify() if (hasPhase('messaging')) await phaseMessaging() if (hasPhase('surface')) await phaseSurface() if (hasPhase('cleanup')) await phaseCleanup() @@ -106,7 +109,7 @@ async function phasePlan() { report.targets = targets const commands = [ 'bun run --filter beeper-cli build', - `BEEPER_E2E_ENV_FILE=.env.e2e BEEPER_E2E_PHASES=targets,install-server,start,login,readiness,verify,messaging,surface,cleanup BEEPER_E2E_RUN_ID=${runID} bun packages/cli/test/e2e-staging.ts`, + `BEEPER_E2E_ENV_FILE=.env.e2e BEEPER_E2E_PHASES=targets,install-server,start,login,readiness,messaging,surface,cleanup BEEPER_E2E_RUN_ID=${runID} bun packages/cli/test/e2e-staging.ts`, `BEEPER_CLI_CONFIG_DIR=${configDir} bun packages/cli/bin/dev.js targets list --json`, ] report.commands.push(...commands.map(command => ({ phase: 'plan', command }))) @@ -121,11 +124,12 @@ async function phaseTargets() { const targets = plannedTargets() report.targets = targets for (const target of targets) { - const args = target.kind === 'remote' - ? ['targets', 'add', 'remote', target.name, target.baseURL, '--json'] - : target.kind === 'desktop' - ? ['targets', 'add', 'desktop', target.name, '--server-env', 'staging', '--port', String(target.port), '--json'] - : ['targets', 'add', 'server', target.name, '--server-env', 'staging', '--port', String(target.port), '--json'] + if (target.kind !== 'remote') { + await ensureManagedTarget(target) + await writeReport() + continue + } + const args = ['targets', 'add', target.name, target.baseURL, '--json'] const result = runCli(args, { allowFailure: true }) if (result.status !== 0 && !`${result.stderr}${result.stdout}`.includes('already exists')) fail(result, args) recordCommand('targets', args, result) @@ -153,7 +157,7 @@ async function phaseStart() { await writeReport() continue } - const args = ['targets', 'start', target.name, '--json'] + const args = ['targets', 'runtime', 'start', target.name, '--json'] const result = runCli(args, { env: serverEnv(), allowFailure: true }) recordCommand('start', args, result) if (result.status !== 0) { @@ -201,9 +205,7 @@ async function phaseReadiness() { const env = target.accessToken ? { BEEPER_ACCESS_TOKEN: target.accessToken } : undefined for (const args of [ ['status', '--target', target.name, '--json'], - ['doctor', '--target', target.name, '--json'], ['setup', '--target', target.name, '--json'], - ['auth', 'status', '--target', target.name, '--json'], ]) { const result = runCli(args, { env, allowFailure: true }) recordCommand('readiness', args, result) @@ -211,31 +213,6 @@ async function phaseReadiness() { } } -async function phaseVerify() { - const targets = (await plannedTargetsWithAuth()).filter(target => target.accessToken) - if (targets.length < 2) { - recordBlock('verify', undefined, 'verify phase needs at least two signed-in targets for device-to-device auth.', [ - `BEEPER_E2E_RUN_ID=${runID} BEEPER_E2E_OTP="$QA_OTP" BEEPER_E2E_PHASES=login bun packages/cli/test/e2e-staging.ts`, - `BEEPER_E2E_RUN_ID=${runID} BEEPER_E2E_PHASES=verify,readiness bun packages/cli/test/e2e-staging.ts`, - ]) - return - } - await phaseVerifySameAccountDevices(targets) - for (const target of targets) { - for (const args of [ - ['verify', 'status', '--target', target.name, '--json'], - ['verify', 'list', '--target', target.name, '--json'], - ['verify', 'show', '--target', target.name, '--json'], - ['verify', 'sas', '--target', target.name, '--json'], - ['verify', 'sas-confirm', '--target', target.name, '--json'], - ]) { - const result = runCli(args, { env: { BEEPER_ACCESS_TOKEN: target.accessToken }, allowFailure: true }) - recordCommand('verify', args, result) - } - } - report.notes.push('Review verify command results. SAS/QR often needs manual matching between the two target UIs.') -} - async function phaseMessaging() { const signedInTargets = (await plannedTargetsWithAuth()).filter(target => target.accessToken) const sender = signedInTargets[0] @@ -316,7 +293,7 @@ async function phaseSurface() { } async function phaseHelpSurface() { - const commands = await generatedCommands() + const commands = await registeredCommands() for (const command of ['', ...commands]) { const args = command ? [...command.split(' '), '--help'] : ['--help'] const result = runCli(args, { allowFailure: true }) @@ -339,8 +316,8 @@ async function phaseApiSurface() { ['api', 'request', 'GET', '/v1/spec', '--target', target.name, '--no-auth', '--json'], ['api', 'request', 'GET', '/v1/app/setup', '--target', target.name, '--json'], ['api', 'request', 'GET', '/v1/app/setup/verifications', '--target', target.name, '--json'], - ['api', 'get', '/v1/accounts', '--target', target.name, '--json'], - ['api', 'get', '/v1/chats?limit=10', '--target', target.name, '--json'], + ['api', 'request', 'GET', '/v1/accounts', '--target', target.name, '--json'], + ['api', 'request', 'GET', '/v1/chats?limit=10', '--target', target.name, '--json'], ]) { const result = runCli(args, { env, allowFailure: true }) recordCommand('api-surface', args, result) @@ -373,36 +350,21 @@ async function phaseCliSurface() { const cases = [ ['version', '--json'], - ['docs', '--json'], - ['man', '--json'], - ['config', 'path', '--json'], - ['config', 'get', '--json'], - ['config', 'set', 'defaultTarget', target.name, '--json'], - ['config', 'get', 'defaultTarget', '--json'], - ['targets', 'show', target.name, '--json'], - ['targets', 'status', target.name, '--json'], - ['targets', 'use', target.name, '--json'], - ['status', '--target', target.name, '--json'], - ['doctor', '--target', target.name, '--json'], - ['auth', 'status', '--target', target.name, '--json'], - ['bridges', 'list', '--target', target.name, '--json'], - ['bridges', 'list', '--target', target.name, '--provider', 'local', '--available', '--json'], - ['bridges', 'show', 'local-dummy', '--target', target.name, '--json'], + ['schema', '--json'], + ['use', 'target', target.name, '--json'], + ['status', target.name, '--json'], ['accounts', 'list', '--target', target.name, '--json'], ['accounts', 'add', '--target', target.name, '--json'], - ['accounts', 'add', 'local-dummy', '--target', target.name, '--flow', 'password', '--field', 'username=cli-e2e', '--field', `password=${dummyPassword()}`, '--non-interactive', '--json'], - ['accounts', 'add', 'local-dummy', '--target', target.name, '--login-id', 'cli-e2e', '--flow', 'password', '--field', 'username=cli-e2e', '--field', `password=${dummyPassword()}`, '--non-interactive', '--json'], - ['accounts', 'add', 'local-dummy', '--target', target.name, '--flow', 'cookies', '--cookie', 'username=cli-e2e-cookies', '--cookie', `password=${dummyPassword()}`, '--non-interactive', '--json'], - ['accounts', 'add', 'local-dummy', '--target', target.name, '--flow', 'localstorage', '--cookie', 'username=cli-e2e-localstorage', '--cookie', `password=${dummyPassword()}`, '--non-interactive', '--json'], - ['accounts', 'add', 'local-dummy', '--target', target.name, '--flow', 'displayandwait', '--non-interactive', '--json'], + ['accounts', 'add', 'local-dummy', '--target', target.name, '--flow', 'password', '--field', 'username=cli-e2e', '--field', `password=${dummyPassword()}`, '--no-input', '--json'], + ['accounts', 'add', 'local-dummy', '--target', target.name, '--login-id', 'cli-e2e', '--flow', 'password', '--field', 'username=cli-e2e', '--field', `password=${dummyPassword()}`, '--no-input', '--json'], + ['accounts', 'add', 'local-dummy', '--target', target.name, '--flow', 'cookies', '--cookie', 'username=cli-e2e-cookies', '--cookie', `password=${dummyPassword()}`, '--no-input', '--json'], + ['accounts', 'add', 'local-dummy', '--target', target.name, '--flow', 'localstorage', '--cookie', 'username=cli-e2e-localstorage', '--cookie', `password=${dummyPassword()}`, '--no-input', '--json'], + ['accounts', 'add', 'local-dummy', '--target', target.name, '--flow', 'displayandwait', '--no-input', '--json'], ['accounts', 'list', '--target', target.name, '--account', 'local-dummy', '--json'], - ['config', 'get', 'defaultAccount', '--json'], ['chats', 'list', '--target', target.name, '--limit', '20', '--json'], - ['chats', 'search', runID, '--target', target.name, '--limit', '10', '--json'], + ['chats', 'list', '--query', runID, '--target', target.name, '--limit', '10', '--json'], ['contacts', 'list', '--target', target.name, '--limit', '20', '--json'], - ['contacts', 'search', 'staging-user', '--target', target.name, '--json'], - ['verify', 'status', '--target', target.name, '--json'], - ['verify', 'list', '--target', target.name, '--json'], + ['contacts', 'list', '--query', 'staging-user', '--target', target.name, '--json'], ] if (target.kind !== 'remote') cases.splice(9, 0, ['targets', 'logs', target.name, '--lines', '5']) @@ -417,13 +379,13 @@ async function phaseCliSurface() { if (sdkChatID) { cases.push( ['chats', 'pin', '--chat', sdkChatID, '--target', target.name, '--json'], - ['chats', 'unpin', '--chat', sdkChatID, '--target', target.name, '--json'], + ['chats', 'pin', '--chat', sdkChatID, '--clear', '--target', target.name, '--json'], ['chats', 'archive', '--chat', sdkChatID, '--target', target.name, '--json'], - ['chats', 'unarchive', '--chat', sdkChatID, '--target', target.name, '--json'], + ['chats', 'archive', '--chat', sdkChatID, '--clear', '--target', target.name, '--json'], ['chats', 'mute', '--chat', sdkChatID, '--target', target.name, '--json'], - ['chats', 'unmute', '--chat', sdkChatID, '--target', target.name, '--json'], - ['chats', 'mark-read', '--chat', sdkChatID, '--target', target.name, '--json'], - ['chats', 'mark-unread', '--chat', sdkChatID, '--target', target.name, '--json'], + ['chats', 'mute', '--chat', sdkChatID, '--clear', '--target', target.name, '--json'], + ['chats', 'read', '--chat', sdkChatID, '--target', target.name, '--json'], + ['chats', 'read', '--chat', sdkChatID, '--unread', '--target', target.name, '--json'], ['chats', 'priority', '--chat', sdkChatID, '--level', 'inbox', '--target', target.name, '--json'], ['chats', 'description', '--chat', sdkChatID, '--description', `CLI E2E ${runID}`, '--target', target.name, '--json'], ['chats', 'description', '--chat', sdkChatID, '--clear', '--target', target.name, '--json'], @@ -431,39 +393,35 @@ async function phaseCliSurface() { ['chats', 'draft', '--chat', sdkChatID, '--clear', '--target', target.name, '--json'], ['chats', 'disappear', '--chat', sdkChatID, '--seconds', 'off', '--target', target.name, '--json'], ['chats', 'remind', '--chat', sdkChatID, '--when', reminderAt, '--target', target.name, '--json'], - ['chats', 'unremind', '--chat', sdkChatID, '--target', target.name, '--json'], - ['presence', '--chat', sdkChatID, '--state', 'typing', '--duration', '1', '--target', target.name, '--json'], + ['chats', 'remind', '--chat', sdkChatID, '--clear', '--target', target.name, '--json'], + ['send', 'presence', '--to', sdkChatID, '--state', 'typing', '--duration', '1', '--target', target.name, '--json'], ['messages', 'search', runID, '--chat', sdkChatID, '--target', target.name, '--limit', '10', '--json'], - ['messages', 'export', '--chat', sdkChatID, '--target', target.name, '--limit', '10', '--output', '-', '--json'], + ['export', '--chat', sdkChatID, '--target', target.name, '--limit-messages', '10', '--no-attachments', '--out', path.join(workDir, 'exports', 'chat'), '--json'], ) } else { - for (const command of ['chats pin/unpin/archive/unarchive/mute/unmute/mark-read/mark-unread/priority/description/draft/disappear/remind/unremind', 'presence']) { + for (const command of ['chats description/draft/disappear/remind', 'send presence']) { report.coverage.skipped.push({ command, reason: 'No Desktop-indexed chat was available from Beeper Server; raw Matrix rooms do not support Desktop chat mutation APIs.' }) } report.coverage.skipped.push({ command: 'messages search --chat', reason: 'No Desktop-indexed chat was available from Beeper Server; raw Matrix rooms are not searched through Desktop message APIs.' }) - report.coverage.skipped.push({ command: 'messages export', reason: 'No Desktop-indexed chat was available from Beeper Server; raw Matrix rooms are not exported through Desktop message APIs.' }) + report.coverage.skipped.push({ command: 'export --chat', reason: 'No Desktop-indexed chat was available from Beeper Server; raw Matrix rooms are not exported through Desktop message APIs.' }) } if (sdkChatID && messageID) { cases.push( - ['messages', 'show', '--chat', sdkChatID, '--id', messageID, '--target', target.name, '--json'], ['messages', 'context', '--chat', sdkChatID, '--id', messageID, '--target', target.name, '--before', '2', '--after', '2', '--json'], ['send', 'react', '--to', sdkChatID, '--id', messageID, '--reaction', '+1', '--target', target.name, '--json'], - ['send', 'unreact', '--to', sdkChatID, '--id', messageID, '--reaction', '+1', '--target', target.name, '--json'], + ['send', 'react', '--to', sdkChatID, '--id', messageID, '--reaction', '+1', '--remove', '--target', target.name, '--json'], ) } else if (!sdkChatID) { - report.coverage.skipped.push({ command: 'messages show/context and send react/unreact', reason: 'No Desktop-indexed chat/message was available from Beeper Server; raw Matrix rooms do not support these Desktop message APIs.' }) + report.coverage.skipped.push({ command: 'messages context and reaction send', reason: 'No Desktop-indexed chat/message was available from Beeper Server; raw Matrix rooms do not support these Desktop message APIs.' }) } for (const args of cases) { const result = runCli(args, { env, allowFailure: true }) recordCommand('cli-surface', args, result) - const expectedDoctorDiagnostic = args[0] === 'doctor' && result.status !== 0 && parseEnvelope(result.stdout)?.data - recordCoverage('commands', args, result, expectedDoctorDiagnostic ? true : undefined) + recordCoverage('commands', args, result) if (args[0] === 'accounts' && args[1] === 'add' && args.length === 5) { report.notes.push('accounts add without a bridge returned the bridge-picker data; local-dummy covers the actual login flow.') - } else if (expectedDoctorDiagnostic) { - report.notes.push('doctor returned non-zero because the target is not fully healthy; JSON diagnostics were still returned.') } else if (result.status !== 0) { recordFailure('cli-surface', target, `beeper ${args.join(' ')} failed with status ${result.status}`) } @@ -479,16 +437,15 @@ async function phaseLocalDummyAccountSurface(target, env) { recordCommand('cli-surface', listArgs, list) recordCoverage('commands', listArgs, list) const accounts = parseEnvelope(list.stdout)?.data - const account = Array.isArray(accounts) ? accounts.find(item => item?.id || item?.accountID) : undefined - const accountID = account?.id ?? account?.accountID + const accountID = firstField(apiItems(accounts), ['id', 'accountID']) if (!accountID) { recordFailure('cli-surface', target, 'local-dummy login completed but accounts list did not return a reusable account ID.') return } for (const args of [ - ['accounts', 'show', accountID, '--target', target.name, '--json'], - ['accounts', 'use', accountID, '--target', target.name, '--json'], - ['config', 'get', 'defaultAccount', '--json'], + ['accounts', 'list', '--account', accountID, '--target', target.name, '--json'], + ['use', 'account', accountID, '--target', target.name, '--json'], + ['accounts', 'list', '--account', accountID, '--target', target.name, '--json'], ]) { const result = runCli(args, { env, allowFailure: true }) recordCommand('cli-surface', args, result) @@ -502,23 +459,8 @@ async function phaseControlSurface() { const target = targets.find(item => item.accessToken) ?? targets[0] if (!target) return - for (const args of [ - ['update', '--server', '--check', '--json'], - ]) { - const result = runCli(args, { env: serverEnv(), allowFailure: true }) - recordCommand('control-surface', args, result) - recordCoverage('commands', args, result) - if (result.status !== 0) recordFailure('control-surface', target, `beeper ${args.join(' ')} failed with status ${result.status}`) - if (args[0] === 'targets' && args[1] === 'restart') { - try { - await waitForInfo(target) - } catch (error) { - recordFailure('control-surface', target, error) - } - } - } if (target.kind === 'server') { - const args = ['targets', 'restart', target.name, '--json'] + const args = ['targets', 'runtime', 'restart', target.name, '--json'] const result = runCli(args, { env: serverEnv(), allowFailure: true }) recordCommand('control-surface', args, result) recordCoverage('commands', args, result) @@ -529,21 +471,20 @@ async function phaseControlSurface() { recordFailure('control-surface', target, error) } } else { - report.coverage.skipped.push({ command: 'targets restart', reason: 'Only server targets are lifecycle-managed by the CLI.' }) + report.coverage.skipped.push({ command: 'targets runtime restart', reason: 'Only local server targets are controlled by the CLI.' }) } const remoteName = `remote-${runID}` for (const args of [ - ['targets', 'add', 'remote', remoteName, 'http://127.0.0.1:9', '--json'], - ['targets', 'show', remoteName, '--json'], - ['targets', 'status', remoteName, '--json'], - ['targets', 'remove', remoteName, '--json'], + ['targets', 'add', remoteName, 'http://127.0.0.1:9', '--json'], + ['status', remoteName, '--json'], + ['remove', 'target', remoteName, '--json'], ]) { const result = runCli(args, { allowFailure: true }) recordCommand('control-surface', args, result) - const expectedUnreachable = args[0] === 'targets' && args[1] === 'status' && result.status !== 0 && parseEnvelope(result.stdout)?.data + const expectedUnreachable = args[0] === 'status' && result.status !== 0 && parseEnvelope(result.stdout)?.data recordCoverage('commands', args, result, expectedUnreachable ? true : undefined) - if (args[0] === 'targets' && args[1] === 'status' && result.status !== 0 && parseEnvelope(result.stdout)?.data) { + if (args[0] === 'status' && result.status !== 0 && parseEnvelope(result.stdout)?.data) { report.notes.push('remote target status returned non-zero because the test URL is intentionally unreachable; JSON diagnostics were still returned.') } else if (result.status !== 0) { recordFailure('control-surface', target, `beeper ${args.join(' ')} failed with status ${result.status}`) @@ -554,7 +495,7 @@ async function phaseControlSurface() { if (logoutTarget) { for (const args of [ ['auth', 'logout', '--target', logoutTarget.name, '--json'], - ['auth', 'status', '--target', logoutTarget.name, '--json'], + ['status', logoutTarget.name, '--json'], ]) { const result = runCli(args, { allowFailure: true }) recordCommand('control-surface', args, result) @@ -564,139 +505,13 @@ async function phaseControlSurface() { } } -async function phaseVerifySameAccountDevices(targets) { - const byUserID = new Map() - for (const target of targets) { - const userID = target.matrix?.userID - if (!userID) continue - const group = byUserID.get(userID) ?? [] - group.push(target) - byUserID.set(userID, group) - } - - const pair = [...byUserID.values()].find(group => group.length >= 2) - if (!pair) { - recordBlock('verify', undefined, 'Device-to-device verification needs two targets signed into the same QA account.', [ - `BEEPER_E2E_OTP="$QA_OTP" BEEPER_E2E_EMAIL_1="$QA_EMAIL_1" BEEPER_E2E_EMAIL_2="$QA_EMAIL_1" BEEPER_E2E_EMAIL_3="$QA_EMAIL_2" BEEPER_E2E_ACCOUNT_COUNT=3 BEEPER_E2E_DESKTOP_TARGETS=0 BEEPER_E2E_SERVER_TARGETS=3 BEEPER_E2E_PHASES=targets,install-server,start,login,readiness,verify,messaging,cleanup bun packages/cli/test/e2e-staging.ts`, - ]) - return - } - - await Promise.all(pair.map(target => waitForVerificationState(target))) - const [initiator, responder] = await verificationPair(pair) - const startArgs = ['verify', 'start', '--target', initiator.name, '--user', responder.matrix.userID, '--json'] - const start = runCli(startArgs, { env: { BEEPER_ACCESS_TOKEN: initiator.accessToken }, allowFailure: true }) - recordCommand('verify-devices', startArgs, start) - - const responderResults = await pollResponderVerification(responder) - const responderVerificationID = verificationIDFromResults(responderResults) - for (const baseArgs of [ - ['verify', 'approve', '--target', responder.name], - ['verify', 'sas', '--target', responder.name], - ]) { - const args = responderVerificationID ? [...baseArgs, '--id', responderVerificationID, '--json'] : [...baseArgs, '--json'] - const result = runCli(args, { env: { BEEPER_ACCESS_TOKEN: responder.accessToken }, allowFailure: true }) - recordCommand('verify-devices', args, result) - } - await sleep(1000) - - const initiatorSASArgs = responderVerificationID - ? ['verify', 'sas', '--target', initiator.name, '--id', responderVerificationID, '--json'] - : ['verify', 'sas', '--target', initiator.name, '--json'] - const initiatorSAS = runCli(initiatorSASArgs, { env: { BEEPER_ACCESS_TOKEN: initiator.accessToken }, allowFailure: true }) - recordCommand('verify-devices', initiatorSASArgs, initiatorSAS) - await sleep(1000) - - for (const args of [ - ['verify', 'show', '--target', responder.name, '--json'], - ['verify', 'show', '--target', initiator.name, '--json'], - ['verify', 'status', '--target', initiator.name, '--json'], - ['verify', 'status', '--target', responder.name, '--json'], - ]) { - const target = args.includes(initiator.name) ? initiator : responder - const result = runCli(args, { env: { BEEPER_ACCESS_TOKEN: target.accessToken }, allowFailure: true }) - recordCommand('verify-devices', args, result) - } - - for (const target of [initiator, responder]) { - const args = responderVerificationID - ? ['verify', 'sas-confirm', '--target', target.name, '--id', responderVerificationID, '--json'] - : ['verify', 'sas-confirm', '--target', target.name, '--json'] - const result = runCli(args, { env: { BEEPER_ACCESS_TOKEN: target.accessToken }, allowFailure: true }) - recordCommand('verify-devices', args, result) - } - await sleep(1000) - for (const args of [ - ['verify', 'status', '--target', initiator.name, '--json'], - ['verify', 'status', '--target', responder.name, '--json'], - ]) { - const target = args.includes(initiator.name) ? initiator : responder - const result = runCli(args, { env: { BEEPER_ACCESS_TOKEN: target.accessToken }, allowFailure: true }) - recordCommand('verify-devices', args, result) - } -} - -async function waitForVerificationState(target) { - for (let attempt = 0; attempt < 30; attempt++) { - const args = ['verify', 'status', '--target', target.name, '--json'] - const result = runCli(args, { env: { BEEPER_ACCESS_TOKEN: target.accessToken }, allowFailure: true }) - recordCommand('verify-devices', args, result) - const state = parseEnvelope(result.stdout)?.data?.state - if (result.status === 0 && (state === 'ready' || state === 'needs-verification' || state === 'needs-recovery-key' || state === 'needs-secrets')) return state - await sleep(1000) - } - throw new Error(`Timed out waiting for ${target.name} to reach a verification-ready state`) -} - -async function verificationPair(pair) { - const states = [] - for (const target of pair) { - const args = ['verify', 'status', '--target', target.name, '--json'] - const result = runCli(args, { env: { BEEPER_ACCESS_TOKEN: target.accessToken }, allowFailure: true }) - recordCommand('verify-devices', args, result) - const data = parseEnvelope(result.stdout)?.data - states.push({ target, verified: data?.app?.e2ee?.verified === true }) - } - const initiator = states.find(item => !item.verified)?.target ?? pair[0] - const responder = states.find(item => item.target !== initiator && item.verified)?.target ?? pair.find(target => target !== initiator) ?? pair[1] - return [initiator, responder] -} - -async function pollResponderVerification(responder) { - const results = [] - for (let attempt = 0; attempt < 12; attempt++) { - const listArgs = ['verify', 'list', '--target', responder.name, '--json'] - const list = runCli(listArgs, { env: { BEEPER_ACCESS_TOKEN: responder.accessToken }, allowFailure: true }) - recordCommand('verify-devices', listArgs, list) - results.push(list) - if (verificationIDFromResults([list])) { - const showArgs = ['verify', 'show', '--target', responder.name, '--json'] - const show = runCli(showArgs, { env: { BEEPER_ACCESS_TOKEN: responder.accessToken }, allowFailure: true }) - recordCommand('verify-devices', showArgs, show) - results.push(show) - return results - } - await sleep(1000) - } - return results -} - -function verificationIDFromResults(results) { - for (const result of results) { - const data = parseEnvelope(result.stdout)?.data - if (Array.isArray(data) && data[0]?.id) return data[0].id - if (data?.id) return data.id - } - return undefined -} - async function phaseCleanup() { for (const target of plannedTargets()) { if (target.kind === 'server') { - const stop = runCli(['targets', 'stop', target.name, '--json'], { allowFailure: true }) - recordCommand('cleanup', ['targets', 'stop', target.name, '--json'], stop) + const stop = runCli(['targets', 'runtime', 'stop', target.name, '--json'], { allowFailure: true }) + recordCommand('cleanup', ['targets', 'runtime', 'stop', target.name, '--json'], stop) } else if (target.kind === 'remote') { - report.notes.push(`Remote Server target ${target.name} was not lifecycle-managed by the harness.`) + report.notes.push(`Remote Server target ${target.name} was not controlled by the harness.`) } else { report.notes.push(`Desktop target ${target.name} may need manual quit if it was launched through the app.`) } @@ -734,7 +549,7 @@ async function plannedTargetsWithAuth() { } function targetPlan(kind, index, ordinal, baseURL) { - const email = process.env[`BEEPER_E2E_EMAIL_${ordinal + 1}`] || `staging-user-${emailBase + ordinal}@example.invalid` + const email = process.env[`BEEPER_E2E_EMAIL_${ordinal + 1}`] || `qatest+${emailBase + ordinal}@beeper.com` const port = Number(process.env[`BEEPER_E2E_PORT_${ordinal + 1}`] || (portStart + ordinal)) return { kind, @@ -747,6 +562,18 @@ function targetPlan(kind, index, ordinal, baseURL) { } } +async function ensureManagedTarget(target) { + const existing = await readTarget(target.name) + if (!existing) await createProfileTarget(target.kind, target.name, { port: target.port, serverEnv: 'staging' }) + report.commands.push({ + phase: 'targets', + command: `prepare local ${target.kind} target ${target.name}`, + status: 0, + stdout: existing ? 'target already exists' : `created ${target.baseURL}`, + stderr: '', + }) +} + async function readPreviousReport() { try { return JSON.parse(await readFile(reportPath, 'utf8')) @@ -812,7 +639,7 @@ function recordLoginBlock(target, args, result) { const command = `beeper ${args.join(' ')}` if (target.kind === 'desktop' && /signed-in local Beeper Desktop session|missing access_token/i.test(output)) { recordBlock('login', target, 'Sign in to the isolated Desktop target, then rerun the login/readiness phases.', [ - `BEEPER_CLI_CONFIG_DIR=${configDir} bun packages/cli/bin/dev.js targets start ${target.name} --json`, + `BEEPER_CLI_CONFIG_DIR=${configDir} bun packages/cli/bin/dev.js targets runtime start ${target.name} --json`, `BEEPER_CLI_CONFIG_DIR=${configDir} bun packages/cli/bin/dev.js setup --target ${target.name} --local --json`, `BEEPER_E2E_RUN_ID=${runID} BEEPER_E2E_OTP="$QA_OTP" BEEPER_E2E_PHASES=login,readiness bun packages/cli/test/e2e-staging.ts`, ]) @@ -820,9 +647,9 @@ function recordLoginBlock(target, args, result) { } if ((target.kind === 'server' || target.kind === 'remote') && /OAuth authorization failed|needs-login|server_error/i.test(output)) { recordBlock('login', target, 'Complete Server setup sign-in, then rerun the login/readiness phases.', [ - `BEEPER_CLI_CONFIG_DIR=${configDir} bun packages/cli/bin/dev.js targets start ${target.name} --json`, + `BEEPER_CLI_CONFIG_DIR=${configDir} bun packages/cli/bin/dev.js targets runtime start ${target.name} --json`, `BEEPER_CLI_CONFIG_DIR=${configDir} bun packages/cli/bin/dev.js auth email start --target ${target.name} --email ${target.email} --json`, - `BEEPER_CLI_CONFIG_DIR=${configDir} bun packages/cli/bin/dev.js auth email response --target ${target.name} --setup-request-id "$SETUP_REQUEST_ID" --code "$QA_OTP" --username "$QA_USERNAME" --yes --json`, + `BEEPER_CLI_CONFIG_DIR=${configDir} bun packages/cli/bin/dev.js auth email response --target ${target.name} --setup-request-id "$SETUP_REQUEST_ID" --code "$QA_OTP" --username "$QA_USERNAME" --force --json`, `BEEPER_E2E_RUN_ID=${runID} BEEPER_E2E_OTP="$QA_OTP" BEEPER_E2E_PHASES=login,readiness bun packages/cli/test/e2e-staging.ts`, ]) return @@ -844,7 +671,7 @@ async function loginServerViaSetupAPI(target) { return false } - const responseArgs = ['auth', 'email', 'response', '--target', target.name, '--setup-request-id', setupRequestID, '--code', otp, '--username', usernameForEmail(target.email), '--yes', '--json'] + const responseArgs = ['auth', 'email', 'response', '--target', target.name, '--setup-request-id', setupRequestID, '--code', otp, '--username', usernameForEmail(target.email), '--force', '--json'] const response = runCli(responseArgs, { allowFailure: true }) recordCommand('login', responseArgs, response) if (response.status !== 0) { @@ -904,24 +731,33 @@ async function findReusableChatID(target, env) { const result = runCli(['chats', 'list', '--target', target.name, '--limit', '20', '--json'], { env, allowFailure: true }) recordCommand('surface-setup', ['chats', 'list', '--target', target.name, '--limit', '20', '--json'], result) const items = parseEnvelope(result.stdout)?.data - const chat = Array.isArray(items) ? items.find(item => item?.id || item?.localChatID || item?.chatID) : undefined - if (!chat) { + const chatID = firstField(apiItems(items), ['localChatID', 'id', 'chatID']) + if (!chatID) { report.coverage.skipped.push({ command: 'Desktop-indexed chat mutation surface', reason: 'No reusable Desktop-indexed chat was returned by chats list.' }) return undefined } - return chat.localChatID ?? chat.id ?? chat.chatID + return chatID } async function findReusableMessageID(target, chatID, env) { const result = runCli(['messages', 'list', '--chat', chatID, '--target', target.name, '--limit', '20', '--json'], { env, allowFailure: true }) recordCommand('surface-setup', ['messages', 'list', '--chat', chatID, '--target', target.name, '--limit', '20', '--json'], result) const items = parseEnvelope(result.stdout)?.data - const message = Array.isArray(items) ? items.find(item => item?.id || item?.messageID || item?.eventID || item?.event_id) : undefined - if (!message) { + const messageID = firstField(apiItems(items), ['id', 'messageID', 'eventID', 'event_id']) + if (!messageID) { report.coverage.skipped.push({ command: 'message-specific Desktop surface', reason: 'No reusable message ID was returned by messages list.' }) return undefined } - return message.id ?? message.messageID ?? message.eventID ?? message.event_id + return messageID +} + +function firstField(items, fields) { + for (const item of items) { + for (const field of fields) { + if (item[field]) return String(item[field]) + } + } + return undefined } function recordCoverage(type, args, result, ok = result.status === 0) { @@ -933,10 +769,9 @@ function recordCoverage(type, args, result, ok = result.status === 0) { }) } -async function generatedCommands() { - const source = await readFile(path.join(repoRoot, 'src/commands.generated.ts'), 'utf8') - return [...source.matchAll(/'([^']+)': Command/g)] - .map(match => match[1].replaceAll(':', ' ')) +async function registeredCommands() { + return commands + .map(command => command.path.join(' ')) .sort() } diff --git a/packages/cli/test/e2e-staging/README.md b/packages/cli/test/e2e-staging/README.md index 0e016fd5..49a24d83 100644 --- a/packages/cli/test/e2e-staging/README.md +++ b/packages/cli/test/e2e-staging/README.md @@ -1,54 +1,43 @@ # Beeper CLI Staging E2E -This harness is for coordinated staging QA of the unreleased Beeper CLI command -surface. It is intentionally explicit: the default run prints a plan and does -not launch apps, download artifacts, or touch the default Desktop instance. +This harness is for coordinated staging QA of the Beeper CLI command surface. +The default run prints a plan only. It does not launch apps, download artifacts, +or touch the default Desktop instance. ## Safety Model - Use a fresh `BEEPER_E2E_RUN_ID` per run. -- Use `BEEPER_E2E_WORKDIR` under `/tmp` unless you need to preserve artifacts. -- The harness writes CLI state under `BEEPER_E2E_CONFIG_DIR`, defaulting to - `/cli-config`. +- Use `BEEPER_E2E_WORKDIR` under `/tmp` unless preserving artifacts. +- The harness writes CLI state under `BEEPER_E2E_CONFIG_DIR`, defaulting to `/cli-config`. - Use non-default PAS ports. The default starts at `24573`, not `23373`. - Every test target uses `--server-env staging`. -- Provide staging account emails through `BEEPER_E2E_EMAIL_*` and verification - codes through `BEEPER_E2E_OTP` only for scripts that explicitly target - verified setup-login APIs. -- Do not run `install-server` unless you intend to download the staging server - artifact. +- Put staging OTPs in `.env.e2e` or set `BEEPER_E2E_ENV_FILE`; reports redact OTPs, setup responses, access tokens, and lead tokens. +- Do not run `install-server` unless you intend to download the staging server artifact. ## Basic Plan ```sh cd path/to/cli -bun run --filter @beeper/cli build +bun run --filter beeper-cli build BEEPER_E2E_RUN_ID=qa-$(date +%Y%m%d-%H%M%S) \ bun packages/cli/test/e2e-staging.ts ``` -The plan output shows the target names, ports, emails, and follow-up command. +The plan output shows target names, ports, emails, and follow-up commands. ## Full Coordinated Surface Run Use this when a staging server binary already exists or `BEEPER_SERVER_BIN` is set. This creates isolated targets, starts them, authenticates Desktop targets -with `beeper setup --local` and Server targets with `beeper setup --oauth`, -checks readiness, attempts device verification commands, runs a small messaging -pass, creates a group when three QA users are available, runs CLI/API surface -coverage, and stops managed server targets. -Server targets sign in through the public setup API using `beeper api post ---no-auth` and the QA OTP from `BEEPER_E2E_OTP`. Put the OTP in `.env.e2e` or -point `BEEPER_E2E_ENV_FILE` at the secret env file. The report redacts OTPs, -setup responses, access tokens, and lead tokens. Desktop targets still use -`beeper setup --local` after the isolated Desktop profile has been signed in -through the app UI. +with `beeper setup --local` and Server targets through the setup API, checks +readiness, runs messaging coverage, creates a group when three QA users are +available, runs CLI/API surface coverage, and stops local server targets. ```sh BEEPER_E2E_RUN_ID=qa-$(date +%Y%m%d-%H%M%S) \ BEEPER_E2E_ENV_FILE=.env.e2e \ -BEEPER_E2E_PHASES=targets,start,login,readiness,verify,messaging,surface,cleanup \ +BEEPER_E2E_PHASES=targets,start,login,readiness,messaging,surface,cleanup \ BEEPER_E2E_ACCOUNT_COUNT=3 \ BEEPER_E2E_DESKTOP_TARGETS=1 \ BEEPER_E2E_SERVER_TARGETS=2 \ @@ -57,8 +46,8 @@ bun packages/cli/test/e2e-staging.ts ``` The report is written to `/tmp/beeper-cli-e2e-/report.json` by default. -Expected human steps are written under `blocked` with concrete follow-up -commands. Real harness failures are written under `failures`. +Expected human steps are written under `blocked`; harness failures are written +under `failures`. The `surface` phase is the consolidated Desktop/Client API coverage pass. It uses CLI commands when the CLI has a first-class command and falls back to @@ -73,7 +62,7 @@ Only run this when you want the CLI to download the staging server artifact: ```sh BEEPER_E2E_RUN_ID=qa-$(date +%Y%m%d-%H%M%S) \ BEEPER_E2E_OTP="$QA_OTP" \ -BEEPER_E2E_PHASES=targets,install-server,start,login,readiness,verify,messaging,cleanup \ +BEEPER_E2E_PHASES=targets,install-server,start,login,readiness,messaging,cleanup \ bun packages/cli/test/e2e-staging.ts ``` @@ -88,25 +77,6 @@ dependencies, and it does not modify lockfiles. ## Manual Coordination Points -Device-to-device auth may require looking at the two target UIs and matching SAS -or QR state. The harness records the CLI attempts for: - -- `verify status` -- `verify list` -- `verify start` -- `verify show` -- `verify sas` -- `verify sas confirm` - -If the verification transaction needs UI confirmation, use the report to find -the target names and ports, complete the UI action, then rerun: - -```sh -BEEPER_E2E_RUN_ID= \ -BEEPER_E2E_PHASES=verify,readiness \ -bun packages/cli/test/e2e-staging.ts -``` - If login is blocked, the report includes target-specific commands for opening the isolated target and rerunning `setup --local` or the setup API commands. Complete the browser or Desktop UI step, then rerun: @@ -114,7 +84,7 @@ Complete the browser or Desktop UI step, then rerun: ```sh BEEPER_E2E_RUN_ID= \ BEEPER_E2E_OTP="$QA_OTP" \ -BEEPER_E2E_PHASES=login,readiness,verify,messaging,cleanup \ +BEEPER_E2E_PHASES=login,readiness,messaging,cleanup \ bun packages/cli/test/e2e-staging.ts ``` @@ -127,7 +97,3 @@ BEEPER_E2E_RUN_ID= \ BEEPER_E2E_PHASES=cleanup \ bun packages/cli/test/e2e-staging.ts ``` - -Desktop targets launched through the app may need to be quit manually. The -harness uses separate `BEEPER_PROFILE`, data directories, and ports, so this -does not require touching the default Desktop profile. diff --git a/packages/cli/test/errors.test.ts b/packages/cli/test/errors.test.ts index 3f4e8c82..ff842c15 100644 --- a/packages/cli/test/errors.test.ts +++ b/packages/cli/test/errors.test.ts @@ -1,30 +1,14 @@ import { describe, expect, it } from 'bun:test' -import { CLIError, ExitCodes, ambiguous, authRequired, notFound, notReady, usageError } from '../src/lib/errors.js' +import { AbortError, CLIError, ExitCodes } from '../src/lib/errors.js' -describe('CLIError factories', () => { - it('attaches exit code 2 for usage', () => { - const err = usageError('bad flag') +describe('CLIError', () => { + it('AbortError carries an explicit exit code', () => { + const err = new AbortError('bad flag', ExitCodes.Usage) expect(err).toBeInstanceOf(CLIError) expect(err.exitCode).toBe(ExitCodes.Usage) expect(err.message).toBe('bad flag') }) - it('attaches exit code 3 for authRequired', () => { - expect(authRequired('sign in').exitCode).toBe(ExitCodes.AuthRequired) - }) - - it('attaches exit code 4 for notReady', () => { - expect(notReady('not ready').exitCode).toBe(ExitCodes.NotReady) - }) - - it('attaches exit code 5 for notFound', () => { - expect(notFound('missing').exitCode).toBe(ExitCodes.NotFound) - }) - - it('attaches exit code 6 for ambiguous', () => { - expect(ambiguous('pick one').exitCode).toBe(ExitCodes.Ambiguous) - }) - it('CLIError instances are Error subclasses', () => { const err = new CLIError('boom', ExitCodes.Generic) expect(err).toBeInstanceOf(Error) diff --git a/packages/cli/test/fixtures/fake-client.ts b/packages/cli/test/fixtures/fake-client.ts deleted file mode 100644 index 5dd83619..00000000 --- a/packages/cli/test/fixtures/fake-client.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Lightweight fake of the @beeper/desktop-api client. Shape matches what - * commands actually call. Pass per-test overrides to swap individual methods. - */ -import { mock } from 'bun:test' - -type Mock = ReturnType - -export type FakeChat = { - id: string - accountID?: string - title?: string - localChatID?: string - network?: string - isArchived?: boolean - isPinned?: boolean - isMuted?: boolean - isLowPriority?: boolean - isMarkedUnread?: boolean - unreadCount?: number - type?: 'single' | 'group' -} - -export type FakeMessage = { - id: string - chatID: string - text?: string - isSender?: boolean - senderID?: string - timestamp?: string - type?: string -} - -export type FakeClient = { - accounts: { - list: Mock - contacts: { list: Mock; search: Mock } - retrieve?: Mock - } - chats: { - list: Mock - retrieve: Mock - search: Mock - update: Mock - archive: Mock - markRead: Mock - markUnread: Mock - notifyAnyway: Mock - start: Mock - messages: { reactions: { add: Mock; delete: Mock } } - reminders: { create: Mock; delete: Mock } - } - messages: { - list: Mock - search: Mock - retrieve: Mock - send: Mock - update: Mock - delete: Mock - } - assets: { upload: Mock; serve: Mock } - bridges: { list: Mock; loginFlows: { list: Mock }; loginSessions: { create: Mock } } - app: any - post: Mock - get: Mock - put: Mock - delete: Mock - focus: Mock -} - -export function makeFakeClient(overrides: Partial = {}): FakeClient { - const empty = () => async function* () {}() as AsyncIterable - const okPage = (items: T[]) => async function* () { for (const it of items) yield it }() as AsyncIterable - - return { - accounts: { - list: mock(async () => []), - contacts: { list: mock(() => empty()), search: mock(async () => ({ items: [] })) }, - ...overrides.accounts, - }, - chats: { - list: mock(() => empty()), - retrieve: mock(async (id: string) => ({ id })), - search: mock(() => empty()), - update: mock(async (id: string, body: any) => ({ id, ...body })), - archive: mock(async () => ({})), - markRead: mock(async () => ({})), - markUnread: mock(async () => ({})), - notifyAnyway: mock(async () => ({})), - start: mock(async () => ({ chatID: '!new:beeper.com' })), - messages: { reactions: { add: mock(async () => ({})), delete: mock(async () => ({})) } }, - reminders: { create: mock(async () => ({})), delete: mock(async () => ({})) }, - ...overrides.chats, - }, - messages: { - list: mock(() => empty()), - search: mock(() => empty()), - retrieve: mock(async (id: string) => ({ id })), - send: mock(async () => ({ pendingMessageID: 'pending-1' })), - update: mock(async (id: string) => ({ id })), - delete: mock(async () => undefined), - ...overrides.messages, - }, - assets: { upload: mock(async () => ({ uploadID: 'upload-1', mimeType: 'application/octet-stream' })), serve: mock(async () => ({ arrayBuffer: async () => new ArrayBuffer(0) })), ...overrides.assets }, - bridges: { list: mock(async () => ({ items: [] })), loginFlows: { list: mock(async () => ({ items: [] })) }, loginSessions: { create: mock(async () => ({})) }, ...overrides.bridges }, - app: overrides.app ?? {}, - post: mock(async () => ({})), - get: mock(async () => ({})), - put: mock(async () => ({})), - delete: mock(async () => ({})), - focus: mock(async () => ({})), - ...overrides, - } -} - -export function chatsPage(items: FakeChat[]) { - return async function* () { for (const it of items) yield it }() -} - -export function messagesPage(items: FakeMessage[]) { - return async function* () { for (const it of items) yield it }() -} diff --git a/packages/cli/test/messages-list-filter.test.ts b/packages/cli/test/messages-list-filter.test.ts deleted file mode 100644 index 231a854f..00000000 --- a/packages/cli/test/messages-list-filter.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, expect, it } from 'bun:test' -import { matchesSender } from '../src/commands/messages/list.js' - -describe('messages list matchesSender', () => { - it('matches "me" for outgoing messages', () => { - expect(matchesSender({ isSender: true }, 'me')).toBe(true) - expect(matchesSender({ isSender: false }, 'me')).toBe(false) - expect(matchesSender({}, 'me')).toBe(false) - }) - - it('matches "others" for incoming messages', () => { - expect(matchesSender({ isSender: false }, 'others')).toBe(true) - expect(matchesSender({}, 'others')).toBe(true) - expect(matchesSender({ isSender: true }, 'others')).toBe(false) - }) - - it('matches by specific senderID', () => { - expect(matchesSender({ senderID: '@alice:beeper.com' }, '@alice:beeper.com')).toBe(true) - expect(matchesSender({ senderID: '@bob:beeper.com' }, '@alice:beeper.com')).toBe(false) - }) - - it('rejects non-objects', () => { - expect(matchesSender(null, 'me')).toBe(false) - expect(matchesSender('hello', 'me')).toBe(false) - }) -}) diff --git a/packages/cli/test/messages-search-validation.test.ts b/packages/cli/test/messages-search-validation.test.ts index bb4c2117..0adc3c40 100644 --- a/packages/cli/test/messages-search-validation.test.ts +++ b/packages/cli/test/messages-search-validation.test.ts @@ -16,10 +16,9 @@ describe('messages search query-or-filter requirement', () => { it('rejects empty query with no filters and exits with usage error', () => { const result = run('messages', 'search', '--json') expect(result.status).toBe(2) - const envelope = JSON.parse(result.stderr) - expect(envelope.ok).toBe(false) - expect(envelope.error.exitCode).toBe(2) - expect(envelope.error.message).toMatch(/Provide a search query or at least one filter flag/) + const payload = JSON.parse(result.stderr) + expect(payload.error.exitCode).toBe(2) + expect(payload.error.message).toMatch(/Provide a search query or at least one filter flag/) }) it('accepts a bare query', () => { diff --git a/packages/cli/test/plugin-sdk.test.ts b/packages/cli/test/plugin-sdk.test.ts deleted file mode 100644 index 2d815aa9..00000000 --- a/packages/cli/test/plugin-sdk.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expect, it } from 'bun:test' -import * as sdk from '../src/plugin-sdk.js' - -describe('plugin-sdk public surface', () => { - it('exports BeeperCommand as a constructor', () => { - expect(typeof sdk.BeeperCommand).toBe('function') - }) - - it('exports the error class and factories', () => { - expect(typeof sdk.CLIError).toBe('function') - expect(sdk.ExitCodes.AuthRequired).toBe(3) - expect(sdk.notFound('x').exitCode).toBe(sdk.ExitCodes.NotFound) - expect(sdk.ambiguous('x').exitCode).toBe(sdk.ExitCodes.Ambiguous) - expect(sdk.notReady('x').exitCode).toBe(sdk.ExitCodes.NotReady) - expect(sdk.authRequired('x').exitCode).toBe(sdk.ExitCodes.AuthRequired) - expect(sdk.usageError('x').exitCode).toBe(sdk.ExitCodes.Usage) - }) - - it('exports printers and resolvers', () => { - for (const name of ['printData', 'printList', 'printSuccess', 'printFailure', 'collectPage', 'startStream', 'printIDs'] as const) { - expect(typeof sdk[name]).toBe('function') - } - for (const name of ['resolveAccountID', 'resolveAccountIDs', 'resolveChatID', 'listAccountIDs', 'userQueryFromInput'] as const) { - expect(typeof sdk[name]).toBe('function') - } - }) - - it('exports target + config helpers', () => { - for (const name of ['createBeeperClient', 'requireToken', 'resolveTarget', 'readConfig', 'updateConfig', 'writeConfig', 'resetConfig', 'getAccessToken', 'getBaseURL', 'configPath'] as const) { - expect(typeof sdk[name]).toBe('function') - } - }) - - it('exports the raw appRequest escape hatch', () => { - expect(typeof sdk.appRequest).toBe('function') - }) - - it('re-exports the oclif primitives plugins need', () => { - expect(typeof sdk.Args).toBe('object') - expect(typeof sdk.Flags).toBe('object') - expect(typeof sdk.Command).toBe('function') - expect(typeof sdk.ux).toBe('object') - }) -}) diff --git a/packages/cli/test/resolve.test.ts b/packages/cli/test/resolve.test.ts new file mode 100644 index 00000000..36390eec --- /dev/null +++ b/packages/cli/test/resolve.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'bun:test' +import { listAccountIDs, resolveAccountID } from '../src/lib/resolve.js' + +const clientWithAccounts = (accounts: unknown[]) => ({ + accounts: { + list: async () => accounts, + }, +}) + +describe('account resolution', () => { + it('uses accountID when present', async () => { + const client = clientWithAccounts([{ accountID: 'whatsapp-main', network: 'whatsapp' }]) + + expect(await resolveAccountID(client, 'whatsapp')).toBe('whatsapp-main') + expect(await listAccountIDs(client)).toEqual(['whatsapp-main']) + }) + + it('falls back to id when accountID is absent', async () => { + const client = clientWithAccounts([{ id: 'matrix-main', network: 'matrix' }]) + + expect(await resolveAccountID(client, 'matrix')).toBe('matrix-main') + expect(await listAccountIDs(client)).toEqual(['matrix-main']) + }) +}) diff --git a/packages/cli/test/watch-filter.test.ts b/packages/cli/test/watch-filter.test.ts deleted file mode 100644 index 5665eeef..00000000 --- a/packages/cli/test/watch-filter.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, it } from 'bun:test' -import { passesFilter } from '../src/commands/watch.js' - -describe('watch passesFilter', () => { - const body = (type: string) => JSON.stringify({ type, chatID: '!x:beeper.com' }) - - it('passes through when no filter is set', () => { - expect(passesFilter(body('message.upserted'))).toBe(true) - }) - - it('respects include set', () => { - const filter = { include: new Set(['message.upserted']) } - expect(passesFilter(body('message.upserted'), filter)).toBe(true) - expect(passesFilter(body('message.deleted'), filter)).toBe(false) - expect(passesFilter(body('chat.upserted'), filter)).toBe(false) - }) - - it('respects exclude set', () => { - const filter = { exclude: new Set(['chat.upserted', 'chat.deleted']) } - expect(passesFilter(body('message.upserted'), filter)).toBe(true) - expect(passesFilter(body('chat.upserted'), filter)).toBe(false) - }) - - it('passes-through events without a type field', () => { - expect(passesFilter(JSON.stringify({ chatID: 'x' }), { include: new Set(['message.upserted']) })).toBe(true) - }) - - it('passes-through unparseable bodies', () => { - expect(passesFilter('not json', { include: new Set(['message.upserted']) })).toBe(true) - }) -}) diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 64a740e5..4b472d66 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -8,6 +8,6 @@ "outDir": "./dist", "rootDir": "./src" }, - "include": ["src/**/*.ts", "src/**/*.tsx"], + "include": ["src/**/*.ts"], "exclude": ["dist", "node_modules"] } diff --git a/packages/npm/.gitignore b/packages/npm/.gitignore deleted file mode 100644 index 65ad7f62..00000000 --- a/packages/npm/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/bin/ -/binaries.json -/LICENSE -/README.md diff --git a/packages/npm/package.json b/packages/npm/package.json deleted file mode 100644 index cfceecf4..00000000 --- a/packages/npm/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "beeper-cli", - "version": "0.6.2", - "description": "Beeper CLI binary launcher", - "license": "MIT", - "type": "module", - "bin": { - "beeper": "bin/beeper.js" - }, - "files": [ - "bin", - "binaries.json", - "README.md", - "LICENSE" - ], - "scripts": { - "build": "bun scripts/build.ts", - "clean": "rm -rf bin binaries.json README.md LICENSE", - "test": "bun run build", - "typecheck": "bun build scripts/build.ts --target=bun --outdir=/tmp/beeper-cli-npm-check --entry-naming='[name].js'" - } -} diff --git a/packages/npm/scripts/build.ts b/packages/npm/scripts/build.ts deleted file mode 100644 index fa7be4c8..00000000 --- a/packages/npm/scripts/build.ts +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bun -/* eslint-disable no-template-curly-in-string */ -import { chmod, cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' -import { existsSync } from 'node:fs' -import { join } from 'node:path' -import { fileURLToPath } from 'node:url' - -const root = fileURLToPath(new URL('..', import.meta.url)) -const cliRoot = fileURLToPath(new URL('../../cli/', import.meta.url)) -const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) -const cliPkg = JSON.parse(await readFile(join(cliRoot, 'package.json'), 'utf8')) -const binariesPath = join(cliRoot, 'dist', 'bin', 'binaries.json') -const binaries = existsSync(binariesPath) - ? JSON.parse(await readFile(binariesPath, 'utf8')) - : { command: 'beeper', package: cliPkg.name, version: cliPkg.version, artifacts: [] } - -if (pkg.version !== cliPkg.version) { - throw new Error(`packages/npm version ${pkg.version} does not match packages/cli version ${cliPkg.version}`) -} - -await rm(join(root, 'bin'), { recursive: true, force: true }) -await rm(join(root, 'binaries.json'), { force: true }) -await rm(join(root, 'README.md'), { force: true }) -await rm(join(root, 'LICENSE'), { force: true }) -await mkdir(join(root, 'bin'), { recursive: true }) -await cp(join(cliRoot, 'README.md'), join(root, 'README.md')) -await cp(join(cliRoot, 'LICENSE'), join(root, 'LICENSE')) -await writeFile(join(root, 'binaries.json'), `${JSON.stringify(binaries, null, 2)}\n`) -await writeFile(join(root, 'bin', 'beeper.js'), launcher()) -await chmod(join(root, 'bin', 'beeper.js'), 0o755) - -function launcher() { - return "#!/usr/bin/env node\nimport { createHash } from 'node:crypto'\nimport { createWriteStream, existsSync } from 'node:fs'\nimport { chmod, mkdir, readFile, rename, rm } from 'node:fs/promises'\nimport { get } from 'node:https'\nimport { homedir, platform as osPlatform, arch as osArch, tmpdir } from 'node:os'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { spawn } from 'node:child_process'\n\nconst packageRoot = join(dirname(fileURLToPath(import.meta.url)), '..')\nconst manifest = JSON.parse(await readFile(join(packageRoot, 'binaries.json'), 'utf8'))\nconst platform = targetPlatform()\nconst artifact = manifest.artifacts.find(item => item.platform === platform)\n\nif (!artifact) {\n console.error(`beeper-cli does not ship a binary for ${process.platform}/${process.arch}.`)\n process.exit(1)\n}\n\nconst cacheDir = process.env.BEEPER_CLI_BINARY_CACHE_DIR || join(homedir(), '.cache', 'beeper-cli', manifest.version)\nconst binPath = join(cacheDir, 'bin', manifest.command || 'beeper')\n\nconst expectedBinarySha256 = artifact.binarySha256 || artifact.sha256\n\nif (!existsSync(binPath) || await sha256(binPath).catch(() => '') !== expectedBinarySha256) {\n const tempDir = join(tmpdir(), `beeper-cli-${manifest.version}-${process.pid}`)\n const archivePath = join(tempDir, artifact.file)\n const downloadURL = `https://github.com/beeper/cli/releases/download/v${manifest.version}/${artifact.file}`\n logStep(`installing beeper-cli ${manifest.version} for ${platform}`)\n await rm(tempDir, { recursive: true, force: true })\n await mkdir(tempDir, { recursive: true })\n await download(downloadURL, archivePath)\n logStep('verifying download')\n const actual = await sha256(archivePath)\n if (actual !== artifact.sha256) {\n await rm(tempDir, { recursive: true, force: true })\n console.error(`beeper-cli binary checksum mismatch for ${artifact.file}.`)\n process.exit(1)\n }\n logStep('extracting binary')\n await extract(archivePath, tempDir)\n const extractedBin = join(tempDir, 'bin', manifest.command || 'beeper')\n await chmod(extractedBin, 0o755)\n logStep(`caching binary in ${cacheDir}`)\n await rm(cacheDir, { recursive: true, force: true })\n await mkdir(dirname(binPath), { recursive: true })\n await rename(extractedBin, binPath)\n await rm(tempDir, { recursive: true, force: true })\n logStep('ready')\n}\n\nif (process.env.BEEPER_CLI_LAUNCHER_DEBUG === '1') logStep(`starting ${binPath}`)\nconst child = spawn(binPath, process.argv.slice(2), { stdio: 'inherit', env: process.env })\nchild.on('exit', (code, signal) => {\n if (signal) process.kill(process.pid, signal)\n process.exit(code ?? 1)\n})\n\nfunction logStep(message) {\n console.error(`beeper-cli: ${message}`)\n}\n\nfunction targetPlatform() {\n const os = osPlatform()\n const cpu = osArch()\n const normalizedOS = os === 'darwin' || os === 'linux' ? os : os === 'win32' ? 'windows' : os\n const normalizedArch = cpu === 'x64' || cpu === 'arm64' ? cpu : cpu\n return `${normalizedOS}-${normalizedArch}`\n}\n\nasync function sha256(path) {\n const hash = createHash('sha256')\n hash.update(await readFile(path))\n return hash.digest('hex')\n}\n\nasync function download(url, destination, redirects = 0) {\n if (redirects > 10) throw new Error(`Too many redirects while downloading ${artifact.file}`)\n\n logStep(`downloading ${artifact.file}`)\n await new Promise((resolve, reject) => {\n get(url, response => {\n if ([301, 302, 303, 307, 308].includes(response.statusCode ?? 0) && response.headers.location) {\n response.resume()\n const nextURL = new URL(response.headers.location, url).toString()\n logStep(`redirecting to ${new URL(nextURL).host}`)\n download(nextURL, destination, redirects + 1).then(resolve, reject)\n return\n }\n if (response.statusCode !== 200) {\n response.resume()\n reject(new Error(`Download failed with HTTP ${response.statusCode}: ${url}`))\n return\n }\n const total = Number(response.headers['content-length'] ?? 0)\n let downloaded = 0\n let nextLoggedPercent = 25\n const file = createWriteStream(destination, { mode: 0o755 })\n response.on('data', chunk => {\n downloaded += chunk.length\n if (!total) return\n const percent = Math.floor(downloaded / total * 100)\n while (percent >= nextLoggedPercent && nextLoggedPercent <= 100) {\n logStep(`downloaded ${nextLoggedPercent}%`)\n nextLoggedPercent += 25\n }\n })\n response.pipe(file)\n file.on('finish', () => file.close(resolve))\n file.on('error', reject)\n }).on('error', reject)\n })\n}\n\nasync function extract(archivePath, destination) {\n if (artifact.file.endsWith('.zip')) {\n await run('/usr/bin/ditto', ['-x', '-k', archivePath, destination])\n return\n }\n if (artifact.file.endsWith('.tar.gz')) {\n await run('tar', ['-xzf', archivePath, '-C', destination])\n return\n }\n throw new Error(`Unsupported beeper-cli archive: ${artifact.file}`)\n}\n\nasync function run(command, args) {\n await new Promise((resolve, reject) => {\n const child = spawn(command, args, { stdio: 'ignore' })\n child.on('error', reject)\n child.on('exit', code => {\n if (code === 0) resolve()\n else reject(new Error(`${command} ${args.join(' ')} exited with ${code}`))\n })\n })\n}" -} - diff --git a/run.sh b/run.sh new file mode 100755 index 00000000..198b924f --- /dev/null +++ b/run.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" +cd packages/cli +exec bun run dev -- "$@" diff --git a/scripts/publish-packages.ts b/scripts/publish-packages.ts deleted file mode 100644 index 9881bc1e..00000000 --- a/scripts/publish-packages.ts +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env bun -import { existsSync } from "node:fs"; -import { readdir, readFile, writeFile } from "node:fs/promises"; -import { dirname, join } from "node:path"; - -const root = process.cwd(); -const args = Bun.argv.slice(2); - -const flags = new Set(args.filter((arg) => arg.startsWith("--"))); -const positional = args.filter((arg) => !arg.startsWith("--")); - -const dryRun = flags.has("--dry-run"); -const skipChecks = flags.has("--skip-checks"); -const skipExisting = flags.has("--skip-existing"); - -const usage = `Usage: bun run publish:packages [version] [--dry-run] [--skip-checks] [--skip-existing] - -Publishes: - - beeper-cli - - @beeper/cli-plugin-* - -All publishable packages are updated to the same version before publishing. -`; - -if (flags.has("--help") || flags.has("-h")) { - console.log(usage); - process.exit(0); -} - -let version = positional[0]; -if (!version) { - version = prompt("Version to publish (for beeper-cli and @beeper/cli-plugin-*):")?.trim(); -} - -if (!version || !/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z-.]+)?$/.test(version)) { - console.error(`Invalid semver version: ${version ?? ""}`); - console.error(usage); - process.exit(1); -} - -const readJson = async (path: string) => JSON.parse(await readFile(path, "utf8")); -const writeJson = async (path: string, value: unknown) => { - await writeFile(path, `${JSON.stringify(value, null, 2)}\n`); -}; - -const run = async (command: string[], options: { cwd?: string; allowFailure?: boolean } = {}) => { - console.log(`$ ${command.join(" ")}`); - const proc = Bun.spawn(command, { - cwd: options.cwd ?? root, - stdin: "inherit", - stdout: "inherit", - stderr: "inherit", - }); - const code = await proc.exited; - if (code !== 0 && !options.allowFailure) { - throw new Error(`Command failed (${code}): ${command.join(" ")}`); - } - return code; -}; - -const packagesDir = join(root, "packages"); -const packageDirs = (await readdir(packagesDir, { withFileTypes: true })) - .filter((entry) => entry.isDirectory()) - .map((entry) => join(packagesDir, entry.name)); - -const packageJsonPaths = packageDirs - .map((dir) => join(dir, "package.json")) - .filter((path) => existsSync(path)); - -const packages = await Promise.all( - packageJsonPaths.map(async (path) => ({ path, dir: dirname(path), json: await readJson(path) })), -); - -const publishable = packages.filter( - (pkg) => pkg.json.name === "beeper-cli" || /^@beeper\/cli-plugin-/.test(pkg.json.name), -); - -if (publishable.length === 0) { - throw new Error("No publishable packages found."); -} - -const publishableNames = new Set(publishable.map((pkg) => pkg.json.name)); -const pluginNames = [...publishableNames].filter((name) => name.startsWith("@beeper/cli-plugin-")); - -for (const pkg of publishable) { - pkg.json.version = version; - - for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"] as const) { - const deps = pkg.json[section]; - if (!deps) continue; - for (const depName of Object.keys(deps)) { - if (publishableNames.has(depName)) deps[depName] = `^${version}`; - } - } - - if (pkg.json.name === "beeper-cli") { - pkg.json.bin ??= {}; - if (pkg.json.bin.beeper === "./bin/run.js") pkg.json.bin.beeper = "bin/run.js"; - - pkg.json.oclif ??= {}; - pkg.json.oclif.jitPlugins ??= {}; - for (const pluginName of pluginNames) { - pkg.json.oclif.jitPlugins[pluginName] = `^${version}`; - } - } - - await writeJson(pkg.path, pkg.json); -} - -console.log(`Updated ${publishable.length} package.json file(s) to ${version}:`); -for (const pkg of publishable) console.log(` - ${pkg.json.name}@${version}`); - -await run(["bun", "install", "--lockfile-only"]); - -if (!skipChecks) { - await run(["bun", "run", "check"]); -} else { - console.warn("Skipping checks because --skip-checks was provided."); -} - -const ordered = [ - ...publishable.filter((pkg) => pkg.json.name === "beeper-cli"), - ...publishable.filter((pkg) => pkg.json.name !== "beeper-cli").sort((a, b) => a.json.name.localeCompare(b.json.name)), -]; - -for (const pkg of ordered) { - if (skipExisting) { - const code = await run(["npm", "view", `${pkg.json.name}@${version}`, "version"], { allowFailure: true }); - if (code === 0) { - console.log(`Skipping already-published ${pkg.json.name}@${version}`); - continue; - } - } - - const command = ["npm", "publish", "--access", "public"]; - if (dryRun) command.push("--dry-run"); - await run(command, { cwd: pkg.dir }); -} - -console.log(dryRun ? "Dry run complete." : `Published ${ordered.length} package(s) at ${version}.`); diff --git a/scripts/release.ts b/scripts/release.ts deleted file mode 100644 index e5366df1..00000000 --- a/scripts/release.ts +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bun -import { readFile, writeFile } from 'node:fs/promises' -import { join } from 'node:path' -import { fileURLToPath } from 'node:url' - -const root = fileURLToPath(new URL('..', import.meta.url)) -const version = process.argv[2] - -if (!version || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) { - console.error('Usage: bun run release ') - console.error('Example: bun run release 0.6.1') - process.exit(2) -} - -await setPackageVersion('packages/cli/package.json', version) -await setPackageVersion('packages/npm/package.json', version) - -await run('bun', ['run', 'readme'], { - cwd: join(root, 'packages/cli'), - env: { ...process.env, PACKAGE_VERSION: version, TAG: `v${version}` }, -}) - -await run('bun', ['run', 'release:local'], { - cwd: join(root, 'packages/cli'), - env: { ...process.env, PACKAGE_VERSION: version, TAG: `v${version}` }, -}) - -async function setPackageVersion(path, nextVersion) { - const absolute = join(root, path) - const pkg = JSON.parse(await readFile(absolute, 'utf8')) - pkg.version = nextVersion - await writeFile(absolute, `${JSON.stringify(pkg, null, 2)}\n`) - console.log(`${path} -> ${nextVersion}`) -} - -async function run(command, args, options = {}) { - const child = Bun.spawn([command, ...args], { - cwd: options.cwd || root, - env: options.env || process.env, - stdin: 'inherit', - stdout: 'inherit', - stderr: 'inherit', - }) - const code = await child.exited - if (code !== 0) throw new Error(`${command} ${args.join(' ')} exited with ${code}`) -} From 6b36b9e9e7eac5033759c3315e2b8f812b4fc4b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Wed, 3 Jun 2026 17:39:59 +0200 Subject: [PATCH 25/26] Add gogcli-style config, safety, and output parity --- INTENT.md | 179 +++++++ packages/cli/README.md | 48 +- packages/cli/docs/commands/README.md | 75 +++ packages/cli/docs/commands/accounts-add.md | 52 ++ packages/cli/docs/commands/accounts-list.md | 40 ++ packages/cli/docs/commands/api-request.md | 47 ++ .../cli/docs/commands/auth-email-response.md | 41 ++ .../cli/docs/commands/auth-email-start.md | 39 ++ packages/cli/docs/commands/auth-logout.md | 33 ++ packages/cli/docs/commands/chats-archive.md | 41 ++ packages/cli/docs/commands/chats-avatar.md | 42 ++ .../cli/docs/commands/chats-description.md | 42 ++ packages/cli/docs/commands/chats-disappear.md | 41 ++ packages/cli/docs/commands/chats-draft.md | 45 ++ packages/cli/docs/commands/chats-focus.md | 43 ++ packages/cli/docs/commands/chats-list.md | 51 ++ packages/cli/docs/commands/chats-mute.md | 41 ++ .../cli/docs/commands/chats-notify-anyway.md | 40 ++ packages/cli/docs/commands/chats-pin.md | 41 ++ packages/cli/docs/commands/chats-priority.md | 41 ++ packages/cli/docs/commands/chats-read.md | 42 ++ packages/cli/docs/commands/chats-remind.md | 43 ++ packages/cli/docs/commands/chats-rename.md | 41 ++ packages/cli/docs/commands/chats-show.md | 41 ++ packages/cli/docs/commands/chats-start.md | 46 ++ packages/cli/docs/commands/completion.md | 39 ++ packages/cli/docs/commands/config-get.md | 43 ++ packages/cli/docs/commands/config-keys.md | 38 ++ packages/cli/docs/commands/config-list.md | 38 ++ packages/cli/docs/commands/config-path.md | 37 ++ packages/cli/docs/commands/config-set.md | 45 ++ packages/cli/docs/commands/config-unset.md | 45 ++ packages/cli/docs/commands/contacts-list.md | 47 ++ packages/cli/docs/commands/doctor.md | 33 ++ packages/cli/docs/commands/exit-codes.md | 38 ++ packages/cli/docs/commands/export.md | 47 ++ packages/cli/docs/commands/install-desktop.md | 40 ++ packages/cli/docs/commands/install-server.md | 40 ++ packages/cli/docs/commands/mcp.md | 43 ++ packages/cli/docs/commands/media-download.md | 45 ++ .../cli/docs/commands/messages-context.md | 43 ++ packages/cli/docs/commands/messages-delete.md | 42 ++ packages/cli/docs/commands/messages-edit.md | 42 ++ packages/cli/docs/commands/messages-list.md | 50 ++ packages/cli/docs/commands/messages-search.md | 69 +++ packages/cli/docs/commands/remove-account.md | 44 ++ packages/cli/docs/commands/remove-target.md | 44 ++ packages/cli/docs/commands/resolve-account.md | 45 ++ packages/cli/docs/commands/resolve-bridge.md | 45 ++ packages/cli/docs/commands/resolve-chat.md | 47 ++ packages/cli/docs/commands/resolve-contact.md | 47 ++ packages/cli/docs/commands/resolve-target.md | 45 ++ packages/cli/docs/commands/schema.md | 44 ++ packages/cli/docs/commands/send-file.md | 47 ++ packages/cli/docs/commands/send-presence.md | 42 ++ packages/cli/docs/commands/send-react.md | 44 ++ packages/cli/docs/commands/send-sticker.md | 46 ++ packages/cli/docs/commands/send-text.md | 48 ++ packages/cli/docs/commands/send-voice.md | 47 ++ packages/cli/docs/commands/setup.md | 63 +++ packages/cli/docs/commands/status.md | 43 ++ packages/cli/docs/commands/targets-add.md | 46 ++ packages/cli/docs/commands/targets-list.md | 37 ++ packages/cli/docs/commands/targets-logs.md | 47 ++ .../docs/commands/targets-runtime-restart.md | 39 ++ .../docs/commands/targets-runtime-start.md | 39 ++ .../cli/docs/commands/targets-runtime-stop.md | 39 ++ packages/cli/docs/commands/targets-tunnel.md | 49 ++ packages/cli/docs/commands/use-account.md | 43 ++ packages/cli/docs/commands/use-target.md | 43 ++ packages/cli/docs/commands/version.md | 33 ++ packages/cli/docs/commands/watch.md | 44 ++ packages/cli/package.json | 2 + packages/cli/scripts/generate-command-docs.ts | 96 ++++ packages/cli/src/cli/commands.ts | 451 +++++++++++++++++- packages/cli/src/cli/main.ts | 73 ++- packages/cli/src/cli/mcp.ts | 70 ++- packages/cli/src/cli/output.ts | 160 ++++++- packages/cli/src/cli/parse.ts | 110 ++++- packages/cli/src/cli/policy.ts | 42 ++ packages/cli/src/cli/schema.ts | 24 +- packages/cli/src/cli/types.ts | 19 + packages/cli/src/lib/errors.ts | 7 +- packages/cli/src/lib/targets.ts | 4 +- packages/cli/test/cli-smoke.ts | 163 ++++++- 85 files changed, 4436 insertions(+), 84 deletions(-) create mode 100644 INTENT.md create mode 100644 packages/cli/docs/commands/README.md create mode 100644 packages/cli/docs/commands/accounts-add.md create mode 100644 packages/cli/docs/commands/accounts-list.md create mode 100644 packages/cli/docs/commands/api-request.md create mode 100644 packages/cli/docs/commands/auth-email-response.md create mode 100644 packages/cli/docs/commands/auth-email-start.md create mode 100644 packages/cli/docs/commands/auth-logout.md create mode 100644 packages/cli/docs/commands/chats-archive.md create mode 100644 packages/cli/docs/commands/chats-avatar.md create mode 100644 packages/cli/docs/commands/chats-description.md create mode 100644 packages/cli/docs/commands/chats-disappear.md create mode 100644 packages/cli/docs/commands/chats-draft.md create mode 100644 packages/cli/docs/commands/chats-focus.md create mode 100644 packages/cli/docs/commands/chats-list.md create mode 100644 packages/cli/docs/commands/chats-mute.md create mode 100644 packages/cli/docs/commands/chats-notify-anyway.md create mode 100644 packages/cli/docs/commands/chats-pin.md create mode 100644 packages/cli/docs/commands/chats-priority.md create mode 100644 packages/cli/docs/commands/chats-read.md create mode 100644 packages/cli/docs/commands/chats-remind.md create mode 100644 packages/cli/docs/commands/chats-rename.md create mode 100644 packages/cli/docs/commands/chats-show.md create mode 100644 packages/cli/docs/commands/chats-start.md create mode 100644 packages/cli/docs/commands/completion.md create mode 100644 packages/cli/docs/commands/config-get.md create mode 100644 packages/cli/docs/commands/config-keys.md create mode 100644 packages/cli/docs/commands/config-list.md create mode 100644 packages/cli/docs/commands/config-path.md create mode 100644 packages/cli/docs/commands/config-set.md create mode 100644 packages/cli/docs/commands/config-unset.md create mode 100644 packages/cli/docs/commands/contacts-list.md create mode 100644 packages/cli/docs/commands/doctor.md create mode 100644 packages/cli/docs/commands/exit-codes.md create mode 100644 packages/cli/docs/commands/export.md create mode 100644 packages/cli/docs/commands/install-desktop.md create mode 100644 packages/cli/docs/commands/install-server.md create mode 100644 packages/cli/docs/commands/mcp.md create mode 100644 packages/cli/docs/commands/media-download.md create mode 100644 packages/cli/docs/commands/messages-context.md create mode 100644 packages/cli/docs/commands/messages-delete.md create mode 100644 packages/cli/docs/commands/messages-edit.md create mode 100644 packages/cli/docs/commands/messages-list.md create mode 100644 packages/cli/docs/commands/messages-search.md create mode 100644 packages/cli/docs/commands/remove-account.md create mode 100644 packages/cli/docs/commands/remove-target.md create mode 100644 packages/cli/docs/commands/resolve-account.md create mode 100644 packages/cli/docs/commands/resolve-bridge.md create mode 100644 packages/cli/docs/commands/resolve-chat.md create mode 100644 packages/cli/docs/commands/resolve-contact.md create mode 100644 packages/cli/docs/commands/resolve-target.md create mode 100644 packages/cli/docs/commands/schema.md create mode 100644 packages/cli/docs/commands/send-file.md create mode 100644 packages/cli/docs/commands/send-presence.md create mode 100644 packages/cli/docs/commands/send-react.md create mode 100644 packages/cli/docs/commands/send-sticker.md create mode 100644 packages/cli/docs/commands/send-text.md create mode 100644 packages/cli/docs/commands/send-voice.md create mode 100644 packages/cli/docs/commands/setup.md create mode 100644 packages/cli/docs/commands/status.md create mode 100644 packages/cli/docs/commands/targets-add.md create mode 100644 packages/cli/docs/commands/targets-list.md create mode 100644 packages/cli/docs/commands/targets-logs.md create mode 100644 packages/cli/docs/commands/targets-runtime-restart.md create mode 100644 packages/cli/docs/commands/targets-runtime-start.md create mode 100644 packages/cli/docs/commands/targets-runtime-stop.md create mode 100644 packages/cli/docs/commands/targets-tunnel.md create mode 100644 packages/cli/docs/commands/use-account.md create mode 100644 packages/cli/docs/commands/use-target.md create mode 100644 packages/cli/docs/commands/version.md create mode 100644 packages/cli/docs/commands/watch.md create mode 100644 packages/cli/scripts/generate-command-docs.ts diff --git a/INTENT.md b/INTENT.md new file mode 100644 index 00000000..5480a0e0 --- /dev/null +++ b/INTENT.md @@ -0,0 +1,179 @@ +# Intent + +This CLI should keep its existing feature set while being reorganized and made +consistent with OpenClaw product language and setup expectations. + +## Product Direction + +- Keep the interactive setup UX: guided steps, prompts, readiness checks, and + clear next actions. +- Align command output, setup/status wording, and user-facing concepts with + OpenClaw where that is the intended product direction. +- For send/message command ergonomics, use `gog` and `wacli` as the closer + references, not OpenClaw's generic `message` command. +- Use real OpenClaw behavior and documented contracts when adding OpenClaw + integration. Do not invent SDK layers or placeholder APIs. + +## Engineering Rules + +- Reorganize and simplify code without deleting working features unless the + feature is explicitly out of scope. +- Preserve existing command capabilities while changing internal structure or + output shape. +- Prefer direct modules and concrete functions over convenience barrels, + wrapper-only layers, duplicate types, and parallel command paths. +- Fold abstractions when they only rename another abstraction or hide a single + call without adding policy, validation, or ownership. +- Keep one coherent implementation for each concern: parsing, output, + configuration, setup state, auth, and API access. +- Do not keep compatibility-only aliases or contracts once product intent says + the new shape replaces them. + +## Guardrails + +- Before deleting a command, endpoint, helper, test, schema, or setup path, + confirm it is intentionally removed from the product scope. +- Treat "OpenClaw alignment" as output and architecture alignment first; it is + not permission to replace feature implementations with thin passthroughs. +- If intent is ambiguous, write down the product question before making the + change. + +## Not Done Yet + +These are known cleanup and alignment tasks that still need product judgment or +implementation. Treat this list as work to finish, not as a decision that every +item must be deleted. + +### Package Exports + +- Decide whether `packages/cli/package.json` should export anything beyond the + executable and `./package.json`. +- If there is no library API, keep exports minimal and do not add convenience + barrels. +- If a library API is needed, export only stable real source concerns, not + `commands`, generated schema, or internal helper modules by default. +- Remove any export path that exists only for tests, MCP mirroring, or legacy + convenience. + +### Command Surface + +- Inventory every command in `beeper --help` and mark it as one of: + keep, rename, fold into another command, hide/internalize, or delete. +- Preserve feature capability while doing that inventory. Do not remove working + account, chat, message, send, setup, or status behavior just to simplify the + list. +- Decide whether `api request`, `schema`, `mcp`, `watch`, `export`, and + `media download` are real product commands or unreleased implementation + utilities. +- Decide whether `contacts list` belongs in the main surface or should be + folded into chat/message resolution. +- Decide whether `install desktop`, `install server`, and + `targets runtime *` remain public commands or become setup-owned internals. + +### Aliases And Duplicate Entrypoints + +- Remove compatibility aliases only after choosing the canonical command. +- Decide whether `use target` and `targets use` should both exist; keep only one + if target selection remains a concept. +- Decide whether `remove target` and `targets remove` should both exist; keep + only one if target removal remains public. +- Decide whether `use account` and `accounts use` should both exist; keep only + one if default-account selection remains public. +- Decide whether `auth email start/response` should remain separate automation + commands or be folded into `setup --email --code`. +- Decide whether `send text/file/voice/sticker/react/presence` should stay as + separate user-facing commands. If changing them, prefer a `gog`/`wacli` style + shape over OpenClaw's generic message surface. + +### Send And Message Ergonomics + +- Use `wacli` as the closest reference for third-party chat sending: + `send text --to --message ` and + `send file --to --file --caption `. +- Use `gog` as the closest reference for content ergonomics: + `--body`, `--body-file -`, `--body-html`, clear send-vs-draft commands, and + explicit reply flags. +- Prefer explicit recipient and content flags for send commands. Do not rely on + positional magic for destructive or outbound actions. +- Require clear dry-run and confirmation behavior for outbound sends. In + interactive mode, ambiguous recipients or broad sends should ask before + sending. +- Keep search/list commands boring and scriptable: query flag or positional + query, `--limit`/`--max`, account/chat filters, and `--json`. +- Prefer `--json` plus `--no-input` for automation, matching `gog` and `wacli`. +- Decide whether multiline text should use `--message-file -` or a more + `gog`-like `--body-file -`; choose one term and apply it consistently. +- Do not use OpenClaw's generic `message send --channel --target --message` + shape as the primary ergonomic reference for this CLI. + +### Global Flags + +- Decide canonical behavior for `--json`; keep one structured output path. +- Decide whether `--plain` is useful or just a second output mode to delete. +- Decide whether `--events` is product behavior or debug plumbing. +- Decide whether `--wrap-untrusted` and safety profiles are still part of this + CLI once OpenClaw owns the trust model. +- Decide whether `--target` remains a public global flag or setup/status should + own endpoint selection. +- Ensure `--no-input` consistently means no prompts everywhere, especially + setup and account login. + +### Setup And OpenClaw Alignment + +- Keep interactive setup as the UX. Reorganize it around clear steps and + readiness states instead of deleting it. +- Rename user-facing setup concepts toward OpenClaw only where the underlying + behavior is real. +- Define how Beeper Desktop/Server setup maps to OpenClaw gateway/channel + setup. This is not done. +- Decide whether OpenClaw onboarding is invoked, embedded through a real SDK, + or used only as a model for output and step structure. +- If invoking OpenClaw, preserve existing Beeper-specific setup capabilities + unless product explicitly says they are replaced. +- Make non-interactive setup output match the same step/readiness model as + interactive setup. + +### Output Contracts + +- Define one JSON envelope for success, dry-run, readiness, setup actions, and + errors. +- Align names like `target`, `account`, `bridge`, `chat`, and `message` with + the intended OpenClaw vocabulary without breaking the feature semantics. +- Decide whether command output should expose raw Desktop API objects or + normalized CLI objects. +- Remove duplicate output paths such as JSON/plain/events if they represent the + same information. +- Make dry-run output consistent across setup, account add, chat mutation, + message send, export, install, and runtime commands. + +### Config And State + +- Decide whether the config model is one endpoint, many targets, or OpenClaw + gateway/channel config. +- If many targets remain, make target selection/removal/listing one coherent + system. +- If one endpoint is chosen, fold target registry code and aliases accordingly. +- Decide whether default account belongs in CLI config or should be derived from + OpenClaw/channel state. +- Remove config fields that exist only for unreleased legacy compatibility. + +### Internal Modules + +- Fold simple wrappers that only rename another module or function. +- Revisit `app-api.ts`, `target-status.ts`, `setup-login.ts`, + `cloudflare-tunnel.ts`, and `export.ts` after command-surface decisions. +- Keep modules split only when they own a real concern: parsing, output, setup, + auth, config/state, API access, install/runtime, or resolution. +- Avoid duplicated types between command handlers and lib modules. +- Import from the real source file; do not create barrels for convenience. + +### Tests And Verification + +- Update smoke tests after command-surface decisions so they assert the intended + public surface, not old accidental breadth. +- Keep focused tests for setup steps, account login, resolution, output + envelopes, and config migration/reset behavior. +- Remove tests only when the behavior they cover is intentionally removed or + covered better elsewhere. +- Keep `bun run typecheck`, `bun packages/cli/test/cli-smoke.ts`, and + `bun run check` green after each cleanup pass. diff --git a/packages/cli/README.md b/packages/cli/README.md index 8e498beb..f5f55ec6 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -16,7 +16,7 @@ For source builds: ```sh bun install bun run check -bun --filter beeper-cli run dev -- --help +bun packages/cli/bin/dev.js --help ``` ## Quick Start @@ -24,6 +24,7 @@ bun --filter beeper-cli run dev -- --help ```sh beeper setup beeper targets list +beeper doctor beeper status beeper chats list --limit 10 beeper messages search "flight" @@ -41,31 +42,45 @@ The live command registry is the source of truth. Use: ```sh beeper --help beeper schema --json +beeper exit-codes --json beeper --help ``` +Generated command docs live in [docs/commands/README.md](docs/commands/README.md) +and are rebuilt from the live registry with +`bun run --cwd packages/cli docs:commands`. + Current command groups: -- `setup`, `status`, `version`, `schema` -- `use account`, `use target`, `remove account`, `remove target` +- `setup`, `status` (`st`), `doctor`, `version`, `exit-codes`, `schema` +- `config get` (`config show`), `config keys` (`config list-keys`, `config names`), `config list` (`config ls`, `config all`), `config path` (`config where`), `config set` (`config add`, `config update`), `config unset` (`config rm`, `config del`, `config remove`) +- `use account` (`accounts use`), `use target` (`targets use`), `remove account` (`accounts remove`, `accounts rm`), `remove target` (`targets remove`, `targets rm`) - `auth email start`, `auth email response`, `auth logout` -- `targets add`, `targets list`, `targets runtime start`, `targets runtime stop`, `targets runtime restart`, `targets logs`, `targets tunnel` +- `targets add`, `targets list` (`targets ls`), `targets runtime start`, `targets runtime stop`, `targets runtime restart`, `targets logs`, `targets tunnel` - `install desktop`, `install server` - `accounts add`, `accounts list` -- `chats list`, `chats show`, `chats start`, `chats archive`, `chats pin`, `chats mute`, `chats read`, `chats rename`, `chats description`, `chats avatar`, `chats priority`, `chats draft`, `chats remind`, `chats disappear`, `chats focus`, `chats notify-anyway` -- `messages list`, `messages search`, `messages context`, `messages edit`, `messages delete` +- `chats list` (`chats ls`), `chats show`, `chats start`, `chats archive`, `chats pin`, `chats mute`, `chats read`, `chats rename`, `chats description`, `chats avatar`, `chats priority`, `chats draft`, `chats remind`, `chats disappear`, `chats focus`, `chats notify-anyway` +- `messages list` (`messages ls`), `messages search` (`messages find`), `messages context`, `messages edit`, `messages delete` - `send text`, `send file`, `send sticker`, `send voice`, `send react`, `send presence` -- `contacts list` +- `contacts list` (`contacts search`, `contacts find`) - `media download`, `export`, `watch` -- `api request`, `mcp` +- `api request`, `mcp`, `completion` - `resolve account`, `resolve bridge`, `resolve chat`, `resolve contact`, `resolve target` ## Global Flags -- Output: `--json`, `--plain`, `--events`, `--debug` -- Targeting: `--target` -- Safety: `--dry-run`, `--safety-profile`, `--wrap-untrusted` -- Interaction: `--no-input`, `--force` +- Output: `--json`/`-j`, `--plain`/`-p`/`--tsv`, `--select`/`--fields`, `--results-only`, `--full`, `--events`, `--debug` +- Targeting/config: `--target`, `--account`/`-a`, `--home`, `--access-token` +- Safety: `--dry-run`/`-n`, `--read-only`/`BEEPER_READONLY`, `--timeout`, `--safety-profile`, `--enable-commands`, `--enable-commands-exact`, `--disable-commands`, `--wrap-untrusted` +- Interaction: `--no-input`, `--force`/`-y` + +Human output uses stable tables and diagnostic summaries. Use `--json` for raw +objects, `--select=id,name` to project JSON fields, and `--plain` for TSV-like +text. + +`mcp` exposes read-only tools by default. Use `mcp --list-tools` to inspect the +enabled tool set, `mcp --allow-tool messages.*` to restrict it, and +`mcp --allow-write` only when write-risk tools should be available. ## Targets @@ -102,6 +117,15 @@ bun --filter beeper-cli run build bun run check ``` +In this repository checkout, the direct package form also works: + +```sh +bun run --cwd packages/cli typecheck +bun run --cwd packages/cli docs:commands +bun run --cwd packages/cli test +bun run --cwd packages/cli build +``` + The package entrypoint is `packages/cli/bin/cli.js`; local development uses `packages/cli/bin/dev.js`. diff --git a/packages/cli/docs/commands/README.md b/packages/cli/docs/commands/README.md new file mode 100644 index 00000000..e2348e0d --- /dev/null +++ b/packages/cli/docs/commands/README.md @@ -0,0 +1,75 @@ +# Command Index + +Generated from the live command registry. Do not edit command pages by hand. + +| Command | Description | Aliases | +| --- | --- | --- | +| [`accounts add`](accounts-add.md) | Connect a chat account by bridge | | +| [`accounts list`](accounts-list.md) | List connected accounts | | +| [`api request`](api-request.md) | Call a raw Desktop API path with any supported HTTP method | | +| [`auth email response`](auth-email-response.md) | Finish email sign-in for a target | | +| [`auth email start`](auth-email-start.md) | Start email sign-in for a target | | +| [`auth logout`](auth-logout.md) | Clear stored authentication | | +| [`chats archive`](chats-archive.md) | Archive or unarchive a chat | | +| [`chats avatar`](chats-avatar.md) | Set or clear a chat avatar | | +| [`chats description`](chats-description.md) | Set or clear a chat description | | +| [`chats disappear`](chats-disappear.md) | Set a disappearing-message timer | | +| [`chats draft`](chats-draft.md) | Set or clear a chat draft | | +| [`chats focus`](chats-focus.md) | Focus a chat in Beeper | | +| [`chats list`](chats-list.md) | List chats | `chats ls` | +| [`chats mute`](chats-mute.md) | Mute or unmute a chat | | +| [`chats notify-anyway`](chats-notify-anyway.md) | Notify a chat anyway | | +| [`chats pin`](chats-pin.md) | Pin or unpin a chat | | +| [`chats priority`](chats-priority.md) | Set chat priority | | +| [`chats read`](chats-read.md) | Mark a chat read or unread | | +| [`chats remind`](chats-remind.md) | Set or clear a chat reminder | | +| [`chats rename`](chats-rename.md) | Rename a chat | | +| [`chats show`](chats-show.md) | Show chat details | | +| [`chats start`](chats-start.md) | Start a chat | | +| [`completion`](completion.md) | Generate shell completion scripts | | +| [`config get`](config-get.md) | Get a config value | `config show` | +| [`config keys`](config-keys.md) | List available config keys | `config list-keys`, `config names` | +| [`config list`](config-list.md) | List all config values | `config ls`, `config all` | +| [`config path`](config-path.md) | Print config file path | `config where` | +| [`config set`](config-set.md) | Set a config value | `config add`, `config update` | +| [`config unset`](config-unset.md) | Unset a config value | `config rm`, `config del`, `config remove` | +| [`contacts list`](contacts-list.md) | List contacts | `contacts search`, `contacts find` | +| [`doctor`](doctor.md) | Run diagnostics for config, target reachability, auth, and readiness | | +| [`exit-codes`](exit-codes.md) | Print stable exit codes for automation | `agent exit-codes`, `exitcodes` | +| [`export`](export.md) | Export accounts, chats, messages, transcripts, and attachments | | +| [`install desktop`](install-desktop.md) | Install Beeper Desktop locally | | +| [`install server`](install-server.md) | Install Beeper Server locally | | +| [`mcp`](mcp.md) | Run a typed MCP stdio server | | +| [`media download`](media-download.md) | Download message media | | +| [`messages context`](messages-context.md) | Show a message with surrounding context | | +| [`messages delete`](messages-delete.md) | Delete a message | | +| [`messages edit`](messages-edit.md) | Edit a message | | +| [`messages list`](messages-list.md) | List chat messages | `messages ls` | +| [`messages search`](messages-search.md) | Search messages across chats | `messages find` | +| [`remove account`](remove-account.md) | Remove an account | `accounts remove`, `accounts rm` | +| [`remove target`](remove-target.md) | Remove a target | `targets remove`, `targets rm` | +| [`resolve account`](resolve-account.md) | Resolve an account selector | | +| [`resolve bridge`](resolve-bridge.md) | Resolve a bridge selector | | +| [`resolve chat`](resolve-chat.md) | Resolve a chat selector | | +| [`resolve contact`](resolve-contact.md) | Resolve a contact selector | | +| [`resolve target`](resolve-target.md) | Resolve a target selector | | +| [`schema`](schema.md) | Print machine-readable command and flag schema | `help-json`, `helpjson` | +| [`send file`](send-file.md) | Send a file message | | +| [`send presence`](send-presence.md) | Send a typing indicator | | +| [`send react`](send-react.md) | Send or remove a reaction | | +| [`send sticker`](send-sticker.md) | Send a sticker | | +| [`send text`](send-text.md) | Send a text message | | +| [`send voice`](send-voice.md) | Send a voice note | | +| [`setup`](setup.md) | Make the selected target ready for messaging | | +| [`status`](status.md) | Show selected target and setup readiness | `st` | +| [`targets add`](targets-add.md) | Add a remote Beeper Desktop or Server target | | +| [`targets list`](targets-list.md) | List configured Beeper targets | `targets ls` | +| [`targets logs`](targets-logs.md) | Print logs for a local Beeper Desktop or Server install | | +| [`targets runtime restart`](targets-runtime-restart.md) | Restart a local server runtime | | +| [`targets runtime start`](targets-runtime-start.md) | Start a local target runtime | | +| [`targets runtime stop`](targets-runtime-stop.md) | Stop a local server runtime | | +| [`targets tunnel`](targets-tunnel.md) | Expose a target through Cloudflare Tunnel | | +| [`use account`](use-account.md) | Select the default account | `accounts use` | +| [`use target`](use-target.md) | Select the default target | `targets use` | +| [`version`](version.md) | Print CLI version | | +| [`watch`](watch.md) | Stream Desktop API WebSocket events | | diff --git a/packages/cli/docs/commands/accounts-add.md b/packages/cli/docs/commands/accounts-add.md new file mode 100644 index 00000000..de18d266 --- /dev/null +++ b/packages/cli/docs/commands/accounts-add.md @@ -0,0 +1,52 @@ +# beeper accounts add +Connect a chat account by bridge +## Usage +```sh +beeper accounts add [bridge] [flags] +``` +## Arguments + +| Name | Description | +| --- | --- | +| `[bridge]` | | + +## Flags + +| Name | Description | +| --- | --- | +| `--cookie ` | Cookie value in name=value form Repeatable. | +| `--field ` | Field value in id=value form Repeatable. | +| `--flow ` | Login flow ID | +| `--guided` | Prompt through login steps Default: `true`. | +| `--login-id ` | Existing login ID to re-login as | +| `--webview` | Use Bun.WebView for cookie login steps Default: `false`. | +| `--webview-backend ` | Bun.WebView backend Default: `chrome`. Values: `auto`, `chrome`, `webkit`. | +| `--webview-timeout ` | Seconds to wait for WebView cookie collection Default: `120`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/accounts-list.md b/packages/cli/docs/commands/accounts-list.md new file mode 100644 index 00000000..1f907b23 --- /dev/null +++ b/packages/cli/docs/commands/accounts-list.md @@ -0,0 +1,40 @@ +# beeper accounts list +List connected accounts +## Usage +```sh +beeper accounts list [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `-a, --account , --acct` | Limit to account selector Repeatable. | +| `--ids` | Print only account IDs Default: `false`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/api-request.md b/packages/cli/docs/commands/api-request.md new file mode 100644 index 00000000..5f4fc134 --- /dev/null +++ b/packages/cli/docs/commands/api-request.md @@ -0,0 +1,47 @@ +# beeper api request +Call a raw Desktop API path with any supported HTTP method +## Usage +```sh +beeper api request [flags] +``` +## Arguments + +| Name | Description | +| --- | --- | +| `` | | +| `` | | + +## Flags + +| Name | Description | +| --- | --- | +| `--body ` | JSON request body | +| `--no-auth` | Call a public API path without a bearer token Default: `false`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/auth-email-response.md b/packages/cli/docs/commands/auth-email-response.md new file mode 100644 index 00000000..176b7e3c --- /dev/null +++ b/packages/cli/docs/commands/auth-email-response.md @@ -0,0 +1,41 @@ +# beeper auth email response +Finish email sign-in for a target +## Usage +```sh +beeper auth email response [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--code ` | Email verification code Required. | +| `--setup-request-id ` | Setup request ID from auth email start Required. | +| `--username ` | Username to use if setup creates a new account | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/auth-email-start.md b/packages/cli/docs/commands/auth-email-start.md new file mode 100644 index 00000000..73ef81de --- /dev/null +++ b/packages/cli/docs/commands/auth-email-start.md @@ -0,0 +1,39 @@ +# beeper auth email start +Start email sign-in for a target +## Usage +```sh +beeper auth email start [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--email ` | Email address Required. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/auth-logout.md b/packages/cli/docs/commands/auth-logout.md new file mode 100644 index 00000000..6169a753 --- /dev/null +++ b/packages/cli/docs/commands/auth-logout.md @@ -0,0 +1,33 @@ +# beeper auth logout +Clear stored authentication +## Usage +```sh +beeper auth logout [flags] +``` +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/chats-archive.md b/packages/cli/docs/commands/chats-archive.md new file mode 100644 index 00000000..41ee3495 --- /dev/null +++ b/packages/cli/docs/commands/chats-archive.md @@ -0,0 +1,41 @@ +# beeper chats archive +Archive or unarchive a chat +## Usage +```sh +beeper chats archive [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--chat ` | Chat selector Required. | +| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--clear` | Unarchive the chat Default: `false`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/chats-avatar.md b/packages/cli/docs/commands/chats-avatar.md new file mode 100644 index 00000000..916573e7 --- /dev/null +++ b/packages/cli/docs/commands/chats-avatar.md @@ -0,0 +1,42 @@ +# beeper chats avatar +Set or clear a chat avatar +## Usage +```sh +beeper chats avatar [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--chat ` | Chat selector Required. | +| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--clear` | Clear the avatar Default: `false`. | +| `--file ` | Avatar image file path | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/chats-description.md b/packages/cli/docs/commands/chats-description.md new file mode 100644 index 00000000..29ac70bb --- /dev/null +++ b/packages/cli/docs/commands/chats-description.md @@ -0,0 +1,42 @@ +# beeper chats description +Set or clear a chat description +## Usage +```sh +beeper chats description [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--chat ` | Chat selector Required. | +| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--clear` | Clear or unset the chosen state Default: `false`. | +| `--description ` | Chat description | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/chats-disappear.md b/packages/cli/docs/commands/chats-disappear.md new file mode 100644 index 00000000..bb5c96ee --- /dev/null +++ b/packages/cli/docs/commands/chats-disappear.md @@ -0,0 +1,41 @@ +# beeper chats disappear +Set a disappearing-message timer +## Usage +```sh +beeper chats disappear [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--chat ` | Chat selector Required. | +| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--seconds ` | Disappearing-message timer in seconds, or off | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/chats-draft.md b/packages/cli/docs/commands/chats-draft.md new file mode 100644 index 00000000..62281a62 --- /dev/null +++ b/packages/cli/docs/commands/chats-draft.md @@ -0,0 +1,45 @@ +# beeper chats draft +Set or clear a chat draft +## Usage +```sh +beeper chats draft [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--chat ` | Chat selector Required. | +| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--clear` | Clear the draft Default: `false`. | +| `--file ` | Draft attachment file path | +| `--filename ` | Draft attachment filename | +| `--mime ` | Draft attachment MIME type | +| `--text ` | Draft text | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/chats-focus.md b/packages/cli/docs/commands/chats-focus.md new file mode 100644 index 00000000..bea8b5c5 --- /dev/null +++ b/packages/cli/docs/commands/chats-focus.md @@ -0,0 +1,43 @@ +# beeper chats focus +Focus a chat in Beeper +## Usage +```sh +beeper chats focus [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--chat ` | Chat selector Required. | +| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--file ` | Draft attachment file path | +| `--message ` | Message ID to focus | +| `--text ` | Draft text | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/chats-list.md b/packages/cli/docs/commands/chats-list.md new file mode 100644 index 00000000..868c2608 --- /dev/null +++ b/packages/cli/docs/commands/chats-list.md @@ -0,0 +1,51 @@ +# beeper chats list +List chats +## Usage +```sh +beeper chats list [flags] +``` +## Aliases + +- `beeper chats ls` + +## Flags + +| Name | Description | +| --- | --- | +| `-a, --account , --acct` | Limit to account selector Repeatable. | +| `--archived` | Only archived chats; use --no-archived to exclude | +| `--ids` | Print preferred chat selectors Default: `false`. | +| `--limit ` | Maximum chats to print Default: `20`. | +| `--low-priority` | Only low-priority chats; use --no-low-priority to exclude | +| `--muted` | Only muted chats; use --no-muted to exclude | +| `--pinned` | Only pinned chats; use --no-pinned to exclude | +| `--query ` | Optional chat lookup query | +| `--unread` | Only unread chats; use --no-unread to exclude | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/chats-mute.md b/packages/cli/docs/commands/chats-mute.md new file mode 100644 index 00000000..3ce40500 --- /dev/null +++ b/packages/cli/docs/commands/chats-mute.md @@ -0,0 +1,41 @@ +# beeper chats mute +Mute or unmute a chat +## Usage +```sh +beeper chats mute [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--chat ` | Chat selector Required. | +| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--clear` | Unmute the chat Default: `false`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/chats-notify-anyway.md b/packages/cli/docs/commands/chats-notify-anyway.md new file mode 100644 index 00000000..bea46090 --- /dev/null +++ b/packages/cli/docs/commands/chats-notify-anyway.md @@ -0,0 +1,40 @@ +# beeper chats notify-anyway +Notify a chat anyway +## Usage +```sh +beeper chats notify-anyway [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--chat ` | Chat selector Required. | +| `--pick ` | Pick the Nth result when selector is ambiguous | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/chats-pin.md b/packages/cli/docs/commands/chats-pin.md new file mode 100644 index 00000000..029f3a1b --- /dev/null +++ b/packages/cli/docs/commands/chats-pin.md @@ -0,0 +1,41 @@ +# beeper chats pin +Pin or unpin a chat +## Usage +```sh +beeper chats pin [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--chat ` | Chat selector Required. | +| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--clear` | Unpin the chat Default: `false`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/chats-priority.md b/packages/cli/docs/commands/chats-priority.md new file mode 100644 index 00000000..59b80009 --- /dev/null +++ b/packages/cli/docs/commands/chats-priority.md @@ -0,0 +1,41 @@ +# beeper chats priority +Set chat priority +## Usage +```sh +beeper chats priority [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--chat ` | Chat selector Required. | +| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--level ` | Chat priority level Values: `inbox`, `low`. Required. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/chats-read.md b/packages/cli/docs/commands/chats-read.md new file mode 100644 index 00000000..d5ecdbb7 --- /dev/null +++ b/packages/cli/docs/commands/chats-read.md @@ -0,0 +1,42 @@ +# beeper chats read +Mark a chat read or unread +## Usage +```sh +beeper chats read [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--chat ` | Chat selector Required. | +| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--message ` | Read marker message ID | +| `--unread` | Mark the chat unread Default: `false`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/chats-remind.md b/packages/cli/docs/commands/chats-remind.md new file mode 100644 index 00000000..f1dec093 --- /dev/null +++ b/packages/cli/docs/commands/chats-remind.md @@ -0,0 +1,43 @@ +# beeper chats remind +Set or clear a chat reminder +## Usage +```sh +beeper chats remind [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--chat ` | Chat selector Required. | +| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--clear` | Clear the reminder Default: `false`. | +| `--dismiss-on-message` | Dismiss reminder when a new message arrives Default: `false`. | +| `--when ` | ISO reminder timestamp | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/chats-rename.md b/packages/cli/docs/commands/chats-rename.md new file mode 100644 index 00000000..15fb2241 --- /dev/null +++ b/packages/cli/docs/commands/chats-rename.md @@ -0,0 +1,41 @@ +# beeper chats rename +Rename a chat +## Usage +```sh +beeper chats rename [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--chat ` | Chat selector Required. | +| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--title ` | Chat title Required. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/chats-show.md b/packages/cli/docs/commands/chats-show.md new file mode 100644 index 00000000..a61145e4 --- /dev/null +++ b/packages/cli/docs/commands/chats-show.md @@ -0,0 +1,41 @@ +# beeper chats show +Show chat details +## Usage +```sh +beeper chats show [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--chat ` | Chat selector Required. | +| `--max-participants ` | Limit participants returned in chat details | +| `--pick ` | Pick the Nth result when selector is ambiguous | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/chats-start.md b/packages/cli/docs/commands/chats-start.md new file mode 100644 index 00000000..b560ec4f --- /dev/null +++ b/packages/cli/docs/commands/chats-start.md @@ -0,0 +1,46 @@ +# beeper chats start +Start a chat +## Usage +```sh +beeper chats start [flags] +``` +## Arguments + +| Name | Description | +| --- | --- | +| `` | | + +## Flags + +| Name | Description | +| --- | --- | +| `--account ` | Account selector | +| `--title ` | Optional initial title for a new group chat | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/completion.md b/packages/cli/docs/commands/completion.md new file mode 100644 index 00000000..d3da285d --- /dev/null +++ b/packages/cli/docs/commands/completion.md @@ -0,0 +1,39 @@ +# beeper completion +Generate shell completion scripts +## Usage +```sh +beeper completion [flags] +``` +## Arguments + +| Name | Description | +| --- | --- | +| `` | bash, zsh, fish, or powershell | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/config-get.md b/packages/cli/docs/commands/config-get.md new file mode 100644 index 00000000..7951b824 --- /dev/null +++ b/packages/cli/docs/commands/config-get.md @@ -0,0 +1,43 @@ +# beeper config get +Get a config value +## Usage +```sh +beeper config get [flags] +``` +## Aliases + +- `beeper config show` + +## Arguments + +| Name | Description | +| --- | --- | +| `` | Config key to get | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/config-keys.md b/packages/cli/docs/commands/config-keys.md new file mode 100644 index 00000000..f382c658 --- /dev/null +++ b/packages/cli/docs/commands/config-keys.md @@ -0,0 +1,38 @@ +# beeper config keys +List available config keys +## Usage +```sh +beeper config keys [flags] +``` +## Aliases + +- `beeper config list-keys` +- `beeper config names` + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/config-list.md b/packages/cli/docs/commands/config-list.md new file mode 100644 index 00000000..c543b644 --- /dev/null +++ b/packages/cli/docs/commands/config-list.md @@ -0,0 +1,38 @@ +# beeper config list +List all config values +## Usage +```sh +beeper config list [flags] +``` +## Aliases + +- `beeper config ls` +- `beeper config all` + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/config-path.md b/packages/cli/docs/commands/config-path.md new file mode 100644 index 00000000..b9bb7a50 --- /dev/null +++ b/packages/cli/docs/commands/config-path.md @@ -0,0 +1,37 @@ +# beeper config path +Print config file path +## Usage +```sh +beeper config path [flags] +``` +## Aliases + +- `beeper config where` + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/config-set.md b/packages/cli/docs/commands/config-set.md new file mode 100644 index 00000000..de3cf450 --- /dev/null +++ b/packages/cli/docs/commands/config-set.md @@ -0,0 +1,45 @@ +# beeper config set +Set a config value +## Usage +```sh +beeper config set [flags] +``` +## Aliases + +- `beeper config add` +- `beeper config update` + +## Arguments + +| Name | Description | +| --- | --- | +| `` | Config key to set | +| `` | Value to set | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/config-unset.md b/packages/cli/docs/commands/config-unset.md new file mode 100644 index 00000000..49cdf897 --- /dev/null +++ b/packages/cli/docs/commands/config-unset.md @@ -0,0 +1,45 @@ +# beeper config unset +Unset a config value +## Usage +```sh +beeper config unset [flags] +``` +## Aliases + +- `beeper config rm` +- `beeper config del` +- `beeper config remove` + +## Arguments + +| Name | Description | +| --- | --- | +| `` | Config key to unset | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/contacts-list.md b/packages/cli/docs/commands/contacts-list.md new file mode 100644 index 00000000..905ad815 --- /dev/null +++ b/packages/cli/docs/commands/contacts-list.md @@ -0,0 +1,47 @@ +# beeper contacts list +List contacts +## Usage +```sh +beeper contacts list [flags] +``` +## Aliases + +- `beeper contacts search` +- `beeper contacts find` + +## Flags + +| Name | Description | +| --- | --- | +| `-a, --account , --acct` | Limit to account selector Repeatable. | +| `--ids` | Print only contact user IDs Default: `false`. | +| `--limit ` | Maximum contacts to print Default: `50`. | +| `--query ` | Optional contact lookup query | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/doctor.md b/packages/cli/docs/commands/doctor.md new file mode 100644 index 00000000..5aa7f598 --- /dev/null +++ b/packages/cli/docs/commands/doctor.md @@ -0,0 +1,33 @@ +# beeper doctor +Run diagnostics for config, target reachability, auth, and readiness +## Usage +```sh +beeper doctor [flags] +``` +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/exit-codes.md b/packages/cli/docs/commands/exit-codes.md new file mode 100644 index 00000000..7c449491 --- /dev/null +++ b/packages/cli/docs/commands/exit-codes.md @@ -0,0 +1,38 @@ +# beeper exit-codes +Print stable exit codes for automation +## Usage +```sh +beeper exit-codes [flags] +``` +## Aliases + +- `beeper agent exit-codes` +- `beeper exitcodes` + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/export.md b/packages/cli/docs/commands/export.md new file mode 100644 index 00000000..d5dbf167 --- /dev/null +++ b/packages/cli/docs/commands/export.md @@ -0,0 +1,47 @@ +# beeper export +Export accounts, chats, messages, transcripts, and attachments +## Usage +```sh +beeper export [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `-a, --account , --acct` | Limit to account selector Repeatable. | +| `--chat ` | Limit to chat selector Repeatable. | +| `--force` | Re-export completed chats Default: `false`. | +| `--limit-chats ` | Maximum chats to export | +| `--limit-messages ` | Maximum messages per chat | +| `--max-participants ` | Maximum participants in chat.json Default: `500`. | +| `--no-attachments` | Skip downloading attachments Default: `false`. | +| `--out ` | Export directory Default: `beeper-export`. | +| `--pick ` | Pick the Nth result when selector is ambiguous | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/install-desktop.md b/packages/cli/docs/commands/install-desktop.md new file mode 100644 index 00000000..a0e01233 --- /dev/null +++ b/packages/cli/docs/commands/install-desktop.md @@ -0,0 +1,40 @@ +# beeper install desktop +Install Beeper Desktop locally +## Usage +```sh +beeper install desktop [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--channel ` | Install release channel Default: `stable`. Values: `stable`, `nightly`. | +| `--server-env ` | Server environment Default: `prod`. Values: `local`, `dev`, `staging`, `prod`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/install-server.md b/packages/cli/docs/commands/install-server.md new file mode 100644 index 00000000..54d82236 --- /dev/null +++ b/packages/cli/docs/commands/install-server.md @@ -0,0 +1,40 @@ +# beeper install server +Install Beeper Server locally +## Usage +```sh +beeper install server [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--channel ` | Install release channel Default: `stable`. Values: `stable`, `nightly`. | +| `--server-env ` | Server environment Default: `prod`. Values: `local`, `dev`, `staging`, `prod`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/mcp.md b/packages/cli/docs/commands/mcp.md new file mode 100644 index 00000000..89e2c42e --- /dev/null +++ b/packages/cli/docs/commands/mcp.md @@ -0,0 +1,43 @@ +# beeper mcp +Run a typed MCP stdio server +## Usage +```sh +beeper mcp [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--allow-tool , --tool` | Tool or command allowlist Repeatable. | +| `--allow-write` | Allow write-risk MCP tools Default: `false`. | +| `--list-tools` | Print enabled MCP tools as JSON and exit Default: `false`. | +| `--max-output-bytes ` | Maximum stdout/stderr bytes captured per tool call Default: `102400`. | +| `--timeout-seconds ` | Per-tool subprocess timeout Default: `60`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/media-download.md b/packages/cli/docs/commands/media-download.md new file mode 100644 index 00000000..435bd23e --- /dev/null +++ b/packages/cli/docs/commands/media-download.md @@ -0,0 +1,45 @@ +# beeper media download +Download message media +## Usage +```sh +beeper media download [flags] +``` +## Arguments + +| Name | Description | +| --- | --- | +| `` | | + +## Flags + +| Name | Description | +| --- | --- | +| `--out ` | Output directory; - streams to stdout Default: `.`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/messages-context.md b/packages/cli/docs/commands/messages-context.md new file mode 100644 index 00000000..4dac8af6 --- /dev/null +++ b/packages/cli/docs/commands/messages-context.md @@ -0,0 +1,43 @@ +# beeper messages context +Show a message with surrounding context +## Usage +```sh +beeper messages context [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--chat ` | Chat selector Required. | +| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--id ` | Message ID Required. | +| `--after ` | Messages after target Default: `10`. | +| `--before ` | Messages before target Default: `10`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/messages-delete.md b/packages/cli/docs/commands/messages-delete.md new file mode 100644 index 00000000..68279f76 --- /dev/null +++ b/packages/cli/docs/commands/messages-delete.md @@ -0,0 +1,42 @@ +# beeper messages delete +Delete a message +## Usage +```sh +beeper messages delete [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--chat ` | Chat selector Required. | +| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--id ` | Message ID Required. | +| `--for-everyone` | Delete for everyone when supported Default: `false`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/messages-edit.md b/packages/cli/docs/commands/messages-edit.md new file mode 100644 index 00000000..15867ae3 --- /dev/null +++ b/packages/cli/docs/commands/messages-edit.md @@ -0,0 +1,42 @@ +# beeper messages edit +Edit a message +## Usage +```sh +beeper messages edit [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--chat ` | Chat selector Required. | +| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--id ` | Message ID Required. | +| `--message ` | New message text Required. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/messages-list.md b/packages/cli/docs/commands/messages-list.md new file mode 100644 index 00000000..86442b5a --- /dev/null +++ b/packages/cli/docs/commands/messages-list.md @@ -0,0 +1,50 @@ +# beeper messages list +List chat messages +## Usage +```sh +beeper messages list [flags] +``` +## Aliases + +- `beeper messages ls` + +## Flags + +| Name | Description | +| --- | --- | +| `--after-cursor ` | Paginate messages newer than this message ID | +| `--asc` | Order oldest first Default: `false`. | +| `--before-cursor ` | Paginate messages older than this message ID | +| `--chat ` | Chat selector Required. | +| `--ids` | Print only message IDs Default: `false`. | +| `--limit ` | Maximum messages to print Default: `50`. | +| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--sender ` | me, others, or a specific user ID | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/messages-search.md b/packages/cli/docs/commands/messages-search.md new file mode 100644 index 00000000..6f30b055 --- /dev/null +++ b/packages/cli/docs/commands/messages-search.md @@ -0,0 +1,69 @@ +# beeper messages search +Search messages across chats +## Usage +```sh +beeper messages search [query] [flags] +``` +## Aliases + +- `beeper messages find` + +## Arguments + +| Name | Description | +| --- | --- | +| `[query]` | | + +## Flags + +| Name | Description | +| --- | --- | +| `-a, --account , --acct` | Limit to account selector Repeatable. | +| `--chat ` | Limit to a chat selector Repeatable. | +| `--chat-type ` | Only group chats or direct messages Values: `group`, `single`. | +| `--after ` | Only messages at or after this ISO timestamp | +| `--before ` | Only messages at or before this ISO timestamp | +| `--exclude-low-priority` | Exclude low-priority chats | +| `--ids` | Print only message IDs Default: `false`. | +| `--include-muted` | Include muted chats Default: `true`. | +| `--limit , --max` | Maximum results Default: `50`. | +| `--media ` | Filter by media type Values: `any`, `video`, `image`, `link`, `file`. Repeatable. | +| `--sender ` | me, others, or a user ID | +| `--fail-empty, --non-empty, --require-results` | Exit with code 3 if no results Default: `false`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | + +## Examples + +```sh +beeper messages search "quarterly report" +``` +```sh +beeper messages search --chat "Work" --sender me --limit 20 +``` diff --git a/packages/cli/docs/commands/remove-account.md b/packages/cli/docs/commands/remove-account.md new file mode 100644 index 00000000..99ec38cf --- /dev/null +++ b/packages/cli/docs/commands/remove-account.md @@ -0,0 +1,44 @@ +# beeper remove account +Remove an account +## Usage +```sh +beeper remove account [flags] +``` +## Aliases + +- `beeper accounts remove` +- `beeper accounts rm` + +## Arguments + +| Name | Description | +| --- | --- | +| `` | Account selector | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/remove-target.md b/packages/cli/docs/commands/remove-target.md new file mode 100644 index 00000000..d40cf119 --- /dev/null +++ b/packages/cli/docs/commands/remove-target.md @@ -0,0 +1,44 @@ +# beeper remove target +Remove a target +## Usage +```sh +beeper remove target [flags] +``` +## Aliases + +- `beeper targets remove` +- `beeper targets rm` + +## Arguments + +| Name | Description | +| --- | --- | +| `` | Target name | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/resolve-account.md b/packages/cli/docs/commands/resolve-account.md new file mode 100644 index 00000000..0c399ef0 --- /dev/null +++ b/packages/cli/docs/commands/resolve-account.md @@ -0,0 +1,45 @@ +# beeper resolve account +Resolve an account selector +## Usage +```sh +beeper resolve account [flags] +``` +## Arguments + +| Name | Description | +| --- | --- | +| `` | | + +## Flags + +| Name | Description | +| --- | --- | +| `--pick ` | Select the Nth candidate | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/resolve-bridge.md b/packages/cli/docs/commands/resolve-bridge.md new file mode 100644 index 00000000..d26680c5 --- /dev/null +++ b/packages/cli/docs/commands/resolve-bridge.md @@ -0,0 +1,45 @@ +# beeper resolve bridge +Resolve a bridge selector +## Usage +```sh +beeper resolve bridge [flags] +``` +## Arguments + +| Name | Description | +| --- | --- | +| `` | | + +## Flags + +| Name | Description | +| --- | --- | +| `--pick ` | Select the Nth candidate | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/resolve-chat.md b/packages/cli/docs/commands/resolve-chat.md new file mode 100644 index 00000000..b84ada8e --- /dev/null +++ b/packages/cli/docs/commands/resolve-chat.md @@ -0,0 +1,47 @@ +# beeper resolve chat +Resolve a chat selector +## Usage +```sh +beeper resolve chat [flags] +``` +## Arguments + +| Name | Description | +| --- | --- | +| `` | | + +## Flags + +| Name | Description | +| --- | --- | +| `-a, --account , --acct` | Limit to account selector Repeatable. | +| `--limit , --max` | Maximum candidates Default: `10`. | +| `--pick ` | Select the Nth candidate | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/resolve-contact.md b/packages/cli/docs/commands/resolve-contact.md new file mode 100644 index 00000000..6d43261a --- /dev/null +++ b/packages/cli/docs/commands/resolve-contact.md @@ -0,0 +1,47 @@ +# beeper resolve contact +Resolve a contact selector +## Usage +```sh +beeper resolve contact [flags] +``` +## Arguments + +| Name | Description | +| --- | --- | +| `` | | + +## Flags + +| Name | Description | +| --- | --- | +| `-a, --account , --acct` | Limit to account selector Repeatable. | +| `--limit , --max` | Maximum candidates Default: `10`. | +| `--pick ` | Select the Nth candidate | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/resolve-target.md b/packages/cli/docs/commands/resolve-target.md new file mode 100644 index 00000000..9797bb5f --- /dev/null +++ b/packages/cli/docs/commands/resolve-target.md @@ -0,0 +1,45 @@ +# beeper resolve target +Resolve a target selector +## Usage +```sh +beeper resolve target [flags] +``` +## Arguments + +| Name | Description | +| --- | --- | +| `` | | + +## Flags + +| Name | Description | +| --- | --- | +| `--pick ` | Select the Nth candidate | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/schema.md b/packages/cli/docs/commands/schema.md new file mode 100644 index 00000000..74e25255 --- /dev/null +++ b/packages/cli/docs/commands/schema.md @@ -0,0 +1,44 @@ +# beeper schema +Print machine-readable command and flag schema +## Usage +```sh +beeper schema ... [flags] +``` +## Aliases + +- `beeper help-json` +- `beeper helpjson` + +## Arguments + +| Name | Description | +| --- | --- | +| `[command ...]` | | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/send-file.md b/packages/cli/docs/commands/send-file.md new file mode 100644 index 00000000..d3c10452 --- /dev/null +++ b/packages/cli/docs/commands/send-file.md @@ -0,0 +1,47 @@ +# beeper send file +Send a file message +## Usage +```sh +beeper send file [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--to ` | Chat selector Required. | +| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--reply-to ` | Send as a reply to this message ID | +| `--wait` | Wait until the message leaves pending state Default: `false`. | +| `--wait-timeout ` | Maximum wait time in ms when --wait is set Default: `30000`. | +| `--file ` | Local file path to upload Required. | +| `--caption ` | Optional caption for file messages | +| `--filename ` | Override displayed filename | +| `--mime ` | Override MIME type | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/send-presence.md b/packages/cli/docs/commands/send-presence.md new file mode 100644 index 00000000..195f511f --- /dev/null +++ b/packages/cli/docs/commands/send-presence.md @@ -0,0 +1,42 @@ +# beeper send presence +Send a typing indicator +## Usage +```sh +beeper send presence [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--to ` | Chat selector Required. | +| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--duration ` | Seconds to keep typing before sending paused | +| `--state ` | Presence indicator to send Default: `typing`. Values: `typing`, `paused`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/send-react.md b/packages/cli/docs/commands/send-react.md new file mode 100644 index 00000000..2c1a4c5e --- /dev/null +++ b/packages/cli/docs/commands/send-react.md @@ -0,0 +1,44 @@ +# beeper send react +Send or remove a reaction +## Usage +```sh +beeper send react [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--to ` | Chat selector Required. | +| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--id ` | Message ID to react to Required. | +| `--reaction ` | Reaction key Required. | +| `--remove` | Remove the reaction Default: `false`. | +| `--transaction ` | Optional transaction ID for deduplication | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/send-sticker.md b/packages/cli/docs/commands/send-sticker.md new file mode 100644 index 00000000..b1abc9b5 --- /dev/null +++ b/packages/cli/docs/commands/send-sticker.md @@ -0,0 +1,46 @@ +# beeper send sticker +Send a sticker +## Usage +```sh +beeper send sticker [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--to ` | Chat selector Required. | +| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--reply-to ` | Send as a reply to this message ID | +| `--wait` | Wait until the message leaves pending state Default: `false`. | +| `--wait-timeout ` | Maximum wait time in ms when --wait is set Default: `30000`. | +| `--file ` | Local sticker file path to upload Required. | +| `--filename ` | Override displayed filename | +| `--mime ` | Override MIME type | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/send-text.md b/packages/cli/docs/commands/send-text.md new file mode 100644 index 00000000..fdd4b957 --- /dev/null +++ b/packages/cli/docs/commands/send-text.md @@ -0,0 +1,48 @@ +# beeper send text +Send a text message +## Usage +```sh +beeper send text [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--to ` | Chat selector Required. | +| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--reply-to ` | Send as a reply to this message ID | +| `--wait` | Wait until the message leaves pending state Default: `false`. | +| `--wait-timeout ` | Maximum wait time in ms when --wait is set Default: `30000`. | +| `--message ` | Message text to send | +| `--message-escapes` | Interpret backslash escapes in --message Default: `false`. | +| `--message-file ` | Read message text from a file path; '-' reads stdin | +| `--mention ` | User ID to mention Repeatable. | +| `--no-preview` | Disable automatic link preview Default: `false`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/send-voice.md b/packages/cli/docs/commands/send-voice.md new file mode 100644 index 00000000..e59da362 --- /dev/null +++ b/packages/cli/docs/commands/send-voice.md @@ -0,0 +1,47 @@ +# beeper send voice +Send a voice note +## Usage +```sh +beeper send voice [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--to ` | Chat selector Required. | +| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--reply-to ` | Send as a reply to this message ID | +| `--wait` | Wait until the message leaves pending state Default: `false`. | +| `--wait-timeout ` | Maximum wait time in ms when --wait is set Default: `30000`. | +| `--file ` | Local voice note file path to upload Required. | +| `--duration ` | Duration in seconds | +| `--filename ` | Override displayed filename | +| `--mime ` | Override MIME type | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/setup.md b/packages/cli/docs/commands/setup.md new file mode 100644 index 00000000..69dd1008 --- /dev/null +++ b/packages/cli/docs/commands/setup.md @@ -0,0 +1,63 @@ +# beeper setup +Make the selected target ready for messaging +## Usage +```sh +beeper setup [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--local` | Use the local Beeper Desktop session on this device Default: `false`. | +| `--oauth` | Authorize the target with browser OAuth/PKCE Default: `false`. | +| `--remote ` | Connect to a remote Beeper Desktop or Server URL | +| `--server` | Set up a local Beeper Server target Default: `false`. | +| `--desktop` | Set up a local Beeper Desktop target Default: `false`. | +| `--install` | Allow installing a missing local runtime Default: `false`. | +| `--channel ` | Install release channel Default: `stable`. Values: `stable`, `nightly`. | +| `--server-env ` | Server environment Default: `prod`. Values: `local`, `dev`, `staging`, `prod`. | +| `--email ` | Sign in with an email address | +| `--username ` | Username to use if setup creates a new account | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | + +## Examples + +```sh +beeper setup +``` +```sh +beeper setup --local +``` +```sh +beeper setup --remote https://desktop.example.com +``` +```sh +beeper setup --desktop --install +``` diff --git a/packages/cli/docs/commands/status.md b/packages/cli/docs/commands/status.md new file mode 100644 index 00000000..95cedf05 --- /dev/null +++ b/packages/cli/docs/commands/status.md @@ -0,0 +1,43 @@ +# beeper status +Show selected target and setup readiness +## Usage +```sh +beeper status [target] [flags] +``` +## Aliases + +- `beeper st` + +## Arguments + +| Name | Description | +| --- | --- | +| `[target]` | Target name. Defaults to the selected target. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/targets-add.md b/packages/cli/docs/commands/targets-add.md new file mode 100644 index 00000000..49d3c0e3 --- /dev/null +++ b/packages/cli/docs/commands/targets-add.md @@ -0,0 +1,46 @@ +# beeper targets add +Add a remote Beeper Desktop or Server target +## Usage +```sh +beeper targets add [flags] +``` +## Arguments + +| Name | Description | +| --- | --- | +| `` | | +| `` | | + +## Flags + +| Name | Description | +| --- | --- | +| `--default` | Set this target as the default after creation Default: `false`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/targets-list.md b/packages/cli/docs/commands/targets-list.md new file mode 100644 index 00000000..9a8e4056 --- /dev/null +++ b/packages/cli/docs/commands/targets-list.md @@ -0,0 +1,37 @@ +# beeper targets list +List configured Beeper targets +## Usage +```sh +beeper targets list [flags] +``` +## Aliases + +- `beeper targets ls` + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/targets-logs.md b/packages/cli/docs/commands/targets-logs.md new file mode 100644 index 00000000..dd8c9e87 --- /dev/null +++ b/packages/cli/docs/commands/targets-logs.md @@ -0,0 +1,47 @@ +# beeper targets logs +Print logs for a local Beeper Desktop or Server install +## Usage +```sh +beeper targets logs [name] [flags] +``` +## Arguments + +| Name | Description | +| --- | --- | +| `[name]` | | + +## Flags + +| Name | Description | +| --- | --- | +| `--lines ` | Lines to print from each log file Default: `200`. | +| `--files ` | Desktop log files to print, newest first Default: `5`. | +| `--all` | Print all matching log files instead of only recent files Default: `false`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/targets-runtime-restart.md b/packages/cli/docs/commands/targets-runtime-restart.md new file mode 100644 index 00000000..e15eeb52 --- /dev/null +++ b/packages/cli/docs/commands/targets-runtime-restart.md @@ -0,0 +1,39 @@ +# beeper targets runtime restart +Restart a local server runtime +## Usage +```sh +beeper targets runtime restart [name] [flags] +``` +## Arguments + +| Name | Description | +| --- | --- | +| `[name]` | | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/targets-runtime-start.md b/packages/cli/docs/commands/targets-runtime-start.md new file mode 100644 index 00000000..53390b7b --- /dev/null +++ b/packages/cli/docs/commands/targets-runtime-start.md @@ -0,0 +1,39 @@ +# beeper targets runtime start +Start a local target runtime +## Usage +```sh +beeper targets runtime start [name] [flags] +``` +## Arguments + +| Name | Description | +| --- | --- | +| `[name]` | | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/targets-runtime-stop.md b/packages/cli/docs/commands/targets-runtime-stop.md new file mode 100644 index 00000000..81f51723 --- /dev/null +++ b/packages/cli/docs/commands/targets-runtime-stop.md @@ -0,0 +1,39 @@ +# beeper targets runtime stop +Stop a local server runtime +## Usage +```sh +beeper targets runtime stop [name] [flags] +``` +## Arguments + +| Name | Description | +| --- | --- | +| `[name]` | | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/targets-tunnel.md b/packages/cli/docs/commands/targets-tunnel.md new file mode 100644 index 00000000..03de60cb --- /dev/null +++ b/packages/cli/docs/commands/targets-tunnel.md @@ -0,0 +1,49 @@ +# beeper targets tunnel +Expose a target through Cloudflare Tunnel +## Usage +```sh +beeper targets tunnel [name] [flags] +``` +## Arguments + +| Name | Description | +| --- | --- | +| `[name]` | Target name. Defaults to the selected target. | + +## Flags + +| Name | Description | +| --- | --- | +| `--cloudflared-path ` | Path to cloudflared. Also configurable with BEEPER_CLOUDFLARED_PATH. | +| `--install` | Download the pinned cloudflared binary if missing or outdated Default: `false`. | +| `--retries ` | Startup retries before giving up Default: `5`. | +| `--timeout ` | Startup timeout, for example 40s or 60000ms | +| `--url-only` | Print only the public tunnel URL Default: `false`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/use-account.md b/packages/cli/docs/commands/use-account.md new file mode 100644 index 00000000..bdc409db --- /dev/null +++ b/packages/cli/docs/commands/use-account.md @@ -0,0 +1,43 @@ +# beeper use account +Select the default account +## Usage +```sh +beeper use account [flags] +``` +## Aliases + +- `beeper accounts use` + +## Arguments + +| Name | Description | +| --- | --- | +| `` | Account selector | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/use-target.md b/packages/cli/docs/commands/use-target.md new file mode 100644 index 00000000..ccc7900d --- /dev/null +++ b/packages/cli/docs/commands/use-target.md @@ -0,0 +1,43 @@ +# beeper use target +Select the default target +## Usage +```sh +beeper use target [flags] +``` +## Aliases + +- `beeper targets use` + +## Arguments + +| Name | Description | +| --- | --- | +| `` | Target name | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/version.md b/packages/cli/docs/commands/version.md new file mode 100644 index 00000000..1066e2b5 --- /dev/null +++ b/packages/cli/docs/commands/version.md @@ -0,0 +1,33 @@ +# beeper version +Print CLI version +## Usage +```sh +beeper version [flags] +``` +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/watch.md b/packages/cli/docs/commands/watch.md new file mode 100644 index 00000000..fe4d55da --- /dev/null +++ b/packages/cli/docs/commands/watch.md @@ -0,0 +1,44 @@ +# beeper watch +Stream Desktop API WebSocket events +## Usage +```sh +beeper watch [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--chat ` | Chat ID to subscribe to; defaults to all chats Repeatable. | +| `--exclude-type ` | Drop events of these types Values: `chat.upserted`, `chat.deleted`, `message.upserted`, `message.deleted`, `message.stream`. Repeatable. | +| `--include-type ` | Only forward events of these types Values: `chat.upserted`, `chat.deleted`, `message.upserted`, `message.deleted`, `message.stream`. Repeatable. | +| `--webhook ` | Forward each event to this URL as POST | +| `--webhook-queue ` | Maximum pending webhook deliveries Default: `64`. | +| `--webhook-secret ` | HMAC-SHA256 secret for X-Beeper-Signature | + +## Global Flags + +| Name | Description | +| --- | --- | +| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | +| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | +| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | +| `--debug` | Default: `false`. | +| `--disable-commands ` | Comma-separated command prefixes to block | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | +| `--enable-commands ` | Comma-separated enabled command prefixes | +| `--enable-commands-exact ` | Comma-separated exact enabled commands | +| `--events` | Default: `false`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | +| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | +| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | +| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--safety-profile ` | Safety profile name or YAML path | +| `--select , --fields, --project` | Select comma-separated JSON fields | +| `--target ` | Target name or URL | +| `--timeout ` | Command timeout, for example 30s or 2m | +| `-v, --version` | Print version and exit Default: `false`. | +| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/package.json b/packages/cli/package.json index ed70cddb..9ca7aa12 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -13,6 +13,7 @@ "files": [ "bin/cli.js", "dist", + "docs", "safety-profiles", "README.md", "LICENSE" @@ -20,6 +21,7 @@ "scripts": { "build": "bun run clean && bun scripts/prepare-desktop-api.ts && tsc -p tsconfig.json", "clean": "rm -rf dist", + "docs:commands": "bun scripts/generate-command-docs.ts", "dev": "bun ./bin/dev.js", "e2e:staging": "bun run build && bun test/e2e-staging.ts", "test": "bun run build && bun ./test/cli-smoke.ts && bun test", diff --git a/packages/cli/scripts/generate-command-docs.ts b/packages/cli/scripts/generate-command-docs.ts new file mode 100644 index 00000000..ef803a16 --- /dev/null +++ b/packages/cli/scripts/generate-command-docs.ts @@ -0,0 +1,96 @@ +import { mkdir, rm, writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import { commands } from '../src/cli/commands.js' +import { globalFlagSpecs } from '../src/cli/parse.js' +import type { ArgSpec, CommandSpec, FlagSpec } from '../src/cli/types.js' + +const root = new URL('..', import.meta.url).pathname +const docsDir = join(root, 'docs', 'commands') + +await rm(docsDir, { recursive: true, force: true }) +await mkdir(docsDir, { recursive: true }) +await writeFile(join(docsDir, 'README.md'), commandIndex()) + +for (const command of commands.filter(command => !command.hidden)) { + await writeFile(join(docsDir, `${command.path.join('-')}.md`), commandDoc(command)) +} + +function commandIndex(): string { + const rows = commands + .filter(command => !command.hidden) + .sort(compareCommands) + .map(command => `| [\`${command.path.join(' ')}\`](${command.path.join('-')}.md) | ${escapeTable(command.description)} | ${aliases(command)} |`) + return [ + '# Command Index', + '', + 'Generated from the live command registry. Do not edit command pages by hand.', + '', + '| Command | Description | Aliases |', + '| --- | --- | --- |', + ...rows, + '', + ].join('\n') +} + +function commandDoc(command: CommandSpec): string { + return [ + `# beeper ${command.path.join(' ')}`, + '', + command.description, + '', + '## Usage', + '', + '```sh', + `beeper ${command.path.join(' ')}${usageArgs(command.args ?? [])} [flags]`, + '```', + '', + command.aliases?.length ? ['## Aliases', '', ...command.aliases.map(alias => `- \`beeper ${alias.join(' ')}\``), ''].join('\n') : undefined, + section('Arguments', (command.args ?? []).map(argRow)), + section('Flags', (command.flags ?? []).map(flagRow)), + section('Global Flags', globalFlagSpecs.map(flagRow)), + command.examples?.length ? ['## Examples', '', ...command.examples.map(example => `\`\`\`sh\n${example}\n\`\`\``), ''].join('\n') : undefined, + ].filter(Boolean).join('\n') +} + +function section(title: string, rows: string[]): string | undefined { + if (!rows.length) return undefined + return [`## ${title}`, '', '| Name | Description |', '| --- | --- |', ...rows, ''].join('\n') +} + +function argRow(arg: ArgSpec): string { + const name = `${arg.required ? '<' : '['}${arg.name}${arg.variadic ? ' ...' : ''}${arg.required ? '>' : ']'}` + return `| \`${name}\` | ${escapeTable(arg.description ?? '')} |` +} + +function flagRow(flag: FlagSpec): string { + const tokens = [ + flag.short ? `-${flag.short}` : undefined, + `--${flag.name}${flag.type === 'boolean' ? '' : ` <${flag.placeholder ?? 'value'}>`}`, + ...(flag.aliases ?? []).map(alias => `--${alias}`), + ].filter(Boolean).join(', ') + const suffixes = [ + flag.default !== undefined ? `Default: \`${String(flag.default)}\`.` : undefined, + flag.enum?.length ? `Values: ${flag.enum.map(value => `\`${value}\``).join(', ')}.` : undefined, + flag.env?.length ? `Env: ${flag.env.map(value => `\`${value}\``).join(', ')}.` : undefined, + flag.multiple ? 'Repeatable.' : undefined, + flag.required ? 'Required.' : undefined, + ].filter(Boolean).join(' ') + return `| \`${tokens}\` | ${escapeTable([flag.description, suffixes].filter(Boolean).join(' '))} |` +} + +function usageArgs(args: ArgSpec[]): string { + if (!args.length) return '' + return ` ${args.map(arg => arg.variadic ? `<${arg.name}> ...` : arg.required ? `<${arg.name}>` : `[${arg.name}]`).join(' ')}` +} + +function aliases(command: CommandSpec): string { + return command.aliases?.length ? command.aliases.map(alias => `\`${alias.join(' ')}\``).join(', ') : '' +} + +function compareCommands(a: CommandSpec, b: CommandSpec): number { + return a.path.join(' ').localeCompare(b.path.join(' ')) +} + +function escapeTable(value: string): string { + return value.replaceAll('|', '\\|').replaceAll('\n', '
') +} diff --git a/packages/cli/src/cli/commands.ts b/packages/cli/src/cli/commands.ts index d498ea21..6cd1cdf3 100644 --- a/packages/cli/src/cli/commands.ts +++ b/packages/cli/src/cli/commands.ts @@ -23,6 +23,7 @@ import { targetLiveStatus } from '../lib/target-status.js' import { promptChoice } from '../lib/prompts.js' import { builtInDesktopTargetID, + configPath, listTargets, publicTarget, readConfig, @@ -31,6 +32,7 @@ import { resolveTarget, updateConfig, writeTarget, + type Config, type Target, } from '../lib/targets.js' import WebSocket from 'ws' @@ -42,8 +44,9 @@ import { startProfile, stopProfile, } from '../lib/profiles.js' -import type { CommandContext, CommandSpec, FlagSpec } from './types.js' +import type { CommandContext, CommandSpec, FlagSpec, GlobalFlags } from './types.js' import { globalFlagSpecs, numberFlag, requiredStringFlag, stringFlag, stringListFlag } from './parse.js' +import { commandVisible } from './policy.js' import { buildSchema } from './schema.js' import { serveMcp } from './mcp.js' import { usage, writeEvent, writeResult } from './output.js' @@ -67,11 +70,13 @@ type SendPayload = { waitTimeoutMs?: number } -const accountFilterFlag: FlagSpec = { name: 'account', type: 'string', multiple: true, description: 'Limit to account selector' } -const candidateLimitFlag: FlagSpec = { name: 'limit', type: 'integer', default: 10, description: 'Maximum candidates' } +const accountFilterFlag: FlagSpec = { name: 'account', short: 'a', aliases: ['acct'], type: 'string', multiple: true, description: 'Limit to account selector' } +const candidateLimitFlag: FlagSpec = { name: 'limit', aliases: ['max'], type: 'integer', default: 10, description: 'Maximum candidates' } const chatFlag: FlagSpec = { name: 'chat', type: 'string', required: true, description: 'Chat selector' } const pickCandidateFlag: FlagSpec = { name: 'pick', type: 'integer', description: 'Select the Nth candidate' } const pickChatFlag: FlagSpec = { name: 'pick', type: 'integer', description: 'Pick the Nth result when selector is ambiguous' } +const configKeys = ['defaultTarget', 'defaultAccount'] as const +type ConfigKey = typeof configKeys[number] const chatFlags: FlagSpec[] = [ chatFlag, @@ -99,20 +104,41 @@ export const commands: CommandSpec[] = [ { description: 'Print CLI version', mcp: true, + output: 'diagnostic', path: ['version'], risk: 'read', run: version, }, { args: [{ name: 'target', description: 'Target name. Defaults to the selected target.' }], + aliases: [['st']], description: 'Show selected target and setup readiness', mcp: true, + output: 'status', path: ['status'], risk: 'read', run: status, }, + { + description: 'Run diagnostics for config, target reachability, auth, and readiness', + mcp: true, + output: 'diagnostic', + path: ['doctor'], + risk: 'read', + run: doctor, + }, + { + aliases: [['agent', 'exit-codes'], ['exitcodes']], + description: 'Print stable exit codes for automation', + mcp: true, + output: 'diagnostic', + path: ['exit-codes'], + risk: 'read', + run: exitCodes, + }, { args: [{ name: 'command', variadic: true }], + aliases: [['help-json'], ['helpjson']], description: 'Print machine-readable command and flag schema', mcp: true, path: ['schema'], @@ -121,11 +147,91 @@ export const commands: CommandSpec[] = [ }, { description: 'Run a typed MCP stdio server', - flags: [{ name: 'allow-write', type: 'boolean', default: false, description: 'Allow write-risk MCP tools' }], + flags: [ + { name: 'allow-tool', aliases: ['tool'], type: 'string', multiple: true, description: 'Tool or command allowlist' }, + { name: 'allow-write', type: 'boolean', default: false, description: 'Allow write-risk MCP tools' }, + { name: 'list-tools', type: 'boolean', default: false, description: 'Print enabled MCP tools as JSON and exit' }, + { name: 'max-output-bytes', type: 'integer', default: 102400, description: 'Maximum stdout/stderr bytes captured per tool call' }, + { name: 'timeout-seconds', type: 'integer', default: 60, description: 'Per-tool subprocess timeout' }, + ], path: ['mcp'], risk: 'read', run: mcp, }, + { + args: [{ name: 'shell', required: true, description: 'bash, zsh, fish, or powershell' }], + description: 'Generate shell completion scripts', + hidden: false, + path: ['completion'], + risk: 'read', + run: completion, + }, + { + aliases: [['config', 'show']], + args: [{ name: 'key', required: true, description: 'Config key to get' }], + description: 'Get a config value', + mcp: true, + output: 'diagnostic', + path: ['config', 'get'], + risk: 'read', + run: configGet, + }, + { + aliases: [['config', 'list-keys'], ['config', 'names']], + description: 'List available config keys', + mcp: true, + path: ['config', 'keys'], + risk: 'read', + run: configKeysCommand, + }, + { + aliases: [['config', 'ls'], ['config', 'all']], + description: 'List all config values', + mcp: true, + output: 'diagnostic', + path: ['config', 'list'], + risk: 'read', + run: configList, + }, + { + aliases: [['config', 'where']], + description: 'Print config file path', + mcp: true, + output: 'diagnostic', + path: ['config', 'path'], + risk: 'read', + run: configPathCommand, + }, + { + aliases: [['config', 'add'], ['config', 'update']], + args: [ + { name: 'key', required: true, description: 'Config key to set' }, + { name: 'value', required: true, description: 'Value to set' }, + ], + description: 'Set a config value', + output: 'diagnostic', + path: ['config', 'set'], + risk: 'write', + run: configSet, + }, + { + aliases: [['config', 'rm'], ['config', 'del'], ['config', 'remove']], + args: [{ name: 'key', required: true, description: 'Config key to unset' }], + description: 'Unset a config value', + output: 'diagnostic', + path: ['config', 'unset'], + risk: 'write', + run: configUnset, + }, + { + args: [{ name: 'words', variadic: true, description: 'Current command line words' }], + description: 'Internal completion helper', + flags: [{ name: 'cword', type: 'integer', default: -1, description: 'Current word index' }], + hidden: true, + path: ['__complete'], + risk: 'read', + run: completeCommand, + }, { description: 'Make the selected target ready for messaging', examples: ['beeper setup', 'beeper setup --local', 'beeper setup --remote https://desktop.example.com', 'beeper setup --desktop --install'], @@ -145,8 +251,10 @@ export const commands: CommandSpec[] = [ run: runSetup, }, { + aliases: [['targets', 'ls']], description: 'List configured Beeper targets', mcp: true, + output: 'targets', path: ['targets', 'list'], risk: 'read', run: targetsList, @@ -218,12 +326,14 @@ export const commands: CommandSpec[] = [ { name: 'ids', type: 'boolean', default: false, description: 'Print only account IDs' }, ], mcp: true, + output: 'accounts', path: ['accounts', 'list'], risk: 'read', run: accountsList, }, { args: [{ name: 'selector', required: true, description: 'Target name' }], + aliases: [['targets', 'use']], description: 'Select the default target', path: ['use', 'target'], risk: 'write', @@ -231,6 +341,7 @@ export const commands: CommandSpec[] = [ }, { args: [{ name: 'selector', required: true, description: 'Account selector' }], + aliases: [['accounts', 'use']], description: 'Select the default account', path: ['use', 'account'], risk: 'write', @@ -255,6 +366,7 @@ export const commands: CommandSpec[] = [ }, { args: [{ name: 'selector', required: true, description: 'Target name' }], + aliases: [['targets', 'remove'], ['targets', 'rm']], description: 'Remove a target', path: ['remove', 'target'], risk: 'destructive', @@ -262,12 +374,14 @@ export const commands: CommandSpec[] = [ }, { args: [{ name: 'selector', required: true, description: 'Account selector' }], + aliases: [['accounts', 'remove'], ['accounts', 'rm']], description: 'Remove an account', path: ['remove', 'account'], risk: 'destructive', run: removeAccount, }, { + aliases: [['contacts', 'search'], ['contacts', 'find']], description: 'List contacts', flags: [ accountFilterFlag, @@ -276,11 +390,13 @@ export const commands: CommandSpec[] = [ { name: 'query', type: 'string', description: 'Optional contact lookup query' }, ], mcp: true, + output: 'contacts', path: ['contacts', 'list'], risk: 'read', run: contactsList, }, { + aliases: [['chats', 'ls']], description: 'List chats', flags: [ accountFilterFlag, @@ -294,6 +410,7 @@ export const commands: CommandSpec[] = [ { name: 'unread', type: 'boolean', description: 'Only unread chats; use --no-unread to exclude' }, ], mcp: true, + output: 'chats', path: ['chats', 'list'], risk: 'read', run: chatsList, @@ -499,6 +616,7 @@ export const commands: CommandSpec[] = [ }, { description: 'List chat messages', + aliases: [['messages', 'ls']], flags: [ { name: 'after-cursor', type: 'string', description: 'Paginate messages newer than this message ID' }, { name: 'asc', type: 'boolean', default: false, description: 'Order oldest first' }, @@ -510,6 +628,7 @@ export const commands: CommandSpec[] = [ { name: 'sender', type: 'string', description: 'me, others, or a specific user ID' }, ], mcp: true, + output: 'messages', path: ['messages', 'list'], risk: 'read', run: messagesList, @@ -593,6 +712,7 @@ export const commands: CommandSpec[] = [ }, { args: [{ name: 'query' }], + aliases: [['messages', 'find']], description: 'Search messages across chats', examples: [ 'beeper messages search "quarterly report"', @@ -607,11 +727,13 @@ export const commands: CommandSpec[] = [ { name: 'exclude-low-priority', type: 'boolean', description: 'Exclude low-priority chats' }, { name: 'ids', type: 'boolean', default: false, description: 'Print only message IDs' }, { name: 'include-muted', type: 'boolean', default: true, description: 'Include muted chats' }, - { name: 'limit', type: 'integer', default: 50, description: 'Maximum results' }, + { name: 'limit', aliases: ['max'], type: 'integer', default: 50, description: 'Maximum results' }, { name: 'media', type: 'string', multiple: true, enum: ['any', 'video', 'image', 'link', 'file'], description: 'Filter by media type' }, { name: 'sender', type: 'string', description: 'me, others, or a user ID' }, + { name: 'fail-empty', aliases: ['non-empty', 'require-results'], type: 'boolean', default: false, description: 'Exit with code 3 if no results' }, ], mcp: true, + output: 'messages', path: ['messages', 'search'], risk: 'read', run: messagesSearch, @@ -632,7 +754,9 @@ export const commands: CommandSpec[] = [ description: 'Send a text message', flags: [ ...sendDeliveryFlags, - { name: 'message', type: 'string', required: true, description: 'Message text to send' }, + { name: 'message', type: 'string', description: 'Message text to send' }, + { name: 'message-escapes', type: 'boolean', default: false, description: 'Interpret backslash escapes in --message' }, + { name: 'message-file', type: 'string', description: "Read message text from a file path; '-' reads stdin" }, { name: 'mention', type: 'string', multiple: true, description: 'User ID to mention' }, { name: 'no-preview', type: 'boolean', default: false, description: 'Disable automatic link preview' }, ], @@ -737,8 +861,12 @@ export const commands: CommandSpec[] = [ }, ] -export function commandHelp(command: CommandSpec): string { - const lines = [`beeper ${command.path.join(' ')}`, '', command.description] +export function commandHelp(command: CommandSpec, globalFlags?: GlobalFlags): string { + if (globalFlags && !commandVisible(command, globalFlags)) return help(globalFlags) + const path = command.path.join(' ') + const usageAliases = (command.aliases ?? []).map(alias => alias.join(' ')).join(', ') + const lines = [`Usage: beeper ${path}${command.args?.length ? ` ${command.args.map(arg => arg.variadic ? `<${arg.name}> ...` : arg.required ? `<${arg.name}>` : `[${arg.name}]`).join(' ')}` : ''} [flags]`, '', command.description] + if (usageAliases) lines.push('', `Aliases: ${usageAliases}`) const args = command.args ?? [] const flags = command.flags ?? [] if (args.length) { @@ -747,25 +875,48 @@ export function commandHelp(command: CommandSpec): string { } if (flags.length) { lines.push('', 'Flags:') - for (const flag of flags) lines.push(` --${flag.name}${flag.type === 'boolean' ? '' : ' '}\t${flag.description ?? ''}`) + for (const flag of flags) lines.push(` ${formatFlag(flag)}\t${flag.description ?? ''}`) } if (command.examples?.length) { lines.push('', 'Examples:', ...command.examples.map(example => ` ${example}`)) } + lines.push('', 'Global flags:') + for (const flag of globalFlagSpecs) lines.push(` ${formatFlag(flag)}\t${flag.description ?? ''}`) return `${lines.join('\n')}\n` } -export function help(): string { - const width = Math.max(...commands.map(command => command.path.join(' ').length)) + 2 - const lines = ['Usage: beeper [flags]', '', 'Commands:'] - for (const command of [...commands].sort((a, b) => a.path.join(' ').localeCompare(b.path.join(' ')))) { - lines.push(` ${command.path.join(' ').padEnd(width)}${command.description}`) +export function help(globalFlags?: GlobalFlags): string { + const visible = commands.filter(command => globalFlags ? commandVisible(command, globalFlags) : !command.hidden) + const width = Math.max(...visible.map(command => command.path.join(' ').length)) + 2 + const lines = [ + 'Usage: beeper [flags]', + '', + 'Beeper CLI for Beeper Desktop and Beeper Server. Built for terminals, scripts, CI, and agents.', + '', + 'Config:', + '', + ` file: ${configPath()}`, + '', + 'Commands:', + ] + for (const command of [...visible].sort((a, b) => a.path.join(' ').localeCompare(b.path.join(' ')))) { + const aliases = command.aliases?.length ? ` (${command.aliases.map(alias => alias.join(' ')).join(', ')})` : '' + lines.push(` ${command.path.join(' ').padEnd(width)}${command.description}${aliases}`) } lines.push('', 'Global flags:') - for (const flag of globalFlagSpecs) lines.push(` --${flag.name}${flag.type === 'boolean' ? '' : ' '}\t${flag.description ?? ''}`) + for (const flag of globalFlagSpecs) lines.push(` ${formatFlag(flag)}\t${flag.description ?? ''}`) + lines.push('', 'Run "beeper --help" for more information on a command.') return `${lines.join('\n')}\n` } +function formatFlag(flag: FlagSpec): string { + const long = `--${flag.name}${flag.type === 'boolean' ? '' : `=${flag.placeholder ?? 'STRING'}`}` + const prefix = flag.short ? `-${flag.short}, ${long}` : ` ${long}` + const aliases = flag.aliases?.length ? ` (${flag.aliases.map(alias => `--${alias}`).join(', ')})` : '' + const env = flag.env?.length ? ` (${flag.env.map(name => `$${name}`).join(',')})` : '' + return `${prefix}${aliases}${env}` +} + async function version(): Promise> { const pkg = await packageInfo() return { name: pkg.name, version: pkg.version } @@ -787,14 +938,124 @@ async function status(ctx: CommandContext): Promise> { } } +async function doctor(ctx: CommandContext): Promise> { + const config = await readConfig() + const targets = await listTargets() + const target = await resolveTarget({ target: ctx.globalFlags.target }).catch(() => undefined) + const live = target ? await targetLiveStatus(target) : undefined + const readiness = target && live && (live as Record).reachable + ? await evaluateReadiness({ baseURL: target.baseURL, target: target.id }).catch(error => ({ state: 'unknown', message: error instanceof Error ? error.message : String(error) })) + : undefined + return { + config_file: process.env.BEEPER_CLI_CONFIG_DIR ? `${process.env.BEEPER_CLI_CONFIG_DIR}/config.json` : undefined, + default_target: config.defaultTarget ?? builtInDesktopTargetID, + default_account: config.defaultAccount, + targets: targets.length || 1, + selected_target: target?.id, + target_type: target?.type, + reachable: isRecord(live) ? live.reachable : false, + authenticated: Boolean(process.env.BEEPER_ACCESS_TOKEN || target?.auth?.accessToken), + readiness: isRecord(readiness) ? readiness.state : undefined, + next: isRecord(readiness) ? readiness.message : undefined, + } +} + +async function exitCodes(): Promise> { + return { + exit_codes: { + ok: 0, + error: ExitCodes.Generic, + usage: ExitCodes.Usage, + empty_results: ExitCodes.EmptyResults, + auth_required: ExitCodes.AuthRequired, + not_ready: ExitCodes.NotReady, + not_found: ExitCodes.NotFound, + ambiguous: ExitCodes.Ambiguous, + cancelled: 130, + command_not_found: ExitCodes.CommandNotFound, + }, + } +} + async function schema(ctx: CommandContext): Promise> { const pkg = await packageInfo() - return buildSchema(commands, String(pkg.version ?? '0'), ctx.args) + return buildSchema(commands, String(pkg.version ?? '0'), ctx.args, ctx.globalFlags) } async function mcp(ctx: CommandContext): Promise { const pkg = await packageInfo() - await serveMcp(commands, ctx.globalFlags, Boolean(ctx.flags['allow-write']), String(pkg.version ?? '0')) + await serveMcp(commands, ctx.globalFlags, { + allowTools: stringListFlag(ctx.flags, 'allow-tool'), + allowWrite: Boolean(ctx.flags['allow-write']), + listTools: Boolean(ctx.flags['list-tools']), + maxOutputBytes: numberFlag(ctx.flags, 'max-output-bytes', 102400), + timeoutSeconds: numberFlag(ctx.flags, 'timeout-seconds', 60), + }, String(pkg.version ?? '0')) +} + +async function completion(ctx: CommandContext): Promise { + const shell = ctx.args[0] + if (!shell) throw usage('completion requires shell') + process.stdout.write(completionScript(shell)) +} + +async function configGet(ctx: CommandContext): Promise> { + const key = parseConfigKey(ctx.args[0]) + const config = await readConfig() + return { key, value: config[key] ?? null } +} + +async function configKeysCommand(): Promise { + return [...configKeys] +} + +async function configList(): Promise> { + const config = await readConfig() + return { + path: configPath(), + defaultTarget: config.defaultTarget ?? null, + defaultAccount: config.defaultAccount ?? null, + } +} + +async function configPathCommand(): Promise> { + return { path: configPath() } +} + +async function configSet(ctx: CommandContext): Promise> { + const key = parseConfigKey(ctx.args[0]) + const value = ctx.args[1] + if (!value) throw usage('config set requires value') + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'config.set', request: { key, value } } + const config = await updateConfig(current => ({ ...current, [key]: value })) + return { key, saved: true, value: config[key] ?? null } +} + +async function configUnset(ctx: CommandContext): Promise> { + const key = parseConfigKey(ctx.args[0]) + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'config.unset', request: { key } } + const config = await updateConfig(current => unsetConfigKey(current, key)) + return { key, removed: true, value: config[key] ?? null } +} + +function parseConfigKey(value: string | undefined): ConfigKey { + if (!value) throw usage(`config key is required. Available keys: ${configKeys.join(', ')}`) + const normalized = value.replaceAll('-', '').replaceAll('_', '').toLowerCase() + const key = configKeys.find(item => item.toLowerCase() === normalized) + if (!key) throw usage(`unknown config key "${value}". Available keys: ${configKeys.join(', ')}`) + return key +} + +function unsetConfigKey(config: Config, key: ConfigKey): Config { + const next = { ...config } + delete next[key] + return next +} + +async function completeCommand(ctx: CommandContext): Promise { + const cword = numberFlag(ctx.flags, 'cword', -1) + const words = ctx.args.length ? ctx.args : ['beeper'] + for (const item of completeWords(words, cword)) process.stdout.write(`${item}\n`) } async function targetsList(): Promise { @@ -1520,6 +1781,9 @@ async function messagesSearch(ctx: CommandContext): Promise { query: ctx.args[0], sender: stringFlag(ctx.flags, 'sender') as 'me' | 'others' | (string & {}) | undefined, }), numberFlag(ctx.flags, 'limit', 50)) + if (!items.length && ctx.flags['fail-empty']) { + throw new AbortError('No messages matched the query or filters.', ExitCodes.EmptyResults, undefined, 'empty_results') + } return ctx.flags.ids ? ids(items.map(apiRecord), 'messageID') : items } @@ -1537,7 +1801,7 @@ async function sendTextLike(ctx: CommandContext): Promise { const kind = ctx.commandPath[1] if (kind !== 'file' && kind !== 'sticker' && kind !== 'text' && kind !== 'voice') throw usage(`Unsupported send command: ${ctx.commandPath.join(' ')}`) const to = stringFlag(ctx.flags, 'to')! - const payload = sendPayload(ctx, kind) + const payload = await sendPayload(ctx, kind) if (ctx.globalFlags.dryRun) return { dry_run: true, op: `send.${kind}`, request: { chat: to, ...payload } } const client = await apiClient(ctx) @@ -1700,13 +1964,14 @@ function jsonBody(ctx: CommandContext): Record { } } -function sendPayload(ctx: CommandContext, kind: SendKind): SendPayload { +async function sendPayload(ctx: CommandContext, kind: SendKind): Promise { if (kind === 'text') { + const message = await messageText(ctx) return { mentions: stringListFlag(ctx.flags, 'mention'), noPreview: Boolean(ctx.flags['no-preview']), replyTo: stringFlag(ctx.flags, 'reply-to'), - text: stringFlag(ctx.flags, 'message')!, + text: message, wait: Boolean(ctx.flags.wait), waitTimeoutMs: numberFlag(ctx.flags, 'wait-timeout', 30_000), } @@ -1726,6 +1991,30 @@ function sendPayload(ctx: CommandContext, kind: SendKind): SendPayload { } } +async function messageText(ctx: CommandContext): Promise { + const literal = stringFlag(ctx.flags, 'message') + const file = stringFlag(ctx.flags, 'message-file') + if (literal && file) throw usage('--message and --message-file cannot be combined') + if (file) return file === '-' ? await readStdin() : readFile(file, 'utf8') + if (literal !== undefined) return ctx.flags['message-escapes'] ? decodeEscapes(literal) : literal + throw usage('send text requires --message or --message-file') +} + +async function readStdin(): Promise { + const chunks: Buffer[] = [] + for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))) + return Buffer.concat(chunks).toString('utf8') +} + +function decodeEscapes(value: string): string { + return value.replaceAll(/\\([nrt\\"])/g, (_, escaped: string) => { + if (escaped === 'n') return '\n' + if (escaped === 'r') return '\r' + if (escaped === 't') return '\t' + return escaped + }) +} + async function sendPresence(ctx: CommandContext): Promise { const state = (stringFlag(ctx.flags, 'state') ?? 'typing') as 'typing' | 'paused' const duration = ctx.flags.duration === undefined ? undefined : numberFlag(ctx.flags, 'duration', 0) @@ -1987,6 +2276,128 @@ function matchesSender(item: unknown, sender: string): boolean { return row.senderID === sender } +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value) +} + +function completionScript(shell: string): string { + const command = 'beeper' + if (shell === 'bash') { + return [ + '_beeper_complete() {', + ' local completions', + ' completions=$(beeper __complete --cword "$COMP_CWORD" -- "${COMP_WORDS[@]}")', + ' COMPREPLY=( $completions )', + '}', + `complete -F _beeper_complete ${command}`, + '', + ].join('\n') + } + if (shell === 'zsh') { + return [ + '#compdef beeper', + '_beeper() {', + ' local -a completions', + ' completions=("${(@f)$(beeper __complete --cword "$((CURRENT - 1))" -- "${words[@]}")}")', + ' _describe "values" completions', + '}', + '_beeper "$@"', + '', + ].join('\n') + } + if (shell === 'fish') { + return `complete -c ${command} -f -a '(beeper __complete --cword (commandline -t | wc -w) -- (commandline -opc))'\n` + } + if (shell === 'powershell' || shell === 'pwsh') { + return [ + `Register-ArgumentCompleter -Native -CommandName ${command} -ScriptBlock {`, + ' param($wordToComplete, $commandAst, $cursorPosition)', + ' $words = $commandAst.ToString().Split(" ", [System.StringSplitOptions]::RemoveEmptyEntries)', + ' $cword = [Math]::Max(0, $words.Length - 1)', + ' beeper __complete --cword $cword -- $words | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_) }', + '}', + '', + ].join('\n') + } + throw usage('completion shell must be one of: bash, zsh, fish, powershell') +} + +function completeWords(words: string[], cword: number): string[] { + const index = normalizeCword(cword, words.length) + const start = isProgramName(words[0]) ? 1 : 0 + if (index < start) return [] + const current = index < words.length ? words[index] ?? '' : '' + const consumed = words.slice(start, Math.min(index, words.length)) + if (consumed.includes('--')) return [] + const node = completionNode(consumed) + if (!node || previousFlagNeedsValue(node.flags, words, index)) return [] + const flags = node.flags + const children = node.children + const suggestions = current.startsWith('-') + ? matching([...flags], current) + : matching([...children, ...flags], current) + return [...new Set(suggestions)].sort() +} + +function completionNode(consumed: string[]): { children: string[]; command?: CommandSpec; flags: string[] } | undefined { + let candidates = commands.filter(command => !command.hidden) + let depth = 0 + for (const word of consumed) { + if (word.startsWith('-')) continue + const next = candidates.filter(command => commandPathVariants(command).some(path => path[depth] === word)) + if (!next.length) break + candidates = next + depth += 1 + } + const exact = candidates.find(command => commandPathVariants(command).some(path => path.length === depth)) + const children = new Set() + for (const command of candidates) { + for (const path of commandPathVariants(command)) { + const part = path[depth] + if (part) children.add(part) + } + } + return { + children: [...children], + command: exact, + flags: flagTokens([...(exact?.flags ?? []), ...globalFlagSpecs]), + } +} + +function commandPathVariants(command: CommandSpec): string[][] { + return [command.path, ...(command.aliases ?? [])] +} + +function flagTokens(flags: FlagSpec[]): string[] { + return flags.flatMap(flag => [ + `--${flag.name}`, + flag.short ? `-${flag.short}` : undefined, + ...(flag.aliases ?? []).map(alias => `--${alias}`), + flag.type === 'boolean' ? `--no-${flag.name}` : undefined, + ]).filter((value): value is string => Boolean(value)) +} + +function previousFlagNeedsValue(flags: string[], words: string[], cword: number): boolean { + const previous = words[cword - 1] + if (!previous?.startsWith('-') || previous.includes('=')) return false + const spec = [...globalFlagSpecs, ...commands.flatMap(command => command.flags ?? [])] + .find(flag => [`--${flag.name}`, flag.short ? `-${flag.short}` : undefined, ...(flag.aliases ?? []).map(alias => `--${alias}`)].includes(previous)) + return Boolean(spec && spec.type !== 'boolean' && flags.includes(previous)) +} + +function matching(values: string[], prefix: string): string[] { + return values.filter(value => value.startsWith(prefix)) +} + +function normalizeCword(cword: number, count: number): number { + if (cword < 0) return Math.max(0, count - 1) + return Math.min(cword, count) +} + +function isProgramName(word?: string): boolean { + return !word || word === 'beeper' || word.endsWith('/beeper') || word.endsWith('/dev.js') || word.endsWith('/cli.js') +} + async function packageInfo(): Promise> { const root = dirname(dirname(dirname(fileURLToPath(import.meta.url)))) return JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) as Record diff --git a/packages/cli/src/cli/main.ts b/packages/cli/src/cli/main.ts index 72983aa9..c2edc1a2 100644 --- a/packages/cli/src/cli/main.ts +++ b/packages/cli/src/cli/main.ts @@ -1,33 +1,84 @@ -import { ExitCodes } from '../lib/errors.js' +import { AbortError, ExitCodes } from '../lib/errors.js' import { commands, commandHelp, help } from './commands.js' import { enforcePolicy } from './policy.js' import { parseCommand } from './parse.js' import { usage, writeError, writeResult } from './output.js' export async function runCli(argv = process.argv.slice(2)): Promise { - let parsed + let parsed: ReturnType | undefined try { parsed = parseCommand(argv, commands) if (parsed.globalFlags.json && parsed.globalFlags.plain) throw usage('cannot combine --json and --plain') + applyGlobalEnvironment(parsed.globalFlags) if (parsed.helpOnly) { - process.stdout.write(help()) + process.stdout.write(help(parsed.globalFlags)) return } if (!parsed.command) throw new Error('missing command') if (parsed.flags.help) { - process.stdout.write(commandHelp(parsed.command)) + process.stdout.write(commandHelp(parsed.command, parsed.globalFlags)) return } enforcePolicy(parsed.command, parsed.globalFlags) - const result = await parsed.command.run({ - args: parsed.positionals, - commandPath: parsed.command.path, - flags: parsed.flags, - globalFlags: parsed.globalFlags, - }) - writeResult(result, parsed.globalFlags) + const flags = commandFlags(parsed.command, parsed.flags, parsed.globalFlags) + const { command, globalFlags, positionals } = parsed + const result = await runWithTimeout(() => command.run({ + args: positionals, + commandPath: command.path, + flags, + globalFlags, + }), globalFlags.timeout) + writeResult(result, globalFlags, command) } catch (error) { const flags = parsed?.globalFlags ?? { events: argv.includes('--events'), json: argv.includes('--json') } process.exitCode = writeError(error, flags) || ExitCodes.Generic } } + +async function runWithTimeout(run: () => Promise, timeout?: string): Promise { + const ms = parseDuration(timeout) + const promise = run() + if (!ms) return promise + let timer: NodeJS.Timeout | undefined + try { + return await Promise.race([ + promise, + new Promise((_, reject) => { + timer = setTimeout(() => reject(new AbortError(`command timed out after ${timeout}`, ExitCodes.Generic, undefined, 'timeout')), ms) + }), + ]) + } finally { + if (timer) clearTimeout(timer) + } +} + +function parseDuration(value: string | undefined): number | undefined { + if (!value) return undefined + const match = /^(\d+)(ms|s|m|h)?$/.exec(value.trim()) + if (!match) throw usage('--timeout must be a duration like 500ms, 30s, 2m, or 1h') + const amount = Number(match[1]) + if (!Number.isSafeInteger(amount) || amount <= 0) throw usage('--timeout must be greater than 0') + const unit = match[2] ?? 'ms' + const factor = unit === 'h' ? 3_600_000 : unit === 'm' ? 60_000 : unit === 's' ? 1_000 : 1 + return amount * factor +} + +function applyGlobalEnvironment(flags: { accessToken?: string; home?: string }): void { + if (flags.home) process.env.BEEPER_CLI_CONFIG_DIR = flags.home + if (flags.accessToken) process.env.BEEPER_ACCESS_TOKEN = flags.accessToken +} + +function commandFlags( + command: { flags?: Array<{ multiple?: boolean; name: string }> }, + flags: Record, + globalFlags: { account?: string[] }, +): Record { + const accountSpec = command.flags?.find(flag => flag.name === 'account') + if (!accountSpec || !globalFlags.account?.length) return flags + const current = flags.account + if (accountSpec.multiple) { + const local = Array.isArray(current) ? current.map(String) : typeof current === 'string' ? [current] : [] + return { ...flags, account: [...globalFlags.account, ...local] } + } + return current === undefined ? { ...flags, account: globalFlags.account[0] } : flags +} diff --git a/packages/cli/src/cli/mcp.ts b/packages/cli/src/cli/mcp.ts index db5ecd8f..23736fbb 100644 --- a/packages/cli/src/cli/mcp.ts +++ b/packages/cli/src/cli/mcp.ts @@ -9,7 +9,20 @@ type JsonRpcRequest = { params?: Record } -export async function serveMcp(commands: CommandSpec[], flags: GlobalFlags, allowWrite: boolean, version: string): Promise { +type McpOptions = { + allowTools: string[] + allowWrite: boolean + listTools: boolean + maxOutputBytes: number + timeoutSeconds: number +} + +export async function serveMcp(commands: CommandSpec[], flags: GlobalFlags, options: McpOptions, version: string): Promise { + const tools = mcpCommands(commands, options) + if (options.listTools) { + process.stdout.write(`${JSON.stringify(mcpTools(tools), null, 2)}\n`) + return + } const buffer: string[] = [] process.stdin.setEncoding('utf8') for await (const chunk of process.stdin) { @@ -19,14 +32,14 @@ export async function serveMcp(commands: CommandSpec[], flags: GlobalFlags, allo while (index !== -1) { const line = joined.slice(0, index).trim() joined = joined.slice(index + 1) - if (line) await handleLine(commands, flags, allowWrite, version, line) + if (line) await handleLine(tools, flags, options, version, line) index = joined.indexOf('\n') } buffer.length = 0 if (joined) buffer.push(joined) } const finalLine = buffer.join('').trim() - if (finalLine) await handleLine(commands, flags, allowWrite, version, finalLine) + if (finalLine) await handleLine(tools, flags, options, version, finalLine) } function mcpTools(commands: CommandSpec[]): Record[] { @@ -50,7 +63,7 @@ function mcpTools(commands: CommandSpec[]): Record[] { })) } -async function handleLine(commands: CommandSpec[], flags: GlobalFlags, allowWrite: boolean, version: string, line: string): Promise { +async function handleLine(commands: CommandSpec[], flags: GlobalFlags, options: McpOptions, version: string, line: string): Promise { let request: JsonRpcRequest = {} try { request = JSON.parse(line) as JsonRpcRequest @@ -66,7 +79,7 @@ async function handleLine(commands: CommandSpec[], flags: GlobalFlags, allowWrit const name = String(request.params?.name ?? '') const tool = commands.find(command => command.mcp && command.path.join('_') === name) if (!tool) throw new Error(`unknown MCP tool: ${name}`) - if (tool.risk !== 'read' && !allowWrite) throw new Error(`MCP tool "${name}" requires mcp --allow-write`) + if (tool.risk !== 'read' && !options.allowWrite) throw new Error(`MCP tool "${name}" requires mcp --allow-write`) const args = request.params?.arguments && typeof request.params.arguments === 'object' ? request.params.arguments as Record : {} @@ -75,8 +88,16 @@ async function handleLine(commands: CommandSpec[], flags: GlobalFlags, allowWrit const toolFlags = flagsFor(tool, args) validateCommandInput(tool, toolFlags, positionals) enforcePolicy(tool, globalFlags) - const result = await tool.run({ args: positionals, commandPath: tool.path, flags: toolFlags, globalFlags }) - respond(request.id, { content: [{ text: JSON.stringify(wrapUntrusted(result)), type: 'text' }] }) + const result = await withTimeout(options.timeoutSeconds, () => tool.run({ args: positionals, commandPath: tool.path, flags: toolFlags, globalFlags })) + const structured = { + exit_code: 0, + risk: tool.risk, + service: tool.path[0], + stdout: wrapUntrusted(result), + stderr: '', + tool: tool.path.join('_'), + } + respond(request.id, { content: [{ text: truncate(JSON.stringify(structured), options.maxOutputBytes), type: 'text' }], structuredContent: structured }) return } if (request.id !== undefined) respond(request.id, {}) @@ -85,6 +106,41 @@ async function handleLine(commands: CommandSpec[], flags: GlobalFlags, allowWrit } } +function mcpCommands(commands: CommandSpec[], options: McpOptions): CommandSpec[] { + return commands + .filter(command => command.mcp) + .filter(command => options.allowWrite || command.risk === 'read') + .filter(command => !options.allowTools.length || options.allowTools.some(pattern => toolMatches(command, pattern))) +} + +function toolMatches(command: CommandSpec, pattern: string): boolean { + const normalized = pattern.trim().toLowerCase().replaceAll(/\s+/g, '.').replaceAll('_', '.') + if (!normalized || normalized === '*' || normalized === 'all') return true + const dotted = command.path.join('.') + const underscored = command.path.join('_') + return normalized === command.risk || normalized === command.path[0] || normalized === dotted || normalized === underscored || (normalized.endsWith('.*') && dotted.startsWith(normalized.slice(0, -2) + '.')) +} + +async function withTimeout(seconds: number, run: () => Promise): Promise { + if (seconds <= 0) throw new Error('--timeout-seconds must be greater than zero') + let timeout: NodeJS.Timeout | undefined + try { + return await Promise.race([ + run(), + new Promise((_, reject) => { + timeout = setTimeout(() => reject(new Error(`MCP tool timed out after ${seconds}s`)), seconds * 1000) + }), + ]) + } finally { + if (timeout) clearTimeout(timeout) + } +} + +function truncate(value: string, maxBytes: number): string { + if (maxBytes <= 0) return value + return Buffer.byteLength(value) <= maxBytes ? value : `${value.slice(0, maxBytes)}...` +} + function inputSchemaForFlag(flag: FlagSpec): Record { const schema = { description: flag.description, diff --git a/packages/cli/src/cli/output.ts b/packages/cli/src/cli/output.ts index e8721c8e..7b6db756 100644 --- a/packages/cli/src/cli/output.ts +++ b/packages/cli/src/cli/output.ts @@ -1,5 +1,5 @@ import { AbortError, CLIError, ExitCodes } from '../lib/errors.js' -import type { GlobalFlags } from './types.js' +import type { CommandSpec, GlobalFlags } from './types.js' type ErrorShape = { code: string @@ -9,10 +9,14 @@ type ErrorShape = { message: string } -export function writeResult(value: unknown, flags: GlobalFlags): void { +export function writeResult(value: unknown, flags: GlobalFlags, command?: CommandSpec): void { if (value === undefined) return - if (flags.json) process.stdout.write(`${JSON.stringify(flags.wrapUntrusted ? wrapUntrusted(value) : value, null, 2)}\n`) - else writeText(value, flags.plain) + if (flags.json) { + const selected = flags.select ? selectFields(value, flags.select) : value + const result = flags.resultsOnly ? primaryResult(selected) : selected + const data = flags.wrapUntrusted ? wrapUntrusted(result) : result + process.stdout.write(`${JSON.stringify(data, null, 2)}\n`) + } else writeText(value, flags.plain, command, flags.full) } export function writeEvent(event: string, data: Record = {}): void { @@ -81,6 +85,7 @@ function normalizeError(error: unknown): Error { function errorCode(code: number, isBug: boolean): string { if (isBug) return 'internal_error' + if (code === ExitCodes.EmptyResults) return 'empty_results' if (code === ExitCodes.AuthRequired) return 'auth_required' if (code === ExitCodes.CommandNotFound) return 'command_not_found' if (code === ExitCodes.NotFound) return 'not_found' @@ -89,10 +94,18 @@ function errorCode(code: number, isBug: boolean): string { return 'runtime_error' } -function writeText(value: unknown, plain = false): void { +function writeText(value: unknown, plain = false, command?: CommandSpec, full = false): void { if (value === undefined) return + if (command) { + const handled = writeCommandText(value, plain, command, full) + if (handled) return + } if (Array.isArray(value)) { - for (const item of value) writeText(item, plain) + if (value.every(isRecord)) { + writeTable(value, Object.keys(value[0] ?? {}).slice(0, 8), plain, full) + return + } + for (const item of value) writeText(item, plain, undefined, full) return } if (!value || typeof value !== 'object') { @@ -106,12 +119,147 @@ function writeText(value: unknown, plain = false): void { } } +function writeCommandText(value: unknown, plain: boolean, command: CommandSpec, full: boolean): boolean { + const kind = command.output + if (kind === 'targets' && Array.isArray(value)) { + writeTable(value.filter(isRecord), ['id', 'default', 'type', 'reachable', 'name', 'baseURL', 'version', 'error'], plain, full, { + baseURL: 'URL', + id: 'ID', + }) + return true + } + if ((kind === 'accounts' || kind === 'chats' || kind === 'contacts' || kind === 'messages') && Array.isArray(value)) { + const rows = value.filter(isRecord) + const keys = preferredColumns(kind, rows) + writeTable(rows, keys, plain, full) + return true + } + if (kind === 'status' && isRecord(value)) { + writeStatus(value, plain) + return true + } + if (kind === 'diagnostic' && isRecord(value)) { + writeDiagnostic(value, plain) + return true + } + return false +} + +function preferredColumns(kind: NonNullable, rows: Record[]): string[] { + if (kind === 'accounts') return firstPresent(rows, ['accountID', 'id', 'default', 'displayName', 'network', 'status']) + if (kind === 'chats') return firstPresent(rows, ['localChatID', 'chatID', 'title', 'accountID', 'type', 'unreadCount', 'isMuted', 'isArchived', 'isPinned']) + if (kind === 'contacts') return firstPresent(rows, ['userID', 'id', 'displayName', 'name', 'accountID', 'phoneNumber', 'email']) + if (kind === 'messages') return firstPresent(rows, ['timestamp', 'date', 'chatID', 'senderID', 'messageID', 'text', 'body']) + return Object.keys(rows[0] ?? {}).slice(0, 8) +} + +function firstPresent(rows: Record[], preferred: string[]): string[] { + const available = new Set(rows.flatMap(row => Object.keys(row))) + const selected = preferred.filter(key => available.has(key)) + return selected.length ? selected : Object.keys(rows[0] ?? {}).slice(0, 8) +} + +function writeStatus(value: Record, plain: boolean): void { + const target = isRecord(value.target) ? value.target : {} + const auth = isRecord(value.auth) ? value.auth : {} + const live = isRecord(value.live) ? value.live : {} + const readiness = isRecord(value.readiness) ? value.readiness : {} + writeDiagnostic({ + target: target.id, + name: target.name, + type: target.type, + url: target.baseURL, + reachable: live.reachable, + version: live.version, + authenticated: auth.authenticated, + auth_source: auth.source, + readiness: readiness.state, + next: readiness.message, + }, plain) +} + +function writeDiagnostic(value: Record, plain: boolean): void { + const rows = Object.entries(value) + .filter(([, item]) => item !== undefined) + .map(([key, item]) => ({ key: key.replaceAll('_', ' ').toUpperCase(), value: humanCell(item) })) + if (plain) { + for (const row of rows) process.stdout.write(`${row.key}\t${row.value.replaceAll('\n', '\\n').replaceAll('\t', '\\t')}\n`) + return + } + const width = Math.max(4, ...rows.map(row => row.key.length)) + 2 + for (const row of rows) process.stdout.write(`${row.key.padEnd(width)}${row.value}\n`) +} + +function writeTable(rows: Record[], columns: string[], plain: boolean, full: boolean, labels: Record = {}): void { + const cleanRows = rows.map(row => Object.fromEntries(columns.map(key => [key, tableCell(row[key], plain || full)]))) + const headings = columns.map(key => labels[key] ?? key.replaceAll(/([a-z])([A-Z])/g, '$1_$2').toUpperCase()) + if (plain) { + process.stdout.write(`${columns.join('\t')}\n`) + for (const row of cleanRows) process.stdout.write(`${columns.map(key => escapePlain(String(row[key] ?? ''))).join('\t')}\n`) + return + } + const widths = columns.map((key, index) => Math.max(headings[index]!.length, ...cleanRows.map(row => String(row[key] ?? '').length))) + process.stdout.write(`${headings.map((heading, index) => heading.padEnd(widths[index]!)).join(' ')}\n`) + for (const row of cleanRows) { + process.stdout.write(`${columns.map((key, index) => String(row[key] ?? '').padEnd(widths[index]!)).join(' ')}\n`) + } +} + +function tableCell(value: unknown, full: boolean): string { + const cell = humanCell(value).replaceAll(/\s+/g, ' ').trim() + return full || cell.length <= 80 ? cell : `${cell.slice(0, 77)}...` +} + +function escapePlain(value: string): string { + return value.replaceAll('\n', '\\n').replaceAll('\t', '\\t') +} + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value) +} + function humanCell(value: unknown): string { if (Array.isArray(value)) return value.map(humanCell).join(', ') if (value && typeof value === 'object') return JSON.stringify(value) return String(value ?? '') } +function primaryResult(value: unknown): unknown { + if (!isRecord(value)) return value + for (const key of ['data', 'items', 'messages', 'chats', 'accounts', 'contacts', 'target', 'result']) { + if (value[key] !== undefined) return value[key] + } + return value +} + +function selectFields(value: unknown, fields: string): unknown { + const paths = fields.split(',').map(field => field.trim()).filter(Boolean) + if (!paths.length) return value + if (Array.isArray(value)) return value.map(item => selectFields(item, fields)) + if (!isRecord(value)) return value + const out: Record = {} + for (const path of paths) { + const selected = getPath(value, path) + if (selected !== undefined) setPath(out, path, selected) + } + return out +} + +function getPath(value: unknown, path: string): unknown { + return path.split('.').reduce((current, part) => isRecord(current) ? current[part] : undefined, value) +} + +function setPath(out: Record, path: string, value: unknown): void { + const parts = path.split('.') + let current = out + for (const part of parts.slice(0, -1)) { + current = current[part] && typeof current[part] === 'object' && !Array.isArray(current[part]) + ? current[part] as Record + : current[part] = {} + } + current[parts.at(-1)!] = value +} + export function wrapUntrusted(value: unknown): unknown { return wrapValue(value, []) } diff --git a/packages/cli/src/cli/parse.ts b/packages/cli/src/cli/parse.ts index e98fb38b..65f44891 100644 --- a/packages/cli/src/cli/parse.ts +++ b/packages/cli/src/cli/parse.ts @@ -10,22 +10,49 @@ type ParsedCommand = { } export const globalFlagSpecs: FlagSpec[] = [ + { name: 'access-token', type: 'string', env: ['BEEPER_ACCESS_TOKEN'], description: 'Use provided access token directly' }, + { name: 'account', aliases: ['acct'], short: 'a', type: 'string', multiple: true, description: 'Account selector for account-aware commands' }, + { name: 'color', type: 'string', enum: ['auto', 'always', 'never'], default: 'auto', description: 'Color output: auto|always|never' }, { name: 'debug', type: 'boolean', default: false }, - { name: 'dry-run', type: 'boolean', default: false }, + { name: 'disable-commands', type: 'string', description: 'Comma-separated command prefixes to block' }, + { name: 'dry-run', aliases: ['dryrun', 'noop', 'preview'], short: 'n', type: 'boolean', default: false, description: 'Do not make changes; print intended actions' }, + { name: 'enable-commands', type: 'string', description: 'Comma-separated enabled command prefixes' }, + { name: 'enable-commands-exact', type: 'string', description: 'Comma-separated exact enabled commands' }, { name: 'events', type: 'boolean', default: false }, - { name: 'force', type: 'boolean', default: false }, - { name: 'json', type: 'boolean', default: false }, - { name: 'no-input', type: 'boolean', default: false }, - { name: 'plain', type: 'boolean', default: false }, + { name: 'force', aliases: ['assume-yes', 'yes'], short: 'y', type: 'boolean', default: false, description: 'Skip confirmations for destructive commands' }, + { name: 'full', type: 'boolean', default: false, description: 'Disable truncation in human table output' }, + { name: 'home', type: 'string', env: ['BEEPER_CLI_CONFIG_DIR'], description: 'Override Beeper CLI config/data root' }, + { name: 'json', aliases: ['machine'], short: 'j', type: 'boolean', default: false, description: 'Output JSON to stdout' }, + { name: 'no-input', aliases: ['non-interactive', 'noninteractive'], type: 'boolean', default: false, description: 'Never prompt; fail instead' }, + { name: 'plain', aliases: ['tsv'], short: 'p', type: 'boolean', default: false, description: 'Output stable TSV-like text' }, + { name: 'read-only', type: 'boolean', default: false, env: ['BEEPER_READONLY'], description: 'Reject commands that intentionally write' }, + { name: 'results-only', type: 'boolean', default: false, description: 'In JSON mode, emit only the primary result' }, { name: 'safety-profile', type: 'string', description: 'Safety profile name or YAML path' }, - { name: 'target', type: 'string' }, - { name: 'wrap-untrusted', type: 'boolean', default: false }, + { name: 'select', aliases: ['fields', 'project'], type: 'string', description: 'Select comma-separated JSON fields' }, + { name: 'target', type: 'string', description: 'Target name or URL' }, + { name: 'timeout', type: 'string', description: 'Command timeout, for example 30s or 2m' }, + { name: 'version', short: 'v', type: 'boolean', default: false, description: 'Print version and exit' }, + { name: 'wrap-untrusted', type: 'boolean', default: false, description: 'Wrap fetched text fields in untrusted-content markers' }, ] export function parseCommand(argv: string[], commands: CommandSpec[]): ParsedCommand { - const helpRequested = argv.includes('--help') + const helpRequested = argv.includes('--help') || argv.includes('-h') const global = parseGlobalFlags(argv) const tokens = parseArgv(argv, globalFlagSpecs, { allowUnknownFlags: true }).positionals + if (argv.includes('--version') || argv.includes('-v')) { + const command = commands.find(item => item.path.join(' ') === 'version') + if (command) return { command, flags: {}, globalFlags: global, positionals: [] } + } + if (argv[0] === '__complete') { + const command = commands.find(item => item.path.join(' ') === '__complete') + if (!command) throw usage('unknown command "__complete"') + return { + command, + flags: { cword: completeCword(argv) }, + globalFlags: global, + positionals: completeWordsFromArgv(argv), + } + } const pathTokens = tokens.filter(token => !token.startsWith('-')) if (pathTokens.length === 0) { return { flags: {}, globalFlags: global, helpOnly: true, positionals: [] } @@ -33,7 +60,7 @@ export function parseCommand(argv: string[], commands: CommandSpec[]): ParsedCom const command = findCommand(commands, pathTokens) if (!command) throw usage(`unknown command "${pathTokens.join(' ')}"`) - const pathLength = command.path.length + const pathLength = matchedPathLength(command, pathTokens) const commandArgs = tokens.slice(pathLength) if (helpRequested) return { command, flags: { help: true }, globalFlags: global, positionals: commandArgs } @@ -42,6 +69,29 @@ export function parseCommand(argv: string[], commands: CommandSpec[]): ParsedCom return { command, flags, globalFlags: global, positionals } } +function completeCword(argv: string[]): number { + const index = argv.indexOf('--cword') + if (index === -1) return -1 + const raw = argv[index + 1] + const parsed = raw && /^-?\d+$/.test(raw) ? Number(raw) : NaN + if (!Number.isSafeInteger(parsed)) throw usage('--cword must be an integer') + return parsed +} + +function completeWordsFromArgv(argv: string[]): string[] { + const separator = argv.indexOf('--') + if (separator !== -1) return argv.slice(separator + 1) + const out: string[] = [] + for (let index = 1; index < argv.length; index += 1) { + if (argv[index] === '--cword') { + index += 1 + continue + } + out.push(argv[index]!) + } + return out +} + export function stringFlag(flags: Record, name: string): string | undefined { const value = flags[name] return typeof value === 'string' ? value : undefined @@ -80,20 +130,42 @@ export function parseFlagValue(flag: FlagSpec, value: unknown): boolean | number function parseGlobalFlags(argv: string[]): GlobalFlags { const raw = parseArgv(argv, globalFlagSpecs, { allowUnknownFlags: true }).flags + const readOnlyFromEnv = envBool('BEEPER_READONLY') return { + accessToken: typeof raw['access-token'] === 'string' && raw['access-token'] ? raw['access-token'] : undefined, + account: Array.isArray(raw.account) ? raw.account.map(String).filter(Boolean) : typeof raw.account === 'string' && raw.account ? [raw.account] : undefined, debug: raw.debug === true, + color: raw.color === 'always' || raw.color === 'never' ? raw.color : 'auto', + disableCommands: typeof raw['disable-commands'] === 'string' && raw['disable-commands'] ? raw['disable-commands'] : undefined, dryRun: raw['dry-run'] === true, + enableCommands: typeof raw['enable-commands'] === 'string' && raw['enable-commands'] ? raw['enable-commands'] : undefined, + enableCommandsExact: typeof raw['enable-commands-exact'] === 'string' && raw['enable-commands-exact'] ? raw['enable-commands-exact'] : undefined, events: raw.events === true, force: raw.force === true, + full: raw.full === true, + home: typeof raw.home === 'string' && raw.home ? raw.home : undefined, json: raw.json === true, noInput: raw['no-input'] === true, plain: raw.plain === true, + readOnly: raw['read-only'] === true || (!hasNoFlag(argv, 'read-only') && readOnlyFromEnv), + resultsOnly: raw['results-only'] === true, safetyProfile: typeof raw['safety-profile'] === 'string' && raw['safety-profile'] ? raw['safety-profile'] : undefined, + select: typeof raw.select === 'string' && raw.select ? raw.select : undefined, target: typeof raw.target === 'string' && raw.target ? raw.target : undefined, + timeout: typeof raw.timeout === 'string' && raw.timeout ? raw.timeout : undefined, wrapUntrusted: raw['wrap-untrusted'] === true, } } +function envBool(name: string): boolean { + const value = process.env[name]?.trim().toLowerCase() + return value === '1' || value === 'true' || value === 'yes' || value === 'on' +} + +function hasNoFlag(argv: string[], name: string): boolean { + return argv.some(token => token === `--no-${name}` || token === `--${name}=false`) +} + function parseArgv( argv: string[], specs: FlagSpec[], @@ -140,8 +212,18 @@ function parseArgv( function findCommand(commands: CommandSpec[], tokens: string[]): CommandSpec | undefined { return commands - .filter(command => command.path.every((part, index) => tokens[index] === part)) - .sort((a, b) => b.path.length - a.path.length)[0] + .filter(command => commandPaths(command).some(path => path.every((part, index) => tokens[index] === part))) + .sort((a, b) => matchedPathLength(b, tokens) - matchedPathLength(a, tokens))[0] +} + +function matchedPathLength(command: CommandSpec, tokens: string[]): number { + return commandPaths(command) + .filter(path => path.every((part, index) => tokens[index] === part)) + .sort((a, b) => b.length - a.length)[0]?.length ?? command.path.length +} + +function commandPaths(command: CommandSpec): string[][] { + return [command.path, ...(command.aliases ?? [])] } function validatePositionals(command: CommandSpec, values: string[]): void { @@ -162,7 +244,11 @@ export function validateCommandInput(command: CommandSpec, flags: Record { const out = new Map() - for (const spec of specs) out.set(spec.name, spec) + for (const spec of specs) { + out.set(spec.name, spec) + if (spec.short) out.set(spec.short, spec) + for (const alias of spec.aliases ?? []) out.set(alias, spec) + } return out } diff --git a/packages/cli/src/cli/policy.ts b/packages/cli/src/cli/policy.ts index 998226b9..357ab3e0 100644 --- a/packages/cli/src/cli/policy.ts +++ b/packages/cli/src/cli/policy.ts @@ -6,12 +6,41 @@ import type { CommandSpec, GlobalFlags } from './types.js' import { usage } from './output.js' export function enforcePolicy(command: CommandSpec, flags: GlobalFlags): void { + enforceCommandFilters(command, flags) + if (flags.readOnly && command.risk !== 'read') { + throw usage(`read-only mode: command "${command.path.join(' ')}" would intentionally modify Beeper or local CLI state`) + } const profile = flags.safetyProfile ? loadSafetyProfile(flags.safetyProfile) : undefined if (profile && !matchesPrefix(profile.allow, command.path)) { throw usage(`command "${command.path.join(' ')}" is blocked by safety profile "${profile.name}"`) } } +export function commandVisible(command: CommandSpec, flags: GlobalFlags): boolean { + if (command.hidden) return false + if (flags.readOnly && command.risk !== 'read') return false + if (!commandAllowedByFilters(command, flags)) return false + const profile = flags.safetyProfile ? loadSafetyProfile(flags.safetyProfile) : undefined + return !profile || matchesPrefix(profile.allow, command.path) +} + +function enforceCommandFilters(command: CommandSpec, flags: GlobalFlags): void { + if (commandAllowedByFilters(command, flags)) return + const path = command.path + if (rulesFromCSV(flags.disableCommands).size && matchesPrefix(rulesFromCSV(flags.disableCommands), path)) throw usage(`command "${path.join(' ')}" is disabled (blocked by --disable-commands)`) + throw usage(`command "${path.join(' ')}" is not enabled (set --enable-commands or --enable-commands-exact to allow it)`) +} + +function commandAllowedByFilters(command: CommandSpec, flags: GlobalFlags): boolean { + const path = command.path + const allow = rulesFromCSV(flags.enableCommands) + const exactAllow = rulesFromCSV(flags.enableCommandsExact) + const deny = rulesFromCSV(flags.disableCommands) + if (deny.size && matchesPrefix(deny, path)) return false + if ((allow.size || exactAllow.size) && !matchesPrefix(allow, path) && !matchesExact(exactAllow, path)) return false + return true +} + function loadSafetyProfile(nameOrPath: string): { allow: Set; name: string } { const path = resolveProfilePath(nameOrPath) if (!path) throw usage(`unknown safety profile "${nameOrPath}"`) @@ -48,6 +77,19 @@ function matchesPrefix(rules: Set, path: string[]): boolean { return false } +function matchesExact(rules: Set, path: string[]): boolean { + return rules.has('*') || rules.has('all') || rules.has(path.join('.')) +} + +function rulesFromCSV(value?: string): Set { + const out = new Set() + for (const part of (value ?? '').split(',')) { + const rule = normalizeRule(part) + if (rule) out.add(rule) + } + return out +} + function normalizeRule(value: string): string { return value.trim().toLowerCase().replaceAll(/\s+/g, '.').replaceAll(/^\.+|\.+$/g, '') } diff --git a/packages/cli/src/cli/schema.ts b/packages/cli/src/cli/schema.ts index aba5c399..c497146d 100644 --- a/packages/cli/src/cli/schema.ts +++ b/packages/cli/src/cli/schema.ts @@ -1,5 +1,6 @@ -import type { ArgSpec, CommandSpec, FlagSpec } from './types.js' +import type { ArgSpec, CommandSpec, FlagSpec, GlobalFlags } from './types.js' import { globalFlagSpecs } from './parse.js' +import { commandVisible } from './policy.js' type SchemaDoc = { build: string @@ -8,9 +9,12 @@ type SchemaDoc = { } type SchemaNode = { + aliases?: string[][] flags?: SchemaFlag[] help: string + hidden?: boolean name: string + output?: string path: string positionals?: SchemaArg[] requirements?: string[] @@ -20,12 +24,16 @@ type SchemaNode = { } type SchemaFlag = { + aliases?: string[] default?: boolean | number | string + envs?: string[] enum?: string[] help?: string multiple?: boolean name: string + placeholder?: string required?: boolean + short?: string type: string } @@ -37,10 +45,11 @@ type SchemaArg = { variadic?: boolean } -export function buildSchema(commands: CommandSpec[], version: string, requested: string[] = []): SchemaDoc { +export function buildSchema(commands: CommandSpec[], version: string, requested: string[] = [], flags?: GlobalFlags): SchemaDoc { + const visible = commands.filter(command => flags ? commandVisible(command, flags) : !command.hidden) const filtered = requested.length - ? commands.filter(command => command.path.join('.').startsWith(requested.join('.'))) - : commands + ? visible.filter(command => command.path.join('.').startsWith(requested.join('.'))) + : visible return { build: version, command: nodeFor(filtered, requested, requested.length ? requested.at(-1) ?? 'beeper' : 'beeper'), @@ -60,9 +69,12 @@ function nodeFor(commands: CommandSpec[], prefix: string[], name: string): Schem .map(child => nodeFor(commands.filter(command => command.path[prefix.length] === child), [...prefix, child], child)) return { + aliases: exact?.aliases, flags: prefix.length === 0 ? schemaFlags(globalFlagSpecs) : schemaFlags(exact?.flags ?? []), help: exact?.description ?? 'Beeper CLI', + hidden: exact?.hidden || undefined, name, + output: exact?.output, path: prefix.join(' '), positionals: exact?.args?.map(schemaArg), requirements: exact ? requirements(exact) : undefined, @@ -74,12 +86,16 @@ function nodeFor(commands: CommandSpec[], prefix: string[], name: string): Schem function schemaFlags(flags: FlagSpec[]): SchemaFlag[] { return flags.map(flag => ({ + aliases: flag.aliases, default: flag.default, + envs: flag.env, enum: flag.enum, help: flag.description, multiple: flag.multiple, name: flag.name, + placeholder: flag.placeholder, required: flag.required, + short: flag.short, type: flag.type, })) } diff --git a/packages/cli/src/cli/types.ts b/packages/cli/src/cli/types.ts index e3bb43d8..9502485b 100644 --- a/packages/cli/src/cli/types.ts +++ b/packages/cli/src/cli/types.ts @@ -1,9 +1,13 @@ export type FlagSpec = { name: string + aliases?: string[] + short?: string default?: boolean | number | string description?: string + env?: string[] enum?: string[] multiple?: boolean + placeholder?: string required?: boolean type: 'boolean' | 'string' | 'integer' } @@ -26,24 +30,39 @@ export type CommandContext = { export type CommandSpec = { args?: ArgSpec[] + aliases?: string[][] description: string examples?: string[] flags?: FlagSpec[] + hidden?: boolean mcp?: boolean + output?: 'accounts' | 'chats' | 'contacts' | 'diagnostic' | 'generic' | 'messages' | 'status' | 'targets' path: string[] risk: CommandRisk run(ctx: CommandContext): Promise } export type GlobalFlags = { + accessToken?: string + account?: string[] + color: 'auto' | 'always' | 'never' debug: boolean + disableCommands?: string dryRun: boolean + enableCommands?: string + enableCommandsExact?: string events: boolean force: boolean + full: boolean + home?: string json: boolean noInput: boolean plain: boolean + readOnly: boolean + resultsOnly: boolean safetyProfile?: string + select?: string target?: string + timeout?: string wrapUntrusted: boolean } diff --git a/packages/cli/src/lib/errors.ts b/packages/cli/src/lib/errors.ts index a337279d..ccb94244 100644 --- a/packages/cli/src/lib/errors.ts +++ b/packages/cli/src/lib/errors.ts @@ -2,8 +2,8 @@ * Beeper CLI exit codes: * 1 generic runtime error * 2 usage error (parsing, missing required flag/arg, invalid combination) - * 3 auth required (no stored token; user must authenticate) - * 4 target/account not ready (target reachable but not signed-in or not verified) + * 3 empty results when --fail-empty/--non-empty is set + * 4 auth required (no stored token; user must authenticate) * 5 not found (selector matched nothing) * 6 ambiguous selector (multiple matches; use exact ID or --pick) * 127 user declined a selector suggestion (POSIX "command not found" semantics) @@ -11,7 +11,8 @@ export const ExitCodes = { Generic: 1, Usage: 2, - AuthRequired: 3, + EmptyResults: 3, + AuthRequired: 4, NotReady: 4, NotFound: 5, Ambiguous: 6, diff --git a/packages/cli/src/lib/targets.ts b/packages/cli/src/lib/targets.ts index 0c4fd1c8..814c4b8e 100644 --- a/packages/cli/src/lib/targets.ts +++ b/packages/cli/src/lib/targets.ts @@ -31,7 +31,7 @@ export type Target = { export type PublicTarget = Omit & { auth?: Pick } -type Config = { +export type Config = { defaultTarget?: string defaultAccount?: string } @@ -45,7 +45,7 @@ export function beeperDir(): string { return process.env.BEEPER_CLI_CONFIG_DIR ?? join(homedir(), '.beeper') } -const configPath = () => join(beeperDir(), 'config.json') +export const configPath = () => join(beeperDir(), 'config.json') const targetsDir = () => join(beeperDir(), 'targets') const profileDataDir = (type: ManagedTargetType, id: string) => join(beeperDir(), 'profiles', type, id) diff --git a/packages/cli/test/cli-smoke.ts b/packages/cli/test/cli-smoke.ts index 75b8d8c6..75afdf2f 100644 --- a/packages/cli/test/cli-smoke.ts +++ b/packages/cli/test/cli-smoke.ts @@ -7,6 +7,7 @@ import { fileURLToPath } from 'node:url' const root = fileURLToPath(new URL('..', import.meta.url)) const configDir = '/tmp/beeper-cli-smoke' rmSync(configDir, { recursive: true, force: true }) +rmSync('/tmp/beeper-cli-smoke-home', { recursive: true, force: true }) const run = (...args: string[]) => spawnSync('bun', ['./bin/dev.js', ...args], { cwd: root, @@ -23,6 +24,16 @@ const ok = (...args: string[]) => { return result.stdout } +const runEnv = (env: Record, ...args: string[]) => spawnSync('bun', ['./bin/dev.js', ...args], { + cwd: root, + encoding: 'utf8', + env: { + ...process.env, + BEEPER_CLI_CONFIG_DIR: configDir, + ...env, + }, +}) + assert.match(ok('--help'), /Usage: beeper /) assert.match(ok('--help'), /targets add/) assert.match(ok('--help'), /targets runtime start\s+Start a local target runtime/) @@ -63,6 +74,22 @@ assert.match(ok('--help'), /resolve target\s+Resolve a target selector/) assert.match(ok('--help'), /watch/) assert.match(ok('--help'), /media download/) assert.match(ok('--help'), /export\s+Export accounts/) +assert.match(ok('--help'), /doctor\s+Run diagnostics/) +assert.match(ok('--help'), /exit-codes\s+Print stable exit codes/) +assert.match(ok('--help'), /Config:\n\n file: /) +assert.match(ok('--help'), /config path\s+Print config file path/) +assert.match(ok('--help'), /config set\s+Set a config value/) +assert.match(ok('--help'), /--full\s+Disable truncation in human table output/) +assert.match(ok('--help'), /--read-only \(\$BEEPER_READONLY\)/) +assert.match(ok('--version', '--json'), /"name": "beeper-cli"/) +assert.match(ok('st'), /READINESS/) +assert.match(ok('doctor'), /SELECTED TARGET/) +assert.match(ok('targets', 'ls'), /ID\s+DEFAULT\s+TYPE/) +assert.equal(existsSync(join(root, 'docs', 'commands', 'README.md')), true) +assert.equal(existsSync(join(root, 'docs', 'commands', 'send-text.md')), true) +assert.match(ok('__complete', '--cword', '2', '--', 'beeper', 'targets', 'l'), /list/) +assert.match(ok('__complete', '--cword', '3', '--', 'beeper', 'send', 'text', '--m'), /--message-file/) +assert.match(ok('completion', 'bash'), /__complete/) assert.match(ok('setup', '--help'), /--remote/) assert.match(ok('targets', 'tunnel', '--help'), /--url-only/) assert.match(ok('accounts', 'add', '--help'), /--webview-backend/) @@ -99,14 +126,69 @@ errorPayload = JSON.parse(result.stderr) assert.equal(errorPayload.error.code, 'usage_error') assert.match(errorPayload.error.message, /--limit must be an integer/) +result = run('version', '--timeout', 'bogus', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.equal(errorPayload.error.code, 'usage_error') +assert.match(errorPayload.error.message, /--timeout must be a duration/) + let payload = JSON.parse(ok('targets', 'list', '--json')) assert.equal(payload[0].id, 'desktop') assert.equal(existsSync(join(configDir, 'config.json')), false) assert.equal(existsSync(join(configDir, 'targets')), false) +payload = JSON.parse(ok('--home', '/tmp/beeper-cli-smoke-home', 'targets', 'list', '--json')) +assert.equal(payload[0].id, 'desktop') +assert.equal(existsSync('/tmp/beeper-cli-smoke-home'), false) + +payload = JSON.parse(ok('config', 'path', '--json')) +assert.equal(payload.path, join(configDir, 'config.json')) + +payload = JSON.parse(ok('config', 'keys', '--json')) +assert.deepEqual(payload, ['defaultTarget', 'defaultAccount']) + +payload = JSON.parse(ok('config', 'set', 'default-target', 'desktop', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'config.set') +assert.equal(payload.request.key, 'defaultTarget') + +result = run('--read-only', 'config', 'set', 'default-target', 'desktop', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /read-only mode/) + +result = runEnv({ BEEPER_READONLY: '1' }, 'config', 'set', 'default-target', 'desktop', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /read-only mode/) + +payload = JSON.parse(runEnv({ BEEPER_READONLY: '1' }, '--no-read-only', 'config', 'set', 'default-target', 'desktop', '--dry-run', '--json').stdout) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'config.set') + payload = JSON.parse(ok('use', 'target', 'desktop', '--json')) assert.equal(payload.defaultTarget, 'desktop') +payload = JSON.parse(ok('config', 'get', 'defaultTarget', '--json')) +assert.equal(payload.key, 'defaultTarget') +assert.equal(payload.value, 'desktop') + +payload = JSON.parse(ok('config', 'list', '--json')) +assert.equal(payload.defaultTarget, 'desktop') +assert.equal(payload.defaultAccount, null) + +payload = JSON.parse(ok('config', 'set', 'default-account', 'matrix', '--json')) +assert.equal(payload.saved, true) +assert.equal(payload.key, 'defaultAccount') +assert.equal(payload.value, 'matrix') + +payload = JSON.parse(ok('config', 'show', 'default_account', '--json')) +assert.equal(payload.value, 'matrix') + +payload = JSON.parse(ok('config', 'rm', 'default-account', '--json')) +assert.equal(payload.removed, true) +assert.equal(payload.value, null) + result = run('--safety-profile', 'readonly', 'use', 'target', 'desktop', '--json') assert.equal(result.status, 2) errorPayload = JSON.parse(result.stderr) @@ -171,6 +253,12 @@ assert.equal(payload.target.type, 'remote') payload = JSON.parse(ok('use', 'target', 'work', '--json')) assert.equal(payload.defaultTarget, 'work') +payload = JSON.parse(ok('targets', 'use', 'desktop', '--json')) +assert.equal(payload.defaultTarget, 'desktop') + +payload = JSON.parse(ok('targets', 'use', 'work', '--json')) +assert.equal(payload.defaultTarget, 'work') + payload = JSON.parse(ok('status', '--json')) assert.equal(payload.auth.authenticated, false) assert.equal(payload.target.id, 'work') @@ -229,6 +317,11 @@ assert.equal(payload.dry_run, true) assert.equal(payload.op, 'remove.target') assert.equal(payload.request.id, 'work') +payload = JSON.parse(ok('targets', 'rm', 'work', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'remove.target') +assert.equal(payload.request.id, 'work') + payload = JSON.parse(ok('api', 'request', 'POST', '/v1/example', '--body', '{"ok":true}', '--dry-run', '--json')) assert.equal(payload.dry_run, true) assert.equal(payload.request.body.ok, true) @@ -247,6 +340,14 @@ assert.equal(payload.dry_run, true) assert.equal(payload.op, 'send.text') assert.equal(payload.request.mentions[0], 'user1') +payload = JSON.parse(ok('send', 'text', '--to', 'chat', '--message', 'hello\\nthere', '--message-escapes', '--dry-run', '--json')) +assert.equal(payload.request.text, 'hello\nthere') + +payload = JSON.parse(ok('-a', 'matrix', 'chats', 'start', '@u:example.org', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'chats.start') +assert.equal(payload.request.account, 'matrix') + payload = JSON.parse(ok('send', 'react', '--to', 'chat', '--id', 'm1', '--reaction', '+1', '--dry-run', '--json')) assert.equal(payload.dry_run, true) assert.equal(payload.op, 'send.react') @@ -334,6 +435,31 @@ assert.equal(payload[0].id, 'work') const schema = JSON.parse(ok('schema', '--json')) assert.equal(schema.schema_version, 1) assert.equal(schema.command.type, 'application') +assert.ok(schema.command.flags.some((flag: { name: string; short?: string }) => flag.name === 'json' && flag.short === 'j')) +assert.ok(schema.command.flags.some((flag: { name: string; short?: string }) => flag.name === 'account' && flag.short === 'a')) +assert.ok(schema.command.flags.some((flag: { name: string }) => flag.name === 'full')) +assert.ok(schema.command.subcommands.some((command: { name: string }) => command.name === 'doctor')) +assert.ok(!schema.command.subcommands.some((command: { name: string }) => command.name === '__complete')) + +let filteredHelp = ok('--read-only', '--help') +assert.match(filteredHelp, /targets list/) +assert.doesNotMatch(filteredHelp, /send text/) + +let filteredSchema = JSON.parse(ok('--read-only', 'schema', '--json')) +assert.equal(schemaPaths(filteredSchema).includes('send text'), false) +assert.equal(schemaPaths(filteredSchema).includes('targets list'), true) + +filteredHelp = ok('--enable-commands', 'messages', '--help') +assert.match(filteredHelp, /messages search/) +assert.doesNotMatch(filteredHelp, /targets list/) + +filteredSchema = JSON.parse(ok('--disable-commands', 'messages.search', 'schema', '--json')) +assert.equal(schemaPaths(filteredSchema).includes('messages search'), false) + +result = spawnSync('bun', ['scripts/generate-command-docs.ts'], { cwd: root, encoding: 'utf8' }) +assert.equal(result.status, 0, result.stderr) +result = spawnSync('git', ['diff', '--quiet', '--', 'packages/cli/docs/commands'], { cwd: join(root, '..', '..'), encoding: 'utf8' }) +assert.equal(result.status, 0, 'generated command docs are out of date') const mcp = spawnSync('bun', ['./bin/dev.js', 'mcp'], { cwd: root, @@ -349,11 +475,23 @@ payload = JSON.parse(mcp.stdout) assert.ok(payload.result.tools.some((tool: { name: string }) => tool.name === 'targets_list')) assert.ok(payload.result.tools.some((tool: { name: string }) => tool.name === 'messages_search')) assert.ok(payload.result.tools.some((tool: { name: string }) => tool.name === 'contacts_list')) -assert.ok(payload.result.tools.some((tool: { name: string }) => tool.name === 'api_request')) +assert.ok(!payload.result.tools.some((tool: { name: string }) => tool.name === 'api_request')) assert.ok(payload.result.tools.some((tool: { name: string }) => tool.name === 'resolve_target')) assert.ok(payload.result.tools.some((tool: { name: string }) => tool.name === 'resolve_chat')) assert.ok(payload.result.tools.some((tool: { name: string }) => tool.name === 'messages_context')) +const mcpAllowWrite = spawnSync('bun', ['./bin/dev.js', 'mcp', '--allow-write', '--list-tools'], { + cwd: root, + encoding: 'utf8', + env: { + ...process.env, + BEEPER_CLI_CONFIG_DIR: configDir, + }, +}) +assert.equal(mcpAllowWrite.status, 0, mcpAllowWrite.stderr) +payload = JSON.parse(mcpAllowWrite.stdout) +assert.ok(payload.some((tool: { name: string }) => tool.name === 'api_request')) + const mcpInitialize = spawnSync('bun', ['./bin/dev.js', 'mcp'], { cwd: root, encoding: 'utf8', @@ -380,8 +518,9 @@ const mcpCall = spawnSync('bun', ['./bin/dev.js', 'mcp'], { assert.equal(mcpCall.status, 0, mcpCall.stderr) payload = JSON.parse(mcpCall.stdout) const mcpVersion = JSON.parse(payload.result.content[0].text) -assert.match(mcpVersion.name, /beeper-cli/) -assert.equal(mcpVersion.version, version.version) +assert.equal(mcpVersion.exit_code, 0) +assert.match(mcpVersion.stdout.name, /beeper-cli/) +assert.equal(mcpVersion.stdout.version, version.version) const mcpEOFCall = spawnSync('bun', ['./bin/dev.js', 'mcp'], { cwd: root, @@ -395,7 +534,7 @@ const mcpEOFCall = spawnSync('bun', ['./bin/dev.js', 'mcp'], { assert.equal(mcpEOFCall.status, 0, mcpEOFCall.stderr) payload = JSON.parse(mcpEOFCall.stdout) assert.equal(payload.id, 4) -assert.equal(JSON.parse(payload.result.content[0].text).version, version.version) +assert.equal(JSON.parse(payload.result.content[0].text).stdout.version, version.version) const mcpDryRunCall = spawnSync('bun', ['./bin/dev.js', '--dry-run', 'mcp'], { cwd: root, @@ -409,9 +548,9 @@ const mcpDryRunCall = spawnSync('bun', ['./bin/dev.js', '--dry-run', 'mcp'], { assert.equal(mcpDryRunCall.status, 0, mcpDryRunCall.stderr) payload = JSON.parse(mcpDryRunCall.stdout) const mcpContext = JSON.parse(payload.result.content[0].text) -assert.equal(mcpContext.dry_run, true) -assert.equal(mcpContext.request.after, 3) -assert.equal(mcpContext.request.before, 4) +assert.equal(mcpContext.stdout.dry_run, true) +assert.equal(mcpContext.stdout.request.after, 3) +assert.equal(mcpContext.stdout.request.before, 4) const mcpInvalidJSON = spawnSync('bun', ['./bin/dev.js', 'mcp'], { cwd: root, @@ -429,3 +568,13 @@ assert.equal(payload.error.code, -32000) assert.match(payload.error.message, /JSON/) rmSync(configDir, { recursive: true, force: true }) + +function schemaPaths(value: unknown): string[] { + if (Array.isArray(value)) return value.flatMap(schemaPaths) + if (!value || typeof value !== 'object') return [] + const row = value as Record + return [ + typeof row.path === 'string' ? row.path : undefined, + ...Object.values(row).flatMap(schemaPaths), + ].filter((item): item is string => Boolean(item)) +} From 7a323a2437ae62ce96b3b4a8f9569f101cfffacc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Thu, 4 Jun 2026 16:09:59 +0200 Subject: [PATCH 26/26] Align CLI send reactions with wacli compatibility --- bun.lock | 186 ++ packages/cli/README.md | 37 +- packages/cli/docs/commands/README.md | 167 +- packages/cli/docs/commands/accounts-add.md | 68 +- packages/cli/docs/commands/accounts-list.md | 54 +- packages/cli/docs/commands/accounts-remove.md | 50 + packages/cli/docs/commands/accounts-show.md | 49 + packages/cli/docs/commands/accounts-use.md | 46 + packages/cli/docs/commands/agent.md | 35 + packages/cli/docs/commands/api-request.md | 50 +- .../cli/docs/commands/auth-email-response.md | 50 +- .../cli/docs/commands/auth-email-start.md | 46 +- packages/cli/docs/commands/auth-list.md | 44 + packages/cli/docs/commands/auth-logout.md | 59 +- packages/cli/docs/commands/auth-manage.md | 67 + packages/cli/docs/commands/auth-services.md | 50 + packages/cli/docs/commands/auth-status.md | 41 + packages/cli/docs/commands/chats-archive.md | 60 +- packages/cli/docs/commands/chats-avatar.md | 62 +- .../cli/docs/commands/chats-description.md | 62 +- packages/cli/docs/commands/chats-disappear.md | 62 +- packages/cli/docs/commands/chats-draft.md | 68 +- packages/cli/docs/commands/chats-focus.md | 69 +- packages/cli/docs/commands/chats-list.md | 57 +- packages/cli/docs/commands/chats-mark-read.md | 53 + .../cli/docs/commands/chats-mark-unread.md | 52 + packages/cli/docs/commands/chats-mute.md | 60 +- .../cli/docs/commands/chats-notify-anyway.md | 60 +- packages/cli/docs/commands/chats-pin.md | 60 +- packages/cli/docs/commands/chats-priority.md | 62 +- packages/cli/docs/commands/chats-read.md | 62 +- packages/cli/docs/commands/chats-remind.md | 62 +- packages/cli/docs/commands/chats-rename.md | 62 +- packages/cli/docs/commands/chats-show.md | 64 +- packages/cli/docs/commands/chats-start.md | 56 +- packages/cli/docs/commands/chats-unarchive.md | 52 + packages/cli/docs/commands/chats-unmute.md | 52 + packages/cli/docs/commands/chats-unpin.md | 52 + packages/cli/docs/commands/completion-bash.md | 41 + packages/cli/docs/commands/completion-fish.md | 41 + .../docs/commands/completion-powershell.md | 45 + packages/cli/docs/commands/completion-zsh.md | 41 + packages/cli/docs/commands/completion.md | 46 +- packages/cli/docs/commands/config-get.md | 46 +- packages/cli/docs/commands/config-keys.md | 51 +- packages/cli/docs/commands/config-list.md | 46 +- packages/cli/docs/commands/config-path.md | 51 +- packages/cli/docs/commands/config-set.md | 46 +- packages/cli/docs/commands/config-unset.md | 46 +- packages/cli/docs/commands/contacts-list.md | 63 +- packages/cli/docs/commands/contacts-show.md | 58 + packages/cli/docs/commands/docs.md | 45 + packages/cli/docs/commands/doctor.md | 56 +- packages/cli/docs/commands/exit-codes.md | 48 +- packages/cli/docs/commands/export.md | 58 +- packages/cli/docs/commands/groups-create.md | 52 + .../cli/docs/commands/groups-description.md | 56 + packages/cli/docs/commands/groups-list.md | 50 + packages/cli/docs/commands/groups-rename.md | 53 + packages/cli/docs/commands/groups-show.md | 55 + packages/cli/docs/commands/help.md | 41 + packages/cli/docs/commands/install-desktop.md | 48 +- packages/cli/docs/commands/install-server.md | 48 +- packages/cli/docs/commands/login.md | 46 + packages/cli/docs/commands/mcp.md | 68 +- packages/cli/docs/commands/me.md | 46 + packages/cli/docs/commands/media-download.md | 60 +- packages/cli/docs/commands/media-message.md | 51 + .../cli/docs/commands/messages-context.md | 62 +- packages/cli/docs/commands/messages-delete.md | 64 +- packages/cli/docs/commands/messages-edit.md | 65 +- packages/cli/docs/commands/messages-export.md | 52 + .../cli/docs/commands/messages-forward.md | 54 + packages/cli/docs/commands/messages-list.md | 62 +- packages/cli/docs/commands/messages-revoke.md | 49 + packages/cli/docs/commands/messages-search.md | 67 +- packages/cli/docs/commands/messages-show.md | 54 + packages/cli/docs/commands/presence-paused.md | 42 + packages/cli/docs/commands/presence-typing.md | 43 + packages/cli/docs/commands/presence.md | 44 + packages/cli/docs/commands/remove-account.md | 44 - packages/cli/docs/commands/remove-target.md | 44 - packages/cli/docs/commands/resolve-account.md | 48 +- packages/cli/docs/commands/resolve-bridge.md | 48 +- packages/cli/docs/commands/resolve-chat.md | 52 +- packages/cli/docs/commands/resolve-contact.md | 52 +- packages/cli/docs/commands/resolve-target.md | 48 +- packages/cli/docs/commands/schema.md | 56 +- packages/cli/docs/commands/search-all.md | 46 + packages/cli/docs/commands/send-file.md | 71 +- packages/cli/docs/commands/send-presence.md | 52 +- packages/cli/docs/commands/send-react.md | 67 +- packages/cli/docs/commands/send-sticker.md | 68 +- packages/cli/docs/commands/send-text.md | 76 +- packages/cli/docs/commands/send-voice.md | 70 +- packages/cli/docs/commands/send.md | 61 + packages/cli/docs/commands/setup.md | 54 +- packages/cli/docs/commands/status.md | 50 +- packages/cli/docs/commands/targets-add.md | 54 +- packages/cli/docs/commands/targets-list.md | 53 +- packages/cli/docs/commands/targets-logs.md | 56 +- packages/cli/docs/commands/targets-remove.md | 50 + .../docs/commands/targets-runtime-restart.md | 52 +- .../docs/commands/targets-runtime-start.md | 52 +- .../cli/docs/commands/targets-runtime-stop.md | 52 +- packages/cli/docs/commands/targets-tunnel.md | 58 +- packages/cli/docs/commands/targets-use.md | 46 + packages/cli/docs/commands/upload.md | 62 + packages/cli/docs/commands/use-account.md | 43 - packages/cli/docs/commands/use-target.md | 43 - packages/cli/docs/commands/version.md | 46 +- packages/cli/docs/commands/watch.md | 57 +- packages/cli/package.json | 4 +- packages/cli/scripts/generate-command-docs.ts | 242 +- packages/cli/src/cli/commands.ts | 2118 +++++++++++++++-- packages/cli/src/cli/main.ts | 33 +- packages/cli/src/cli/mcp.ts | 443 +++- packages/cli/src/cli/output.ts | 113 +- packages/cli/src/cli/parse.ts | 235 +- packages/cli/src/cli/policy.ts | 22 +- packages/cli/src/cli/schema.ts | 400 +++- packages/cli/src/cli/types.ts | 5 +- packages/cli/src/lib/desktop-auth.ts | 4 +- packages/cli/src/lib/errors.ts | 2 +- packages/cli/src/lib/targets.ts | 2 +- packages/cli/test/cli-smoke.ts | 1642 ++++++++++++- .../test/messages-search-validation.test.ts | 5 + packages/cli/test/output.test.ts | 126 + packages/cli/test/webhook-headers.test.ts | 20 + 129 files changed, 9301 insertions(+), 2354 deletions(-) create mode 100644 packages/cli/docs/commands/accounts-remove.md create mode 100644 packages/cli/docs/commands/accounts-show.md create mode 100644 packages/cli/docs/commands/accounts-use.md create mode 100644 packages/cli/docs/commands/agent.md create mode 100644 packages/cli/docs/commands/auth-list.md create mode 100644 packages/cli/docs/commands/auth-manage.md create mode 100644 packages/cli/docs/commands/auth-services.md create mode 100644 packages/cli/docs/commands/auth-status.md create mode 100644 packages/cli/docs/commands/chats-mark-read.md create mode 100644 packages/cli/docs/commands/chats-mark-unread.md create mode 100644 packages/cli/docs/commands/chats-unarchive.md create mode 100644 packages/cli/docs/commands/chats-unmute.md create mode 100644 packages/cli/docs/commands/chats-unpin.md create mode 100644 packages/cli/docs/commands/completion-bash.md create mode 100644 packages/cli/docs/commands/completion-fish.md create mode 100644 packages/cli/docs/commands/completion-powershell.md create mode 100644 packages/cli/docs/commands/completion-zsh.md create mode 100644 packages/cli/docs/commands/contacts-show.md create mode 100644 packages/cli/docs/commands/docs.md create mode 100644 packages/cli/docs/commands/groups-create.md create mode 100644 packages/cli/docs/commands/groups-description.md create mode 100644 packages/cli/docs/commands/groups-list.md create mode 100644 packages/cli/docs/commands/groups-rename.md create mode 100644 packages/cli/docs/commands/groups-show.md create mode 100644 packages/cli/docs/commands/help.md create mode 100644 packages/cli/docs/commands/login.md create mode 100644 packages/cli/docs/commands/me.md create mode 100644 packages/cli/docs/commands/media-message.md create mode 100644 packages/cli/docs/commands/messages-export.md create mode 100644 packages/cli/docs/commands/messages-forward.md create mode 100644 packages/cli/docs/commands/messages-revoke.md create mode 100644 packages/cli/docs/commands/messages-show.md create mode 100644 packages/cli/docs/commands/presence-paused.md create mode 100644 packages/cli/docs/commands/presence-typing.md create mode 100644 packages/cli/docs/commands/presence.md delete mode 100644 packages/cli/docs/commands/remove-account.md delete mode 100644 packages/cli/docs/commands/remove-target.md create mode 100644 packages/cli/docs/commands/search-all.md create mode 100644 packages/cli/docs/commands/send.md create mode 100644 packages/cli/docs/commands/targets-remove.md create mode 100644 packages/cli/docs/commands/targets-use.md create mode 100644 packages/cli/docs/commands/upload.md delete mode 100644 packages/cli/docs/commands/use-account.md delete mode 100644 packages/cli/docs/commands/use-target.md create mode 100644 packages/cli/test/output.test.ts create mode 100644 packages/cli/test/webhook-headers.test.ts diff --git a/bun.lock b/bun.lock index 9dc2bba0..d7218c52 100644 --- a/bun.lock +++ b/bun.lock @@ -13,9 +13,11 @@ }, "dependencies": { "@beeper/desktop-api": "github:beeper/desktop-api-js#next", + "@modelcontextprotocol/sdk": "^1.29.0", "qrcode": "1.5.4", "ws": "^8.20.1", "yaml": "^2.9.0", + "zod": "^4.4.3", }, "devDependencies": { "@types/bun": "^1.3.3", @@ -32,6 +34,10 @@ "packages": { "@beeper/desktop-api": ["@beeper/desktop-api@github:beeper/desktop-api-js#1d94580", {}, "beeper-desktop-api-js-1d94580", "sha512-HVItzImUS2nsk45TXPQEAIhqWU0kK+Og72Wg0mvJbjzy9BqO9f+cQWIcf73B4TCoxb/MEIYB6p4K0nrYBHS4bQ=="], + "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], + + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/node": ["@types/node@20.19.41", "", { "dependencies": { "undici-types": "6.21.0" } }, "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ=="], @@ -40,14 +46,28 @@ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "20.19.41" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "beeper-cli": ["beeper-cli@workspace:packages/cli"], + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], "cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], @@ -56,50 +76,210 @@ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], + + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "express-rate-limit": ["express-rate-limit@8.5.2", "", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], + + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "5.0.0", "path-exists": "4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "hono": ["hono@4.12.23", "", {}, "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "2.2.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "2.3.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], + + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], + "qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "8.0.0", "is-fullwidth-code-point": "3.0.0", "strip-ansi": "6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "4.3.0", "string-width": "4.2.3", "strip-ansi": "6.0.1" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "ws": ["ws@8.20.1", "", {}, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="], "y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], @@ -109,5 +289,11 @@ "yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], "yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], + + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + + "type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], } } diff --git a/packages/cli/README.md b/packages/cli/README.md index f5f55ec6..f06e5d45 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -41,6 +41,7 @@ The live command registry is the source of truth. Use: ```sh beeper --help +beeper docs beeper schema --json beeper exit-codes --json beeper --help @@ -52,35 +53,43 @@ and are rebuilt from the live registry with Current command groups: -- `setup`, `status` (`st`), `doctor`, `version`, `exit-codes`, `schema` +- `setup`, `status` (`st`), `doctor`, `docs` (`help-docs`), `help`, `version`, `agent`, `exit-codes` (`agent exit-codes`, `agent exitcodes`, `agent exit-code`), `schema` - `config get` (`config show`), `config keys` (`config list-keys`, `config names`), `config list` (`config ls`, `config all`), `config path` (`config where`), `config set` (`config add`, `config update`), `config unset` (`config rm`, `config del`, `config remove`) -- `use account` (`accounts use`), `use target` (`targets use`), `remove account` (`accounts remove`, `accounts rm`), `remove target` (`targets remove`, `targets rm`) -- `auth email start`, `auth email response`, `auth logout` +- `accounts use` (`use account`, `account use`), `targets use` (`use target`, `target use`), `accounts remove` (`remove account`, `accounts rm`), `targets remove` (`remove target`, `targets rm`) +- `login`, `auth email start`, `auth email response`, `auth logout` (`logout`) - `targets add`, `targets list` (`targets ls`), `targets runtime start`, `targets runtime stop`, `targets runtime restart`, `targets logs`, `targets tunnel` - `install desktop`, `install server` - `accounts add`, `accounts list` -- `chats list` (`chats ls`), `chats show`, `chats start`, `chats archive`, `chats pin`, `chats mute`, `chats read`, `chats rename`, `chats description`, `chats avatar`, `chats priority`, `chats draft`, `chats remind`, `chats disappear`, `chats focus`, `chats notify-anyway` -- `messages list` (`messages ls`), `messages search` (`messages find`), `messages context`, `messages edit`, `messages delete` -- `send text`, `send file`, `send sticker`, `send voice`, `send react`, `send presence` +- `chats list` (`chats ls`, `ls`, `list`), `chats show`, `chats start`, `chats archive`, `chats pin`, `chats mute`, `chats read`, `chats rename`, `chats description`, `chats avatar`, `chats priority`, `chats draft`, `chats remind`, `chats disappear`, `chats focus` (`open`, `focus`), `chats notify-anyway` +- `messages list` (`messages ls`), `messages search` (`messages find`, `search`, `find`), `messages context`, `messages edit`, `messages delete` +- `send text` (`message`, `msg`), `send file`, `send sticker`, `send voice`, `send react`, `send presence` (`presence`) - `contacts list` (`contacts search`, `contacts find`) -- `media download`, `export`, `watch` -- `api request`, `mcp`, `completion` +- `media download` (`download`, `dl`), `export`, `watch` +- `api request`, `mcp`, `completion` (`completion bash`, `completion zsh`, `completion fish`, `completion powershell`) - `resolve account`, `resolve bridge`, `resolve chat`, `resolve contact`, `resolve target` ## Global Flags -- Output: `--json`/`-j`, `--plain`/`-p`/`--tsv`, `--select`/`--fields`, `--results-only`, `--full`, `--events`, `--debug` -- Targeting/config: `--target`, `--account`/`-a`, `--home`, `--access-token` -- Safety: `--dry-run`/`-n`, `--read-only`/`BEEPER_READONLY`, `--timeout`, `--safety-profile`, `--enable-commands`, `--enable-commands-exact`, `--disable-commands`, `--wrap-untrusted` +- Output: `--json`/`-j`/`BEEPER_JSON`, `BEEPER_AUTO_JSON` for piped stdout, `--plain`/`-p`/`--tsv`/`BEEPER_PLAIN`, `--select`/`--fields`/`--project`/`BEEPER_SELECT`/`BEEPER_FIELDS`/`BEEPER_PROJECT`, `--results-only`, `--full`, `--events`/`BEEPER_EVENTS`, `--verbose`/`-v`/`--debug`/`BEEPER_DEBUG` +- Targeting/config: `--target`/`BEEPER_TARGET`, `--account`/`-a`/`BEEPER_ACCOUNT`, `--home`/`--store`/`BEEPER_HOME`/`BEEPER_STORE_DIR`/`BEEPER_CLI_CONFIG_DIR`, `--access-token` +- Safety: `--dry-run`/`-n`/`BEEPER_DRY_RUN`, `--read-only`/`--readonly`/`BEEPER_READONLY`, `--timeout`/`BEEPER_TIMEOUT`, `--safety-profile`, `--enable-commands`/`BEEPER_ENABLE_COMMANDS`, `--enable-commands-exact`/`BEEPER_ENABLE_COMMANDS_EXACT`, `--disable-commands`/`BEEPER_DISABLE_COMMANDS`, `--wrap-untrusted`/`BEEPER_WRAP_UNTRUSTED` - Interaction: `--no-input`, `--force`/`-y` Human output uses stable tables and diagnostic summaries. Use `--json` for raw objects, `--select=id,name` to project JSON fields, and `--plain` for TSV-like text. -`mcp` exposes read-only tools by default. Use `mcp --list-tools` to inspect the -enabled tool set, `mcp --allow-tool messages.*` to restrict it, and -`mcp --allow-write` only when write-risk tools should be available. +Durations accept compact forms like `500ms`, `30s`, `2m`, `5m0s`, and `1h30m`. + +`mcp` exposes a curated read-only tool set by default. Use `mcp --list-tools` +to inspect canonical tool names such as `messages_search`, `messages_context`, +and `targets_list`. Use `mcp --allow-tool messages.*` or +`mcp --allow-tool targets_list` to restrict it. Use `mcp --allow-write` only +when the curated write tools (`send_text`, `send_react`, `chats_read`, and +`messages_edit`) should be available. The default transport is stdio; use +`mcp --transport http` to run a Streamable HTTP server on +`http://127.0.0.1:7331/mcp`, with `--http-host`, `--http-port`, and +`--http-path` available for binding changes. ## Targets diff --git a/packages/cli/docs/commands/README.md b/packages/cli/docs/commands/README.md index e2348e0d..8d2edc1e 100644 --- a/packages/cli/docs/commands/README.md +++ b/packages/cli/docs/commands/README.md @@ -2,74 +2,155 @@ Generated from the live command registry. Do not edit command pages by hand. +## Root Commands + +| Usage | Description | +| --- | --- | +| `beeper message (msg) [] [ ...] [flags]` | Send a text message (alias for 'beeper send text') | +| `beeper ls (list) [flags]` | List chats (alias for 'beeper chats list') | +| `beeper search (find) [] [flags]` | Search messages across chats (alias for 'beeper messages search') | +| `beeper open (browse,focus) [] [flags]` | Focus a chat in Beeper (alias for 'beeper chats focus') | +| `beeper download (dl) [] [flags]` | Download message media (alias for 'beeper media download') | +| `beeper upload (up,put) [flags]` | Send a file message | +| `beeper login (auth add,auth login) [flags]` | Start email sign-in for a target | +| `beeper logout [] [flags]` | Clear stored authentication (alias for 'beeper auth logout') | +| `beeper status (st) [] [flags]` | Show auth, config, selected target, and setup readiness | +| `beeper me (whoami,who-am-i) [flags]` | Show selected account and target identity | +| `beeper whoami (who-am-i) [flags]` | Show selected account and target identity (alias for 'beeper me') | +| `beeper setup [flags]` | Make the selected target ready for messaging | +| `beeper send [] [ ...] [flags]` | Send a text message | +| `beeper chats (chat) [flags]` | List and manage chats | +| `beeper messages [flags]` | List, search, edit, and delete messages | +| `beeper accounts (account) [flags]` | Manage connected chat accounts | +| `beeper contacts (contact) [flags]` | List and search contacts | +| `beeper presence [flags]` | Send presence indicators | +| `beeper media [flags]` | Download message media | +| `beeper targets (target) [flags]` | Manage Beeper Desktop and Server targets | +| `beeper resolve [flags]` | Resolve Beeper selectors | +| `beeper export [flags]` | Export accounts, chats, messages, transcripts, and attachments | +| `beeper watch [flags]` | Stream Desktop API WebSocket events | +| `beeper doctor (auth doctor) [flags]` | Run diagnostics for config, target reachability, auth, and readiness | +| `beeper auth [flags]` | Authenticate and manage stored credentials | +| `beeper install [flags]` | Install Beeper Desktop or Beeper Server | +| `beeper api [flags]` | Call raw Beeper Desktop API endpoints | +| `beeper config [flags]` | Manage configuration | +| `beeper docs (help-docs) [flags]` | Print command documentation locations | +| `beeper schema (help-json,helpjson) [ ...] [flags]` | Machine-readable command/flag schema | +| `beeper mcp [flags]` | Run a typed, allowlisted MCP server over stdio or HTTP | +| `beeper agent [flags]` | Agent-friendly helpers | +| `beeper exit-codes (agent exit-codes,agent exitcodes,agent exit-code,exitcodes) [flags]` | Print stable exit codes for automation | +| `beeper completion [flags]` | Generate shell completion scripts | +| `beeper help [ ...] [flags]` | Show help for a command | +| `beeper version [flags]` | Print version | +| `beeper groups [flags]` | groups commands | +| `beeper search-all (find-all) [flags]` | Search chats, group participants, and messages together (alias for 'beeper search all') | + +## Full Command Reference + | Command | Description | Aliases | | --- | --- | --- | -| [`accounts add`](accounts-add.md) | Connect a chat account by bridge | | -| [`accounts list`](accounts-list.md) | List connected accounts | | +| [`accounts add`](accounts-add.md) | Connect a chat account by bridge | `accounts create`, `accounts new`, `account add`, `account create`, `account new` | +| [`accounts list`](accounts-list.md) | List connected accounts | `accounts ls`, `account list`, `account ls` | +| [`accounts remove`](accounts-remove.md) | Remove an account | `accounts rm`, `accounts del`, `remove account`, `account remove`, `account rm`, `account del` | +| [`accounts show`](accounts-show.md) | Show one connected account | `accounts get`, `accounts info`, `account show`, `account get`, `account info` | +| [`accounts use`](accounts-use.md) | Select the default account | `use account`, `account use` | +| [`agent`](agent.md) | Agent-friendly helpers | | | [`api request`](api-request.md) | Call a raw Desktop API path with any supported HTTP method | | | [`auth email response`](auth-email-response.md) | Finish email sign-in for a target | | | [`auth email start`](auth-email-start.md) | Start email sign-in for a target | | -| [`auth logout`](auth-logout.md) | Clear stored authentication | | -| [`chats archive`](chats-archive.md) | Archive or unarchive a chat | | -| [`chats avatar`](chats-avatar.md) | Set or clear a chat avatar | | -| [`chats description`](chats-description.md) | Set or clear a chat description | | -| [`chats disappear`](chats-disappear.md) | Set a disappearing-message timer | | -| [`chats draft`](chats-draft.md) | Set or clear a chat draft | | -| [`chats focus`](chats-focus.md) | Focus a chat in Beeper | | -| [`chats list`](chats-list.md) | List chats | `chats ls` | -| [`chats mute`](chats-mute.md) | Mute or unmute a chat | | -| [`chats notify-anyway`](chats-notify-anyway.md) | Notify a chat anyway | | -| [`chats pin`](chats-pin.md) | Pin or unpin a chat | | -| [`chats priority`](chats-priority.md) | Set chat priority | | -| [`chats read`](chats-read.md) | Mark a chat read or unread | | -| [`chats remind`](chats-remind.md) | Set or clear a chat reminder | | -| [`chats rename`](chats-rename.md) | Rename a chat | | -| [`chats show`](chats-show.md) | Show chat details | | -| [`chats start`](chats-start.md) | Start a chat | | +| [`auth list`](auth-list.md) | List stored target credentials | `auth ls` | +| [`auth logout`](auth-logout.md) | Clear stored authentication | `logout`, `auth remove`, `auth rm`, `auth del` | +| [`auth manage`](auth-manage.md) | Make the selected target ready for messaging | `auth setup`, `auth connect` | +| [`auth services`](auth-services.md) | List supported account login services and bridges | `auth bridges` | +| [`auth status`](auth-status.md) | Show auth configuration and stored target credential status | | +| [`chats archive`](chats-archive.md) | Archive or unarchive a chat | `chat archive` | +| [`chats avatar`](chats-avatar.md) | Set or clear a chat avatar | `chat avatar` | +| [`chats description`](chats-description.md) | Set or clear a chat description | `chat description` | +| [`chats disappear`](chats-disappear.md) | Set a disappearing-message timer | `chat disappear` | +| [`chats draft`](chats-draft.md) | Set or clear a chat draft | `chat draft` | +| [`chats focus`](chats-focus.md) | Focus a chat in Beeper | `chat focus`, `open`, `browse`, `focus` | +| [`chats list`](chats-list.md) | List chats | `chats ls`, `chat list`, `chat ls`, `ls`, `list` | +| [`chats mark-read`](chats-mark-read.md) | Mark a chat as read | `chat mark-read` | +| [`chats mark-unread`](chats-mark-unread.md) | Mark a chat as unread | `chat mark-unread` | +| [`chats mute`](chats-mute.md) | Mute or unmute a chat | `chat mute` | +| [`chats notify-anyway`](chats-notify-anyway.md) | Notify a chat anyway | `chat notify-anyway` | +| [`chats pin`](chats-pin.md) | Pin or unpin a chat | `chat pin` | +| [`chats priority`](chats-priority.md) | Set chat priority | `chat priority` | +| [`chats read`](chats-read.md) | Mark a chat read or unread | `chat read` | +| [`chats remind`](chats-remind.md) | Set or clear a chat reminder | `chat remind` | +| [`chats rename`](chats-rename.md) | Rename a chat | `chat rename` | +| [`chats show`](chats-show.md) | Show chat details | `chats info`, `chat show`, `chat info` | +| [`chats start`](chats-start.md) | Start a chat | `chat start` | +| [`chats unarchive`](chats-unarchive.md) | Unarchive a chat | `chat unarchive` | +| [`chats unmute`](chats-unmute.md) | Unmute a chat | `chat unmute` | +| [`chats unpin`](chats-unpin.md) | Unpin a chat | `chat unpin` | | [`completion`](completion.md) | Generate shell completion scripts | | +| [`completion bash`](completion-bash.md) | Generate the autocompletion script for bash | | +| [`completion fish`](completion-fish.md) | Generate the autocompletion script for fish | | +| [`completion powershell`](completion-powershell.md) | Generate the autocompletion script for powershell | `completion pwsh` | +| [`completion zsh`](completion-zsh.md) | Generate the autocompletion script for zsh | | | [`config get`](config-get.md) | Get a config value | `config show` | | [`config keys`](config-keys.md) | List available config keys | `config list-keys`, `config names` | | [`config list`](config-list.md) | List all config values | `config ls`, `config all` | | [`config path`](config-path.md) | Print config file path | `config where` | | [`config set`](config-set.md) | Set a config value | `config add`, `config update` | | [`config unset`](config-unset.md) | Unset a config value | `config rm`, `config del`, `config remove` | -| [`contacts list`](contacts-list.md) | List contacts | `contacts search`, `contacts find` | -| [`doctor`](doctor.md) | Run diagnostics for config, target reachability, auth, and readiness | | -| [`exit-codes`](exit-codes.md) | Print stable exit codes for automation | `agent exit-codes`, `exitcodes` | +| [`contacts list`](contacts-list.md) | List contacts | `contacts ls`, `contacts search`, `contacts find`, `contact list`, `contact ls`, `contact search`, `contact find` | +| [`contacts show`](contacts-show.md) | Show one contact | `contacts get`, `contacts info`, `contact show`, `contact get`, `contact info` | +| [`docs`](docs.md) | Print command documentation locations | `help-docs` | +| [`doctor`](doctor.md) | Run diagnostics for config, target reachability, auth, and readiness | `auth doctor` | +| [`exit-codes`](exit-codes.md) | Print stable exit codes for automation | `agent exit-codes`, `agent exitcodes`, `agent exit-code`, `exitcodes` | | [`export`](export.md) | Export accounts, chats, messages, transcripts, and attachments | | +| [`groups create`](groups-create.md) | Create a group chat | `groups add`, `groups new`, `group create`, `group add`, `group new` | +| [`groups description`](groups-description.md) | Set or clear a group description | `groups topic`, `group description`, `group topic` | +| [`groups list`](groups-list.md) | List group chats | `groups ls`, `group list`, `group ls` | +| [`groups rename`](groups-rename.md) | Rename a group | `group rename` | +| [`groups show`](groups-show.md) | Show group details | `groups info`, `group show`, `group info` | +| [`help`](help.md) | Show help for a command | | | [`install desktop`](install-desktop.md) | Install Beeper Desktop locally | | | [`install server`](install-server.md) | Install Beeper Server locally | | -| [`mcp`](mcp.md) | Run a typed MCP stdio server | | -| [`media download`](media-download.md) | Download message media | | +| [`login`](login.md) | Start email sign-in for a target | `auth add`, `auth login` | +| [`mcp`](mcp.md) | Run a typed, allowlisted MCP server over stdio or HTTP | | +| [`me`](me.md) | Show selected account and target identity | `whoami`, `who-am-i` | +| [`media download`](media-download.md) | Download message media | `media dl`, `download`, `dl` | +| [`media message`](media-message.md) | Download media for a message | | | [`messages context`](messages-context.md) | Show a message with surrounding context | | -| [`messages delete`](messages-delete.md) | Delete a message | | -| [`messages edit`](messages-edit.md) | Edit a message | | +| [`messages delete`](messages-delete.md) | Delete a message | `messages rm`, `messages del`, `messages remove` | +| [`messages edit`](messages-edit.md) | Edit a message | `messages update`, `messages set` | +| [`messages export`](messages-export.md) | Export messages as JSON | | +| [`messages forward`](messages-forward.md) | Forward a message | | | [`messages list`](messages-list.md) | List chat messages | `messages ls` | -| [`messages search`](messages-search.md) | Search messages across chats | `messages find` | -| [`remove account`](remove-account.md) | Remove an account | `accounts remove`, `accounts rm` | -| [`remove target`](remove-target.md) | Remove a target | `targets remove`, `targets rm` | +| [`messages revoke`](messages-revoke.md) | Delete a sent message for everyone | | +| [`messages search`](messages-search.md) | Search messages across chats | `messages find`, `search`, `find` | +| [`messages show`](messages-show.md) | Show one message | `messages get`, `messages info` | +| [`presence`](presence.md) | Send presence indicators | | +| [`presence paused`](presence-paused.md) | Send a 'paused' indicator (stop typing) to a chat | | +| [`presence typing`](presence-typing.md) | Send a 'composing' (typing) indicator to a chat | | | [`resolve account`](resolve-account.md) | Resolve an account selector | | | [`resolve bridge`](resolve-bridge.md) | Resolve a bridge selector | | | [`resolve chat`](resolve-chat.md) | Resolve a chat selector | | | [`resolve contact`](resolve-contact.md) | Resolve a contact selector | | | [`resolve target`](resolve-target.md) | Resolve a target selector | | -| [`schema`](schema.md) | Print machine-readable command and flag schema | `help-json`, `helpjson` | +| [`schema`](schema.md) | Machine-readable command/flag schema | `help-json`, `helpjson` | +| [`search all`](search-all.md) | Search chats, group participants, and messages together | `search-all`, `find-all` | +| [`send`](send.md) | Send a text message | | | [`send file`](send-file.md) | Send a file message | | | [`send presence`](send-presence.md) | Send a typing indicator | | -| [`send react`](send-react.md) | Send or remove a reaction | | +| [`send react`](send-react.md) | Send or remove a reaction | `send reaction` | | [`send sticker`](send-sticker.md) | Send a sticker | | -| [`send text`](send-text.md) | Send a text message | | +| [`send text`](send-text.md) | Send a text message | `message`, `msg` | | [`send voice`](send-voice.md) | Send a voice note | | | [`setup`](setup.md) | Make the selected target ready for messaging | | -| [`status`](status.md) | Show selected target and setup readiness | `st` | -| [`targets add`](targets-add.md) | Add a remote Beeper Desktop or Server target | | -| [`targets list`](targets-list.md) | List configured Beeper targets | `targets ls` | -| [`targets logs`](targets-logs.md) | Print logs for a local Beeper Desktop or Server install | | -| [`targets runtime restart`](targets-runtime-restart.md) | Restart a local server runtime | | -| [`targets runtime start`](targets-runtime-start.md) | Start a local target runtime | | -| [`targets runtime stop`](targets-runtime-stop.md) | Stop a local server runtime | | -| [`targets tunnel`](targets-tunnel.md) | Expose a target through Cloudflare Tunnel | | -| [`use account`](use-account.md) | Select the default account | `accounts use` | -| [`use target`](use-target.md) | Select the default target | `targets use` | -| [`version`](version.md) | Print CLI version | | +| [`status`](status.md) | Show auth, config, selected target, and setup readiness | `st` | +| [`targets add`](targets-add.md) | Add a remote Beeper Desktop or Server target | `target add` | +| [`targets list`](targets-list.md) | List configured Beeper targets | `targets ls`, `target list`, `target ls` | +| [`targets logs`](targets-logs.md) | Print logs for a local Beeper Desktop or Server install | `target logs` | +| [`targets remove`](targets-remove.md) | Remove a target | `targets rm`, `targets del`, `remove target`, `target remove`, `target rm`, `target del` | +| [`targets runtime restart`](targets-runtime-restart.md) | Restart a local server runtime | `target runtime restart` | +| [`targets runtime start`](targets-runtime-start.md) | Start a local target runtime | `target runtime start` | +| [`targets runtime stop`](targets-runtime-stop.md) | Stop a local server runtime | `target runtime stop` | +| [`targets tunnel`](targets-tunnel.md) | Expose a target through Cloudflare Tunnel | `target tunnel` | +| [`targets use`](targets-use.md) | Select the default target | `use target`, `target use` | +| [`upload`](upload.md) | Send a file message | `up`, `put` | +| [`version`](version.md) | Print version | | | [`watch`](watch.md) | Stream Desktop API WebSocket events | | diff --git a/packages/cli/docs/commands/accounts-add.md b/packages/cli/docs/commands/accounts-add.md index de18d266..2c3b24dc 100644 --- a/packages/cli/docs/commands/accounts-add.md +++ b/packages/cli/docs/commands/accounts-add.md @@ -2,51 +2,61 @@ Connect a chat account by bridge ## Usage ```sh -beeper accounts add [bridge] [flags] +beeper accounts add (accounts create,accounts new,account add,account create,account new) [] [flags] ``` +## Aliases + +- `beeper accounts create` +- `beeper accounts new` +- `beeper account add` +- `beeper account create` +- `beeper account new` + ## Arguments | Name | Description | | --- | --- | -| `[bridge]` | | +| `[]` | Bridge ID, name, or network to connect | ## Flags | Name | Description | | --- | --- | -| `--cookie ` | Cookie value in name=value form Repeatable. | -| `--field ` | Field value in id=value form Repeatable. | -| `--flow ` | Login flow ID | +| `--cookie=STRING` | Cookie value in name=value form Repeatable. | +| `--field=STRING` | Field value in id=value form Repeatable. | +| `--flow=STRING` | Login flow ID | | `--guided` | Prompt through login steps Default: `true`. | -| `--login-id ` | Existing login ID to re-login as | +| `--login-id=STRING` | Existing login ID to re-login as | | `--webview` | Use Bun.WebView for cookie login steps Default: `false`. | -| `--webview-backend ` | Bun.WebView backend Default: `chrome`. Values: `auto`, `chrome`, `webkit`. | -| `--webview-timeout ` | Seconds to wait for WebView cookie collection Default: `120`. | +| `--webview-backend="chrome"` | Bun.WebView backend Default: `chrome`. Values: `auto`, `chrome`, `webkit`. | +| `--webview-timeout=120` | Seconds to wait for WebView cookie collection Default: `120`. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/accounts-list.md b/packages/cli/docs/commands/accounts-list.md index 1f907b23..9a722b0b 100644 --- a/packages/cli/docs/commands/accounts-list.md +++ b/packages/cli/docs/commands/accounts-list.md @@ -2,39 +2,47 @@ List connected accounts ## Usage ```sh -beeper accounts list [flags] +beeper accounts list (accounts ls,account list,account ls) [flags] ``` +## Aliases + +- `beeper accounts ls` +- `beeper account list` +- `beeper account ls` + ## Flags | Name | Description | | --- | --- | -| `-a, --account , --acct` | Limit to account selector Repeatable. | +| `-a, --account=STRING, --acct` | Limit to account selector Repeatable. | | `--ids` | Print only account IDs Default: `false`. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/accounts-remove.md b/packages/cli/docs/commands/accounts-remove.md new file mode 100644 index 00000000..7794c8f5 --- /dev/null +++ b/packages/cli/docs/commands/accounts-remove.md @@ -0,0 +1,50 @@ +# beeper accounts remove +Remove an account +## Usage +```sh +beeper accounts remove (accounts rm,accounts del,remove account,account remove,account rm,account del) [flags] +``` +## Aliases + +- `beeper accounts rm` +- `beeper accounts del` +- `beeper remove account` +- `beeper account remove` +- `beeper account rm` +- `beeper account del` + +## Arguments + +| Name | Description | +| --- | --- | +| `` | Account selector | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/accounts-show.md b/packages/cli/docs/commands/accounts-show.md new file mode 100644 index 00000000..4f67517f --- /dev/null +++ b/packages/cli/docs/commands/accounts-show.md @@ -0,0 +1,49 @@ +# beeper accounts show +Show one connected account +## Usage +```sh +beeper accounts show (accounts get,accounts info,account show,account get,account info) [flags] +``` +## Aliases + +- `beeper accounts get` +- `beeper accounts info` +- `beeper account show` +- `beeper account get` +- `beeper account info` + +## Arguments + +| Name | Description | +| --- | --- | +| `` | Account selector | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/accounts-use.md b/packages/cli/docs/commands/accounts-use.md new file mode 100644 index 00000000..1578f3db --- /dev/null +++ b/packages/cli/docs/commands/accounts-use.md @@ -0,0 +1,46 @@ +# beeper accounts use +Select the default account +## Usage +```sh +beeper accounts use (use account,account use) [flags] +``` +## Aliases + +- `beeper use account` +- `beeper account use` + +## Arguments + +| Name | Description | +| --- | --- | +| `` | Account selector | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/agent.md b/packages/cli/docs/commands/agent.md new file mode 100644 index 00000000..71e7c0ea --- /dev/null +++ b/packages/cli/docs/commands/agent.md @@ -0,0 +1,35 @@ +# beeper agent +Agent-friendly helpers +## Usage +```sh +beeper agent [flags] +``` +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/api-request.md b/packages/cli/docs/commands/api-request.md index 5f4fc134..a96651b4 100644 --- a/packages/cli/docs/commands/api-request.md +++ b/packages/cli/docs/commands/api-request.md @@ -8,40 +8,42 @@ beeper api request [flags] | Name | Description | | --- | --- | -| `` | | -| `` | | +| `` | HTTP method: GET, POST, PUT, PATCH, or DELETE | +| `` | Desktop API path, for example /v1/info | ## Flags | Name | Description | | --- | --- | -| `--body ` | JSON request body | +| `--body=STRING` | JSON request body | | `--no-auth` | Call a public API path without a bearer token Default: `false`. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/auth-email-response.md b/packages/cli/docs/commands/auth-email-response.md index 176b7e3c..200d891b 100644 --- a/packages/cli/docs/commands/auth-email-response.md +++ b/packages/cli/docs/commands/auth-email-response.md @@ -8,34 +8,36 @@ beeper auth email response [flags] | Name | Description | | --- | --- | -| `--code ` | Email verification code Required. | -| `--setup-request-id ` | Setup request ID from auth email start Required. | -| `--username ` | Username to use if setup creates a new account | +| `--code=STRING` | Email verification code Required. | +| `--setup-request-id=STRING` | Setup request ID from auth email start Required. | +| `--username=STRING` | Username to use if setup creates a new account | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/auth-email-start.md b/packages/cli/docs/commands/auth-email-start.md index 73ef81de..cd4f0666 100644 --- a/packages/cli/docs/commands/auth-email-start.md +++ b/packages/cli/docs/commands/auth-email-start.md @@ -8,32 +8,34 @@ beeper auth email start [flags] | Name | Description | | --- | --- | -| `--email ` | Email address Required. | +| `--email=STRING` | Email address Required. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/auth-list.md b/packages/cli/docs/commands/auth-list.md new file mode 100644 index 00000000..b6fd4d5e --- /dev/null +++ b/packages/cli/docs/commands/auth-list.md @@ -0,0 +1,44 @@ +# beeper auth list +List stored target credentials +## Usage +```sh +beeper auth list (auth ls) [flags] +``` +## Aliases + +- `beeper auth ls` + +## JSON Output + +Default JSON output is an object containing the `accounts` field. +Use `--json --results-only` to emit only `accounts`. + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/auth-logout.md b/packages/cli/docs/commands/auth-logout.md index 6169a753..859608d7 100644 --- a/packages/cli/docs/commands/auth-logout.md +++ b/packages/cli/docs/commands/auth-logout.md @@ -2,32 +2,47 @@ Clear stored authentication ## Usage ```sh -beeper auth logout [flags] +beeper auth logout (logout,auth remove,auth rm,auth del) [] [flags] ``` +## Aliases + +- `beeper logout` +- `beeper auth remove` +- `beeper auth rm` +- `beeper auth del` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Target name. Defaults to the selected target. | + ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/auth-manage.md b/packages/cli/docs/commands/auth-manage.md new file mode 100644 index 00000000..1732bd6f --- /dev/null +++ b/packages/cli/docs/commands/auth-manage.md @@ -0,0 +1,67 @@ +# beeper auth manage +Make the selected target ready for messaging +## Usage +```sh +beeper auth manage (auth setup,auth connect) [flags] +``` +## Aliases + +- `beeper auth setup` +- `beeper auth connect` + +## Flags + +| Name | Description | +| --- | --- | +| `--local` | Use the local Beeper Desktop session on this device Default: `false`. | +| `--oauth` | Authorize the target with browser OAuth/PKCE Default: `false`. | +| `--remote=STRING` | Connect to a remote Beeper Desktop or Server URL | +| `--server` | Set up a local Beeper Server target Default: `false`. | +| `--desktop` | Set up a local Beeper Desktop target Default: `false`. | +| `--install` | Allow installing a missing local runtime Default: `false`. | +| `--channel="stable"` | Install release channel Default: `stable`. Values: `stable`, `nightly`. | +| `--server-env="prod"` | Server environment Default: `prod`. Values: `local`, `dev`, `staging`, `prod`. | +| `--email=STRING` | Sign in with an email address | +| `--username=STRING` | Username to use if setup creates a new account | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | + +## Examples + +```sh +beeper auth manage +``` +```sh +beeper auth manage --local +``` +```sh +beeper auth manage --oauth +``` diff --git a/packages/cli/docs/commands/auth-services.md b/packages/cli/docs/commands/auth-services.md new file mode 100644 index 00000000..ee90050f --- /dev/null +++ b/packages/cli/docs/commands/auth-services.md @@ -0,0 +1,50 @@ +# beeper auth services +List supported account login services and bridges +## Usage +```sh +beeper auth services (auth bridges) [flags] +``` +## Aliases + +- `beeper auth bridges` + +## JSON Output + +Default JSON output is an object containing the `services` field. +Use `--json --results-only` to emit only `services`. + +## Flags + +| Name | Description | +| --- | --- | +| `--markdown` | Output a Markdown table Default: `false`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/auth-status.md b/packages/cli/docs/commands/auth-status.md new file mode 100644 index 00000000..0f555891 --- /dev/null +++ b/packages/cli/docs/commands/auth-status.md @@ -0,0 +1,41 @@ +# beeper auth status +Show auth configuration and stored target credential status +## Usage +```sh +beeper auth status [] [flags] +``` +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Target name. Defaults to the selected target. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/chats-archive.md b/packages/cli/docs/commands/chats-archive.md index 41ee3495..93328cd6 100644 --- a/packages/cli/docs/commands/chats-archive.md +++ b/packages/cli/docs/commands/chats-archive.md @@ -2,40 +2,52 @@ Archive or unarchive a chat ## Usage ```sh -beeper chats archive [flags] +beeper chats archive (chat archive) [] [flags] ``` +## Aliases + +- `beeper chat archive` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Chat selector. Used when --chat is omitted. | + ## Flags | Name | Description | | --- | --- | -| `--chat ` | Chat selector Required. | -| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--chat=STRING, --jid` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | | `--clear` | Unarchive the chat Default: `false`. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/chats-avatar.md b/packages/cli/docs/commands/chats-avatar.md index 916573e7..f151ec42 100644 --- a/packages/cli/docs/commands/chats-avatar.md +++ b/packages/cli/docs/commands/chats-avatar.md @@ -2,41 +2,53 @@ Set or clear a chat avatar ## Usage ```sh -beeper chats avatar [flags] +beeper chats avatar (chat avatar) [] [flags] ``` +## Aliases + +- `beeper chat avatar` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Chat selector. Used when --chat is omitted. | + ## Flags | Name | Description | | --- | --- | -| `--chat ` | Chat selector Required. | -| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--chat=STRING, --jid` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | | `--clear` | Clear the avatar Default: `false`. | -| `--file ` | Avatar image file path | +| `--file=STRING` | Avatar image file path | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/chats-description.md b/packages/cli/docs/commands/chats-description.md index 29ac70bb..a4559bee 100644 --- a/packages/cli/docs/commands/chats-description.md +++ b/packages/cli/docs/commands/chats-description.md @@ -2,41 +2,53 @@ Set or clear a chat description ## Usage ```sh -beeper chats description [flags] +beeper chats description (chat description) [] [flags] ``` +## Aliases + +- `beeper chat description` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Chat selector. Used when --chat is omitted. | + ## Flags | Name | Description | | --- | --- | -| `--chat ` | Chat selector Required. | -| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--chat=STRING, --jid` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | | `--clear` | Clear or unset the chosen state Default: `false`. | -| `--description ` | Chat description | +| `--description=STRING` | Chat description | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/chats-disappear.md b/packages/cli/docs/commands/chats-disappear.md index bb5c96ee..4ca4e8f5 100644 --- a/packages/cli/docs/commands/chats-disappear.md +++ b/packages/cli/docs/commands/chats-disappear.md @@ -2,40 +2,52 @@ Set a disappearing-message timer ## Usage ```sh -beeper chats disappear [flags] +beeper chats disappear (chat disappear) [] [flags] ``` +## Aliases + +- `beeper chat disappear` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Chat selector. Used when --chat is omitted. | + ## Flags | Name | Description | | --- | --- | -| `--chat ` | Chat selector Required. | -| `--pick ` | Pick the Nth result when selector is ambiguous | -| `--seconds ` | Disappearing-message timer in seconds, or off | +| `--chat=STRING, --jid` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | +| `--seconds=STRING, --duration, --ephemeral-duration` | Disappearing-message timer in seconds, duration form like 24h/7d/90d, or off | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/chats-draft.md b/packages/cli/docs/commands/chats-draft.md index 62281a62..4a1d6830 100644 --- a/packages/cli/docs/commands/chats-draft.md +++ b/packages/cli/docs/commands/chats-draft.md @@ -2,44 +2,56 @@ Set or clear a chat draft ## Usage ```sh -beeper chats draft [flags] +beeper chats draft (chat draft) [] [flags] ``` +## Aliases + +- `beeper chat draft` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Chat selector. Used when --chat is omitted. | + ## Flags | Name | Description | | --- | --- | -| `--chat ` | Chat selector Required. | -| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--chat=STRING, --jid` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | | `--clear` | Clear the draft Default: `false`. | -| `--file ` | Draft attachment file path | -| `--filename ` | Draft attachment filename | -| `--mime ` | Draft attachment MIME type | -| `--text ` | Draft text | +| `--file=STRING` | Draft attachment file path | +| `--filename=STRING` | Draft attachment filename | +| `--mime=STRING` | Draft attachment MIME type | +| `--text=STRING` | Draft text | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/chats-focus.md b/packages/cli/docs/commands/chats-focus.md index bea8b5c5..30a8d46a 100644 --- a/packages/cli/docs/commands/chats-focus.md +++ b/packages/cli/docs/commands/chats-focus.md @@ -2,42 +2,57 @@ Focus a chat in Beeper ## Usage ```sh -beeper chats focus [flags] +beeper chats focus (chat focus,open,browse,focus) [] [flags] ``` +## Aliases + +- `beeper chat focus` +- `beeper open` +- `beeper browse` +- `beeper focus` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Chat selector. Used when --chat is omitted. | + ## Flags | Name | Description | | --- | --- | -| `--chat ` | Chat selector Required. | -| `--pick ` | Pick the Nth result when selector is ambiguous | -| `--file ` | Draft attachment file path | -| `--message ` | Message ID to focus | -| `--text ` | Draft text | +| `--chat=STRING, --jid` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | +| `--file=STRING` | Draft attachment file path | +| `--message=STRING` | Message ID to focus | +| `--text=STRING` | Draft text | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/chats-list.md b/packages/cli/docs/commands/chats-list.md index 868c2608..e007267b 100644 --- a/packages/cli/docs/commands/chats-list.md +++ b/packages/cli/docs/commands/chats-list.md @@ -2,50 +2,57 @@ List chats ## Usage ```sh -beeper chats list [flags] +beeper chats list (chats ls,chat list,chat ls,ls,list) [flags] ``` ## Aliases - `beeper chats ls` +- `beeper chat list` +- `beeper chat ls` +- `beeper ls` +- `beeper list` ## Flags | Name | Description | | --- | --- | -| `-a, --account , --acct` | Limit to account selector Repeatable. | +| `-a, --account=STRING, --acct` | Limit to account selector Repeatable. | | `--archived` | Only archived chats; use --no-archived to exclude | | `--ids` | Print preferred chat selectors Default: `false`. | -| `--limit ` | Maximum chats to print Default: `20`. | +| `--limit=50` | Maximum chats to print Default: `50`. | | `--low-priority` | Only low-priority chats; use --no-low-priority to exclude | | `--muted` | Only muted chats; use --no-muted to exclude | | `--pinned` | Only pinned chats; use --no-pinned to exclude | -| `--query ` | Optional chat lookup query | +| `--query=STRING` | Optional chat lookup query | +| `--type=STRING, --chat-type` | Only direct messages, group chats, or all chats Values: `single`, `group`, `any`. | | `--unread` | Only unread chats; use --no-unread to exclude | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/chats-mark-read.md b/packages/cli/docs/commands/chats-mark-read.md new file mode 100644 index 00000000..479f7218 --- /dev/null +++ b/packages/cli/docs/commands/chats-mark-read.md @@ -0,0 +1,53 @@ +# beeper chats mark-read +Mark a chat as read +## Usage +```sh +beeper chats mark-read (chat mark-read) [] [flags] +``` +## Aliases + +- `beeper chat mark-read` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Chat selector. Used when --chat is omitted. | + +## Flags + +| Name | Description | +| --- | --- | +| `--chat=STRING, --jid` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | +| `--message=STRING` | Read marker message ID | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/chats-mark-unread.md b/packages/cli/docs/commands/chats-mark-unread.md new file mode 100644 index 00000000..e346a7f1 --- /dev/null +++ b/packages/cli/docs/commands/chats-mark-unread.md @@ -0,0 +1,52 @@ +# beeper chats mark-unread +Mark a chat as unread +## Usage +```sh +beeper chats mark-unread (chat mark-unread) [] [flags] +``` +## Aliases + +- `beeper chat mark-unread` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Chat selector. Used when --chat is omitted. | + +## Flags + +| Name | Description | +| --- | --- | +| `--chat=STRING, --jid` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/chats-mute.md b/packages/cli/docs/commands/chats-mute.md index 3ce40500..964dd28e 100644 --- a/packages/cli/docs/commands/chats-mute.md +++ b/packages/cli/docs/commands/chats-mute.md @@ -2,40 +2,52 @@ Mute or unmute a chat ## Usage ```sh -beeper chats mute [flags] +beeper chats mute (chat mute) [] [flags] ``` +## Aliases + +- `beeper chat mute` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Chat selector. Used when --chat is omitted. | + ## Flags | Name | Description | | --- | --- | -| `--chat ` | Chat selector Required. | -| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--chat=STRING, --jid` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | | `--clear` | Unmute the chat Default: `false`. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/chats-notify-anyway.md b/packages/cli/docs/commands/chats-notify-anyway.md index bea46090..2bf93ff6 100644 --- a/packages/cli/docs/commands/chats-notify-anyway.md +++ b/packages/cli/docs/commands/chats-notify-anyway.md @@ -2,39 +2,51 @@ Notify a chat anyway ## Usage ```sh -beeper chats notify-anyway [flags] +beeper chats notify-anyway (chat notify-anyway) [] [flags] ``` +## Aliases + +- `beeper chat notify-anyway` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Chat selector. Used when --chat is omitted. | + ## Flags | Name | Description | | --- | --- | -| `--chat ` | Chat selector Required. | -| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--chat=STRING, --jid` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/chats-pin.md b/packages/cli/docs/commands/chats-pin.md index 029f3a1b..783f7a44 100644 --- a/packages/cli/docs/commands/chats-pin.md +++ b/packages/cli/docs/commands/chats-pin.md @@ -2,40 +2,52 @@ Pin or unpin a chat ## Usage ```sh -beeper chats pin [flags] +beeper chats pin (chat pin) [] [flags] ``` +## Aliases + +- `beeper chat pin` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Chat selector. Used when --chat is omitted. | + ## Flags | Name | Description | | --- | --- | -| `--chat ` | Chat selector Required. | -| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--chat=STRING, --jid` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | | `--clear` | Unpin the chat Default: `false`. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/chats-priority.md b/packages/cli/docs/commands/chats-priority.md index 59b80009..fea4584b 100644 --- a/packages/cli/docs/commands/chats-priority.md +++ b/packages/cli/docs/commands/chats-priority.md @@ -2,40 +2,52 @@ Set chat priority ## Usage ```sh -beeper chats priority [flags] +beeper chats priority (chat priority) [] [flags] ``` +## Aliases + +- `beeper chat priority` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Chat selector. Used when --chat is omitted. | + ## Flags | Name | Description | | --- | --- | -| `--chat ` | Chat selector Required. | -| `--pick ` | Pick the Nth result when selector is ambiguous | -| `--level ` | Chat priority level Values: `inbox`, `low`. Required. | +| `--chat=STRING, --jid` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | +| `--level=STRING` | Chat priority level Values: `inbox`, `low`. Required. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/chats-read.md b/packages/cli/docs/commands/chats-read.md index d5ecdbb7..d3913fd2 100644 --- a/packages/cli/docs/commands/chats-read.md +++ b/packages/cli/docs/commands/chats-read.md @@ -2,41 +2,53 @@ Mark a chat read or unread ## Usage ```sh -beeper chats read [flags] +beeper chats read (chat read) [] [flags] ``` +## Aliases + +- `beeper chat read` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Chat selector. Used when --chat is omitted. | + ## Flags | Name | Description | | --- | --- | -| `--chat ` | Chat selector Required. | -| `--pick ` | Pick the Nth result when selector is ambiguous | -| `--message ` | Read marker message ID | +| `--chat=STRING, --jid` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | +| `--message=STRING` | Read marker message ID | | `--unread` | Mark the chat unread Default: `false`. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/chats-remind.md b/packages/cli/docs/commands/chats-remind.md index f1dec093..dae4c8c2 100644 --- a/packages/cli/docs/commands/chats-remind.md +++ b/packages/cli/docs/commands/chats-remind.md @@ -2,42 +2,54 @@ Set or clear a chat reminder ## Usage ```sh -beeper chats remind [flags] +beeper chats remind (chat remind) [] [flags] ``` +## Aliases + +- `beeper chat remind` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Chat selector. Used when --chat is omitted. | + ## Flags | Name | Description | | --- | --- | -| `--chat ` | Chat selector Required. | -| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--chat=STRING, --jid` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | | `--clear` | Clear the reminder Default: `false`. | | `--dismiss-on-message` | Dismiss reminder when a new message arrives Default: `false`. | -| `--when ` | ISO reminder timestamp | +| `--when=STRING` | ISO reminder timestamp | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/chats-rename.md b/packages/cli/docs/commands/chats-rename.md index 15fb2241..5b1a49fd 100644 --- a/packages/cli/docs/commands/chats-rename.md +++ b/packages/cli/docs/commands/chats-rename.md @@ -2,40 +2,52 @@ Rename a chat ## Usage ```sh -beeper chats rename [flags] +beeper chats rename (chat rename) [] [flags] ``` +## Aliases + +- `beeper chat rename` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Chat selector. Used when --chat is omitted. | + ## Flags | Name | Description | | --- | --- | -| `--chat ` | Chat selector Required. | -| `--pick ` | Pick the Nth result when selector is ambiguous | -| `--title ` | Chat title Required. | +| `--chat=STRING, --jid` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | +| `--title=STRING` | Chat title Required. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/chats-show.md b/packages/cli/docs/commands/chats-show.md index a61145e4..c3fa69a8 100644 --- a/packages/cli/docs/commands/chats-show.md +++ b/packages/cli/docs/commands/chats-show.md @@ -2,40 +2,54 @@ Show chat details ## Usage ```sh -beeper chats show [flags] +beeper chats show (chats info,chat show,chat info) [] [flags] ``` +## Aliases + +- `beeper chats info` +- `beeper chat show` +- `beeper chat info` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Chat selector. Used when --chat is omitted. | + ## Flags | Name | Description | | --- | --- | -| `--chat ` | Chat selector Required. | -| `--max-participants ` | Limit participants returned in chat details | -| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--chat=STRING, --jid` | Chat selector | +| `--max-participants=INTEGER` | Limit participants returned in chat details | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/chats-start.md b/packages/cli/docs/commands/chats-start.md index b560ec4f..9f3f056f 100644 --- a/packages/cli/docs/commands/chats-start.md +++ b/packages/cli/docs/commands/chats-start.md @@ -2,45 +2,51 @@ Start a chat ## Usage ```sh -beeper chats start [flags] +beeper chats start (chat start) [flags] ``` +## Aliases + +- `beeper chat start` + ## Arguments | Name | Description | | --- | --- | -| `` | | +| `` | User ID, phone, handle, or contact selector | ## Flags | Name | Description | | --- | --- | -| `--account ` | Account selector | -| `--title ` | Optional initial title for a new group chat | +| `--account=STRING` | Account selector | +| `--title=STRING` | Optional initial title for a new group chat | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/chats-unarchive.md b/packages/cli/docs/commands/chats-unarchive.md new file mode 100644 index 00000000..65e1dc26 --- /dev/null +++ b/packages/cli/docs/commands/chats-unarchive.md @@ -0,0 +1,52 @@ +# beeper chats unarchive +Unarchive a chat +## Usage +```sh +beeper chats unarchive (chat unarchive) [] [flags] +``` +## Aliases + +- `beeper chat unarchive` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Chat selector. Used when --chat is omitted. | + +## Flags + +| Name | Description | +| --- | --- | +| `--chat=STRING, --jid` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/chats-unmute.md b/packages/cli/docs/commands/chats-unmute.md new file mode 100644 index 00000000..c2aaab4d --- /dev/null +++ b/packages/cli/docs/commands/chats-unmute.md @@ -0,0 +1,52 @@ +# beeper chats unmute +Unmute a chat +## Usage +```sh +beeper chats unmute (chat unmute) [] [flags] +``` +## Aliases + +- `beeper chat unmute` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Chat selector. Used when --chat is omitted. | + +## Flags + +| Name | Description | +| --- | --- | +| `--chat=STRING, --jid` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/chats-unpin.md b/packages/cli/docs/commands/chats-unpin.md new file mode 100644 index 00000000..aa47edeb --- /dev/null +++ b/packages/cli/docs/commands/chats-unpin.md @@ -0,0 +1,52 @@ +# beeper chats unpin +Unpin a chat +## Usage +```sh +beeper chats unpin (chat unpin) [] [flags] +``` +## Aliases + +- `beeper chat unpin` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Chat selector. Used when --chat is omitted. | + +## Flags + +| Name | Description | +| --- | --- | +| `--chat=STRING, --jid` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/completion-bash.md b/packages/cli/docs/commands/completion-bash.md new file mode 100644 index 00000000..79376d29 --- /dev/null +++ b/packages/cli/docs/commands/completion-bash.md @@ -0,0 +1,41 @@ +# beeper completion bash +Generate the autocompletion script for bash +## Usage +```sh +beeper completion bash [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--no-descriptions` | Accepted for compatibility; completion descriptions are not emitted Default: `false`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/completion-fish.md b/packages/cli/docs/commands/completion-fish.md new file mode 100644 index 00000000..d30bb1fa --- /dev/null +++ b/packages/cli/docs/commands/completion-fish.md @@ -0,0 +1,41 @@ +# beeper completion fish +Generate the autocompletion script for fish +## Usage +```sh +beeper completion fish [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--no-descriptions` | Accepted for compatibility; completion descriptions are not emitted Default: `false`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/completion-powershell.md b/packages/cli/docs/commands/completion-powershell.md new file mode 100644 index 00000000..0f3224c4 --- /dev/null +++ b/packages/cli/docs/commands/completion-powershell.md @@ -0,0 +1,45 @@ +# beeper completion powershell +Generate the autocompletion script for powershell +## Usage +```sh +beeper completion powershell (completion pwsh) [flags] +``` +## Aliases + +- `beeper completion pwsh` + +## Flags + +| Name | Description | +| --- | --- | +| `--no-descriptions` | Accepted for compatibility; completion descriptions are not emitted Default: `false`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/completion-zsh.md b/packages/cli/docs/commands/completion-zsh.md new file mode 100644 index 00000000..258fdbab --- /dev/null +++ b/packages/cli/docs/commands/completion-zsh.md @@ -0,0 +1,41 @@ +# beeper completion zsh +Generate the autocompletion script for zsh +## Usage +```sh +beeper completion zsh [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--no-descriptions` | Accepted for compatibility; completion descriptions are not emitted Default: `false`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/completion.md b/packages/cli/docs/commands/completion.md index d3da285d..233add2b 100644 --- a/packages/cli/docs/commands/completion.md +++ b/packages/cli/docs/commands/completion.md @@ -8,32 +8,34 @@ beeper completion [flags] | Name | Description | | --- | --- | -| `` | bash, zsh, fish, or powershell | +| `` | Shell (bash\|zsh\|fish\|powershell) Values: `bash`, `fish`, `powershell`, `zsh`. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/config-get.md b/packages/cli/docs/commands/config-get.md index 7951b824..e9694ba8 100644 --- a/packages/cli/docs/commands/config-get.md +++ b/packages/cli/docs/commands/config-get.md @@ -2,7 +2,7 @@ Get a config value ## Usage ```sh -beeper config get [flags] +beeper config get (config show) [flags] ``` ## Aliases @@ -18,26 +18,28 @@ beeper config get [flags] | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/config-keys.md b/packages/cli/docs/commands/config-keys.md index f382c658..2c1b4e6e 100644 --- a/packages/cli/docs/commands/config-keys.md +++ b/packages/cli/docs/commands/config-keys.md @@ -2,37 +2,44 @@ List available config keys ## Usage ```sh -beeper config keys [flags] +beeper config keys (config list-keys,config names) [flags] ``` ## Aliases - `beeper config list-keys` - `beeper config names` +## JSON Output + +Default JSON output is an object containing the `keys` field. +Use `--json --results-only` to emit only `keys`. + ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/config-list.md b/packages/cli/docs/commands/config-list.md index c543b644..2d97c580 100644 --- a/packages/cli/docs/commands/config-list.md +++ b/packages/cli/docs/commands/config-list.md @@ -2,7 +2,7 @@ List all config values ## Usage ```sh -beeper config list [flags] +beeper config list (config ls,config all) [flags] ``` ## Aliases @@ -13,26 +13,28 @@ beeper config list [flags] | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/config-path.md b/packages/cli/docs/commands/config-path.md index b9bb7a50..42d4a2ba 100644 --- a/packages/cli/docs/commands/config-path.md +++ b/packages/cli/docs/commands/config-path.md @@ -2,36 +2,43 @@ Print config file path ## Usage ```sh -beeper config path [flags] +beeper config path (config where) [flags] ``` ## Aliases - `beeper config where` +## JSON Output + +Default JSON output is an object containing the `path` field. +Use `--json --results-only` to emit only `path`. + ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/config-set.md b/packages/cli/docs/commands/config-set.md index de3cf450..58ad58e9 100644 --- a/packages/cli/docs/commands/config-set.md +++ b/packages/cli/docs/commands/config-set.md @@ -2,7 +2,7 @@ Set a config value ## Usage ```sh -beeper config set [flags] +beeper config set (config add,config update) [flags] ``` ## Aliases @@ -20,26 +20,28 @@ beeper config set [flags] | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/config-unset.md b/packages/cli/docs/commands/config-unset.md index 49cdf897..e54763b9 100644 --- a/packages/cli/docs/commands/config-unset.md +++ b/packages/cli/docs/commands/config-unset.md @@ -2,7 +2,7 @@ Unset a config value ## Usage ```sh -beeper config unset [flags] +beeper config unset (config rm,config del,config remove) [flags] ``` ## Aliases @@ -20,26 +20,28 @@ beeper config unset [flags] | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/contacts-list.md b/packages/cli/docs/commands/contacts-list.md index 905ad815..b4989c72 100644 --- a/packages/cli/docs/commands/contacts-list.md +++ b/packages/cli/docs/commands/contacts-list.md @@ -2,46 +2,59 @@ List contacts ## Usage ```sh -beeper contacts list [flags] +beeper contacts list (contacts ls,contacts search,contacts find,contact list,contact ls,contact search,contact find) [] [flags] ``` ## Aliases +- `beeper contacts ls` - `beeper contacts search` - `beeper contacts find` +- `beeper contact list` +- `beeper contact ls` +- `beeper contact search` +- `beeper contact find` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Optional contact lookup query. Used when --query is omitted. | ## Flags | Name | Description | | --- | --- | -| `-a, --account , --acct` | Limit to account selector Repeatable. | +| `-a, --account=STRING, --acct` | Limit to account selector Repeatable. | | `--ids` | Print only contact user IDs Default: `false`. | -| `--limit ` | Maximum contacts to print Default: `50`. | -| `--query ` | Optional contact lookup query | +| `--limit=50` | Maximum contacts to print Default: `50`. | +| `--query=STRING` | Optional contact lookup query | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/contacts-show.md b/packages/cli/docs/commands/contacts-show.md new file mode 100644 index 00000000..cd90cb60 --- /dev/null +++ b/packages/cli/docs/commands/contacts-show.md @@ -0,0 +1,58 @@ +# beeper contacts show +Show one contact +## Usage +```sh +beeper contacts show (contacts get,contacts info,contact show,contact get,contact info) [] [flags] +``` +## Aliases + +- `beeper contacts get` +- `beeper contacts info` +- `beeper contact show` +- `beeper contact get` +- `beeper contact info` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Contact selector. Used when --jid is omitted. | + +## Flags + +| Name | Description | +| --- | --- | +| `-a, --account=STRING, --acct` | Limit to account selector Repeatable. | +| `--jid=STRING` | Contact JID or user ID | +| `--limit=10, --max` | Maximum candidates Default: `10`. | +| `--pick=INTEGER` | Select the Nth candidate | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/docs.md b/packages/cli/docs/commands/docs.md new file mode 100644 index 00000000..a987ddf3 --- /dev/null +++ b/packages/cli/docs/commands/docs.md @@ -0,0 +1,45 @@ +# beeper docs +Print command documentation locations +## Usage +```sh +beeper docs (help-docs) [flags] +``` +## Aliases + +- `beeper help-docs` + +## Flags + +| Name | Description | +| --- | --- | +| `--url, --url-only` | Print only the documentation URL Default: `false`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/doctor.md b/packages/cli/docs/commands/doctor.md index 5aa7f598..6bee4055 100644 --- a/packages/cli/docs/commands/doctor.md +++ b/packages/cli/docs/commands/doctor.md @@ -2,32 +2,44 @@ Run diagnostics for config, target reachability, auth, and readiness ## Usage ```sh -beeper doctor [flags] +beeper doctor (auth doctor) [flags] ``` +## Aliases + +- `beeper auth doctor` + +## Flags + +| Name | Description | +| --- | --- | +| `--connect` | Accepted for compatibility; doctor always checks selected target reachability Default: `false`. | + ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/exit-codes.md b/packages/cli/docs/commands/exit-codes.md index 7c449491..916d32c5 100644 --- a/packages/cli/docs/commands/exit-codes.md +++ b/packages/cli/docs/commands/exit-codes.md @@ -2,37 +2,41 @@ Print stable exit codes for automation ## Usage ```sh -beeper exit-codes [flags] +beeper exit-codes (agent exit-codes,agent exitcodes,agent exit-code,exitcodes) [flags] ``` ## Aliases - `beeper agent exit-codes` +- `beeper agent exitcodes` +- `beeper agent exit-code` - `beeper exitcodes` ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/export.md b/packages/cli/docs/commands/export.md index d5dbf167..e03dcdb0 100644 --- a/packages/cli/docs/commands/export.md +++ b/packages/cli/docs/commands/export.md @@ -8,40 +8,42 @@ beeper export [flags] | Name | Description | | --- | --- | -| `-a, --account , --acct` | Limit to account selector Repeatable. | -| `--chat ` | Limit to chat selector Repeatable. | +| `-a, --account=STRING, --acct` | Limit to account selector Repeatable. | +| `--chat=STRING` | Limit to chat selector Repeatable. | | `--force` | Re-export completed chats Default: `false`. | -| `--limit-chats ` | Maximum chats to export | -| `--limit-messages ` | Maximum messages per chat | -| `--max-participants ` | Maximum participants in chat.json Default: `500`. | +| `--limit-chats=INTEGER` | Maximum chats to export | +| `--limit-messages=INTEGER` | Maximum messages per chat | +| `--max-participants=500` | Maximum participants in chat.json Default: `500`. | | `--no-attachments` | Skip downloading attachments Default: `false`. | -| `--out ` | Export directory Default: `beeper-export`. | -| `--pick ` | Pick the Nth result when selector is ambiguous | +| `--out="beeper-export"` | Export directory Default: `beeper-export`. | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/groups-create.md b/packages/cli/docs/commands/groups-create.md new file mode 100644 index 00000000..d5804093 --- /dev/null +++ b/packages/cli/docs/commands/groups-create.md @@ -0,0 +1,52 @@ +# beeper groups create +Create a group chat +## Usage +```sh +beeper groups create (groups add,groups new,group create,group add,group new) [flags] +``` +## Aliases + +- `beeper groups add` +- `beeper groups new` +- `beeper group create` +- `beeper group add` +- `beeper group new` + +## Flags + +| Name | Description | +| --- | --- | +| `--account=STRING` | Account selector | +| `--name=STRING, --title` | Group name Required. | +| `--user=STRING` | Initial participant user ID, phone, or handle Repeatable. Required. | +| `--message=STRING` | Optional first message text | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/groups-description.md b/packages/cli/docs/commands/groups-description.md new file mode 100644 index 00000000..49096096 --- /dev/null +++ b/packages/cli/docs/commands/groups-description.md @@ -0,0 +1,56 @@ +# beeper groups description +Set or clear a group description +## Usage +```sh +beeper groups description (groups topic,group description,group topic) [] [flags] +``` +## Aliases + +- `beeper groups topic` +- `beeper group description` +- `beeper group topic` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Group chat selector. Used when --jid is omitted. | + +## Flags + +| Name | Description | +| --- | --- | +| `--jid=STRING, --chat` | Group chat selector | +| `--clear` | Clear the description Default: `false`. | +| `--description=STRING, --topic` | Group description | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/groups-list.md b/packages/cli/docs/commands/groups-list.md new file mode 100644 index 00000000..63bb8762 --- /dev/null +++ b/packages/cli/docs/commands/groups-list.md @@ -0,0 +1,50 @@ +# beeper groups list +List group chats +## Usage +```sh +beeper groups list (groups ls,group list,group ls) [flags] +``` +## Aliases + +- `beeper groups ls` +- `beeper group list` +- `beeper group ls` + +## Flags + +| Name | Description | +| --- | --- | +| `-a, --account=STRING, --acct` | Limit to account selector Repeatable. | +| `--ids` | Print preferred chat selectors Default: `false`. | +| `--limit=50` | Maximum groups to print Default: `50`. | +| `--query=STRING` | Optional group lookup query | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/groups-rename.md b/packages/cli/docs/commands/groups-rename.md new file mode 100644 index 00000000..e710397d --- /dev/null +++ b/packages/cli/docs/commands/groups-rename.md @@ -0,0 +1,53 @@ +# beeper groups rename +Rename a group +## Usage +```sh +beeper groups rename (group rename) [] [flags] +``` +## Aliases + +- `beeper group rename` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Group chat selector. Used when --jid is omitted. | + +## Flags + +| Name | Description | +| --- | --- | +| `--jid=STRING, --chat` | Group chat selector | +| `--name=STRING, --title` | New group name Required. | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/groups-show.md b/packages/cli/docs/commands/groups-show.md new file mode 100644 index 00000000..abaa963d --- /dev/null +++ b/packages/cli/docs/commands/groups-show.md @@ -0,0 +1,55 @@ +# beeper groups show +Show group details +## Usage +```sh +beeper groups show (groups info,group show,group info) [] [flags] +``` +## Aliases + +- `beeper groups info` +- `beeper group show` +- `beeper group info` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Group chat selector. Used when --jid is omitted. | + +## Flags + +| Name | Description | +| --- | --- | +| `--jid=STRING, --chat` | Group chat selector | +| `--max-participants=INTEGER` | Limit participants returned in group details | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/help.md b/packages/cli/docs/commands/help.md new file mode 100644 index 00000000..b9dd018e --- /dev/null +++ b/packages/cli/docs/commands/help.md @@ -0,0 +1,41 @@ +# beeper help +Show help for a command +## Usage +```sh +beeper help [ ...] [flags] +``` +## Arguments + +| Name | Description | +| --- | --- | +| `[ ...]` | Command path to describe | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/install-desktop.md b/packages/cli/docs/commands/install-desktop.md index a0e01233..c2d262d5 100644 --- a/packages/cli/docs/commands/install-desktop.md +++ b/packages/cli/docs/commands/install-desktop.md @@ -8,33 +8,35 @@ beeper install desktop [flags] | Name | Description | | --- | --- | -| `--channel ` | Install release channel Default: `stable`. Values: `stable`, `nightly`. | -| `--server-env ` | Server environment Default: `prod`. Values: `local`, `dev`, `staging`, `prod`. | +| `--channel="stable"` | Install release channel Default: `stable`. Values: `stable`, `nightly`. | +| `--server-env="prod"` | Server environment Default: `prod`. Values: `local`, `dev`, `staging`, `prod`. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/install-server.md b/packages/cli/docs/commands/install-server.md index 54d82236..00f59565 100644 --- a/packages/cli/docs/commands/install-server.md +++ b/packages/cli/docs/commands/install-server.md @@ -8,33 +8,35 @@ beeper install server [flags] | Name | Description | | --- | --- | -| `--channel ` | Install release channel Default: `stable`. Values: `stable`, `nightly`. | -| `--server-env ` | Server environment Default: `prod`. Values: `local`, `dev`, `staging`, `prod`. | +| `--channel="stable"` | Install release channel Default: `stable`. Values: `stable`, `nightly`. | +| `--server-env="prod"` | Server environment Default: `prod`. Values: `local`, `dev`, `staging`, `prod`. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/login.md b/packages/cli/docs/commands/login.md new file mode 100644 index 00000000..db4ab35b --- /dev/null +++ b/packages/cli/docs/commands/login.md @@ -0,0 +1,46 @@ +# beeper login +Start email sign-in for a target +## Usage +```sh +beeper login (auth add,auth login) [flags] +``` +## Aliases + +- `beeper auth add` +- `beeper auth login` + +## Arguments + +| Name | Description | +| --- | --- | +| `` | Email address | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/mcp.md b/packages/cli/docs/commands/mcp.md index 89e2c42e..a7d6c265 100644 --- a/packages/cli/docs/commands/mcp.md +++ b/packages/cli/docs/commands/mcp.md @@ -1,5 +1,5 @@ # beeper mcp -Run a typed MCP stdio server +Run a typed, allowlisted MCP server over stdio or HTTP ## Usage ```sh beeper mcp [flags] @@ -8,36 +8,54 @@ beeper mcp [flags] | Name | Description | | --- | --- | -| `--allow-tool , --tool` | Tool or command allowlist Repeatable. | +| `--allow-tool=ALLOW-TOOL,..., --tool` | Tool or service allowlist (default: all read-only tools). Examples: targets.*,messages_search,send Repeatable. | | `--allow-write` | Allow write-risk MCP tools Default: `false`. | +| `--transport="stdio"` | MCP transport Default: `stdio`. Values: `stdio`, `http`. | +| `--http-host="127.0.0.1"` | Host for --transport=http Default: `127.0.0.1`. | +| `--http-port=7331` | Port for --transport=http. Use 0 to choose a free port Default: `7331`. | +| `--http-path="/mcp"` | HTTP path for --transport=http Default: `/mcp`. | | `--list-tools` | Print enabled MCP tools as JSON and exit Default: `false`. | -| `--max-output-bytes ` | Maximum stdout/stderr bytes captured per tool call Default: `102400`. | -| `--timeout-seconds ` | Per-tool subprocess timeout Default: `60`. | +| `--max-output-bytes=102400` | Maximum stdout/stderr bytes captured per tool call Default: `102400`. | +| `--timeout-seconds=60` | Per-tool subprocess timeout Default: `60`. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | + +## Examples + +```sh +beeper mcp +``` +```sh +beeper mcp --allow-tool targets.*,messages --list-tools +``` +```sh +beeper mcp --allow-write --allow-tool send_text +``` diff --git a/packages/cli/docs/commands/me.md b/packages/cli/docs/commands/me.md new file mode 100644 index 00000000..c3d1c62d --- /dev/null +++ b/packages/cli/docs/commands/me.md @@ -0,0 +1,46 @@ +# beeper me +Show selected account and target identity +## Usage +```sh +beeper me (whoami,who-am-i) [flags] +``` +## Aliases + +- `beeper whoami` +- `beeper who-am-i` + +## Flags + +| Name | Description | +| --- | --- | +| `-a, --account=STRING, --acct` | Limit to account selector Repeatable. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/media-download.md b/packages/cli/docs/commands/media-download.md index 435bd23e..43caccf9 100644 --- a/packages/cli/docs/commands/media-download.md +++ b/packages/cli/docs/commands/media-download.md @@ -2,44 +2,56 @@ Download message media ## Usage ```sh -beeper media download [flags] +beeper media download (media dl,download,dl) [] [flags] ``` +## Aliases + +- `beeper media dl` +- `beeper download` +- `beeper dl` + ## Arguments | Name | Description | | --- | --- | -| `` | | +| `[]` | Media URL to download. Use --id with --chat to download from a message. | ## Flags | Name | Description | | --- | --- | -| `--out ` | Output directory; - streams to stdout Default: `.`. | +| `--out=".", --output` | Output directory or file; - streams to stdout Default: `.`. | +| `--chat=STRING` | Chat selector for --id message lookup | +| `--id=STRING` | Message ID containing media | +| `--index=1` | Attachment index to download, 1-based Default: `1`. | +| `--poster` | Download attachment poster image when available Default: `false`. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/media-message.md b/packages/cli/docs/commands/media-message.md new file mode 100644 index 00000000..50dbb8c1 --- /dev/null +++ b/packages/cli/docs/commands/media-message.md @@ -0,0 +1,51 @@ +# beeper media message +Download media for a message +## Usage +```sh +beeper media message [] [flags] +``` +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Message ID containing media. Used when --id is omitted. | + +## Flags + +| Name | Description | +| --- | --- | +| `--out=".", --output` | Output directory or file; - streams to stdout Default: `.`. | +| `--chat=STRING, --jid` | Chat selector Required. | +| `--id=STRING` | Message ID containing media | +| `--index=1` | Attachment index to download, 1-based Default: `1`. | +| `--poster` | Download attachment poster image when available Default: `false`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/messages-context.md b/packages/cli/docs/commands/messages-context.md index 4dac8af6..79a4555b 100644 --- a/packages/cli/docs/commands/messages-context.md +++ b/packages/cli/docs/commands/messages-context.md @@ -2,42 +2,50 @@ Show a message with surrounding context ## Usage ```sh -beeper messages context [flags] +beeper messages context [] [flags] ``` +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Message ID. Used when --id is omitted. | + ## Flags | Name | Description | | --- | --- | -| `--chat ` | Chat selector Required. | -| `--pick ` | Pick the Nth result when selector is ambiguous | -| `--id ` | Message ID Required. | -| `--after ` | Messages after target Default: `10`. | -| `--before ` | Messages before target Default: `10`. | +| `--chat=STRING, --jid` | Chat selector Required. | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | +| `--id=STRING` | Message ID | +| `--after=10` | Messages after target Default: `10`. | +| `--before=10` | Messages before target Default: `10`. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/messages-delete.md b/packages/cli/docs/commands/messages-delete.md index 68279f76..0740ef02 100644 --- a/packages/cli/docs/commands/messages-delete.md +++ b/packages/cli/docs/commands/messages-delete.md @@ -2,41 +2,55 @@ Delete a message ## Usage ```sh -beeper messages delete [flags] +beeper messages delete (messages rm,messages del,messages remove) [] [flags] ``` +## Aliases + +- `beeper messages rm` +- `beeper messages del` +- `beeper messages remove` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Message ID. Used when --id is omitted. | + ## Flags | Name | Description | | --- | --- | -| `--chat ` | Chat selector Required. | -| `--pick ` | Pick the Nth result when selector is ambiguous | -| `--id ` | Message ID Required. | +| `--chat=STRING, --jid` | Chat selector Required. | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | +| `--id=STRING` | Message ID | | `--for-everyone` | Delete for everyone when supported Default: `false`. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/messages-edit.md b/packages/cli/docs/commands/messages-edit.md index 15867ae3..89fd03d4 100644 --- a/packages/cli/docs/commands/messages-edit.md +++ b/packages/cli/docs/commands/messages-edit.md @@ -2,41 +2,54 @@ Edit a message ## Usage ```sh -beeper messages edit [flags] +beeper messages edit (messages update,messages set) [] [flags] ``` +## Aliases + +- `beeper messages update` +- `beeper messages set` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Message ID. Used when --id is omitted. | + ## Flags | Name | Description | | --- | --- | -| `--chat ` | Chat selector Required. | -| `--pick ` | Pick the Nth result when selector is ambiguous | -| `--id ` | Message ID Required. | -| `--message ` | New message text Required. | +| `--chat=STRING, --jid` | Chat selector Required. | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | +| `--id=STRING` | Message ID | +| `--message=STRING` | New message text Required. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/messages-export.md b/packages/cli/docs/commands/messages-export.md new file mode 100644 index 00000000..05a9fb36 --- /dev/null +++ b/packages/cli/docs/commands/messages-export.md @@ -0,0 +1,52 @@ +# beeper messages export +Export messages as JSON +## Usage +```sh +beeper messages export [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--after-cursor=STRING, --after` | Paginate messages newer than this message ID | +| `--asc` | Order oldest first Default: `false`. | +| `--before-cursor=STRING, --before` | Paginate messages older than this message ID | +| `--chat=STRING, --jid` | Chat selector Required. | +| `--limit=1000` | Maximum messages to export Default: `1000`. | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | +| `--sender=STRING` | me, others, or a specific user ID | +| `--from-me` | Only messages sent by me Default: `false`. | +| `--from-them` | Only messages sent by others Default: `false`. | +| `--type=STRING` | Only messages of this kind Values: `text`, `image`, `video`, `audio`, `document`, `file`, `link`. | +| `--has-media` | Only messages with media Default: `false`. | +| `--output=STRING, --out` | Write JSON export to file instead of stdout | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/messages-forward.md b/packages/cli/docs/commands/messages-forward.md new file mode 100644 index 00000000..5612a8b6 --- /dev/null +++ b/packages/cli/docs/commands/messages-forward.md @@ -0,0 +1,54 @@ +# beeper messages forward +Forward a message +## Usage +```sh +beeper messages forward [] [flags] +``` +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Source message ID. Used when --id is omitted. | + +## Flags + +| Name | Description | +| --- | --- | +| `--chat=STRING, --jid` | Chat selector Required. | +| `--id=STRING` | Source message ID | +| `--to=STRING` | Destination chat selector Required. | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | +| `--attachment-index=1` | Attachment index to forward when the message has media, 1-based Default: `1`. | +| `--post-send-wait="2s"` | Compatibility alias for waiting after forward, for example 2s or 500ms; 0 disables waiting Default: `2s`. | +| `--wait` | Wait until the forwarded message leaves pending state Default: `false`. | +| `--wait-timeout=30000` | Maximum wait time in ms when --wait is set Default: `30000`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/messages-list.md b/packages/cli/docs/commands/messages-list.md index 86442b5a..fd970eef 100644 --- a/packages/cli/docs/commands/messages-list.md +++ b/packages/cli/docs/commands/messages-list.md @@ -2,7 +2,7 @@ List chat messages ## Usage ```sh -beeper messages list [flags] +beeper messages list (messages ls) [flags] ``` ## Aliases @@ -12,39 +12,45 @@ beeper messages list [flags] | Name | Description | | --- | --- | -| `--after-cursor ` | Paginate messages newer than this message ID | +| `--after-cursor=STRING, --after` | Paginate messages newer than this message ID | | `--asc` | Order oldest first Default: `false`. | -| `--before-cursor ` | Paginate messages older than this message ID | -| `--chat ` | Chat selector Required. | +| `--before-cursor=STRING, --before` | Paginate messages older than this message ID | +| `--chat=STRING, --jid` | Chat selector Required. | +| `--limit=50` | Maximum messages to print Default: `50`. | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | +| `--sender=STRING` | me, others, or a specific user ID | +| `--from-me` | Only messages sent by me Default: `false`. | +| `--from-them` | Only messages sent by others Default: `false`. | +| `--type=STRING` | Only messages of this kind Values: `text`, `image`, `video`, `audio`, `document`, `file`, `link`. | +| `--has-media` | Only messages with media Default: `false`. | | `--ids` | Print only message IDs Default: `false`. | -| `--limit ` | Maximum messages to print Default: `50`. | -| `--pick ` | Pick the Nth result when selector is ambiguous | -| `--sender ` | me, others, or a specific user ID | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/messages-revoke.md b/packages/cli/docs/commands/messages-revoke.md new file mode 100644 index 00000000..2ab5b919 --- /dev/null +++ b/packages/cli/docs/commands/messages-revoke.md @@ -0,0 +1,49 @@ +# beeper messages revoke +Delete a sent message for everyone +## Usage +```sh +beeper messages revoke [] [flags] +``` +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Message ID. Used when --id is omitted. | + +## Flags + +| Name | Description | +| --- | --- | +| `--chat=STRING, --jid` | Chat selector Required. | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | +| `--id=STRING` | Message ID | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/messages-search.md b/packages/cli/docs/commands/messages-search.md index 6f30b055..64d17f1f 100644 --- a/packages/cli/docs/commands/messages-search.md +++ b/packages/cli/docs/commands/messages-search.md @@ -2,62 +2,67 @@ Search messages across chats ## Usage ```sh -beeper messages search [query] [flags] +beeper messages search (messages find,search,find) [] [flags] ``` ## Aliases - `beeper messages find` +- `beeper search` +- `beeper find` ## Arguments | Name | Description | | --- | --- | -| `[query]` | | +| `[]` | Search query. Optional when a filter such as --sender or --has-media is provided. | ## Flags | Name | Description | | --- | --- | -| `-a, --account , --acct` | Limit to account selector Repeatable. | -| `--chat ` | Limit to a chat selector Repeatable. | -| `--chat-type ` | Only group chats or direct messages Values: `group`, `single`. | -| `--after ` | Only messages at or after this ISO timestamp | -| `--before ` | Only messages at or before this ISO timestamp | +| `-a, --account=STRING, --acct` | Limit to account selector Repeatable. | +| `--chat=STRING` | Limit to a chat selector Repeatable. | +| `--chat-type=STRING` | Only group chats or direct messages Values: `group`, `single`. | +| `--after=STRING` | Only messages at or after this ISO timestamp | +| `--before=STRING` | Only messages at or before this ISO timestamp | | `--exclude-low-priority` | Exclude low-priority chats | | `--ids` | Print only message IDs Default: `false`. | | `--include-muted` | Include muted chats Default: `true`. | -| `--limit , --max` | Maximum results Default: `50`. | -| `--media ` | Filter by media type Values: `any`, `video`, `image`, `link`, `file`. Repeatable. | -| `--sender ` | me, others, or a user ID | +| `--limit=50, --max` | Maximum results Default: `50`. | +| `--media=STRING` | Filter by media type Values: `any`, `video`, `image`, `link`, `file`. Repeatable. | +| `--sender=STRING, --from` | me, others, or a user ID | +| `--has-media` | Only messages with media Default: `false`. | | `--fail-empty, --non-empty, --require-results` | Exit with code 3 if no results Default: `false`. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | ## Examples diff --git a/packages/cli/docs/commands/messages-show.md b/packages/cli/docs/commands/messages-show.md new file mode 100644 index 00000000..aa60c902 --- /dev/null +++ b/packages/cli/docs/commands/messages-show.md @@ -0,0 +1,54 @@ +# beeper messages show +Show one message +## Usage +```sh +beeper messages show (messages get,messages info) [] [flags] +``` +## Aliases + +- `beeper messages get` +- `beeper messages info` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Message ID. Used when --id is omitted. | + +## Flags + +| Name | Description | +| --- | --- | +| `--chat=STRING, --jid` | Chat selector Required. | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | +| `--id=STRING` | Message ID | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/presence-paused.md b/packages/cli/docs/commands/presence-paused.md new file mode 100644 index 00000000..e6aef206 --- /dev/null +++ b/packages/cli/docs/commands/presence-paused.md @@ -0,0 +1,42 @@ +# beeper presence paused +Send a 'paused' indicator (stop typing) to a chat +## Usage +```sh +beeper presence paused [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--to=STRING` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/presence-typing.md b/packages/cli/docs/commands/presence-typing.md new file mode 100644 index 00000000..9dea6705 --- /dev/null +++ b/packages/cli/docs/commands/presence-typing.md @@ -0,0 +1,43 @@ +# beeper presence typing +Send a 'composing' (typing) indicator to a chat +## Usage +```sh +beeper presence typing [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--to=STRING` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | +| `--duration=INTEGER` | Seconds to keep typing before sending paused | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/presence.md b/packages/cli/docs/commands/presence.md new file mode 100644 index 00000000..71bf49af --- /dev/null +++ b/packages/cli/docs/commands/presence.md @@ -0,0 +1,44 @@ +# beeper presence +Send presence indicators +## Usage +```sh +beeper presence [flags] +``` +## Flags + +| Name | Description | +| --- | --- | +| `--to=STRING` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | +| `--duration=INTEGER` | Seconds to keep typing before sending paused | +| `--state="typing"` | Presence indicator to send Default: `typing`. Values: `typing`, `paused`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/remove-account.md b/packages/cli/docs/commands/remove-account.md deleted file mode 100644 index 99ec38cf..00000000 --- a/packages/cli/docs/commands/remove-account.md +++ /dev/null @@ -1,44 +0,0 @@ -# beeper remove account -Remove an account -## Usage -```sh -beeper remove account [flags] -``` -## Aliases - -- `beeper accounts remove` -- `beeper accounts rm` - -## Arguments - -| Name | Description | -| --- | --- | -| `` | Account selector | - -## Global Flags - -| Name | Description | -| --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | -| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | -| `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/remove-target.md b/packages/cli/docs/commands/remove-target.md deleted file mode 100644 index d40cf119..00000000 --- a/packages/cli/docs/commands/remove-target.md +++ /dev/null @@ -1,44 +0,0 @@ -# beeper remove target -Remove a target -## Usage -```sh -beeper remove target [flags] -``` -## Aliases - -- `beeper targets remove` -- `beeper targets rm` - -## Arguments - -| Name | Description | -| --- | --- | -| `` | Target name | - -## Global Flags - -| Name | Description | -| --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | -| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | -| `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/resolve-account.md b/packages/cli/docs/commands/resolve-account.md index 0c399ef0..3f698db0 100644 --- a/packages/cli/docs/commands/resolve-account.md +++ b/packages/cli/docs/commands/resolve-account.md @@ -8,38 +8,40 @@ beeper resolve account [flags] | Name | Description | | --- | --- | -| `` | | +| `` | Account selector | ## Flags | Name | Description | | --- | --- | -| `--pick ` | Select the Nth candidate | +| `--pick=INTEGER` | Select the Nth candidate | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/resolve-bridge.md b/packages/cli/docs/commands/resolve-bridge.md index d26680c5..e397fd58 100644 --- a/packages/cli/docs/commands/resolve-bridge.md +++ b/packages/cli/docs/commands/resolve-bridge.md @@ -8,38 +8,40 @@ beeper resolve bridge [flags] | Name | Description | | --- | --- | -| `` | | +| `` | Bridge selector | ## Flags | Name | Description | | --- | --- | -| `--pick ` | Select the Nth candidate | +| `--pick=INTEGER` | Select the Nth candidate | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/resolve-chat.md b/packages/cli/docs/commands/resolve-chat.md index b84ada8e..3f22e6e8 100644 --- a/packages/cli/docs/commands/resolve-chat.md +++ b/packages/cli/docs/commands/resolve-chat.md @@ -8,40 +8,42 @@ beeper resolve chat [flags] | Name | Description | | --- | --- | -| `` | | +| `` | Chat selector | ## Flags | Name | Description | | --- | --- | -| `-a, --account , --acct` | Limit to account selector Repeatable. | -| `--limit , --max` | Maximum candidates Default: `10`. | -| `--pick ` | Select the Nth candidate | +| `-a, --account=STRING, --acct` | Limit to account selector Repeatable. | +| `--limit=10, --max` | Maximum candidates Default: `10`. | +| `--pick=INTEGER` | Select the Nth candidate | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/resolve-contact.md b/packages/cli/docs/commands/resolve-contact.md index 6d43261a..7628e03f 100644 --- a/packages/cli/docs/commands/resolve-contact.md +++ b/packages/cli/docs/commands/resolve-contact.md @@ -8,40 +8,42 @@ beeper resolve contact [flags] | Name | Description | | --- | --- | -| `` | | +| `` | Contact selector | ## Flags | Name | Description | | --- | --- | -| `-a, --account , --acct` | Limit to account selector Repeatable. | -| `--limit , --max` | Maximum candidates Default: `10`. | -| `--pick ` | Select the Nth candidate | +| `-a, --account=STRING, --acct` | Limit to account selector Repeatable. | +| `--limit=10, --max` | Maximum candidates Default: `10`. | +| `--pick=INTEGER` | Select the Nth candidate | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/resolve-target.md b/packages/cli/docs/commands/resolve-target.md index 9797bb5f..809cb1b7 100644 --- a/packages/cli/docs/commands/resolve-target.md +++ b/packages/cli/docs/commands/resolve-target.md @@ -8,38 +8,40 @@ beeper resolve target [flags] | Name | Description | | --- | --- | -| `` | | +| `` | Target selector | ## Flags | Name | Description | | --- | --- | -| `--pick ` | Select the Nth candidate | +| `--pick=INTEGER` | Select the Nth candidate | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/schema.md b/packages/cli/docs/commands/schema.md index 74e25255..11d5235a 100644 --- a/packages/cli/docs/commands/schema.md +++ b/packages/cli/docs/commands/schema.md @@ -1,8 +1,8 @@ # beeper schema -Print machine-readable command and flag schema +Machine-readable command/flag schema ## Usage ```sh -beeper schema ... [flags] +beeper schema (help-json,helpjson) [ ...] [flags] ``` ## Aliases @@ -13,32 +13,40 @@ beeper schema ... [flags] | Name | Description | | --- | --- | -| `[command ...]` | | +| `[ ...]` | Optional command path to describe. Default: entire CLI | + +## Flags + +| Name | Description | +| --- | --- | +| `--include-hidden` | Include hidden commands Default: `false`. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/search-all.md b/packages/cli/docs/commands/search-all.md new file mode 100644 index 00000000..25feb8b1 --- /dev/null +++ b/packages/cli/docs/commands/search-all.md @@ -0,0 +1,46 @@ +# beeper search all +Search chats, group participants, and messages together +## Usage +```sh +beeper search all (search-all,find-all) [flags] +``` +## Aliases + +- `beeper search-all` +- `beeper find-all` + +## Arguments + +| Name | Description | +| --- | --- | +| `` | Search query | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/send-file.md b/packages/cli/docs/commands/send-file.md index d3c10452..9ea2f552 100644 --- a/packages/cli/docs/commands/send-file.md +++ b/packages/cli/docs/commands/send-file.md @@ -2,46 +2,57 @@ Send a file message ## Usage ```sh -beeper send file [flags] +beeper send file [] [flags] ``` +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Local file path to upload. Used when --file is omitted. | + ## Flags | Name | Description | | --- | --- | -| `--to ` | Chat selector Required. | -| `--pick ` | Pick the Nth result when selector is ambiguous | -| `--reply-to ` | Send as a reply to this message ID | +| `--to=STRING` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | +| `--reply-to=STRING` | Send as a reply to this message ID | +| `--reply-to-sender=STRING` | Accepted for compatibility; Beeper replies only need --reply-to | | `--wait` | Wait until the message leaves pending state Default: `false`. | -| `--wait-timeout ` | Maximum wait time in ms when --wait is set Default: `30000`. | -| `--file ` | Local file path to upload Required. | -| `--caption ` | Optional caption for file messages | -| `--filename ` | Override displayed filename | -| `--mime ` | Override MIME type | +| `--wait-timeout=30000` | Maximum wait time in ms when --wait is set Default: `30000`. | +| `--post-send-wait=STRING` | Compatibility alias for waiting after send, for example 2s or 500ms; 0 disables waiting | +| `--file=STRING` | Local file path to upload | +| `--caption=STRING` | Optional caption for file messages | +| `--filename=STRING` | Override displayed filename | +| `--mime=STRING` | Override MIME type | +| `--ptt` | Send audio as a voice note Default: `false`. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/send-presence.md b/packages/cli/docs/commands/send-presence.md index 195f511f..8df9a769 100644 --- a/packages/cli/docs/commands/send-presence.md +++ b/packages/cli/docs/commands/send-presence.md @@ -8,35 +8,37 @@ beeper send presence [flags] | Name | Description | | --- | --- | -| `--to ` | Chat selector Required. | -| `--pick ` | Pick the Nth result when selector is ambiguous | -| `--duration ` | Seconds to keep typing before sending paused | -| `--state ` | Presence indicator to send Default: `typing`. Values: `typing`, `paused`. | +| `--to=STRING` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | +| `--duration=INTEGER` | Seconds to keep typing before sending paused | +| `--state="typing"` | Presence indicator to send Default: `typing`. Values: `typing`, `paused`. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/send-react.md b/packages/cli/docs/commands/send-react.md index 2c1a4c5e..f12f516b 100644 --- a/packages/cli/docs/commands/send-react.md +++ b/packages/cli/docs/commands/send-react.md @@ -2,43 +2,56 @@ Send or remove a reaction ## Usage ```sh -beeper send react [flags] +beeper send react (send reaction) [] [flags] ``` +## Aliases + +- `beeper send reaction` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Message ID to react to. Used when --id is omitted. | + ## Flags | Name | Description | | --- | --- | -| `--to ` | Chat selector Required. | -| `--pick ` | Pick the Nth result when selector is ambiguous | -| `--id ` | Message ID to react to Required. | -| `--reaction ` | Reaction key Required. | +| `--to=STRING` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | +| `--id=STRING` | Message ID to react to | +| `--reaction="+1"` | Reaction key; empty string removes the reaction Default: `+1`. | | `--remove` | Remove the reaction Default: `false`. | -| `--transaction ` | Optional transaction ID for deduplication | +| `--post-send-wait=STRING` | Compatibility alias for waiting after send, for example 2s or 500ms; 0 disables waiting | +| `--transaction=STRING` | Optional transaction ID for deduplication | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/send-sticker.md b/packages/cli/docs/commands/send-sticker.md index b1abc9b5..6ce2b6fc 100644 --- a/packages/cli/docs/commands/send-sticker.md +++ b/packages/cli/docs/commands/send-sticker.md @@ -2,45 +2,55 @@ Send a sticker ## Usage ```sh -beeper send sticker [flags] +beeper send sticker [] [flags] ``` +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Local sticker file path to upload. Used when --file is omitted. | + ## Flags | Name | Description | | --- | --- | -| `--to ` | Chat selector Required. | -| `--pick ` | Pick the Nth result when selector is ambiguous | -| `--reply-to ` | Send as a reply to this message ID | +| `--to=STRING` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | +| `--reply-to=STRING` | Send as a reply to this message ID | +| `--reply-to-sender=STRING` | Accepted for compatibility; Beeper replies only need --reply-to | | `--wait` | Wait until the message leaves pending state Default: `false`. | -| `--wait-timeout ` | Maximum wait time in ms when --wait is set Default: `30000`. | -| `--file ` | Local sticker file path to upload Required. | -| `--filename ` | Override displayed filename | -| `--mime ` | Override MIME type | +| `--wait-timeout=30000` | Maximum wait time in ms when --wait is set Default: `30000`. | +| `--post-send-wait=STRING` | Compatibility alias for waiting after send, for example 2s or 500ms; 0 disables waiting | +| `--file=STRING` | Local sticker file path to upload | +| `--filename=STRING` | Override displayed filename | +| `--mime=STRING` | Override MIME type | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/send-text.md b/packages/cli/docs/commands/send-text.md index fdd4b957..02b86287 100644 --- a/packages/cli/docs/commands/send-text.md +++ b/packages/cli/docs/commands/send-text.md @@ -2,47 +2,65 @@ Send a text message ## Usage ```sh -beeper send text [flags] +beeper send text (message,msg) [] [ ...] [flags] ``` +## Aliases + +- `beeper message` +- `beeper msg` + +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Chat selector. Used when --to is omitted. | +| `[ ...]` | Message text. Used when --message and --message-file are omitted. | + ## Flags | Name | Description | | --- | --- | -| `--to ` | Chat selector Required. | -| `--pick ` | Pick the Nth result when selector is ambiguous | -| `--reply-to ` | Send as a reply to this message ID | +| `--to=STRING` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | +| `--reply-to=STRING` | Send as a reply to this message ID | +| `--reply-to-sender=STRING` | Accepted for compatibility; Beeper replies only need --reply-to | | `--wait` | Wait until the message leaves pending state Default: `false`. | -| `--wait-timeout ` | Maximum wait time in ms when --wait is set Default: `30000`. | -| `--message ` | Message text to send | +| `--wait-timeout=30000` | Maximum wait time in ms when --wait is set Default: `30000`. | +| `--post-send-wait=STRING` | Compatibility alias for waiting after send, for example 2s or 500ms; 0 disables waiting | +| `--message=STRING` | Message text to send | | `--message-escapes` | Interpret backslash escapes in --message Default: `false`. | -| `--message-file ` | Read message text from a file path; '-' reads stdin | -| `--mention ` | User ID to mention Repeatable. | +| `--message-file=STRING` | Read message text from a file path; '-' reads stdin | +| `--mention=STRING` | User ID to mention Repeatable. | | `--no-preview` | Disable automatic link preview Default: `false`. | +| `--ephemeral` | Send with this chat's disappearing-message timer Default: `false`. | +| `--ephemeral-duration=STRING` | Set the chat disappearing-message timer before sending, for example 24h, 7d, 90d, or 168h | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/send-voice.md b/packages/cli/docs/commands/send-voice.md index e59da362..b54825e6 100644 --- a/packages/cli/docs/commands/send-voice.md +++ b/packages/cli/docs/commands/send-voice.md @@ -2,46 +2,56 @@ Send a voice note ## Usage ```sh -beeper send voice [flags] +beeper send voice [] [flags] ``` +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Local voice note file path to upload. Used when --file is omitted. | + ## Flags | Name | Description | | --- | --- | -| `--to ` | Chat selector Required. | -| `--pick ` | Pick the Nth result when selector is ambiguous | -| `--reply-to ` | Send as a reply to this message ID | +| `--to=STRING` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | +| `--reply-to=STRING` | Send as a reply to this message ID | +| `--reply-to-sender=STRING` | Accepted for compatibility; Beeper replies only need --reply-to | | `--wait` | Wait until the message leaves pending state Default: `false`. | -| `--wait-timeout ` | Maximum wait time in ms when --wait is set Default: `30000`. | -| `--file ` | Local voice note file path to upload Required. | -| `--duration ` | Duration in seconds | -| `--filename ` | Override displayed filename | -| `--mime ` | Override MIME type | +| `--wait-timeout=30000` | Maximum wait time in ms when --wait is set Default: `30000`. | +| `--post-send-wait=STRING` | Compatibility alias for waiting after send, for example 2s or 500ms; 0 disables waiting | +| `--file=STRING` | Local voice note file path to upload | +| `--duration=INTEGER` | Duration in seconds | +| `--filename=STRING` | Override displayed filename | +| `--mime=STRING` | Override MIME type | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/send.md b/packages/cli/docs/commands/send.md new file mode 100644 index 00000000..11325c3e --- /dev/null +++ b/packages/cli/docs/commands/send.md @@ -0,0 +1,61 @@ +# beeper send +Send a text message +## Usage +```sh +beeper send [] [ ...] [flags] +``` +## Arguments + +| Name | Description | +| --- | --- | +| `[]` | Chat selector. Used when --to is omitted. | +| `[ ...]` | Message text. Used when --message and --message-file are omitted. | + +## Flags + +| Name | Description | +| --- | --- | +| `--to=STRING` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | +| `--reply-to=STRING` | Send as a reply to this message ID | +| `--reply-to-sender=STRING` | Accepted for compatibility; Beeper replies only need --reply-to | +| `--wait` | Wait until the message leaves pending state Default: `false`. | +| `--wait-timeout=30000` | Maximum wait time in ms when --wait is set Default: `30000`. | +| `--post-send-wait=STRING` | Compatibility alias for waiting after send, for example 2s or 500ms; 0 disables waiting | +| `--message=STRING` | Message text to send | +| `--message-escapes` | Interpret backslash escapes in --message Default: `false`. | +| `--message-file=STRING` | Read message text from a file path; '-' reads stdin | +| `--mention=STRING` | User ID to mention Repeatable. | +| `--no-preview` | Disable automatic link preview Default: `false`. | +| `--ephemeral` | Send with this chat's disappearing-message timer Default: `false`. | +| `--ephemeral-duration=STRING` | Set the chat disappearing-message timer before sending, for example 24h, 7d, 90d, or 168h | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/setup.md b/packages/cli/docs/commands/setup.md index 69dd1008..ff5b5a05 100644 --- a/packages/cli/docs/commands/setup.md +++ b/packages/cli/docs/commands/setup.md @@ -10,42 +10,44 @@ beeper setup [flags] | --- | --- | | `--local` | Use the local Beeper Desktop session on this device Default: `false`. | | `--oauth` | Authorize the target with browser OAuth/PKCE Default: `false`. | -| `--remote ` | Connect to a remote Beeper Desktop or Server URL | +| `--remote=STRING` | Connect to a remote Beeper Desktop or Server URL | | `--server` | Set up a local Beeper Server target Default: `false`. | | `--desktop` | Set up a local Beeper Desktop target Default: `false`. | | `--install` | Allow installing a missing local runtime Default: `false`. | -| `--channel ` | Install release channel Default: `stable`. Values: `stable`, `nightly`. | -| `--server-env ` | Server environment Default: `prod`. Values: `local`, `dev`, `staging`, `prod`. | -| `--email ` | Sign in with an email address | -| `--username ` | Username to use if setup creates a new account | +| `--channel="stable"` | Install release channel Default: `stable`. Values: `stable`, `nightly`. | +| `--server-env="prod"` | Server environment Default: `prod`. Values: `local`, `dev`, `staging`, `prod`. | +| `--email=STRING` | Sign in with an email address | +| `--username=STRING` | Username to use if setup creates a new account | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | ## Examples diff --git a/packages/cli/docs/commands/status.md b/packages/cli/docs/commands/status.md index 95cedf05..899738d7 100644 --- a/packages/cli/docs/commands/status.md +++ b/packages/cli/docs/commands/status.md @@ -1,8 +1,8 @@ # beeper status -Show selected target and setup readiness +Show auth, config, selected target, and setup readiness ## Usage ```sh -beeper status [target] [flags] +beeper status (st) [] [flags] ``` ## Aliases @@ -12,32 +12,34 @@ beeper status [target] [flags] | Name | Description | | --- | --- | -| `[target]` | Target name. Defaults to the selected target. | +| `[]` | Target name. Defaults to the selected target. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/targets-add.md b/packages/cli/docs/commands/targets-add.md index 49d3c0e3..d4df78e8 100644 --- a/packages/cli/docs/commands/targets-add.md +++ b/packages/cli/docs/commands/targets-add.md @@ -2,14 +2,18 @@ Add a remote Beeper Desktop or Server target ## Usage ```sh -beeper targets add [flags] +beeper targets add (target add) [flags] ``` +## Aliases + +- `beeper target add` + ## Arguments | Name | Description | | --- | --- | -| `` | | -| `` | | +| `` | Target name | +| `` | Target base URL | ## Flags @@ -21,26 +25,28 @@ beeper targets add [flags] | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/targets-list.md b/packages/cli/docs/commands/targets-list.md index 9a8e4056..ac727dc9 100644 --- a/packages/cli/docs/commands/targets-list.md +++ b/packages/cli/docs/commands/targets-list.md @@ -2,36 +2,45 @@ List configured Beeper targets ## Usage ```sh -beeper targets list [flags] +beeper targets list (targets ls,target list,target ls) [flags] ``` ## Aliases - `beeper targets ls` +- `beeper target list` +- `beeper target ls` + +## JSON Output + +Default JSON output is an object containing the `targets` field. +Use `--json --results-only` to emit only `targets`. ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/targets-logs.md b/packages/cli/docs/commands/targets-logs.md index dd8c9e87..718abe18 100644 --- a/packages/cli/docs/commands/targets-logs.md +++ b/packages/cli/docs/commands/targets-logs.md @@ -2,46 +2,52 @@ Print logs for a local Beeper Desktop or Server install ## Usage ```sh -beeper targets logs [name] [flags] +beeper targets logs (target logs) [] [flags] ``` +## Aliases + +- `beeper target logs` + ## Arguments | Name | Description | | --- | --- | -| `[name]` | | +| `[]` | Target name. Defaults to the selected target. | ## Flags | Name | Description | | --- | --- | -| `--lines ` | Lines to print from each log file Default: `200`. | -| `--files ` | Desktop log files to print, newest first Default: `5`. | +| `--lines=200` | Lines to print from each log file Default: `200`. | +| `--files=5` | Desktop log files to print, newest first Default: `5`. | | `--all` | Print all matching log files instead of only recent files Default: `false`. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/targets-remove.md b/packages/cli/docs/commands/targets-remove.md new file mode 100644 index 00000000..07569124 --- /dev/null +++ b/packages/cli/docs/commands/targets-remove.md @@ -0,0 +1,50 @@ +# beeper targets remove +Remove a target +## Usage +```sh +beeper targets remove (targets rm,targets del,remove target,target remove,target rm,target del) [flags] +``` +## Aliases + +- `beeper targets rm` +- `beeper targets del` +- `beeper remove target` +- `beeper target remove` +- `beeper target rm` +- `beeper target del` + +## Arguments + +| Name | Description | +| --- | --- | +| `` | Target name | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/targets-runtime-restart.md b/packages/cli/docs/commands/targets-runtime-restart.md index e15eeb52..f824bc8e 100644 --- a/packages/cli/docs/commands/targets-runtime-restart.md +++ b/packages/cli/docs/commands/targets-runtime-restart.md @@ -2,38 +2,44 @@ Restart a local server runtime ## Usage ```sh -beeper targets runtime restart [name] [flags] +beeper targets runtime restart (target runtime restart) [] [flags] ``` +## Aliases + +- `beeper target runtime restart` + ## Arguments | Name | Description | | --- | --- | -| `[name]` | | +| `[]` | Target name. Defaults to the selected target. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/targets-runtime-start.md b/packages/cli/docs/commands/targets-runtime-start.md index 53390b7b..f41bd01c 100644 --- a/packages/cli/docs/commands/targets-runtime-start.md +++ b/packages/cli/docs/commands/targets-runtime-start.md @@ -2,38 +2,44 @@ Start a local target runtime ## Usage ```sh -beeper targets runtime start [name] [flags] +beeper targets runtime start (target runtime start) [] [flags] ``` +## Aliases + +- `beeper target runtime start` + ## Arguments | Name | Description | | --- | --- | -| `[name]` | | +| `[]` | Target name. Defaults to the selected target. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/targets-runtime-stop.md b/packages/cli/docs/commands/targets-runtime-stop.md index 81f51723..fbc753b0 100644 --- a/packages/cli/docs/commands/targets-runtime-stop.md +++ b/packages/cli/docs/commands/targets-runtime-stop.md @@ -2,38 +2,44 @@ Stop a local server runtime ## Usage ```sh -beeper targets runtime stop [name] [flags] +beeper targets runtime stop (target runtime stop) [] [flags] ``` +## Aliases + +- `beeper target runtime stop` + ## Arguments | Name | Description | | --- | --- | -| `[name]` | | +| `[]` | Target name. Defaults to the selected target. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/targets-tunnel.md b/packages/cli/docs/commands/targets-tunnel.md index 03de60cb..9545d279 100644 --- a/packages/cli/docs/commands/targets-tunnel.md +++ b/packages/cli/docs/commands/targets-tunnel.md @@ -2,48 +2,54 @@ Expose a target through Cloudflare Tunnel ## Usage ```sh -beeper targets tunnel [name] [flags] +beeper targets tunnel (target tunnel) [] [flags] ``` +## Aliases + +- `beeper target tunnel` + ## Arguments | Name | Description | | --- | --- | -| `[name]` | Target name. Defaults to the selected target. | +| `[]` | Target name. Defaults to the selected target. | ## Flags | Name | Description | | --- | --- | -| `--cloudflared-path ` | Path to cloudflared. Also configurable with BEEPER_CLOUDFLARED_PATH. | +| `--cloudflared-path=STRING` | Path to cloudflared. Also configurable with BEEPER_CLOUDFLARED_PATH. | | `--install` | Download the pinned cloudflared binary if missing or outdated Default: `false`. | -| `--retries ` | Startup retries before giving up Default: `5`. | -| `--timeout ` | Startup timeout, for example 40s or 60000ms | +| `--retries=5` | Startup retries before giving up Default: `5`. | +| `--timeout=STRING` | Startup timeout, for example 40s or 60000ms | | `--url-only` | Print only the public tunnel URL Default: `false`. | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/targets-use.md b/packages/cli/docs/commands/targets-use.md new file mode 100644 index 00000000..bde56c06 --- /dev/null +++ b/packages/cli/docs/commands/targets-use.md @@ -0,0 +1,46 @@ +# beeper targets use +Select the default target +## Usage +```sh +beeper targets use (use target,target use) [flags] +``` +## Aliases + +- `beeper use target` +- `beeper target use` + +## Arguments + +| Name | Description | +| --- | --- | +| `` | Target name | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/upload.md b/packages/cli/docs/commands/upload.md new file mode 100644 index 00000000..9c0696bc --- /dev/null +++ b/packages/cli/docs/commands/upload.md @@ -0,0 +1,62 @@ +# beeper upload +Send a file message +## Usage +```sh +beeper upload (up,put) [flags] +``` +## Aliases + +- `beeper up` +- `beeper put` + +## Arguments + +| Name | Description | +| --- | --- | +| `` | Local file path to upload | + +## Flags + +| Name | Description | +| --- | --- | +| `--to=STRING` | Chat selector | +| `--pick=INTEGER` | Pick the Nth result when selector is ambiguous | +| `--reply-to=STRING` | Send as a reply to this message ID | +| `--reply-to-sender=STRING` | Accepted for compatibility; Beeper replies only need --reply-to | +| `--wait` | Wait until the message leaves pending state Default: `false`. | +| `--wait-timeout=30000` | Maximum wait time in ms when --wait is set Default: `30000`. | +| `--post-send-wait=STRING` | Compatibility alias for waiting after send, for example 2s or 500ms; 0 disables waiting | +| `--caption=STRING` | Optional caption for file messages | +| `--filename=STRING` | Override displayed filename | +| `--mime=STRING` | Override MIME type | +| `--ptt` | Send audio as a voice note Default: `false`. | + +## Global Flags + +| Name | Description | +| --- | --- | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | +| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | +| `--full` | Disable truncation in human table output Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/use-account.md b/packages/cli/docs/commands/use-account.md deleted file mode 100644 index bdc409db..00000000 --- a/packages/cli/docs/commands/use-account.md +++ /dev/null @@ -1,43 +0,0 @@ -# beeper use account -Select the default account -## Usage -```sh -beeper use account [flags] -``` -## Aliases - -- `beeper accounts use` - -## Arguments - -| Name | Description | -| --- | --- | -| `` | Account selector | - -## Global Flags - -| Name | Description | -| --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | -| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | -| `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/use-target.md b/packages/cli/docs/commands/use-target.md deleted file mode 100644 index ccc7900d..00000000 --- a/packages/cli/docs/commands/use-target.md +++ /dev/null @@ -1,43 +0,0 @@ -# beeper use target -Select the default target -## Usage -```sh -beeper use target [flags] -``` -## Aliases - -- `beeper targets use` - -## Arguments - -| Name | Description | -| --- | --- | -| `` | Target name | - -## Global Flags - -| Name | Description | -| --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | -| `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | -| `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | diff --git a/packages/cli/docs/commands/version.md b/packages/cli/docs/commands/version.md index 1066e2b5..d057341e 100644 --- a/packages/cli/docs/commands/version.md +++ b/packages/cli/docs/commands/version.md @@ -1,5 +1,5 @@ # beeper version -Print CLI version +Print version ## Usage ```sh beeper version [flags] @@ -8,26 +8,28 @@ beeper version [flags] | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/docs/commands/watch.md b/packages/cli/docs/commands/watch.md index fe4d55da..a74fcefa 100644 --- a/packages/cli/docs/commands/watch.md +++ b/packages/cli/docs/commands/watch.md @@ -8,37 +8,40 @@ beeper watch [flags] | Name | Description | | --- | --- | -| `--chat ` | Chat ID to subscribe to; defaults to all chats Repeatable. | -| `--exclude-type ` | Drop events of these types Values: `chat.upserted`, `chat.deleted`, `message.upserted`, `message.deleted`, `message.stream`. Repeatable. | -| `--include-type ` | Only forward events of these types Values: `chat.upserted`, `chat.deleted`, `message.upserted`, `message.deleted`, `message.stream`. Repeatable. | -| `--webhook ` | Forward each event to this URL as POST | -| `--webhook-queue ` | Maximum pending webhook deliveries Default: `64`. | -| `--webhook-secret ` | HMAC-SHA256 secret for X-Beeper-Signature | +| `--chat=STRING` | Chat ID to subscribe to; defaults to all chats Repeatable. | +| `--exclude-type=STRING` | Drop events of these types Values: `chat.upserted`, `chat.deleted`, `message.upserted`, `message.deleted`, `message.stream`. Repeatable. | +| `--include-type=STRING` | Only forward events of these types Values: `chat.upserted`, `chat.deleted`, `message.upserted`, `message.deleted`, `message.stream`. Repeatable. | +| `--webhook=STRING` | Forward each event to this URL as POST | +| `--webhook-allow-private` | Accepted for compatibility; Beeper watch allows private webhook URLs Default: `false`. | +| `--webhook-queue=64` | Maximum pending webhook deliveries Default: `64`. | +| `--webhook-secret=STRING` | HMAC-SHA256 secret for X-Beeper-Signature and X-Wacli-Signature | ## Global Flags | Name | Description | | --- | --- | -| `--access-token ` | Use provided access token directly Env: `BEEPER_ACCESS_TOKEN`. | -| `-a, --account , --acct` | Account selector for account-aware commands Repeatable. | -| `--color ` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. | -| `--debug` | Default: `false`. | -| `--disable-commands ` | Comma-separated command prefixes to block | -| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions Default: `false`. | -| `--enable-commands ` | Comma-separated enabled command prefixes | -| `--enable-commands-exact ` | Comma-separated exact enabled commands | -| `--events` | Default: `false`. | +| `-h, --help` | Show context-sensitive help Default: `false`. | +| `--color="auto"` | Color output: auto\|always\|never Default: `auto`. Values: `auto`, `always`, `never`. Env: `BEEPER_COLOR`. | +| `--home=STRING, --store` | Override Beeper CLI config/data/state/cache root Env: `BEEPER_HOME`, `BEEPER_STORE_DIR`, `BEEPER_CLI_CONFIG_DIR`. | +| `-a, --account=STRING, --acct` | Account selector for account-aware commands Env: `BEEPER_ACCOUNT`. Repeatable. | +| `--access-token=STRING` | Use provided access token directly (bypasses stored target auth) Env: `BEEPER_ACCESS_TOKEN`. | +| `--enable-commands=STRING` | Comma-separated enabled command prefixes; dot paths allowed Env: `BEEPER_ENABLE_COMMANDS`. | +| `--enable-commands-exact=STRING` | Comma-separated exact enabled commands; parent commands do not enable children Env: `BEEPER_ENABLE_COMMANDS_EXACT`. | +| `--disable-commands=STRING` | Comma-separated command prefixes to block; dot paths allowed Env: `BEEPER_DISABLE_COMMANDS`. | +| `-j, --json, --machine` | Output JSON to stdout (best for scripting) Default: `false`. Env: `BEEPER_JSON`. | +| `-p, --plain, --tsv` | Output stable, parseable text to stdout (TSV-like; no colors) Default: `false`. Env: `BEEPER_PLAIN`. | +| `--wrap-untrusted` | In JSON/raw output, wrap fetched text fields in untrusted-content markers Default: `false`. Env: `BEEPER_WRAP_UNTRUSTED`. | +| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | +| `--select=STRING, --fields, --project` | In JSON mode, select comma-separated fields; dot paths allowed Env: `BEEPER_SELECT`, `BEEPER_FIELDS`, `BEEPER_PROJECT`. | +| `-n, --dry-run, --dryrun, --noop, --preview` | Do not make changes; print intended actions and exit successfully Default: `false`. Env: `BEEPER_DRY_RUN`. | | `-y, --force, --assume-yes, --yes` | Skip confirmations for destructive commands Default: `false`. | +| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead (useful for CI) Default: `false`. | +| `-v, --verbose, --debug` | Enable verbose logging Default: `false`. Env: `BEEPER_DEBUG`. | +| `--version` | Print version and exit Default: `false`. | +| `--events` | Emit machine-readable NDJSON lifecycle events on stderr Default: `false`. Env: `BEEPER_EVENTS`. | | `--full` | Disable truncation in human table output Default: `false`. | -| `--home ` | Override Beeper CLI config/data root Env: `BEEPER_CLI_CONFIG_DIR`. | -| `-j, --json, --machine` | Output JSON to stdout Default: `false`. | -| `--no-input, --non-interactive, --noninteractive` | Never prompt; fail instead Default: `false`. | -| `-p, --plain, --tsv` | Output stable TSV-like text Default: `false`. | -| `--read-only` | Reject commands that intentionally write Default: `false`. Env: `BEEPER_READONLY`. | -| `--results-only` | In JSON mode, emit only the primary result Default: `false`. | -| `--safety-profile ` | Safety profile name or YAML path | -| `--select , --fields, --project` | Select comma-separated JSON fields | -| `--target ` | Target name or URL | -| `--timeout ` | Command timeout, for example 30s or 2m | -| `-v, --version` | Print version and exit Default: `false`. | -| `--wrap-untrusted` | Wrap fetched text fields in untrusted-content markers Default: `false`. | +| `--lock-wait=STRING` | Accepted for compatibility; Beeper CLI does not use a local store lock | +| `--read-only, --readonly` | Reject commands that intentionally write Beeper or local CLI state Default: `false`. Env: `BEEPER_READONLY`. | +| `--safety-profile=STRING` | Safety profile name or YAML path | +| `--target=STRING` | Target name or URL Env: `BEEPER_TARGET`. | +| `--timeout=STRING` | Command timeout, for example 30s, 2m, 5m0s, or 1h30m Env: `BEEPER_TIMEOUT`. | diff --git a/packages/cli/package.json b/packages/cli/package.json index 9ca7aa12..8b98be7d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -29,9 +29,11 @@ }, "dependencies": { "@beeper/desktop-api": "github:beeper/desktop-api-js#next", + "@modelcontextprotocol/sdk": "^1.29.0", "qrcode": "1.5.4", "ws": "^8.20.1", - "yaml": "^2.9.0" + "yaml": "^2.9.0", + "zod": "^4.4.3" }, "devDependencies": { "@types/bun": "^1.3.3", diff --git a/packages/cli/scripts/generate-command-docs.ts b/packages/cli/scripts/generate-command-docs.ts index ef803a16..371264ae 100644 --- a/packages/cli/scripts/generate-command-docs.ts +++ b/packages/cli/scripts/generate-command-docs.ts @@ -16,8 +16,10 @@ for (const command of commands.filter(command => !command.hidden)) { } function commandIndex(): string { - const rows = commands - .filter(command => !command.hidden) + const visible = commands.filter(command => !command.hidden) + const rootRows = rootCommandRows(visible) + .map(row => `| \`${row.usage}\` | ${escapeTable(row.description)} |`) + const rows = visible .sort(compareCommands) .map(command => `| [\`${command.path.join(' ')}\`](${command.path.join('-')}.md) | ${escapeTable(command.description)} | ${aliases(command)} |`) return [ @@ -25,6 +27,14 @@ function commandIndex(): string { '', 'Generated from the live command registry. Do not edit command pages by hand.', '', + '## Root Commands', + '', + '| Usage | Description |', + '| --- | --- |', + ...rootRows, + '', + '## Full Command Reference', + '', '| Command | Description | Aliases |', '| --- | --- | --- |', ...rows, @@ -41,31 +51,57 @@ function commandDoc(command: CommandSpec): string { '## Usage', '', '```sh', - `beeper ${command.path.join(' ')}${usageArgs(command.args ?? [])} [flags]`, + commandUsage(command), '```', '', command.aliases?.length ? ['## Aliases', '', ...command.aliases.map(alias => `- \`beeper ${alias.join(' ')}\``), ''].join('\n') : undefined, + jsonOutputSection(command), section('Arguments', (command.args ?? []).map(argRow)), section('Flags', (command.flags ?? []).map(flagRow)), - section('Global Flags', globalFlagSpecs.map(flagRow)), + section('Global Flags', displayFlags(globalFlagSpecs).map(flagRow)), command.examples?.length ? ['## Examples', '', ...command.examples.map(example => `\`\`\`sh\n${example}\n\`\`\``), ''].join('\n') : undefined, ].filter(Boolean).join('\n') } +function jsonOutputSection(command: CommandSpec): string | undefined { + const key = primaryResultKey(command) + if (!key) return undefined + return [ + '## JSON Output', + '', + `Default JSON output is an object containing the \`${key}\` field.`, + `Use \`--json --results-only\` to emit only \`${key}\`.`, + '', + ].join('\n') +} + +function primaryResultKey(command: CommandSpec): string | undefined { + const path = command.path.join(' ') + if (path === 'auth list') return 'accounts' + if (path === 'auth services') return 'services' + if (path === 'config keys') return 'keys' + if (path === 'config path') return 'path' + if (path === 'targets list') return 'targets' + return undefined +} + function section(title: string, rows: string[]): string | undefined { if (!rows.length) return undefined return [`## ${title}`, '', '| Name | Description |', '| --- | --- |', ...rows, ''].join('\n') } function argRow(arg: ArgSpec): string { - const name = `${arg.required ? '<' : '['}${arg.name}${arg.variadic ? ' ...' : ''}${arg.required ? '>' : ']'}` - return `| \`${name}\` | ${escapeTable(arg.description ?? '')} |` + const name = formatArgUsage(arg) + const suffixes = [ + arg.enum?.length ? `Values: ${arg.enum.map(value => `\`${value}\``).join(', ')}.` : undefined, + ].filter(Boolean).join(' ') + return `| \`${name}\` | ${escapeTable([arg.description, suffixes].filter(Boolean).join(' '))} |` } function flagRow(flag: FlagSpec): string { const tokens = [ flag.short ? `-${flag.short}` : undefined, - `--${flag.name}${flag.type === 'boolean' ? '' : ` <${flag.placeholder ?? 'value'}>`}`, + `--${flag.name}${flagValueUsage(flag)}`, ...(flag.aliases ?? []).map(alias => `--${alias}`), ].filter(Boolean).join(', ') const suffixes = [ @@ -78,15 +114,205 @@ function flagRow(flag: FlagSpec): string { return `| \`${tokens}\` | ${escapeTable([flag.description, suffixes].filter(Boolean).join(' '))} |` } +function displayFlags(flags: FlagSpec[]): FlagSpec[] { + const priority = new Map([ + ['help', 0], + ['color', 1], + ['home', 2], + ['account', 3], + ['access-token', 4], + ['enable-commands', 5], + ['enable-commands-exact', 6], + ['disable-commands', 7], + ['json', 8], + ['plain', 9], + ['wrap-untrusted', 10], + ['results-only', 11], + ['select', 12], + ['dry-run', 13], + ['force', 14], + ['no-input', 15], + ['verbose', 16], + ['version', 17], + ]) + return [...flags].sort((a, b) => (priority.get(a.name) ?? 100) - (priority.get(b.name) ?? 100) || a.name.localeCompare(b.name)) +} + function usageArgs(args: ArgSpec[]): string { if (!args.length) return '' - return ` ${args.map(arg => arg.variadic ? `<${arg.name}> ...` : arg.required ? `<${arg.name}>` : `[${arg.name}]`).join(' ')}` + return ` ${args.map(formatArgUsage).join(' ')}` +} + +function formatArgUsage(arg: ArgSpec): string { + if (arg.variadic) return arg.required ? `<${arg.name}> ...` : `[<${arg.name}> ...]` + return arg.required ? `<${arg.name}>` : `[<${arg.name}>]` } function aliases(command: CommandSpec): string { return command.aliases?.length ? command.aliases.map(alias => `\`${alias.join(' ')}\``).join(', ') : '' } +function usageAliases(command: CommandSpec): string { + const canonical = command.path.join(' ') + const values = (command.aliases ?? []) + .map(alias => alias.join(' ')) + .filter(alias => alias !== canonical) + return values.length ? `(${[...new Set(values)].join(',')})` : '' +} + +function commandUsage(command: CommandSpec): string { + const hasChildren = commands.some(candidate => !candidate.hidden && candidate.path.length > command.path.length && candidate.path.slice(0, command.path.length).every((part, index) => part === command.path[index])) + if (hasChildren && !command.args?.length) return `beeper ${command.path.join(' ')} [flags]` + return `beeper ${[command.path.join(' '), usageAliases(command)].filter(Boolean).join(' ')}${usageArgs(command.args ?? [])} [flags]` +} + +function rootCommandRows(visible: CommandSpec[]): Array<{ description: string; sort: string; usage: string }> { + const rows = new Map() + const topLevel = new Set(visible.map(command => command.path[0]).filter((part): part is string => Boolean(part))) + for (const command of visible) { + if (command.path.length === 1) { + const hasChildren = visible.some(candidate => + candidate.path.length > 1 && candidate.path[0] === command.path[0] + || (candidate.aliases ?? []).some(alias => alias.length > 1 && alias[0] === command.path[0])) + rows.set(command.path[0]!, { + description: command.description, + sort: command.path[0]!, + usage: hasChildren && !command.args?.length ? `beeper ${command.path[0]} [flags]` : commandUsage(command), + }) + } + } + for (const command of visible) { + if (command.path.length <= 1) continue + const rootAliases = (command.aliases ?? []) + .filter(alias => alias.length === 1) + .map(alias => alias[0]!) + if (!rootAliases.length) continue + const name = rootAliases[0]! + if (rows.has(name)) continue + const alternateAliases = rootAliases.slice(1) + rows.set(name, { + description: `${command.description} (alias for 'beeper ${command.path.join(' ')}')`, + sort: name, + usage: `beeper ${name}${alternateAliases.length ? ` (${alternateAliases.join(',')})` : ''}${usageArgs(command.args ?? [])} [flags]`, + }) + } + const me = visible.find(command => command.path.join(' ') === 'me') + const whoamiAliases = me?.aliases?.filter(alias => alias.length === 1 && alias[0]?.startsWith('who')).map(alias => alias[0]!) ?? [] + if (me && whoamiAliases.length && !rows.has(whoamiAliases[0]!)) { + const name = whoamiAliases[0]! + rows.set(name, { + description: `${me.description} (alias for 'beeper ${me.path.join(' ')}')`, + sort: name, + usage: `beeper ${name}${whoamiAliases.length > 1 ? ` (${whoamiAliases.slice(1).join(',')})` : ''}${usageArgs(me.args ?? [])} [flags]`, + }) + } + for (const name of topLevel) { + if (rows.has(name)) continue + const aliases = namespaceAliases(name, visible) + rows.set(name, { + description: rootNamespaceDescription(name), + sort: name, + usage: `beeper ${name}${aliases.length ? ` (${aliases.join(',')})` : ''} [flags]`, + }) + } + return [...rows.values()].sort((a, b) => rootCommandPriority(a.sort) - rootCommandPriority(b.sort) || a.sort.localeCompare(b.sort)) +} + +function rootCommandPriority(name: string): number { + const order = [ + 'message', + 'ls', + 'search', + 'open', + 'download', + 'upload', + 'login', + 'logout', + 'status', + 'me', + 'whoami', + 'setup', + 'send', + 'chats', + 'messages', + 'accounts', + 'contacts', + 'presence', + 'media', + 'targets', + 'use', + 'remove', + 'resolve', + 'export', + 'watch', + 'doctor', + 'auth', + 'install', + 'api', + 'config', + 'docs', + 'schema', + 'mcp', + 'agent', + 'exit-codes', + 'completion', + 'help', + 'version', + ] + const index = order.indexOf(name) + return index === -1 ? order.length : index +} + +function namespaceAliases(name: string, visible: CommandSpec[]): string[] { + const allowed = singularNamespaceAliases()[name] ?? [] + const aliases = new Set() + for (const command of visible) { + if (command.path[0] !== name) continue + for (const alias of command.aliases ?? []) { + if (alias.length < 2) continue + const aliasRoot = alias[0] + if (aliasRoot && allowed.includes(aliasRoot)) aliases.add(aliasRoot) + } + } + return [...aliases].sort((a, b) => rootCommandPriority(a) - rootCommandPriority(b) || a.localeCompare(b)) +} + +function singularNamespaceAliases(): Record { + return { + accounts: ['account'], + chats: ['chat'], + contacts: ['contact'], + targets: ['target'], + } +} + +function rootNamespaceDescription(name: string): string { + const descriptions: Record = { + accounts: 'Manage connected chat accounts', + api: 'Call raw Beeper Desktop API endpoints', + auth: 'Authenticate and manage stored credentials', + chats: 'List and manage chats', + config: 'Manage configuration', + contacts: 'List and search contacts', + install: 'Install Beeper Desktop or Beeper Server', + media: 'Download message media', + messages: 'List, search, edit, and delete messages', + presence: 'Send presence indicators', + remove: 'Remove configured resources', + resolve: 'Resolve Beeper selectors', + send: 'Send messages, files, reactions, and presence', + targets: 'Manage Beeper Desktop and Server targets', + use: 'Select default resources', + } + return descriptions[name] ?? `${name} commands` +} + +function flagValueUsage(flag: FlagSpec): string { + if (flag.type === 'boolean') return '' + if (flag.default !== undefined) return `=${JSON.stringify(flag.default)}` + return `=${flag.placeholder ?? (flag.type === 'integer' ? 'INTEGER' : 'STRING')}` +} + function compareCommands(a: CommandSpec, b: CommandSpec): number { return a.path.join(' ').localeCompare(b.path.join(' ')) } diff --git a/packages/cli/src/cli/commands.ts b/packages/cli/src/cli/commands.ts index 6cd1cdf3..13316f17 100644 --- a/packages/cli/src/cli/commands.ts +++ b/packages/cli/src/cli/commands.ts @@ -1,5 +1,5 @@ import { createHmac } from 'node:crypto' -import { createReadStream } from 'node:fs' +import { createReadStream, existsSync, readFileSync } from 'node:fs' import { stdout as output } from 'node:process' import { setTimeout as sleep } from 'node:timers/promises' import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises' @@ -44,7 +44,7 @@ import { startProfile, stopProfile, } from '../lib/profiles.js' -import type { CommandContext, CommandSpec, FlagSpec, GlobalFlags } from './types.js' +import type { ArgSpec, CommandContext, CommandSpec, FlagSpec, GlobalFlags } from './types.js' import { globalFlagSpecs, numberFlag, requiredStringFlag, stringFlag, stringListFlag } from './parse.js' import { commandVisible } from './policy.js' import { buildSchema } from './schema.js' @@ -52,7 +52,7 @@ import { serveMcp } from './mcp.js' import { usage, writeEvent, writeResult } from './output.js' import { runSetup } from './setup.js' -type WebhookConfig = { inflight: number; max: number; queue: Array<{ body: string; signature?: string }>; secret?: string; url: string } +type WebhookConfig = { inflight: number; max: number; queue: Array<{ body: string; secret?: string }>; secret?: string; url: string } type EventFilter = { include?: Set; exclude?: Set } type AttachmentType = 'sticker' | 'voice-note' type SendKind = 'file' | 'sticker' | 'text' | 'voice' @@ -61,10 +61,15 @@ type SendPayload = { duration?: number file?: string fileName?: string + forwardedUpload?: Record mentions?: string[] mimeType?: string noPreview?: boolean + ephemeral?: boolean + ephemeralDuration?: string + messageExpirySeconds?: number replyTo?: string + replyToSender?: string text: string wait?: boolean waitTimeoutMs?: number @@ -72,7 +77,9 @@ type SendPayload = { const accountFilterFlag: FlagSpec = { name: 'account', short: 'a', aliases: ['acct'], type: 'string', multiple: true, description: 'Limit to account selector' } const candidateLimitFlag: FlagSpec = { name: 'limit', aliases: ['max'], type: 'integer', default: 10, description: 'Maximum candidates' } -const chatFlag: FlagSpec = { name: 'chat', type: 'string', required: true, description: 'Chat selector' } +const chatArg: ArgSpec = { name: 'chat', description: 'Chat selector. Used when --chat is omitted.' } +const chatFlag: FlagSpec = { name: 'chat', aliases: ['jid'], type: 'string', required: true, description: 'Chat selector' } +const optionalChatFlag: FlagSpec = { ...chatFlag, required: false } const pickCandidateFlag: FlagSpec = { name: 'pick', type: 'integer', description: 'Select the Nth candidate' } const pickChatFlag: FlagSpec = { name: 'pick', type: 'integer', description: 'Pick the Nth result when selector is ambiguous' } const configKeys = ['defaultTarget', 'defaultAccount'] as const @@ -82,28 +89,67 @@ const chatFlags: FlagSpec[] = [ chatFlag, pickChatFlag, ] +const optionalChatFlags: FlagSpec[] = [ + optionalChatFlag, + pickChatFlag, +] const installFlags: FlagSpec[] = [ { name: 'channel', type: 'string', enum: ['stable', 'nightly'], default: 'stable', description: 'Install release channel' }, { name: 'server-env', type: 'string', enum: ['local', 'dev', 'staging', 'prod'], default: 'prod', description: 'Server environment' }, ] +const setupCommandFlags: FlagSpec[] = [ + { name: 'local', type: 'boolean', default: false, description: 'Use the local Beeper Desktop session on this device' }, + { name: 'oauth', type: 'boolean', default: false, description: 'Authorize the target with browser OAuth/PKCE' }, + { name: 'remote', type: 'string', description: 'Connect to a remote Beeper Desktop or Server URL' }, + { name: 'server', type: 'boolean', default: false, description: 'Set up a local Beeper Server target' }, + { name: 'desktop', type: 'boolean', default: false, description: 'Set up a local Beeper Desktop target' }, + { name: 'install', type: 'boolean', default: false, description: 'Allow installing a missing local runtime' }, + ...installFlags, + { name: 'email', type: 'string', description: 'Sign in with an email address' }, + { name: 'username', type: 'string', description: 'Username to use if setup creates a new account' }, +] + const sendChatFlags: FlagSpec[] = [ - { name: 'to', type: 'string', required: true, description: 'Chat selector' }, + { name: 'to', type: 'string', description: 'Chat selector' }, pickChatFlag, ] const sendDeliveryFlags: FlagSpec[] = [ ...sendChatFlags, { name: 'reply-to', type: 'string', description: 'Send as a reply to this message ID' }, + { name: 'reply-to-sender', type: 'string', description: 'Accepted for compatibility; Beeper replies only need --reply-to' }, { name: 'wait', type: 'boolean', default: false, description: 'Wait until the message leaves pending state' }, { name: 'wait-timeout', type: 'integer', default: 30_000, description: 'Maximum wait time in ms when --wait is set' }, + { name: 'post-send-wait', type: 'string', description: 'Compatibility alias for waiting after send, for example 2s or 500ms; 0 disables waiting' }, ] +const presenceFlags: FlagSpec[] = [ + ...sendChatFlags, + { name: 'duration', type: 'integer', description: 'Seconds to keep typing before sending paused' }, + { name: 'state', type: 'string', enum: ['typing', 'paused'], default: 'typing', description: 'Presence indicator to send' }, +] + +function messageListFlags(limitDefault = 50, limitDescription = 'Maximum messages to print'): FlagSpec[] { + return [ + { name: 'after-cursor', aliases: ['after'], type: 'string', description: 'Paginate messages newer than this message ID' }, + { name: 'asc', type: 'boolean', default: false, description: 'Order oldest first' }, + { name: 'before-cursor', aliases: ['before'], type: 'string', description: 'Paginate messages older than this message ID' }, + chatFlag, + { name: 'limit', type: 'integer', default: limitDefault, description: limitDescription }, + pickChatFlag, + { name: 'sender', type: 'string', description: 'me, others, or a specific user ID' }, + { name: 'from-me', type: 'boolean', default: false, description: 'Only messages sent by me' }, + { name: 'from-them', type: 'boolean', default: false, description: 'Only messages sent by others' }, + { name: 'type', type: 'string', enum: ['text', 'image', 'video', 'audio', 'document', 'file', 'link'], description: 'Only messages of this kind' }, + { name: 'has-media', type: 'boolean', default: false, description: 'Only messages with media' }, + ] +} + export const commands: CommandSpec[] = [ { - description: 'Print CLI version', - mcp: true, + description: 'Print version', output: 'diagnostic', path: ['version'], risk: 'read', @@ -112,7 +158,7 @@ export const commands: CommandSpec[] = [ { args: [{ name: 'target', description: 'Target name. Defaults to the selected target.' }], aliases: [['st']], - description: 'Show selected target and setup readiness', + description: 'Show auth, config, selected target, and setup readiness', mcp: true, output: 'status', path: ['status'], @@ -120,36 +166,79 @@ export const commands: CommandSpec[] = [ run: status, }, { - description: 'Run diagnostics for config, target reachability, auth, and readiness', + aliases: [['whoami'], ['who-am-i']], + description: 'Show selected account and target identity', + flags: [accountFilterFlag], mcp: true, + path: ['me'], + risk: 'read', + run: me, + }, + { + aliases: [['auth', 'doctor']], + description: 'Run diagnostics for config, target reachability, auth, and readiness', + flags: [{ name: 'connect', type: 'boolean', default: false, description: 'Accepted for compatibility; doctor always checks selected target reachability' }], output: 'diagnostic', path: ['doctor'], risk: 'read', run: doctor, }, { - aliases: [['agent', 'exit-codes'], ['exitcodes']], + description: 'Agent-friendly helpers', + output: 'diagnostic', + path: ['agent'], + risk: 'read', + run: agent, + }, + { + aliases: [['agent', 'exit-codes'], ['agent', 'exitcodes'], ['agent', 'exit-code'], ['exitcodes']], description: 'Print stable exit codes for automation', - mcp: true, output: 'diagnostic', path: ['exit-codes'], + rawJson: true, risk: 'read', run: exitCodes, }, { - args: [{ name: 'command', variadic: true }], + aliases: [['help-docs']], + description: 'Print command documentation locations', + flags: [{ name: 'url', aliases: ['url-only'], type: 'boolean', default: false, description: 'Print only the documentation URL' }], + output: 'diagnostic', + path: ['docs'], + risk: 'read', + run: docs, + }, + { + args: [{ name: 'command', variadic: true, description: 'Command path to describe' }], + description: 'Show help for a command', + path: ['help'], + risk: 'read', + run: helpCommand, + }, + { + args: [{ name: 'command', variadic: true, description: 'Optional command path to describe. Default: entire CLI' }], aliases: [['help-json'], ['helpjson']], - description: 'Print machine-readable command and flag schema', - mcp: true, + description: 'Machine-readable command/flag schema', + flags: [{ name: 'include-hidden', type: 'boolean', default: false, description: 'Include hidden commands' }], path: ['schema'], + rawJson: true, risk: 'read', run: schema, }, { - description: 'Run a typed MCP stdio server', + description: 'Run a typed, allowlisted MCP server over stdio or HTTP', + examples: [ + 'beeper mcp', + 'beeper mcp --allow-tool targets.*,messages --list-tools', + 'beeper mcp --allow-write --allow-tool send_text', + ], flags: [ - { name: 'allow-tool', aliases: ['tool'], type: 'string', multiple: true, description: 'Tool or command allowlist' }, + { name: 'allow-tool', aliases: ['tool'], type: 'string', multiple: true, placeholder: 'ALLOW-TOOL,...', description: 'Tool or service allowlist (default: all read-only tools). Examples: targets.*,messages_search,send' }, { name: 'allow-write', type: 'boolean', default: false, description: 'Allow write-risk MCP tools' }, + { name: 'transport', type: 'string', enum: ['stdio', 'http'], default: 'stdio', description: 'MCP transport' }, + { name: 'http-host', type: 'string', default: '127.0.0.1', description: 'Host for --transport=http' }, + { name: 'http-port', type: 'integer', default: 7331, description: 'Port for --transport=http. Use 0 to choose a free port' }, + { name: 'http-path', type: 'string', default: '/mcp', description: 'HTTP path for --transport=http' }, { name: 'list-tools', type: 'boolean', default: false, description: 'Print enabled MCP tools as JSON and exit' }, { name: 'max-output-bytes', type: 'integer', default: 102400, description: 'Maximum stdout/stderr bytes captured per tool call' }, { name: 'timeout-seconds', type: 'integer', default: 60, description: 'Per-tool subprocess timeout' }, @@ -159,18 +248,46 @@ export const commands: CommandSpec[] = [ run: mcp, }, { - args: [{ name: 'shell', required: true, description: 'bash, zsh, fish, or powershell' }], + args: [{ name: 'shell', required: true, enum: ['bash', 'fish', 'powershell', 'zsh'], description: 'Shell (bash|zsh|fish|powershell)' }], description: 'Generate shell completion scripts', hidden: false, path: ['completion'], risk: 'read', run: completion, }, + { + description: 'Generate the autocompletion script for bash', + flags: [{ name: 'no-descriptions', type: 'boolean', default: false, description: 'Accepted for compatibility; completion descriptions are not emitted' }], + path: ['completion', 'bash'], + risk: 'read', + run: completionShell, + }, + { + description: 'Generate the autocompletion script for fish', + flags: [{ name: 'no-descriptions', type: 'boolean', default: false, description: 'Accepted for compatibility; completion descriptions are not emitted' }], + path: ['completion', 'fish'], + risk: 'read', + run: completionShell, + }, + { + aliases: [['completion', 'pwsh']], + description: 'Generate the autocompletion script for powershell', + flags: [{ name: 'no-descriptions', type: 'boolean', default: false, description: 'Accepted for compatibility; completion descriptions are not emitted' }], + path: ['completion', 'powershell'], + risk: 'read', + run: completionShell, + }, + { + description: 'Generate the autocompletion script for zsh', + flags: [{ name: 'no-descriptions', type: 'boolean', default: false, description: 'Accepted for compatibility; completion descriptions are not emitted' }], + path: ['completion', 'zsh'], + risk: 'read', + run: completionShell, + }, { aliases: [['config', 'show']], args: [{ name: 'key', required: true, description: 'Config key to get' }], description: 'Get a config value', - mcp: true, output: 'diagnostic', path: ['config', 'get'], risk: 'read', @@ -179,7 +296,6 @@ export const commands: CommandSpec[] = [ { aliases: [['config', 'list-keys'], ['config', 'names']], description: 'List available config keys', - mcp: true, path: ['config', 'keys'], risk: 'read', run: configKeysCommand, @@ -187,7 +303,6 @@ export const commands: CommandSpec[] = [ { aliases: [['config', 'ls'], ['config', 'all']], description: 'List all config values', - mcp: true, output: 'diagnostic', path: ['config', 'list'], risk: 'read', @@ -196,7 +311,6 @@ export const commands: CommandSpec[] = [ { aliases: [['config', 'where']], description: 'Print config file path', - mcp: true, output: 'diagnostic', path: ['config', 'path'], risk: 'read', @@ -235,23 +349,13 @@ export const commands: CommandSpec[] = [ { description: 'Make the selected target ready for messaging', examples: ['beeper setup', 'beeper setup --local', 'beeper setup --remote https://desktop.example.com', 'beeper setup --desktop --install'], - flags: [ - { name: 'local', type: 'boolean', default: false, description: 'Use the local Beeper Desktop session on this device' }, - { name: 'oauth', type: 'boolean', default: false, description: 'Authorize the target with browser OAuth/PKCE' }, - { name: 'remote', type: 'string', description: 'Connect to a remote Beeper Desktop or Server URL' }, - { name: 'server', type: 'boolean', default: false, description: 'Set up a local Beeper Server target' }, - { name: 'desktop', type: 'boolean', default: false, description: 'Set up a local Beeper Desktop target' }, - { name: 'install', type: 'boolean', default: false, description: 'Allow installing a missing local runtime' }, - ...installFlags, - { name: 'email', type: 'string', description: 'Sign in with an email address' }, - { name: 'username', type: 'string', description: 'Username to use if setup creates a new account' }, - ], + flags: setupCommandFlags, path: ['setup'], risk: 'write', run: runSetup, }, { - aliases: [['targets', 'ls']], + aliases: [['targets', 'ls'], ['target', 'list'], ['target', 'ls']], description: 'List configured Beeper targets', mcp: true, output: 'targets', @@ -260,9 +364,13 @@ export const commands: CommandSpec[] = [ run: targetsList, }, { - args: [{ name: 'name', required: true }, { name: 'url', required: true }], + args: [ + { name: 'name', required: true, description: 'Target name' }, + { name: 'url', required: true, description: 'Target base URL' }, + ], description: 'Add a remote Beeper Desktop or Server target', flags: [{ name: 'default', type: 'boolean', default: false, description: 'Set this target as the default after creation' }], + aliases: [['target', 'add']], path: ['targets', 'add'], risk: 'write', run: targetsAdd, @@ -277,6 +385,7 @@ export const commands: CommandSpec[] = [ { name: 'timeout', type: 'string', description: 'Startup timeout, for example 40s or 60000ms' }, { name: 'url-only', type: 'boolean', default: false, description: 'Print only the public tunnel URL' }, ], + aliases: [['target', 'tunnel']], path: ['targets', 'tunnel'], risk: 'write', run: targetsTunnel, @@ -296,11 +405,54 @@ export const commands: CommandSpec[] = [ run: installCommand, }, { + args: [{ name: 'target', description: 'Target name. Defaults to the selected target.' }], description: 'Clear stored authentication', + aliases: [['logout'], ['auth', 'remove'], ['auth', 'rm'], ['auth', 'del']], path: ['auth', 'logout'], risk: 'write', run: authLogout, }, + { + aliases: [['auth', 'ls']], + description: 'List stored target credentials', + output: 'auth', + path: ['auth', 'list'], + risk: 'read', + run: authList, + }, + { + args: [{ name: 'target', description: 'Target name. Defaults to the selected target.' }], + description: 'Show auth configuration and stored target credential status', + output: 'diagnostic', + path: ['auth', 'status'], + risk: 'read', + run: authStatus, + }, + { + aliases: [['auth', 'bridges']], + description: 'List supported account login services and bridges', + flags: [{ name: 'markdown', type: 'boolean', default: false, description: 'Output a Markdown table' }], + path: ['auth', 'services'], + risk: 'read', + run: authServices, + }, + { + aliases: [['auth', 'setup'], ['auth', 'connect']], + description: 'Make the selected target ready for messaging', + examples: ['beeper auth manage', 'beeper auth manage --local', 'beeper auth manage --oauth'], + flags: setupCommandFlags, + path: ['auth', 'manage'], + risk: 'write', + run: runSetup, + }, + { + args: [{ name: 'email', required: true, description: 'Email address' }], + aliases: [['auth', 'add'], ['auth', 'login']], + description: 'Start email sign-in for a target', + path: ['login'], + risk: 'write', + run: login, + }, { description: 'Start email sign-in for a target', flags: [{ name: 'email', type: 'string', required: true, description: 'Email address' }], @@ -320,6 +472,7 @@ export const commands: CommandSpec[] = [ run: authEmailResponse, }, { + aliases: [['accounts', 'ls'], ['account', 'list'], ['account', 'ls']], description: 'List connected accounts', flags: [ accountFilterFlag, @@ -331,24 +484,34 @@ export const commands: CommandSpec[] = [ risk: 'read', run: accountsList, }, + { + args: [{ name: 'selector', required: true, description: 'Account selector' }], + aliases: [['accounts', 'get'], ['accounts', 'info'], ['account', 'show'], ['account', 'get'], ['account', 'info']], + description: 'Show one connected account', + mcp: true, + output: 'accounts', + path: ['accounts', 'show'], + risk: 'read', + run: accountsShow, + }, { args: [{ name: 'selector', required: true, description: 'Target name' }], - aliases: [['targets', 'use']], + aliases: [['use', 'target'], ['target', 'use']], description: 'Select the default target', - path: ['use', 'target'], + path: ['targets', 'use'], risk: 'write', run: useTarget, }, { args: [{ name: 'selector', required: true, description: 'Account selector' }], - aliases: [['accounts', 'use']], + aliases: [['use', 'account'], ['account', 'use']], description: 'Select the default account', - path: ['use', 'account'], + path: ['accounts', 'use'], risk: 'write', run: useAccount, }, { - args: [{ name: 'bridge' }], + args: [{ name: 'bridge', description: 'Bridge ID, name, or network to connect' }], description: 'Connect a chat account by bridge', flags: [ { name: 'cookie', type: 'string', multiple: true, description: 'Cookie value in name=value form' }, @@ -360,28 +523,30 @@ export const commands: CommandSpec[] = [ { name: 'webview-backend', type: 'string', enum: ['auto', 'chrome', 'webkit'], default: 'chrome', description: 'Bun.WebView backend' }, { name: 'webview-timeout', type: 'integer', default: 120, description: 'Seconds to wait for WebView cookie collection' }, ], + aliases: [['accounts', 'create'], ['accounts', 'new'], ['account', 'add'], ['account', 'create'], ['account', 'new']], path: ['accounts', 'add'], risk: 'write', run: accountsAdd, }, { args: [{ name: 'selector', required: true, description: 'Target name' }], - aliases: [['targets', 'remove'], ['targets', 'rm']], + aliases: [['targets', 'rm'], ['targets', 'del'], ['remove', 'target'], ['target', 'remove'], ['target', 'rm'], ['target', 'del']], description: 'Remove a target', - path: ['remove', 'target'], + path: ['targets', 'remove'], risk: 'destructive', run: removeTargetCommand, }, { args: [{ name: 'selector', required: true, description: 'Account selector' }], - aliases: [['accounts', 'remove'], ['accounts', 'rm']], + aliases: [['accounts', 'rm'], ['accounts', 'del'], ['remove', 'account'], ['account', 'remove'], ['account', 'rm'], ['account', 'del']], description: 'Remove an account', - path: ['remove', 'account'], + path: ['accounts', 'remove'], risk: 'destructive', run: removeAccount, }, { - aliases: [['contacts', 'search'], ['contacts', 'find']], + args: [{ name: 'query', description: 'Optional contact lookup query. Used when --query is omitted.' }], + aliases: [['contacts', 'ls'], ['contacts', 'search'], ['contacts', 'find'], ['contact', 'list'], ['contact', 'ls'], ['contact', 'search'], ['contact', 'find']], description: 'List contacts', flags: [ accountFilterFlag, @@ -396,17 +561,34 @@ export const commands: CommandSpec[] = [ run: contactsList, }, { - aliases: [['chats', 'ls']], + args: [{ name: 'selector', description: 'Contact selector. Used when --jid is omitted.' }], + aliases: [['contacts', 'get'], ['contacts', 'info'], ['contact', 'show'], ['contact', 'get'], ['contact', 'info']], + description: 'Show one contact', + flags: [ + accountFilterFlag, + { name: 'jid', type: 'string', description: 'Contact JID or user ID' }, + candidateLimitFlag, + pickCandidateFlag, + ], + mcp: true, + output: 'contacts', + path: ['contacts', 'show'], + risk: 'read', + run: contactsShow, + }, + { + aliases: [['chats', 'ls'], ['chat', 'list'], ['chat', 'ls'], ['ls'], ['list']], description: 'List chats', flags: [ accountFilterFlag, { name: 'archived', type: 'boolean', description: 'Only archived chats; use --no-archived to exclude' }, { name: 'ids', type: 'boolean', default: false, description: 'Print preferred chat selectors' }, - { name: 'limit', type: 'integer', default: 20, description: 'Maximum chats to print' }, + { name: 'limit', type: 'integer', default: 50, description: 'Maximum chats to print' }, { name: 'low-priority', type: 'boolean', description: 'Only low-priority chats; use --no-low-priority to exclude' }, { name: 'muted', type: 'boolean', description: 'Only muted chats; use --no-muted to exclude' }, { name: 'pinned', type: 'boolean', description: 'Only pinned chats; use --no-pinned to exclude' }, { name: 'query', type: 'string', description: 'Optional chat lookup query' }, + { name: 'type', aliases: ['chat-type'], type: 'string', enum: ['single', 'group', 'any'], description: 'Only direct messages, group chats, or all chats' }, { name: 'unread', type: 'boolean', description: 'Only unread chats; use --no-unread to exclude' }, ], mcp: true, @@ -416,9 +598,78 @@ export const commands: CommandSpec[] = [ run: chatsList, }, { + aliases: [['groups', 'ls'], ['group', 'list'], ['group', 'ls']], + description: 'List group chats', + flags: [ + accountFilterFlag, + { name: 'ids', type: 'boolean', default: false, description: 'Print preferred chat selectors' }, + { name: 'limit', type: 'integer', default: 50, description: 'Maximum groups to print' }, + { name: 'query', type: 'string', description: 'Optional group lookup query' }, + ], + output: 'chats', + path: ['groups', 'list'], + risk: 'read', + run: groupsList, + }, + { + args: [{ name: 'jid', description: 'Group chat selector. Used when --jid is omitted.' }], + aliases: [['groups', 'info'], ['group', 'show'], ['group', 'info']], + description: 'Show group details', + flags: [ + { name: 'jid', aliases: ['chat'], type: 'string', description: 'Group chat selector' }, + { name: 'max-participants', type: 'integer', description: 'Limit participants returned in group details' }, + pickChatFlag, + ], + path: ['groups', 'show'], + risk: 'read', + run: groupsShow, + }, + { + aliases: [['groups', 'add'], ['groups', 'new'], ['group', 'create'], ['group', 'add'], ['group', 'new']], + description: 'Create a group chat', + flags: [ + { name: 'account', type: 'string', description: 'Account selector' }, + { name: 'name', aliases: ['title'], type: 'string', required: true, description: 'Group name' }, + { name: 'user', type: 'string', multiple: true, required: true, description: 'Initial participant user ID, phone, or handle' }, + { name: 'message', type: 'string', description: 'Optional first message text' }, + ], + path: ['groups', 'create'], + risk: 'write', + run: groupsCreate, + }, + { + args: [{ name: 'jid', description: 'Group chat selector. Used when --jid is omitted.' }], + aliases: [['group', 'rename']], + description: 'Rename a group', + flags: [ + { name: 'jid', aliases: ['chat'], type: 'string', description: 'Group chat selector' }, + { name: 'name', aliases: ['title'], type: 'string', required: true, description: 'New group name' }, + pickChatFlag, + ], + path: ['groups', 'rename'], + risk: 'write', + run: groupsRename, + }, + { + args: [{ name: 'jid', description: 'Group chat selector. Used when --jid is omitted.' }], + aliases: [['groups', 'topic'], ['group', 'description'], ['group', 'topic']], + description: 'Set or clear a group description', + flags: [ + { name: 'jid', aliases: ['chat'], type: 'string', description: 'Group chat selector' }, + { name: 'clear', type: 'boolean', default: false, description: 'Clear the description' }, + { name: 'description', aliases: ['topic'], type: 'string', description: 'Group description' }, + pickChatFlag, + ], + path: ['groups', 'description'], + risk: 'write', + run: groupsDescription, + }, + { + args: [{ name: 'chat', description: 'Chat selector. Used when --chat is omitted.' }], + aliases: [['chats', 'info'], ['chat', 'show'], ['chat', 'info']], description: 'Show chat details', flags: [ - chatFlag, + { ...chatFlag, required: false }, { name: 'max-participants', type: 'integer', description: 'Limit participants returned in chat details' }, pickChatFlag, ], @@ -428,7 +679,8 @@ export const commands: CommandSpec[] = [ run: chatsShow, }, { - args: [{ name: 'user', required: true }], + args: [{ name: 'user', required: true, description: 'User ID, phone, handle, or contact selector' }], + aliases: [['chat', 'start']], description: 'Start a chat', flags: [ { name: 'account', type: 'string', description: 'Account selector' }, @@ -439,37 +691,74 @@ export const commands: CommandSpec[] = [ run: chatsStart, }, { + args: [chatArg], + aliases: [['chat', 'archive']], description: 'Archive or unarchive a chat', - flags: [...chatFlags, { name: 'clear', type: 'boolean', default: false, description: 'Unarchive the chat' }], + flags: [...optionalChatFlags, { name: 'clear', type: 'boolean', default: false, description: 'Unarchive the chat' }], path: ['chats', 'archive'], risk: 'write', run: chatsSetFlag, }, { + args: [chatArg], + aliases: [['chat', 'unarchive']], + description: 'Unarchive a chat', + flags: optionalChatFlags, + path: ['chats', 'unarchive'], + risk: 'write', + run: chatsSetFlag, + }, + { + args: [chatArg], + aliases: [['chat', 'pin']], description: 'Pin or unpin a chat', - flags: [...chatFlags, { name: 'clear', type: 'boolean', default: false, description: 'Unpin the chat' }], + flags: [...optionalChatFlags, { name: 'clear', type: 'boolean', default: false, description: 'Unpin the chat' }], path: ['chats', 'pin'], risk: 'write', run: chatsSetFlag, }, { + args: [chatArg], + aliases: [['chat', 'unpin']], + description: 'Unpin a chat', + flags: optionalChatFlags, + path: ['chats', 'unpin'], + risk: 'write', + run: chatsSetFlag, + }, + { + args: [chatArg], + aliases: [['chat', 'mute']], description: 'Mute or unmute a chat', - flags: [...chatFlags, { name: 'clear', type: 'boolean', default: false, description: 'Unmute the chat' }], + flags: [...optionalChatFlags, { name: 'clear', type: 'boolean', default: false, description: 'Unmute the chat' }], path: ['chats', 'mute'], risk: 'write', run: chatsSetFlag, }, { + args: [chatArg], + aliases: [['chat', 'unmute']], + description: 'Unmute a chat', + flags: optionalChatFlags, + path: ['chats', 'unmute'], + risk: 'write', + run: chatsSetFlag, + }, + { + args: [chatArg], + aliases: [['chat', 'rename']], description: 'Rename a chat', - flags: [...chatFlags, { name: 'title', type: 'string', required: true, description: 'Chat title' }], + flags: [...optionalChatFlags, { name: 'title', type: 'string', required: true, description: 'Chat title' }], path: ['chats', 'rename'], risk: 'write', run: chatsRename, }, { + args: [chatArg], + aliases: [['chat', 'description']], description: 'Set or clear a chat description', flags: [ - ...chatFlags, + ...optionalChatFlags, { name: 'clear', type: 'boolean', default: false, description: 'Clear or unset the chosen state' }, { name: 'description', type: 'string', description: 'Chat description' }, ], @@ -478,9 +767,11 @@ export const commands: CommandSpec[] = [ run: chatsDescription, }, { + args: [chatArg], + aliases: [['chat', 'avatar']], description: 'Set or clear a chat avatar', flags: [ - ...chatFlags, + ...optionalChatFlags, { name: 'clear', type: 'boolean', default: false, description: 'Clear the avatar' }, { name: 'file', type: 'string', description: 'Avatar image file path' }, ], @@ -489,27 +780,52 @@ export const commands: CommandSpec[] = [ run: chatsAvatar, }, { + args: [chatArg], + aliases: [['chat', 'priority']], description: 'Set chat priority', - flags: [...chatFlags, { name: 'level', type: 'string', required: true, enum: ['inbox', 'low'], description: 'Chat priority level' }], + flags: [...optionalChatFlags, { name: 'level', type: 'string', required: true, enum: ['inbox', 'low'], description: 'Chat priority level' }], path: ['chats', 'priority'], risk: 'write', run: chatsPriority, }, { + args: [chatArg], + aliases: [['chat', 'read']], description: 'Mark a chat read or unread', flags: [ - ...chatFlags, + ...optionalChatFlags, { name: 'message', type: 'string', description: 'Read marker message ID' }, { name: 'unread', type: 'boolean', default: false, description: 'Mark the chat unread' }, ], + mcp: true, path: ['chats', 'read'], risk: 'write', run: chatsRead, }, { + args: [chatArg], + aliases: [['chat', 'mark-read']], + description: 'Mark a chat as read', + flags: [...optionalChatFlags, { name: 'message', type: 'string', description: 'Read marker message ID' }], + path: ['chats', 'mark-read'], + risk: 'write', + run: chatsRead, + }, + { + args: [chatArg], + aliases: [['chat', 'mark-unread']], + description: 'Mark a chat as unread', + flags: optionalChatFlags, + path: ['chats', 'mark-unread'], + risk: 'write', + run: chatsRead, + }, + { + args: [chatArg], + aliases: [['chat', 'draft']], description: 'Set or clear a chat draft', flags: [ - ...chatFlags, + ...optionalChatFlags, { name: 'clear', type: 'boolean', default: false, description: 'Clear the draft' }, { name: 'file', type: 'string', description: 'Draft attachment file path' }, { name: 'filename', type: 'string', description: 'Draft attachment filename' }, @@ -521,9 +837,11 @@ export const commands: CommandSpec[] = [ run: chatsDraft, }, { + args: [chatArg], + aliases: [['chat', 'remind']], description: 'Set or clear a chat reminder', flags: [ - ...chatFlags, + ...optionalChatFlags, { name: 'clear', type: 'boolean', default: false, description: 'Clear the reminder' }, { name: 'dismiss-on-message', type: 'boolean', default: false, description: 'Dismiss reminder when a new message arrives' }, { name: 'when', type: 'string', description: 'ISO reminder timestamp' }, @@ -533,19 +851,24 @@ export const commands: CommandSpec[] = [ run: chatsRemind, }, { + args: [chatArg], + aliases: [['chat', 'disappear']], description: 'Set a disappearing-message timer', flags: [ - ...chatFlags, - { name: 'seconds', type: 'string', description: 'Disappearing-message timer in seconds, or off' }, + ...optionalChatFlags, + { name: 'seconds', aliases: ['duration', 'ephemeral-duration'], type: 'string', description: 'Disappearing-message timer in seconds, duration form like 24h/7d/90d, or off' }, ], path: ['chats', 'disappear'], risk: 'write', run: chatsDisappear, }, { + args: [{ name: 'chat', description: 'Chat selector. Used when --chat is omitted.' }], + aliases: [['chat', 'focus'], ['open'], ['browse'], ['focus']], description: 'Focus a chat in Beeper', flags: [ - ...chatFlags, + { ...chatFlag, required: false }, + pickChatFlag, { name: 'file', type: 'string', description: 'Draft attachment file path' }, { name: 'message', type: 'string', description: 'Message ID to focus' }, { name: 'text', type: 'string', description: 'Draft text' }, @@ -555,14 +878,25 @@ export const commands: CommandSpec[] = [ run: chatsFocus, }, { + args: [chatArg], + aliases: [['chat', 'notify-anyway']], description: 'Notify a chat anyway', - flags: chatFlags, + flags: optionalChatFlags, path: ['chats', 'notify-anyway'], risk: 'write', run: chatsNotifyAnyway, }, { - args: [{ name: 'selector', required: true }], + aliases: [['search-all'], ['find-all']], + args: [{ name: 'query', required: true, description: 'Search query' }], + description: 'Search chats, group participants, and messages together', + output: 'diagnostic', + path: ['search', 'all'], + risk: 'read', + run: unifiedSearch, + }, + { + args: [{ name: 'selector', required: true, description: 'Account selector' }], description: 'Resolve an account selector', flags: [pickCandidateFlag], mcp: true, @@ -571,16 +905,15 @@ export const commands: CommandSpec[] = [ run: resolveAccount, }, { - args: [{ name: 'selector', required: true }], + args: [{ name: 'selector', required: true, description: 'Bridge selector' }], description: 'Resolve a bridge selector', flags: [pickCandidateFlag], - mcp: true, path: ['resolve', 'bridge'], risk: 'read', run: resolveBridge, }, { - args: [{ name: 'selector', required: true }], + args: [{ name: 'selector', required: true, description: 'Chat selector' }], description: 'Resolve a chat selector', flags: [ accountFilterFlag, @@ -593,7 +926,7 @@ export const commands: CommandSpec[] = [ run: resolveChat, }, { - args: [{ name: 'selector', required: true }], + args: [{ name: 'selector', required: true, description: 'Contact selector' }], description: 'Resolve a contact selector', flags: [ accountFilterFlag, @@ -606,7 +939,7 @@ export const commands: CommandSpec[] = [ run: resolveContact, }, { - args: [{ name: 'selector', required: true }], + args: [{ name: 'selector', required: true, description: 'Target selector' }], description: 'Resolve a target selector', flags: [pickCandidateFlag], mcp: true, @@ -617,16 +950,7 @@ export const commands: CommandSpec[] = [ { description: 'List chat messages', aliases: [['messages', 'ls']], - flags: [ - { name: 'after-cursor', type: 'string', description: 'Paginate messages newer than this message ID' }, - { name: 'asc', type: 'boolean', default: false, description: 'Order oldest first' }, - { name: 'before-cursor', type: 'string', description: 'Paginate messages older than this message ID' }, - chatFlag, - { name: 'ids', type: 'boolean', default: false, description: 'Print only message IDs' }, - { name: 'limit', type: 'integer', default: 50, description: 'Maximum messages to print' }, - pickChatFlag, - { name: 'sender', type: 'string', description: 'me, others, or a specific user ID' }, - ], + flags: [...messageListFlags(), { name: 'ids', type: 'boolean', default: false, description: 'Print only message IDs' }], mcp: true, output: 'messages', path: ['messages', 'list'], @@ -634,11 +958,12 @@ export const commands: CommandSpec[] = [ run: messagesList, }, { + args: [{ name: 'id', description: 'Message ID. Used when --id is omitted.' }], description: 'Show a message with surrounding context', flags: [ chatFlag, pickChatFlag, - { name: 'id', type: 'string', required: true, description: 'Message ID' }, + { name: 'id', type: 'string', description: 'Message ID' }, { name: 'after', type: 'integer', default: 10, description: 'Messages after target' }, { name: 'before', type: 'integer', default: 10, description: 'Messages before target' }, ], @@ -648,29 +973,88 @@ export const commands: CommandSpec[] = [ run: messagesContext, }, { + args: [{ name: 'id', description: 'Message ID. Used when --id is omitted.' }], + aliases: [['messages', 'get'], ['messages', 'info']], + description: 'Show one message', + flags: [ + chatFlag, + pickChatFlag, + { name: 'id', type: 'string', description: 'Message ID' }, + ], + mcp: true, + path: ['messages', 'show'], + risk: 'read', + run: messagesContext, + }, + { + description: 'Export messages as JSON', + flags: [ + ...messageListFlags(1000, 'Maximum messages to export'), + { name: 'output', aliases: ['out'], type: 'string', description: 'Write JSON export to file instead of stdout' }, + ], + output: 'messages', + path: ['messages', 'export'], + risk: 'read', + run: messagesExport, + }, + { + args: [{ name: 'id', description: 'Source message ID. Used when --id is omitted.' }], + description: 'Forward a message', + flags: [ + chatFlag, + { name: 'id', type: 'string', description: 'Source message ID' }, + { name: 'to', type: 'string', required: true, description: 'Destination chat selector' }, + pickChatFlag, + { name: 'attachment-index', type: 'integer', default: 1, description: 'Attachment index to forward when the message has media, 1-based' }, + { name: 'post-send-wait', type: 'string', default: '2s', description: 'Compatibility alias for waiting after forward, for example 2s or 500ms; 0 disables waiting' }, + { name: 'wait', type: 'boolean', default: false, description: 'Wait until the forwarded message leaves pending state' }, + { name: 'wait-timeout', type: 'integer', default: 30_000, description: 'Maximum wait time in ms when --wait is set' }, + ], + path: ['messages', 'forward'], + risk: 'write', + run: messagesForward, + }, + { + args: [{ name: 'id', description: 'Message ID. Used when --id is omitted.' }], + aliases: [['messages', 'update'], ['messages', 'set']], description: 'Edit a message', flags: [ chatFlag, pickChatFlag, - { name: 'id', type: 'string', required: true, description: 'Message ID' }, + { name: 'id', type: 'string', description: 'Message ID' }, { name: 'message', type: 'string', required: true, description: 'New message text' }, ], + mcp: true, path: ['messages', 'edit'], risk: 'write', run: messagesEdit, }, { + args: [{ name: 'id', description: 'Message ID. Used when --id is omitted.' }], + aliases: [['messages', 'rm'], ['messages', 'del'], ['messages', 'remove']], description: 'Delete a message', flags: [ chatFlag, pickChatFlag, - { name: 'id', type: 'string', required: true, description: 'Message ID' }, + { name: 'id', type: 'string', description: 'Message ID' }, { name: 'for-everyone', type: 'boolean', default: false, description: 'Delete for everyone when supported' }, ], path: ['messages', 'delete'], risk: 'destructive', run: messagesDelete, }, + { + args: [{ name: 'id', description: 'Message ID. Used when --id is omitted.' }], + description: 'Delete a sent message for everyone', + flags: [ + chatFlag, + pickChatFlag, + { name: 'id', type: 'string', description: 'Message ID' }, + ], + path: ['messages', 'revoke'], + risk: 'destructive', + run: messagesDelete, + }, { description: 'Stream Desktop API WebSocket events', flags: [ @@ -678,21 +1062,43 @@ export const commands: CommandSpec[] = [ { name: 'exclude-type', type: 'string', multiple: true, enum: ['chat.upserted', 'chat.deleted', 'message.upserted', 'message.deleted', 'message.stream'], description: 'Drop events of these types' }, { name: 'include-type', type: 'string', multiple: true, enum: ['chat.upserted', 'chat.deleted', 'message.upserted', 'message.deleted', 'message.stream'], description: 'Only forward events of these types' }, { name: 'webhook', type: 'string', description: 'Forward each event to this URL as POST' }, + { name: 'webhook-allow-private', type: 'boolean', default: false, description: 'Accepted for compatibility; Beeper watch allows private webhook URLs' }, { name: 'webhook-queue', type: 'integer', default: 64, description: 'Maximum pending webhook deliveries' }, - { name: 'webhook-secret', type: 'string', description: 'HMAC-SHA256 secret for X-Beeper-Signature' }, + { name: 'webhook-secret', type: 'string', description: 'HMAC-SHA256 secret for X-Beeper-Signature and X-Wacli-Signature' }, ], path: ['watch'], risk: 'read', run: watch, }, { - args: [{ name: 'url', required: true }], + args: [{ name: 'url', description: 'Media URL to download. Use --id with --chat to download from a message.' }], + aliases: [['media', 'dl'], ['download'], ['dl']], description: 'Download message media', - flags: [{ name: 'out', type: 'string', default: '.', description: 'Output directory; - streams to stdout' }], + flags: [ + { name: 'out', aliases: ['output'], type: 'string', default: '.', description: 'Output directory or file; - streams to stdout' }, + { name: 'chat', type: 'string', description: 'Chat selector for --id message lookup' }, + { name: 'id', type: 'string', description: 'Message ID containing media' }, + { name: 'index', type: 'integer', default: 1, description: 'Attachment index to download, 1-based' }, + { name: 'poster', type: 'boolean', default: false, description: 'Download attachment poster image when available' }, + ], path: ['media', 'download'], risk: 'write', run: mediaDownload, }, + { + args: [{ name: 'id', description: 'Message ID containing media. Used when --id is omitted.' }], + description: 'Download media for a message', + flags: [ + { name: 'out', aliases: ['output'], type: 'string', default: '.', description: 'Output directory or file; - streams to stdout' }, + chatFlag, + { name: 'id', type: 'string', description: 'Message ID containing media' }, + { name: 'index', type: 'integer', default: 1, description: 'Attachment index to download, 1-based' }, + { name: 'poster', type: 'boolean', default: false, description: 'Download attachment poster image when available' }, + ], + path: ['media', 'message'], + risk: 'write', + run: mediaDownload, + }, { description: 'Export accounts, chats, messages, transcripts, and attachments', flags: [ @@ -711,8 +1117,8 @@ export const commands: CommandSpec[] = [ run: exportCommand, }, { - args: [{ name: 'query' }], - aliases: [['messages', 'find']], + args: [{ name: 'query', description: 'Search query. Optional when a filter such as --sender or --has-media is provided.' }], + aliases: [['messages', 'find'], ['search'], ['find']], description: 'Search messages across chats', examples: [ 'beeper messages search "quarterly report"', @@ -729,7 +1135,8 @@ export const commands: CommandSpec[] = [ { name: 'include-muted', type: 'boolean', default: true, description: 'Include muted chats' }, { name: 'limit', aliases: ['max'], type: 'integer', default: 50, description: 'Maximum results' }, { name: 'media', type: 'string', multiple: true, enum: ['any', 'video', 'image', 'link', 'file'], description: 'Filter by media type' }, - { name: 'sender', type: 'string', description: 'me, others, or a user ID' }, + { name: 'sender', aliases: ['from'], type: 'string', description: 'me, others, or a user ID' }, + { name: 'has-media', type: 'boolean', default: false, description: 'Only messages with media' }, { name: 'fail-empty', aliases: ['non-empty', 'require-results'], type: 'boolean', default: false, description: 'Exit with code 3 if no results' }, ], mcp: true, @@ -739,18 +1146,24 @@ export const commands: CommandSpec[] = [ run: messagesSearch, }, { - args: [{ name: 'method', required: true }, { name: 'path', required: true }], + args: [ + { name: 'method', required: true, description: 'HTTP method: GET, POST, PUT, PATCH, or DELETE' }, + { name: 'path', required: true, description: 'Desktop API path, for example /v1/info' }, + ], description: 'Call a raw Desktop API path with any supported HTTP method', flags: [ { name: 'body', type: 'string', description: 'JSON request body' }, { name: 'no-auth', type: 'boolean', default: false, description: 'Call a public API path without a bearer token' }, ], - mcp: true, path: ['api', 'request'], risk: 'write', run: apiCommand, }, { + args: [ + { name: 'to', description: 'Chat selector. Used when --to is omitted.' }, + { name: 'message', description: 'Message text. Used when --message and --message-file are omitted.', variadic: true }, + ], description: 'Send a text message', flags: [ ...sendDeliveryFlags, @@ -759,29 +1172,71 @@ export const commands: CommandSpec[] = [ { name: 'message-file', type: 'string', description: "Read message text from a file path; '-' reads stdin" }, { name: 'mention', type: 'string', multiple: true, description: 'User ID to mention' }, { name: 'no-preview', type: 'boolean', default: false, description: 'Disable automatic link preview' }, + { name: 'ephemeral', type: 'boolean', default: false, description: 'Send with this chat\'s disappearing-message timer' }, + { name: 'ephemeral-duration', type: 'string', description: 'Set the chat disappearing-message timer before sending, for example 24h, 7d, 90d, or 168h' }, ], + path: ['send'], + risk: 'write', + run: sendTextLike, + }, + { + aliases: [['message'], ['msg']], + args: [ + { name: 'to', description: 'Chat selector. Used when --to is omitted.' }, + { name: 'message', description: 'Message text. Used when --message and --message-file are omitted.', variadic: true }, + ], + description: 'Send a text message', + flags: [ + ...sendDeliveryFlags, + { name: 'message', type: 'string', description: 'Message text to send' }, + { name: 'message-escapes', type: 'boolean', default: false, description: 'Interpret backslash escapes in --message' }, + { name: 'message-file', type: 'string', description: "Read message text from a file path; '-' reads stdin" }, + { name: 'mention', type: 'string', multiple: true, description: 'User ID to mention' }, + { name: 'no-preview', type: 'boolean', default: false, description: 'Disable automatic link preview' }, + { name: 'ephemeral', type: 'boolean', default: false, description: 'Send with this chat\'s disappearing-message timer' }, + { name: 'ephemeral-duration', type: 'string', description: 'Set the chat disappearing-message timer before sending, for example 24h, 7d, 90d, or 168h' }, + ], + mcp: true, path: ['send', 'text'], risk: 'write', run: sendTextLike, }, { + args: [{ name: 'localPath', description: 'Local file path to upload. Used when --file is omitted.' }], description: 'Send a file message', flags: [ ...sendDeliveryFlags, - { name: 'file', type: 'string', required: true, description: 'Local file path to upload' }, + { name: 'file', type: 'string', description: 'Local file path to upload' }, { name: 'caption', type: 'string', description: 'Optional caption for file messages' }, { name: 'filename', type: 'string', description: 'Override displayed filename' }, { name: 'mime', type: 'string', description: 'Override MIME type' }, + { name: 'ptt', type: 'boolean', default: false, description: 'Send audio as a voice note' }, ], path: ['send', 'file'], risk: 'write', run: sendTextLike, }, { + aliases: [['up'], ['put']], + args: [{ name: 'localPath', required: true, description: 'Local file path to upload' }], + description: 'Send a file message', + flags: [ + ...sendDeliveryFlags, + { name: 'caption', type: 'string', description: 'Optional caption for file messages' }, + { name: 'filename', type: 'string', description: 'Override displayed filename' }, + { name: 'mime', type: 'string', description: 'Override MIME type' }, + { name: 'ptt', type: 'boolean', default: false, description: 'Send audio as a voice note' }, + ], + path: ['upload'], + risk: 'write', + run: uploadFile, + }, + { + args: [{ name: 'localPath', description: 'Local sticker file path to upload. Used when --file is omitted.' }], description: 'Send a sticker', flags: [ ...sendDeliveryFlags, - { name: 'file', type: 'string', required: true, description: 'Local sticker file path to upload' }, + { name: 'file', type: 'string', description: 'Local sticker file path to upload' }, { name: 'filename', type: 'string', description: 'Override displayed filename' }, { name: 'mime', type: 'string', description: 'Override MIME type' }, ], @@ -790,10 +1245,11 @@ export const commands: CommandSpec[] = [ run: sendTextLike, }, { + args: [{ name: 'localPath', description: 'Local voice note file path to upload. Used when --file is omitted.' }], description: 'Send a voice note', flags: [ ...sendDeliveryFlags, - { name: 'file', type: 'string', required: true, description: 'Local voice note file path to upload' }, + { name: 'file', type: 'string', description: 'Local voice note file path to upload' }, { name: 'duration', type: 'integer', description: 'Duration in seconds' }, { name: 'filename', type: 'string', description: 'Override displayed filename' }, { name: 'mime', type: 'string', description: 'Override MIME type' }, @@ -803,52 +1259,80 @@ export const commands: CommandSpec[] = [ run: sendTextLike, }, { + aliases: [['send', 'reaction']], + args: [{ name: 'id', description: 'Message ID to react to. Used when --id is omitted.' }], description: 'Send or remove a reaction', flags: [ ...sendChatFlags, - { name: 'id', type: 'string', required: true, description: 'Message ID to react to' }, - { name: 'reaction', type: 'string', required: true, description: 'Reaction key' }, + { name: 'id', type: 'string', description: 'Message ID to react to' }, + { name: 'reaction', type: 'string', default: '+1', description: 'Reaction key; empty string removes the reaction' }, { name: 'remove', type: 'boolean', default: false, description: 'Remove the reaction' }, + { name: 'post-send-wait', type: 'string', description: 'Compatibility alias for waiting after send, for example 2s or 500ms; 0 disables waiting' }, { name: 'transaction', type: 'string', description: 'Optional transaction ID for deduplication' }, ], + mcp: true, path: ['send', 'react'], risk: 'write', run: sendReact, }, { description: 'Send a typing indicator', + flags: presenceFlags, + path: ['send', 'presence'], + risk: 'write', + run: sendPresence, + }, + { + description: 'Send presence indicators', + flags: presenceFlags, + path: ['presence'], + risk: 'write', + run: sendPresence, + }, + { + description: "Send a 'composing' (typing) indicator to a chat", flags: [ ...sendChatFlags, { name: 'duration', type: 'integer', description: 'Seconds to keep typing before sending paused' }, - { name: 'state', type: 'string', enum: ['typing', 'paused'], default: 'typing', description: 'Presence indicator to send' }, ], - path: ['send', 'presence'], + path: ['presence', 'typing'], risk: 'write', run: sendPresence, }, { - args: [{ name: 'name' }], + description: "Send a 'paused' indicator (stop typing) to a chat", + flags: sendChatFlags, + path: ['presence', 'paused'], + risk: 'write', + run: sendPresence, + }, + { + args: [{ name: 'name', description: 'Target name. Defaults to the selected target.' }], + aliases: [['target', 'runtime', 'start']], description: 'Start a local target runtime', path: ['targets', 'runtime', 'start'], risk: 'write', run: targetsRuntime, }, { - args: [{ name: 'name' }], + args: [{ name: 'name', description: 'Target name. Defaults to the selected target.' }], + aliases: [['target', 'runtime', 'stop']], description: 'Stop a local server runtime', path: ['targets', 'runtime', 'stop'], risk: 'write', run: targetsRuntime, }, { - args: [{ name: 'name' }], + args: [{ name: 'name', description: 'Target name. Defaults to the selected target.' }], + aliases: [['target', 'runtime', 'restart']], description: 'Restart a local server runtime', path: ['targets', 'runtime', 'restart'], risk: 'write', run: targetsRuntime, }, { - args: [{ name: 'name' }], + args: [{ name: 'name', description: 'Target name. Defaults to the selected target.' }], + aliases: [['target', 'logs']], description: 'Print logs for a local Beeper Desktop or Server install', flags: [ { name: 'lines', type: 'integer', default: 200, description: 'Lines to print from each log file' }, @@ -862,78 +1346,467 @@ export const commands: CommandSpec[] = [ ] export function commandHelp(command: CommandSpec, globalFlags?: GlobalFlags): string { - if (globalFlags && !commandVisible(command, globalFlags)) return help(globalFlags) + if (globalFlags && !commandVisible(command, globalFlags) && !childCommandRows(command.path, globalFlags).length) return help(globalFlags) const path = command.path.join(' ') - const usageAliases = (command.aliases ?? []).map(alias => alias.join(' ')).join(', ') - const lines = [`Usage: beeper ${path}${command.args?.length ? ` ${command.args.map(arg => arg.variadic ? `<${arg.name}> ...` : arg.required ? `<${arg.name}>` : `[${arg.name}]`).join(' ')}` : ''} [flags]`, '', command.description] - if (usageAliases) lines.push('', `Aliases: ${usageAliases}`) + const usagePath = [path, formatUsageAliases(command)].filter(Boolean).join(' ') + const children = childCommandRows(command.path, globalFlags) + const argsUsage = command.args?.length ? ` ${command.args.map(formatArgUsage).join(' ')}` : children.length ? ' ' : '' + const lines = [`Usage: beeper ${usagePath}${argsUsage} [flags]`, `Build: ${buildInfo()}`, '', command.description] const args = command.args ?? [] - const flags = command.flags ?? [] if (args.length) { lines.push('', 'Arguments:') - for (const arg of args) lines.push(` ${arg.name}${arg.required ? '' : '?'}${arg.variadic ? '...' : ''}\t${arg.description ?? ''}`) + lines.push(...formatHelpRows(args.map(arg => [formatArgLabel(arg), arg.description ?? '']))) } + lines.push('', 'Flags:') + lines.push(...formatHelpRows(displayFlags(globalFlagSpecs).map(flag => [formatFlag(flag), formatFlagDescription(flag)]))) + const flags = command.flags ?? [] if (flags.length) { - lines.push('', 'Flags:') - for (const flag of flags) lines.push(` ${formatFlag(flag)}\t${flag.description ?? ''}`) + lines.push('') + lines.push(...formatHelpRows(flags.map(flag => [formatFlag(flag), formatFlagDescription(flag)]))) } if (command.examples?.length) { lines.push('', 'Examples:', ...command.examples.map(example => ` ${example}`)) } - lines.push('', 'Global flags:') - for (const flag of globalFlagSpecs) lines.push(` ${formatFlag(flag)}\t${flag.description ?? ''}`) + if (children.length) { + lines.push('', 'Commands:') + for (const child of children) { + lines.push(` ${formatCommandUsage(child.command, { path: child.displayPath, prefix: command.path })}`, ` ${child.command.description}`, '') + } + lines.pop() + } return `${lines.join('\n')}\n` } export function help(globalFlags?: GlobalFlags): string { const visible = commands.filter(command => globalFlags ? commandVisible(command, globalFlags) : !command.hidden) - const width = Math.max(...visible.map(command => command.path.join(' ').length)) + 2 + const configRoot = beeperConfigRootInfo() const lines = [ 'Usage: beeper [flags]', + `Build: ${buildInfo()}`, '', 'Beeper CLI for Beeper Desktop and Beeper Server. Built for terminals, scripts, CI, and agents.', '', 'Config:', '', ` file: ${configPath()}`, + ` root: ${configRoot.path} (source: ${configRoot.source})`, '', - 'Commands:', + 'Flags:', ] - for (const command of [...visible].sort((a, b) => a.path.join(' ').localeCompare(b.path.join(' ')))) { - const aliases = command.aliases?.length ? ` (${command.aliases.map(alias => alias.join(' ')).join(', ')})` : '' - lines.push(` ${command.path.join(' ').padEnd(width)}${command.description}${aliases}`) + lines.push(...formatHelpRows(displayFlags(globalFlagSpecs).map(flag => [formatFlag(flag), formatFlagDescription(flag)]))) + lines.push( + '', + 'Commands:', + ) + for (const row of rootCommandRows(visible)) { + lines.push(` ${row.usage}`, ` ${row.description}`, '') } - lines.push('', 'Global flags:') - for (const flag of globalFlagSpecs) lines.push(` ${formatFlag(flag)}\t${flag.description ?? ''}`) - lines.push('', 'Run "beeper --help" for more information on a command.') + lines.push('Run "beeper --help" for more information on a command.') return `${lines.join('\n')}\n` } +function rootCommandRows(visible: CommandSpec[]): Array<{ description: string; sort: string; usage: string }> { + const rows = new Map() + const topLevel = new Set(visible.map(command => command.path[0]).filter((part): part is string => Boolean(part))) + for (const command of visible) { + if (command.path.length === 1) { + const hasChildren = visible.some(candidate => + candidate.path.length > 1 && candidate.path[0] === command.path[0] + || (candidate.aliases ?? []).some(alias => alias.length > 1 && alias[0] === command.path[0])) + rows.set(command.path[0]!, { + description: command.description, + sort: command.path[0]!, + usage: hasChildren && !command.args?.length ? `${command.path[0]} [flags]` : formatCommandUsage(command), + }) + } + } + for (const command of visible) { + if (command.path.length <= 1) continue + const aliases = (command.aliases ?? []) + .filter(alias => alias.length === 1) + .map(alias => alias[0]!) + if (!aliases.length) continue + const name = aliases[0]! + if (rows.has(name)) continue + const alternateAliases = aliases.slice(1) + rows.set(name, { + description: `${command.description} (alias for '${command.path.join(' ')}')`, + sort: name, + usage: `${name}${alternateAliases.length ? ` (${alternateAliases.join(',')})` : ''}${command.args?.length ? ` ${command.args.map(formatArgUsage).join(' ')}` : ''} [flags]`, + }) + } + const me = visible.find(command => command.path.join(' ') === 'me') + const whoamiAliases = me?.aliases?.filter(alias => alias.length === 1 && alias[0]?.startsWith('who')) ?? [] + if (me && whoamiAliases.length && !rows.has(whoamiAliases[0]![0]!)) { + const name = whoamiAliases[0]![0]! + rows.set(name, { + description: `${me.description} (alias for '${me.path.join(' ')}')`, + sort: name, + usage: `${name}${whoamiAliases.length > 1 ? ` (${whoamiAliases.slice(1).map(alias => alias[0]).join(',')})` : ''} [flags]`, + }) + } + for (const name of topLevel) { + if (rows.has(name)) continue + const aliases = namespaceAliases(name, visible) + rows.set(name, { + description: rootNamespaceDescription(name), + sort: name, + usage: `${name}${aliases.length ? ` (${aliases.join(',')})` : ''} [flags]`, + }) + } + return [...rows.values()].sort((a, b) => rootCommandPriority(a.sort) - rootCommandPriority(b.sort) || a.sort.localeCompare(b.sort)) +} + +function rootCommandPriority(name: string): number { + const order = [ + 'message', + 'ls', + 'search', + 'open', + 'download', + 'upload', + 'login', + 'logout', + 'status', + 'me', + 'whoami', + 'setup', + 'send', + 'chats', + 'groups', + 'messages', + 'accounts', + 'contacts', + 'presence', + 'media', + 'targets', + 'use', + 'remove', + 'resolve', + 'export', + 'watch', + 'doctor', + 'auth', + 'install', + 'api', + 'config', + 'docs', + 'schema', + 'mcp', + 'agent', + 'exit-codes', + 'completion', + 'help', + 'version', + ] + const index = order.indexOf(name) + return index === -1 ? order.length : index +} + +function namespaceAliases(name: string, visible: CommandSpec[]): string[] { + const allowed = singularNamespaceAliases()[name] ?? [] + const aliases = new Set() + for (const command of visible) { + if (command.path[0] !== name) continue + for (const alias of command.aliases ?? []) { + if (alias.length < 2) continue + const aliasRoot = alias[0] + if (aliasRoot && allowed.includes(aliasRoot)) aliases.add(aliasRoot) + } + } + return [...aliases].sort((a, b) => rootCommandPriority(a) - rootCommandPriority(b) || a.localeCompare(b)) +} + +function singularNamespaceAliases(): Record { + return { + accounts: ['account'], + chats: ['chat'], + contacts: ['contact'], + groups: ['group'], + targets: ['target'], + } +} + +function rootNamespaceDescription(name: string): string { + const descriptions: Record = { + account: 'Manage connected chat accounts', + accounts: 'Manage connected chat accounts', + api: 'Call raw Beeper Desktop API endpoints', + auth: 'Authenticate and manage stored credentials', + chat: 'List and manage chats', + chats: 'List and manage chats', + config: 'Manage configuration', + contact: 'List and search contacts', + contacts: 'List and search contacts', + group: 'List and manage group chats', + groups: 'List and manage group chats', + install: 'Install Beeper Desktop or Beeper Server', + media: 'Download message media', + messages: 'List, search, edit, and delete messages', + presence: 'Send presence indicators', + remove: 'Remove configured resources', + resolve: 'Resolve Beeper selectors', + search: 'Search Beeper', + send: 'Send messages, files, reactions, and presence', + target: 'Manage Beeper Desktop and Server targets', + targets: 'Manage Beeper Desktop and Server targets', + use: 'Select default resources', + } + return descriptions[name] ?? `${name} commands` +} + function formatFlag(flag: FlagSpec): string { - const long = `--${flag.name}${flag.type === 'boolean' ? '' : `=${flag.placeholder ?? 'STRING'}`}` + const long = `--${flag.name}${flagValueUsage(flag)}` const prefix = flag.short ? `-${flag.short}, ${long}` : ` ${long}` const aliases = flag.aliases?.length ? ` (${flag.aliases.map(alias => `--${alias}`).join(', ')})` : '' + return `${prefix}${aliases}` +} + +function formatFlagDescription(flag: FlagSpec): string { const env = flag.env?.length ? ` (${flag.env.map(name => `$${name}`).join(',')})` : '' - return `${prefix}${aliases}${env}` + return `${flag.description ?? ''}${env}` +} + +function formatHelpRows(rows: Array<[string, string]>): string[] { + const width = rows.reduce((max, [label]) => Math.max(max, label.length), 0) + return rows.flatMap(([label, description]) => { + const text = description.trim() + if (!text) return [` ${label}`] + return wrapHelpText(text, 120 - width - 4).map((line, index) => { + const prefix = index === 0 ? label.padEnd(width + 2) : ''.padEnd(width + 2) + return ` ${prefix}${line}` + }) + }) +} + +function wrapHelpText(text: string, width: number): string[] { + const limit = Math.max(24, width) + const words = text.split(/\s+/) + const lines: string[] = [] + let line = '' + for (const word of words) { + if (!line) { + line = word + continue + } + if (line.length + 1 + word.length > limit) { + lines.push(line) + line = word + continue + } + line = `${line} ${word}` + } + if (line) lines.push(line) + return lines +} + +function displayFlags(flags: FlagSpec[]): FlagSpec[] { + const priority = new Map([ + ['help', 0], + ['color', 1], + ['home', 2], + ['account', 3], + ['access-token', 4], + ['enable-commands', 5], + ['enable-commands-exact', 6], + ['disable-commands', 7], + ['json', 8], + ['plain', 9], + ['wrap-untrusted', 10], + ['results-only', 11], + ['select', 12], + ['dry-run', 13], + ['force', 14], + ['no-input', 15], + ['verbose', 16], + ['version', 17], + ['events', 18], + ['full', 19], + ['lock-wait', 20], + ['read-only', 21], + ['safety-profile', 22], + ['target', 23], + ['timeout', 24], + ]) + return [...flags].sort((a, b) => (priority.get(a.name) ?? 100) - (priority.get(b.name) ?? 100) || a.name.localeCompare(b.name)) +} + +function flagValueUsage(flag: FlagSpec): string { + if (flag.type === 'boolean') return '' + if (flag.default !== undefined) return `=${JSON.stringify(flag.default)}` + return `=${flag.placeholder ?? (flag.type === 'integer' ? 'INTEGER' : 'STRING')}` +} + +function formatUsageAliases(command: CommandSpec, prefix?: string[], canonical = displayPath(command.path, prefix)): string { + const aliases = (command.aliases ?? []) + .filter(alias => !prefix?.length || (alias.length > prefix.length && alias.slice(0, prefix.length).every((part, index) => part === prefix[index]))) + .map(alias => displayPath(alias, prefix)) + .filter(alias => alias !== canonical) + return aliases.length ? `(${[...new Set(aliases)].join(',')})` : '' +} + +function formatCommandUsage(command: CommandSpec, options: { path?: string[]; prefix?: string[] } = {}): string { + const path = displayPath(options.path ?? command.path, options.prefix) + const usagePath = [path, formatUsageAliases(command, options.prefix, path)].filter(Boolean).join(' ') + const args = command.args?.length ? ` ${command.args.map(formatArgUsage).join(' ')}` : '' + return `${usagePath}${args} [flags]` +} + +function displayPath(path: string[], prefix?: string[]): string { + if (prefix?.length && path.length > prefix.length && path.slice(0, prefix.length).every((part, index) => part === prefix[index])) { + return path.slice(prefix.length).join(' ') + } + return path.join(' ') +} + +function formatArgUsage(arg: { name: string; required?: boolean; variadic?: boolean }): string { + if (arg.variadic) return arg.required ? `<${arg.name}> ...` : `[<${arg.name}> ...]` + return arg.required ? `<${arg.name}>` : `[<${arg.name}>]` +} + +function formatArgLabel(arg: { name: string; required?: boolean; variadic?: boolean }): string { + if (arg.variadic) return arg.required ? `<${arg.name} ...>` : `[<${arg.name}> ...]` + return arg.required ? `<${arg.name}>` : `[<${arg.name}>]` +} + +function childCommandRows(path: string[], globalFlags?: GlobalFlags): Array<{ command: CommandSpec; displayPath: string[] }> { + const visible = commands.filter(command => globalFlags ? commandVisible(command, globalFlags) : !command.hidden) + return visible + .map(command => { + const displayPath = commandPathVariants(command).find(variant => variant.length > path.length && variant.slice(0, path.length).every((part, index) => part === path[index])) + return displayPath ? { command, displayPath } : undefined + }) + .filter((row): row is { command: CommandSpec; displayPath: string[] } => Boolean(row)) + .sort((a, b) => subcommandPriority(path, a.displayPath) - subcommandPriority(path, b.displayPath) || a.displayPath.join(' ').localeCompare(b.displayPath.join(' '))) +} + +function subcommandPriority(parent: string[], displayPath: string[]): number { + const relative = parent.length ? displayPath.slice(parent.length).join(' ') : displayPath.join(' ') + const orders: Record = { + account: ['list', 'show', 'add', 'use', 'remove'], + accounts: ['list', 'show', 'add', 'use', 'remove'], + auth: ['add', 'list', 'email start', 'email response', 'logout', 'status'], + chat: ['list', 'show', 'start', 'archive', 'unarchive', 'pin', 'unpin', 'mute', 'unmute', 'read', 'mark-read', 'mark-unread', 'rename', 'description', 'avatar', 'priority', 'draft', 'remind', 'disappear', 'focus', 'notify-anyway'], + chats: ['list', 'show', 'start', 'archive', 'unarchive', 'pin', 'unpin', 'mute', 'unmute', 'read', 'mark-read', 'mark-unread', 'rename', 'description', 'avatar', 'priority', 'draft', 'remind', 'disappear', 'focus', 'notify-anyway'], + config: ['get', 'keys', 'set', 'unset', 'list', 'path'], + contact: ['list', 'show'], + contacts: ['list', 'show'], + group: ['list', 'show', 'create', 'rename', 'description'], + groups: ['list', 'show', 'create', 'rename', 'description'], + media: ['download', 'message'], + messages: ['list', 'search', 'context', 'show', 'export', 'forward', 'edit', 'delete', 'revoke'], + presence: ['typing', 'paused'], + search: ['all'], + send: ['text', 'file', 'voice', 'sticker', 'react', 'presence'], + target: ['list', 'use', 'add', 'remove', 'logs', 'runtime start', 'runtime stop', 'runtime restart', 'tunnel'], + 'target runtime': ['start', 'stop', 'restart'], + targets: ['list', 'use', 'add', 'remove', 'logs', 'runtime start', 'runtime stop', 'runtime restart', 'tunnel'], + 'targets runtime': ['start', 'stop', 'restart'], + } + const order = orders[parent.join(' ')] ?? [] + const index = order.indexOf(relative) + return index === -1 ? order.length : index } async function version(): Promise> { const pkg = await packageInfo() - return { name: pkg.name, version: pkg.version } + return { + name: pkg.name, + version: pkg.version, + commit: process.env.BEEPER_BUILD_COMMIT ?? '', + date: process.env.BEEPER_BUILD_DATE ?? '', + } } async function status(ctx: CommandContext): Promise> { + const config = await readConfig() const target = await resolveTarget({ target: ctx.args[0] ?? ctx.globalFlags.target }) return { auth: { authenticated: Boolean(process.env.BEEPER_ACCESS_TOKEN || target.auth?.accessToken), - clientID: target.auth?.clientID, - expiresAt: target.auth?.expiresAt, - scope: target.auth?.scope, + clientID: target.auth?.clientID, + expiresAt: target.auth?.expiresAt, + scope: target.auth?.scope, + source: process.env.BEEPER_ACCESS_TOKEN ? 'env' : target.auth?.source ?? (target.auth?.accessToken ? 'target' : 'none'), + }, + config: configStatus(config), + live: await targetLiveStatus(target), + readiness: await evaluateReadiness({ baseURL: target.baseURL, target: target.id }), + target: publicTarget(target), + } +} + +async function authList(): Promise> { + const config = await readConfig() + const targets = await listTargets() + const rows = targets.length ? targets : [await resolveTarget({ target: builtInDesktopTargetID })] + return { accounts: rows.map(target => ({ + authenticated: Boolean(target.auth?.accessToken), + baseURL: target.baseURL, + clientID: target.auth?.clientID, + default: config.defaultTarget ? config.defaultTarget === target.id : target.id === builtInDesktopTargetID, + expiresAt: target.auth?.expiresAt, + scope: target.auth?.scope, + source: target.auth?.source ?? 'none', + target: target.id, + tokenType: target.auth?.tokenType, + type: target.type, + })) } +} + +async function authStatus(ctx: CommandContext): Promise> { + const config = await readConfig() + const target = await resolveTarget({ target: ctx.args[0] ?? ctx.globalFlags.target }) + return { + auth: { + authenticated: Boolean(process.env.BEEPER_ACCESS_TOKEN || target.auth?.accessToken), + clientID: target.auth?.clientID, + expiresAt: target.auth?.expiresAt, + scope: target.auth?.scope, + source: process.env.BEEPER_ACCESS_TOKEN ? 'env' : target.auth?.source ?? (target.auth?.accessToken ? 'target' : 'none'), + tokenType: process.env.BEEPER_ACCESS_TOKEN ? 'Bearer' : target.auth?.tokenType, + }, + config: configStatus(config), + target: { + baseURL: target.baseURL, + default: config.defaultTarget ? config.defaultTarget === target.id : target.id === builtInDesktopTargetID, + id: target.id, + type: target.type, + }, + } +} + +function configStatus(config: Config): Record { + return { + defaultAccount: config.defaultAccount ?? null, + defaultTarget: config.defaultTarget ?? builtInDesktopTargetID, + exists: existsSync(configPath()), + path: configPath(), + } +} + +async function me(ctx: CommandContext): Promise> { + const config = await readConfig() + const target = await resolveTarget({ target: ctx.globalFlags.target }) + const accountSelectors = stringListFlag(ctx.flags, 'account') + const request = { + accounts: accountSelectors, + defaultAccount: config.defaultAccount, + target: target.id, + } + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'me', request } + const client = await apiClient(ctx) + const accountIDs = await resolveAccountIDs(client, accountSelectors, { allowMultiplePerInput: true, applyDefault: false }) + const accounts = apiItems(await client.accounts.list()) + .filter(row => !accountIDs?.length || accountIDs.includes(accountIDForRow(row))) + .map(row => ({ ...row, default: accountIDForRow(row) === config.defaultAccount || undefined })) + return { + accounts, + auth: { + authenticated: Boolean(process.env.BEEPER_ACCESS_TOKEN || target.auth?.accessToken), source: process.env.BEEPER_ACCESS_TOKEN ? 'env' : target.auth?.source ?? (target.auth?.accessToken ? 'target' : 'none'), }, - live: await targetLiveStatus(target), - readiness: await evaluateReadiness({ baseURL: target.baseURL, target: target.id }), + defaultAccount: config.defaultAccount, target: publicTarget(target), } } @@ -977,9 +1850,55 @@ async function exitCodes(): Promise> { } } +async function agent(): Promise> { + return { + helpers: [ + { command: 'agent exit-codes', description: 'Print stable exit codes for automation' }, + ], + } +} + +async function docs(ctx: CommandContext): Promise | string> { + const url = 'https://github.com/beeper/desktop-api-cli/tree/main/packages/cli' + if (ctx.flags.url || !ctx.globalFlags.json) return url + const root = dirname(dirname(dirname(fileURLToPath(import.meta.url)))) + return { + url, + commands: join(root, 'docs', 'commands', 'README.md'), + package: join(root, 'README.md'), + relative_commands: 'docs/commands/README.md', + } +} + +async function helpCommand(ctx: CommandContext): Promise { + const target = commandForHelp(ctx.args, ctx.globalFlags) + process.stdout.write(target ? commandHelp(target, ctx.globalFlags) : help(ctx.globalFlags)) +} + async function schema(ctx: CommandContext): Promise> { const pkg = await packageInfo() - return buildSchema(commands, String(pkg.version ?? '0'), ctx.args, ctx.globalFlags) + return buildSchema(commands, String(pkg.version ?? '0'), ctx.args, ctx.globalFlags, { + includeHidden: Boolean(ctx.flags['include-hidden']), + }) +} + +function commandForHelp(args: string[], globalFlags: GlobalFlags): CommandSpec | undefined { + const parts = args.flatMap(part => part.trim().split(/\s+/)).filter(Boolean) + if (!parts.length) return undefined + const exact = commands + .filter(command => commandVisible(command, globalFlags)) + .find(command => commandPathVariants(command).some(path => path.length === parts.length && path.every((part, index) => part === parts[index]))) + if (exact) return exact + const hasChildren = commands + .filter(command => commandVisible(command, globalFlags)) + .some(command => commandPathVariants(command).some(path => parts.length < path.length && parts.every((part, index) => path[index] === part))) + if (!hasChildren) return undefined + return { + description: parts.length === 1 ? rootNamespaceDescription(parts[0]!) : `${parts.join(' ')} commands`, + path: parts, + risk: 'read', + run: async () => undefined, + } } async function mcp(ctx: CommandContext): Promise { @@ -987,9 +1906,13 @@ async function mcp(ctx: CommandContext): Promise { await serveMcp(commands, ctx.globalFlags, { allowTools: stringListFlag(ctx.flags, 'allow-tool'), allowWrite: Boolean(ctx.flags['allow-write']), + httpHost: stringFlag(ctx.flags, 'http-host') ?? '127.0.0.1', + httpPath: stringFlag(ctx.flags, 'http-path') ?? '/mcp', + httpPort: numberFlag(ctx.flags, 'http-port', 7331), listTools: Boolean(ctx.flags['list-tools']), maxOutputBytes: numberFlag(ctx.flags, 'max-output-bytes', 102400), timeoutSeconds: numberFlag(ctx.flags, 'timeout-seconds', 60), + transport: stringFlag(ctx.flags, 'transport') === 'http' ? 'http' : 'stdio', }, String(pkg.version ?? '0')) } @@ -999,14 +1922,18 @@ async function completion(ctx: CommandContext): Promise { process.stdout.write(completionScript(shell)) } +async function completionShell(ctx: CommandContext): Promise { + process.stdout.write(completionScript(ctx.commandPath[1] ?? '')) +} + async function configGet(ctx: CommandContext): Promise> { const key = parseConfigKey(ctx.args[0]) const config = await readConfig() return { key, value: config[key] ?? null } } -async function configKeysCommand(): Promise { - return [...configKeys] +async function configKeysCommand(): Promise> { + return { keys: [...configKeys] } } async function configList(): Promise> { @@ -1055,14 +1982,14 @@ function unsetConfigKey(config: Config, key: ConfigKey): Config { async function completeCommand(ctx: CommandContext): Promise { const cword = numberFlag(ctx.flags, 'cword', -1) const words = ctx.args.length ? ctx.args : ['beeper'] - for (const item of completeWords(words, cword)) process.stdout.write(`${item}\n`) + for (const item of completeWords(words, cword, ctx.globalFlags)) process.stdout.write(`${item}\n`) } -async function targetsList(): Promise { +async function targetsList(): Promise> { const config = await readConfig() const targets = await listTargets() const rows = targets.length ? targets : [await resolveTarget({ target: builtInDesktopTargetID })] - return Promise.all(rows.map(async target => ({ + return { targets: await Promise.all(rows.map(async target => ({ baseURL: target.baseURL, default: config.defaultTarget ? config.defaultTarget === target.id : target.id === builtInDesktopTargetID, id: target.id, @@ -1070,7 +1997,7 @@ async function targetsList(): Promise { name: target.name ?? target.id, type: target.type, ...await targetLiveStatus(target), - }))) + }))) } } async function targetsAdd(ctx: CommandContext): Promise> { @@ -1198,11 +2125,22 @@ async function accountsList(ctx: CommandContext): Promise { const config = await readConfig() const rows = apiItems(await client.accounts.list()) const items = rows - .filter(row => !selected?.length || selected.includes(String(row.accountID ?? row.id))) - .map(row => ({ ...row, default: (row.accountID ?? row.id) === config.defaultAccount || undefined })) + .filter(row => !selected?.length || selected.includes(accountIDForRow(row))) + .map(row => ({ ...row, default: accountIDForRow(row) === config.defaultAccount || undefined })) return ctx.flags.ids ? ids(items, 'accountID') : items } +async function accountsShow(ctx: CommandContext): Promise { + const selector = ctx.args[0]! + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'accounts.show', request: { selector } } + const client = await apiClient(ctx) + const accountID = await resolveAccountID(client, selector) + const config = await readConfig() + const item = apiItems(await client.accounts.list()).find(row => accountIDForRow(row) === accountID) + if (!item) throw new AbortError(`No account matches "${selector}"`, ExitCodes.NotFound, undefined, 'not_found') + return { ...item, default: accountID === config.defaultAccount || undefined } +} + async function useTarget(ctx: CommandContext): Promise> { const target = await resolveTarget({ target: ctx.args[0]! }) if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'use.target', request: { defaultTarget: target.id } } @@ -1290,6 +2228,28 @@ async function accountsAdd(ctx: CommandContext): Promise { return undefined } +async function authServices(ctx: CommandContext): Promise { + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'auth.services', request: { target: ctx.globalFlags.target } } + const client = await apiClient(ctx) + const bridges = apiItems(await client.bridges.list()) + const services = bridges.map(bridge => ({ + bridge_id: bridge.id, + name: bridge.displayName ?? bridge.name ?? bridge.id, + provider: bridge.provider, + service: bridge.type ?? bridge.network ?? bridge.id, + status: bridge.status ?? 'available', + supports_multiple_accounts: bridge.supportsMultipleAccounts, + })) + if (ctx.globalFlags.json) return { services } + if (ctx.globalFlags.plain) return services + if (ctx.flags.markdown) { + printBridgeServicesMarkdown(services) + return undefined + } + printAvailableBridges(bridges) + return undefined +} + async function removeTargetCommand(ctx: CommandContext): Promise> { const input = ctx.args[0]! if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'remove.target', request: { id: input } } @@ -1309,10 +2269,13 @@ async function removeAccount(ctx: CommandContext): Promise { + const queryFlag = stringFlag(ctx.flags, 'query') + const queryArg = ctx.args[0] + if (queryFlag && queryArg) throw usage('--query and positional cannot be combined') + const query = queryFlag ?? queryArg const client = await apiClient(ctx) const accountIDs = await resolveAccountIDs(client, stringListFlag(ctx.flags, 'account'), { allowMultiplePerInput: true }) ?? await listAccountIDs(client) const limit = numberFlag(ctx.flags, 'limit', 50) - const query = stringFlag(ctx.flags, 'query') const items: Array> = [] for (const accountID of accountIDs) { const remaining = limit - items.length @@ -1323,12 +2286,41 @@ async function contactsList(ctx: CommandContext): Promise { return ctx.flags.ids ? ids(items, 'userID') : items } +async function contactsShow(ctx: CommandContext): Promise { + const selectorArg = ctx.args[0] + const jid = stringFlag(ctx.flags, 'jid') + if (selectorArg && jid) throw usage('--jid and positional cannot be combined') + const selector = jid ?? selectorArg + if (!selector) throw usage('contacts show requires or --jid') + const request = { + accounts: stringListFlag(ctx.flags, 'account'), + jid, + limit: numberFlag(ctx.flags, 'limit', 10), + pick: ctx.flags.pick, + selector, + } + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'contacts.show', request } + const client = await apiClient(ctx) + const candidates = await contactCandidates(client, selector, stringListFlag(ctx.flags, 'account'), request.limit) + if (!candidates.length) throw new AbortError(`No contact matches "${selector}"`, ExitCodes.NotFound, undefined, 'not_found') + const pick = numberFlag(ctx.flags, 'pick', 0) + if (pick) { + const selected = candidates[pick - 1] + if (!selected) throw new AbortError(`--pick ${pick} is out of range; ${candidates.length} candidate(s) available`, ExitCodes.NotFound, undefined, 'not_found') + return selected + } + if (candidates.length > 1) { + throw new AbortError(`Ambiguous contact "${selector}". Use --pick N:\n${candidates.map((contact, index) => ` ${index + 1}. ${contactLabel(contact)}`).join('\n')}`, ExitCodes.Ambiguous, undefined, 'ambiguous_selector') + } + return candidates[0] +} + async function chatsList(ctx: CommandContext): Promise { const client = await apiClient(ctx) const accountIDs = await resolveAccountIDs(client, stringListFlag(ctx.flags, 'account'), { allowMultiplePerInput: true }) const query = stringFlag(ctx.flags, 'query') if (query) { - const items = (await collectPage(client.chats.search({ accountIDs, query }), numberFlag(ctx.flags, 'limit', 20))) + const items = (await collectPage(client.chats.search({ accountIDs, query }), numberFlag(ctx.flags, 'limit', 50))) .map(apiRecord) .filter(row => matchesChatFilters(row, ctx)) return ctx.flags.ids ? ids(items, 'localChatID') : items @@ -1337,17 +2329,35 @@ async function chatsList(ctx: CommandContext): Promise { for await (const item of client.chats.list({ accountIDs })) { const row = apiRecord(item) if (matchesChatFilters(row, ctx)) items.push(row) - if (items.length >= numberFlag(ctx.flags, 'limit', 20)) break + if (items.length >= numberFlag(ctx.flags, 'limit', 50)) break } return ctx.flags.ids ? ids(items, 'localChatID') : items } +async function groupsList(ctx: CommandContext): Promise { + return chatsList({ ...ctx, flags: { ...ctx.flags, type: 'group' } }) +} + async function chatsShow(ctx: CommandContext): Promise { const client = await apiClient(ctx) - const chatID = await resolveChatID(client, stringFlag(ctx.flags, 'chat')!, chatResolutionOptions(ctx)) + const flagChat = stringFlag(ctx.flags, 'chat') + const positionalChat = ctx.args[0] + if (flagChat && positionalChat) throw usage('--chat and positional cannot be combined') + const chat = flagChat ?? positionalChat + if (!chat) throw usage('chats show requires --chat or ') + const chatID = await resolveChatID(client, chat, chatResolutionOptions(ctx)) return client.chats.retrieve(chatID, { maxParticipantCount: numberFlag(ctx.flags, 'max-participants', 0) || undefined }) } +async function groupsShow(ctx: CommandContext): Promise { + const flagJid = stringFlag(ctx.flags, 'jid') + const positionalJid = ctx.args[0] + if (flagJid && positionalJid) throw usage('--jid and positional cannot be combined') + const jid = flagJid ?? positionalJid + if (!jid) throw usage('groups show requires --jid or ') + return chatsShow({ ...ctx, args: [], flags: { ...ctx.flags, chat: jid } }) +} + async function chatsStart(ctx: CommandContext): Promise { const user = ctx.args[0] if (!user) throw usage('chats start requires user') @@ -1360,18 +2370,58 @@ async function chatsStart(ctx: CommandContext): Promise { return client.chats.start(payload) } +async function groupsCreate(ctx: CommandContext): Promise { + const users = stringListFlag(ctx.flags, 'user') + if (!users.length) throw usage('groups create requires at least one --user') + const account = stringFlag(ctx.flags, 'account') + const title = stringFlag(ctx.flags, 'name')! + const messageText = stringFlag(ctx.flags, 'message') + const participantIDs = users.map(user => user.trim()).filter(Boolean) + if (!participantIDs.length) throw usage('groups create requires at least one non-empty --user') + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'groups.create', request: { account, messageText, participantIDs, title } } + const client = await apiClient(ctx) + const accountID = account ? await resolveAccountID(client, account) : await defaultAccountID(client) + return client.chats.create({ accountID, messageText, participantIDs, title, type: 'group' }) +} + +async function groupsRename(ctx: CommandContext): Promise { + return chatsUpdate({ ...ctx, args: [], flags: { ...ctx.flags, chat: groupSelector(ctx), title: stringFlag(ctx.flags, 'name') } }, 'rename', { title: stringFlag(ctx.flags, 'name') }) +} + +async function groupsDescription(ctx: CommandContext): Promise { + return chatsDescription({ ...ctx, args: [], flags: { ...ctx.flags, chat: groupSelector(ctx), description: stringFlag(ctx.flags, 'description') ?? stringFlag(ctx.flags, 'topic') } }) +} + +function groupSelector(ctx: CommandContext): string { + const flagJid = stringFlag(ctx.flags, 'jid') + const positionalJid = ctx.args[0] + if (flagJid && positionalJid) throw usage('--jid and positional cannot be combined') + const jid = flagJid ?? positionalJid + if (!jid) throw usage(`${ctx.commandPath.join(' ')} requires --jid or `) + return jid +} + async function chatsUpdate(ctx: CommandContext, op: string, update: Record): Promise { - if (ctx.globalFlags.dryRun) return { dry_run: true, op: `chats.${op}`, request: { chat: stringFlag(ctx.flags, 'chat'), pick: ctx.flags.pick, ...update } } + const chat = chatSelector(ctx) + if (ctx.globalFlags.dryRun) return { dry_run: true, op: `chats.${op}`, request: { chat, pick: ctx.flags.pick, ...update } } const client = await apiClient(ctx) - const chatID = await chatIDFromFlag(client, ctx, 'chat') + const chatID = await resolveChatID(client, chat, chatResolutionOptions(ctx)) return client.chats.update(chatID, update) } async function chatsSetFlag(ctx: CommandContext): Promise { const action = ctx.commandPath[1] ?? '' - const field = ({ archive: 'isArchived', mute: 'isMuted', pin: 'isPinned' } as const)[action] - if (!field) throw usage(`Unsupported chat command: ${ctx.commandPath.join(' ')}`) - return chatsUpdate(ctx, action, { [field]: !ctx.flags.clear }) + const spec = ({ + archive: ['isArchived', true], + mute: ['isMuted', true], + pin: ['isPinned', true], + unarchive: ['isArchived', false], + unmute: ['isMuted', false], + unpin: ['isPinned', false], + } as const)[action] + if (!spec) throw usage(`Unsupported chat command: ${ctx.commandPath.join(' ')}`) + const [field, defaultValue] = spec + return chatsUpdate(ctx, action, { [field]: ctx.flags.clear ? false : defaultValue }) } async function chatsRename(ctx: CommandContext): Promise { @@ -1395,72 +2445,91 @@ async function chatsAvatar(ctx: CommandContext): Promise { async function chatsPriority(ctx: CommandContext): Promise { const level = stringFlag(ctx.flags, 'level')! const update = level === 'inbox' ? { isArchived: false, isLowPriority: false } : { isLowPriority: true } - if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'chats.priority', request: { chat: stringFlag(ctx.flags, 'chat'), level, pick: ctx.flags.pick, update } } + const chat = chatSelector(ctx) + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'chats.priority', request: { chat, level, pick: ctx.flags.pick, update } } const client = await apiClient(ctx) - const chatID = await chatIDFromFlag(client, ctx, 'chat') + const chatID = await resolveChatID(client, chat, chatResolutionOptions(ctx)) return client.chats.update(chatID, update) } async function chatsRead(ctx: CommandContext): Promise { const messageID = stringFlag(ctx.flags, 'message') - const read = !ctx.flags.unread - if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'chats.read', request: { chat: stringFlag(ctx.flags, 'chat'), messageID, pick: ctx.flags.pick, read } } + const read = ctx.commandPath[1] === 'mark-unread' ? false : !ctx.flags.unread + const chat = chatSelector(ctx) + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'chats.read', request: { chat, messageID, pick: ctx.flags.pick, read } } const client = await apiClient(ctx) - const chatID = await chatIDFromFlag(client, ctx, 'chat') + const chatID = await resolveChatID(client, chat, chatResolutionOptions(ctx)) return read ? client.chats.markRead(chatID, { messageID }) : client.chats.markUnread(chatID, { messageID }) } async function chatsDraft(ctx: CommandContext): Promise { const clear = Boolean(ctx.flags.clear) + const chat = chatSelector(ctx) if (!clear && ctx.flags.text === undefined) throw usage('Provide --text TEXT, optionally with --file PATH, or --clear.') if (clear && (ctx.flags.text !== undefined || ctx.flags.file)) throw usage('--clear cannot be combined with --text or --file.') if (clear) { - if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'chats.draft', request: { chat: stringFlag(ctx.flags, 'chat'), draft: null, pick: ctx.flags.pick } } + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'chats.draft', request: { chat, draft: null, pick: ctx.flags.pick } } const client = await apiClient(ctx) - const chatID = await chatIDFromFlag(client, ctx, 'chat') + const chatID = await resolveChatID(client, chat, chatResolutionOptions(ctx)) return client.chats.update(chatID, { draft: null }) } const draft = { file: stringFlag(ctx.flags, 'file'), fileName: stringFlag(ctx.flags, 'filename'), mimeType: stringFlag(ctx.flags, 'mime'), text: stringFlag(ctx.flags, 'text') } - if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'chats.draft', request: { chat: stringFlag(ctx.flags, 'chat'), draft, pick: ctx.flags.pick } } + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'chats.draft', request: { chat, draft, pick: ctx.flags.pick } } const client = await apiClient(ctx) - const chatID = await chatIDFromFlag(client, ctx, 'chat') + const chatID = await resolveChatID(client, chat, chatResolutionOptions(ctx)) const upload = draft.file ? await client.assets.upload({ file: createReadStream(draft.file), fileName: draft.fileName, mimeType: draft.mimeType }) : undefined return client.chats.update(chatID, { draft: { text: draft.text, attachments: upload?.uploadID ? { [upload.uploadID]: upload } : undefined } }) } async function chatsRemind(ctx: CommandContext): Promise { + const chat = chatSelector(ctx) if (ctx.flags.clear) { if (ctx.flags.when || ctx.flags['dismiss-on-message']) throw usage('--clear cannot be combined with --when or --dismiss-on-message') - if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'chats.remind', request: { chat: stringFlag(ctx.flags, 'chat'), pick: ctx.flags.pick, reminder: null } } + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'chats.remind', request: { chat, pick: ctx.flags.pick, reminder: null } } const client = await apiClient(ctx) - const chatID = await chatIDFromFlag(client, ctx, 'chat') + const chatID = await resolveChatID(client, chat, chatResolutionOptions(ctx)) await client.chats.reminders.delete(chatID) return { chatID, reminderCleared: true } } const when = requiredStringFlag(ctx.flags, 'when') const reminder = { dismissOnIncomingMessage: Boolean(ctx.flags['dismiss-on-message']) || undefined, remindAt: when } - if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'chats.remind', request: { chat: stringFlag(ctx.flags, 'chat'), pick: ctx.flags.pick, reminder } } + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'chats.remind', request: { chat, pick: ctx.flags.pick, reminder } } const client = await apiClient(ctx) - const chatID = await chatIDFromFlag(client, ctx, 'chat') + const chatID = await resolveChatID(client, chat, chatResolutionOptions(ctx)) await client.chats.reminders.create(chatID, { reminder }) return { chatID, remindAt: when, reminderSet: true } } async function chatsDisappear(ctx: CommandContext): Promise { - const raw = requiredStringFlag(ctx.flags, 'seconds').toLowerCase() - const messageExpirySeconds = raw === 'off' ? null : /^\d+$/.test(raw) ? Number(raw) : NaN - if (messageExpirySeconds !== null && (!Number.isSafeInteger(messageExpirySeconds) || messageExpirySeconds < 0)) throw usage('--seconds must be a positive integer or "off"') - if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'chats.disappear', request: { chat: stringFlag(ctx.flags, 'chat'), messageExpirySeconds, pick: ctx.flags.pick } } + const messageExpirySeconds = parseDisappearSeconds(requiredStringFlag(ctx.flags, 'seconds')) return chatsUpdate(ctx, 'disappear', { messageExpirySeconds }) } +function parseDisappearSeconds(value: string): number | null { + const raw = value.trim().toLowerCase() + if (raw === 'off') return null + if (/^\d+$/.test(raw)) { + const seconds = Number(raw) + if (Number.isSafeInteger(seconds) && seconds >= 0) return seconds + } + const match = raw.match(/^(\d+)(s|m|h|d)$/) + if (match) { + const amount = Number(match[1]) + const factors: Record = { d: 86_400, h: 3_600, m: 60, s: 1 } + const seconds = amount * factors[match[2]!]! + if (Number.isSafeInteger(seconds) && seconds >= 0) return seconds + } + throw usage('--seconds must be a positive integer, a duration like 24h/7d/90d, or "off"') +} + async function chatsFocus(ctx: CommandContext): Promise { + const chat = ctx.args[0] ?? stringFlag(ctx.flags, 'chat') if (ctx.globalFlags.dryRun) { return { dry_run: true, op: 'chats.focus', request: { - chat: stringFlag(ctx.flags, 'chat'), + chat, draftAttachmentPath: stringFlag(ctx.flags, 'file'), draftText: stringFlag(ctx.flags, 'text'), messageID: stringFlag(ctx.flags, 'message'), @@ -1469,7 +2538,8 @@ async function chatsFocus(ctx: CommandContext): Promise { } } const client = await apiClient(ctx) - const chatID = await chatIDFromFlag(client, ctx, 'chat') + if (!chat) throw usage('chats focus requires --chat or chat') + const chatID = await resolveChatID(client, chat, chatResolutionOptions(ctx)) const request = { chatID, draftAttachmentPath: stringFlag(ctx.flags, 'file'), @@ -1480,12 +2550,39 @@ async function chatsFocus(ctx: CommandContext): Promise { } async function chatsNotifyAnyway(ctx: CommandContext): Promise { - if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'chats.notify-anyway', request: { chat: stringFlag(ctx.flags, 'chat'), pick: ctx.flags.pick } } + const chat = chatSelector(ctx) + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'chats.notify-anyway', request: { chat, pick: ctx.flags.pick } } const client = await apiClient(ctx) - const chatID = await chatIDFromFlag(client, ctx, 'chat') + const chatID = await resolveChatID(client, chat, chatResolutionOptions(ctx)) return client.chats.notifyAnyway(chatID) } +function chatSelector(ctx: CommandContext): string { + const flagChat = stringFlag(ctx.flags, 'chat') + const positionalChat = ctx.args[0] + if (flagChat && positionalChat) throw usage('--chat and positional cannot be combined') + const chat = flagChat ?? positionalChat + if (!chat) throw usage(`${ctx.commandPath.join(' ')} requires --chat or `) + return chat +} + +async function unifiedSearch(ctx: CommandContext): Promise { + const query = ctx.args[0]! + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'search.all', request: { query } } + const client = await apiClient(ctx) + const result = await client.search({ query }) as Record + if (ctx.globalFlags.json || ctx.globalFlags.plain) return result + const results = isRecord(result.results) ? result.results : {} + const messages = isRecord(results.messages) ? results.messages : {} + return { + chats: Array.isArray(results.chats) ? results.chats.length : 0, + in_groups: Array.isArray(results.in_groups) ? results.in_groups.length : 0, + messages: Array.isArray(messages.items) ? messages.items.length : 0, + has_more_messages: messages.hasMore, + query, + } +} + async function resolveAccount(ctx: CommandContext): Promise { const selector = ctx.args[0]! const client = await apiClient(ctx) @@ -1527,16 +2624,7 @@ async function resolveChat(ctx: CommandContext): Promise { async function resolveContact(ctx: CommandContext): Promise { const selector = ctx.args[0]! const client = await apiClient(ctx) - const accountIDs = await resolveAccountIDs(client, stringListFlag(ctx.flags, 'account'), { allowMultiplePerInput: true }) ?? await listAccountIDs(client) - const candidates: Record[] = [] - for (const accountID of accountIDs) { - try { - const result = await client.accounts.contacts.search(accountID, { query: selector }) - candidates.push(...apiItems(result).slice(0, numberFlag(ctx.flags, 'limit', 10)).map(item => ({ ...item, accountID }))) - } catch (error) { - if (!ignorableLookupError(error)) throw error - } - } + const candidates = await contactCandidates(client, selector, stringListFlag(ctx.flags, 'account'), numberFlag(ctx.flags, 'limit', 10)) return resolution(ctx, 'contact', selector, candidates.map(contact => ({ accountID: contact.accountID, displayName: contact.displayName ?? contact.fullName ?? contact.name, @@ -1547,6 +2635,26 @@ async function resolveContact(ctx: CommandContext): Promise { }))) } +async function contactCandidates(client: any, selector: string, accountSelectors: string[], limit: number): Promise[]> { + const accountIDs = await resolveAccountIDs(client, accountSelectors, { allowMultiplePerInput: true }) ?? await listAccountIDs(client) + const candidates: Record[] = [] + for (const accountID of accountIDs) { + try { + const result = await client.accounts.contacts.search(accountID, { query: selector }) + candidates.push(...apiItems(result).slice(0, limit).map(item => ({ ...item, accountID }))) + } catch (error) { + if (!ignorableLookupError(error)) throw error + } + } + return candidates.slice(0, limit) +} + +function contactLabel(contact: Record): string { + const name = contact.displayName ?? contact.fullName ?? contact.name ?? contact.username ?? contact.id ?? contact.userID + const account = contact.accountID ? ` (${String(contact.accountID)})` : '' + return `${String(name ?? 'contact')}${account}` +} + async function resolveTargetCommand(ctx: CommandContext): Promise { const selector = ctx.args[0]! const normalized = normalizeSelector(selector) @@ -1595,6 +2703,30 @@ async function resolveBridge(ctx: CommandContext): Promise { } async function messagesList(ctx: CommandContext): Promise { + const items = await collectListedMessages(ctx) + return ctx.flags.ids ? ids(items.map(apiRecord), 'messageID') : items +} + +async function messagesExport(ctx: CommandContext): Promise { + const request = { + afterCursor: stringFlag(ctx.flags, 'after-cursor'), + asc: Boolean(ctx.flags.asc), + beforeCursor: stringFlag(ctx.flags, 'before-cursor'), + chat: stringFlag(ctx.flags, 'chat'), + limit: numberFlag(ctx.flags, 'limit', 1000), + output: stringFlag(ctx.flags, 'output'), + pick: ctx.flags.pick, + sender: messageSenderFilter(ctx), + } + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'messages.export', request } + const items = await collectListedMessages(ctx) + const out = stringFlag(ctx.flags, 'output') + if (!out) return items + await writeFile(out, `${JSON.stringify(items, null, 2)}\n`) + return { count: items.length, path: out } +} + +async function collectListedMessages(ctx: CommandContext): Promise { const chat = stringFlag(ctx.flags, 'chat')! const before = stringFlag(ctx.flags, 'before-cursor') const after = stringFlag(ctx.flags, 'after-cursor') @@ -1604,26 +2736,96 @@ async function messagesList(ctx: CommandContext): Promise { let items = await collectMessages(client.messages.list(chatID, { cursor: before ?? after, direction: before ? 'before' : after ? 'after' : undefined, - }), numberFlag(ctx.flags, 'limit', 50), stringFlag(ctx.flags, 'sender')) + }), numberFlag(ctx.flags, 'limit', 50), messageListFilter(ctx)) if (ctx.flags.asc) items = [...items].reverse() - return ctx.flags.ids ? ids(items.map(apiRecord), 'messageID') : items + return items } async function messagesContext(ctx: CommandContext): Promise { - const id = stringFlag(ctx.flags, 'id')! + const id = messageID(ctx) + const showOnly = ctx.commandPath[1] === 'show' + const beforeCount = showOnly ? 0 : numberFlag(ctx.flags, 'before', 10) + const afterCount = showOnly ? 0 : numberFlag(ctx.flags, 'after', 10) if (ctx.globalFlags.dryRun) { - return { dry_run: true, op: 'messages.context', request: { after: numberFlag(ctx.flags, 'after', 10), before: numberFlag(ctx.flags, 'before', 10), chat: stringFlag(ctx.flags, 'chat'), messageID: id, pick: ctx.flags.pick } } + return { dry_run: true, op: showOnly ? 'messages.show' : 'messages.context', request: { after: afterCount, before: beforeCount, chat: stringFlag(ctx.flags, 'chat'), messageID: id, pick: ctx.flags.pick } } } const client = await apiClient(ctx) const chatID = await chatIDFromFlag(client, ctx, 'chat') const message = client.messages.retrieve ? await client.messages.retrieve(id, { chatID }) : undefined - const before = await collectPage(client.messages.list(chatID, { cursor: id, direction: 'before' }), numberFlag(ctx.flags, 'before', 10)) - const after = await collectPage(client.messages.list(chatID, { cursor: id, direction: 'after' }), numberFlag(ctx.flags, 'after', 10)) + if (showOnly) return { chatID, message, messageID: id } + const before = await collectPage(client.messages.list(chatID, { cursor: id, direction: 'before' }), beforeCount) + const after = await collectPage(client.messages.list(chatID, { cursor: id, direction: 'after' }), afterCount) return { after, before, chatID, message, messageID: id } } +async function messagesForward(ctx: CommandContext): Promise { + const id = messageID(ctx) + const to = stringFlag(ctx.flags, 'to')! + const attachmentIndex = numberFlag(ctx.flags, 'attachment-index', 1) + if (attachmentIndex <= 0) throw usage('--attachment-index must be a positive integer') + const delivery = sendDelivery(ctx) + const request = { + attachmentIndex, + chat: stringFlag(ctx.flags, 'chat'), + messageID: id, + pick: ctx.flags.pick, + to, + wait: delivery.wait, + waitTimeoutMs: delivery.waitTimeoutMs, + } + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'messages.forward', request } + + const client = await apiClient(ctx) + const sourceChatID = await chatIDFromFlag(client, ctx, 'chat') + const targetChatID = await resolveChatID(client, to, chatResolutionOptions(ctx)) + const message = await client.messages.retrieve(id, { chatID: sourceChatID }) as Record + const payload = await forwardPayload(client, message, attachmentIndex) + const sent = await sendMessage(client, { ...payload, chatID: targetChatID, ...delivery }) + return { forwarded: true, sourceChatID, sourceMessageID: id, targetChatID, ...sent } +} + +async function forwardPayload(client: any, message: Record, attachmentIndex: number): Promise { + const text = typeof message.text === 'string' ? message.text : '' + const attachments = Array.isArray(message.attachments) ? message.attachments as Array> : [] + if (!attachments.length) { + if (!text) throw usage('source message has no text or forwardable attachment') + return { text } + } + + const attachment = attachments[attachmentIndex - 1] + if (!attachment) throw usage(`source message has no attachment at index ${attachmentIndex}`) + const url = typeof attachment.id === 'string' ? attachment.id : typeof attachment.srcURL === 'string' ? attachment.srcURL : undefined + if (!url) throw usage(`source message attachment ${attachmentIndex} has no forwardable URL`) + const response = url.startsWith('mxc://') || url.startsWith('localmxc://') + ? await client.assets.serve({ url }) + : await fetch(url) + if (!response.ok) throw usage(`failed to fetch source attachment: HTTP ${response.status}`) + const buffer = Buffer.from(await response.arrayBuffer()) + const upload = await client.assets.uploadBase64({ + content: buffer.toString('base64'), + fileName: typeof attachment.fileName === 'string' ? attachment.fileName : undefined, + mimeType: typeof attachment.mimeType === 'string' ? attachment.mimeType : undefined, + }) + if (!upload?.uploadID) throw new Error('Forward upload did not return an uploadID') + const attachmentType = forwardAttachmentType(attachment) + return { + attachmentType, + duration: typeof attachment.duration === 'number' ? attachment.duration : upload.duration, + fileName: upload.fileName ?? (typeof attachment.fileName === 'string' ? attachment.fileName : undefined), + mimeType: upload.mimeType ?? (typeof attachment.mimeType === 'string' ? attachment.mimeType : undefined), + text, + forwardedUpload: upload, + } +} + +function forwardAttachmentType(attachment: Record): AttachmentType | undefined { + if (attachment.isSticker) return 'sticker' + if (attachment.isVoiceNote) return 'voice-note' + return undefined +} + async function messagesEdit(ctx: CommandContext): Promise { - const id = stringFlag(ctx.flags, 'id')! + const id = messageID(ctx) const text = stringFlag(ctx.flags, 'message')! if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'messages.edit', request: { chat: stringFlag(ctx.flags, 'chat'), messageID: id, pick: ctx.flags.pick, text } } const client = await apiClient(ctx) @@ -1632,15 +2834,25 @@ async function messagesEdit(ctx: CommandContext): Promise { } async function messagesDelete(ctx: CommandContext): Promise { - const id = stringFlag(ctx.flags, 'id')! - const forEveryone = Boolean(ctx.flags['for-everyone']) - if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'messages.delete', request: { chat: stringFlag(ctx.flags, 'chat'), forEveryone, messageID: id, pick: ctx.flags.pick } } + const id = messageID(ctx) + const revoke = ctx.commandPath[1] === 'revoke' + const forEveryone = revoke || Boolean(ctx.flags['for-everyone']) + if (ctx.globalFlags.dryRun) return { dry_run: true, op: revoke ? 'messages.revoke' : 'messages.delete', request: { chat: stringFlag(ctx.flags, 'chat'), forEveryone, messageID: id, pick: ctx.flags.pick } } const client = await apiClient(ctx) const chatID = await chatIDFromFlag(client, ctx, 'chat') await client.messages.delete(id, { chatID, forEveryone: forEveryone || undefined }) return { chatID, deleted: true, forEveryone, messageID: id } } +function messageID(ctx: CommandContext): string { + const flagID = stringFlag(ctx.flags, 'id') + const positionalID = ctx.args[0] + if (flagID && positionalID) throw usage('--id and positional cannot be combined') + const id = flagID ?? positionalID + if (!id) throw usage(`${ctx.commandPath.join(' ')} requires --id or `) + return id +} + async function watch(ctx: CommandContext): Promise { if (ctx.flags['webhook-secret'] && !ctx.flags.webhook) throw usage('--webhook-secret requires --webhook URL') const include = stringListFlag(ctx.flags, 'include-type') @@ -1695,13 +2907,14 @@ async function watch(ctx: CommandContext): Promise { } async function mediaDownload(ctx: CommandContext): Promise { - const url = ctx.args[0] - if (!url) throw usage('media download requires url') const out = stringFlag(ctx.flags, 'out') ?? '.' - if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'media.download', request: { out, url } } + if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'media.download', request: { ...mediaDownloadDryRunRequest(ctx), out } } + const request = await mediaDownloadRequest(ctx) const client = await apiClient(ctx) - const response = await client.assets.serve({ url }) + const response = request.url.startsWith('mxc://') || request.url.startsWith('localmxc://') + ? await client.assets.serve({ url: request.url }) + : await fetch(request.url) if (!response.ok) throw usage(`Failed to download media: HTTP ${response.status}`) const buffer = Buffer.from(await response.arrayBuffer()) if (out === '-') { @@ -1709,10 +2922,92 @@ async function mediaDownload(ctx: CommandContext): Promise { return undefined } - await mkdir(out, { recursive: true }) - const path = join(out, basename(new URL(url).pathname) || 'media') + const path = outputPath(out, request.fileName || fileNameFromURL(request.url, request.mimeType)) + await mkdir(dirname(path), { recursive: true }) await writeFile(path, buffer) - return { bytes: buffer.length, path } + return { bytes: buffer.length, messageID: request.messageID, path, url: request.url } +} + +function mediaDownloadDryRunRequest(ctx: CommandContext): Record { + const messageID = mediaMessageID(ctx) + if (messageID) { + return { chat: stringFlag(ctx.flags, 'chat'), index: numberFlag(ctx.flags, 'index', 1), messageID, poster: Boolean(ctx.flags.poster) } + } + const url = ctx.args[0] + if (!url) throw usage('media download requires or --id with --chat') + return { url } +} + +async function mediaDownloadRequest(ctx: CommandContext): Promise<{ fileName?: string; messageID?: string; mimeType?: string; url: string }> { + const messageID = mediaMessageID(ctx) + if (!messageID) { + const url = ctx.args[0] + if (!url) throw usage('media download requires or --id with --chat') + return { url } + } + const chat = stringFlag(ctx.flags, 'chat') + if (!chat) throw usage('--chat is required when --id is used') + const index = numberFlag(ctx.flags, 'index', 1) + if (index <= 0) throw usage('--index must be a positive integer') + const client = await apiClient(ctx) + const chatID = await chatIDFromFlag(client, ctx, 'chat') + const message = await client.messages.retrieve(messageID, { chatID }) as { attachments?: Array> } + const attachment = message.attachments?.[index - 1] + if (!attachment) throw usage(`message "${messageID}" has no attachment at index ${index}`) + const source = ctx.flags.poster ? attachment.posterImg : attachment.id ?? attachment.srcURL + if (typeof source !== 'string' || !source) throw usage(`message "${messageID}" attachment ${index} has no downloadable URL`) + return { + fileName: typeof attachment.fileName === 'string' ? attachment.fileName : undefined, + messageID, + mimeType: typeof attachment.mimeType === 'string' ? attachment.mimeType : undefined, + url: source, + } +} + +function mediaMessageID(ctx: CommandContext): string | undefined { + const flagID = stringFlag(ctx.flags, 'id') + const isMessageCommand = ctx.commandPath.join(' ') === 'media message' + if (flagID && !isMessageCommand && ctx.args[0]) throw usage('Use either positional or --id, not both') + const positionalID = isMessageCommand ? ctx.args[0] : undefined + if (flagID && positionalID) throw usage('--id and positional cannot be combined') + return flagID ?? positionalID +} + +function outputPath(out: string, fileName: string): string { + if (out.endsWith('/') || out === '.' || out === '..') return join(out, safeFileName(fileName)) + try { + const parsed = new URL(out) + if (parsed.protocol === 'file:') return fileURLToPath(parsed) + } catch { /* not a URL */ } + return out.includes('.') ? out : join(out, safeFileName(fileName)) +} + +function fileNameFromURL(url: string, mimeType?: string): string { + try { + const parsed = new URL(url) + const name = basename(parsed.pathname) + if (name) return name + } catch { /* fall through */ } + return `media${extensionForMimeType(mimeType)}` +} + +function safeFileName(value: string): string { + const normalized = basename(value).replace(/[/\\?%*:|"<>]+/g, '_').trim() + return normalized.slice(0, 160) || 'media' +} + +function extensionForMimeType(mimeType?: string): string { + if (!mimeType) return '' + const known: Record = { + 'audio/mpeg': '.mp3', + 'image/gif': '.gif', + 'image/jpeg': '.jpg', + 'image/png': '.png', + 'video/mp4': '.mp4', + } + if (known[mimeType]) return known[mimeType]! + const subtype = mimeType.split('/')[1] + return subtype && !subtype.includes('+') ? `.${subtype}` : '' } async function exportCommand(ctx: CommandContext): Promise { @@ -1757,6 +3052,7 @@ async function messagesSearch(ctx: CommandContext): Promise { const accountSelectors = stringListFlag(ctx.flags, 'account') const chatSelectors = stringListFlag(ctx.flags, 'chat') const mediaTypes = stringListFlag(ctx.flags, 'media') as Array<'any' | 'video' | 'image' | 'link' | 'file'> + if (ctx.flags['has-media'] && !mediaTypes.includes('any')) mediaTypes.unshift('any') const hasFilter = Boolean( accountSelectors.length || chatSelectors.length || ctx.flags['chat-type'] || ctx.flags.after || ctx.flags.before || mediaTypes.length || ctx.flags.sender, @@ -1798,27 +3094,52 @@ async function apiCommand(ctx: CommandContext): Promise { } async function sendTextLike(ctx: CommandContext): Promise { - const kind = ctx.commandPath[1] + const kind = ctx.commandPath.length === 1 && ctx.commandPath[0] === 'send' ? 'text' : ctx.commandPath[1] if (kind !== 'file' && kind !== 'sticker' && kind !== 'text' && kind !== 'voice') throw usage(`Unsupported send command: ${ctx.commandPath.join(' ')}`) - const to = stringFlag(ctx.flags, 'to')! + const to = sendDestination(ctx) const payload = await sendPayload(ctx, kind) if (ctx.globalFlags.dryRun) return { dry_run: true, op: `send.${kind}`, request: { chat: to, ...payload } } const client = await apiClient(ctx) const chatID = await resolveChatID(client, to, chatResolutionOptions(ctx)) - return sendMessage(client, { ...payload, chatID }) + const ephemeral = await applySendEphemeral(client, chatID, payload) + const sent = await sendMessage(client, { ...payload, chatID }) + return ephemeral ? { ...sent, ephemeral } : sent +} + +async function uploadFile(ctx: CommandContext): Promise { + if (stringFlag(ctx.flags, 'file')) throw usage('Use positional with upload, not --file') + return sendTextLike({ + ...ctx, + args: [], + commandPath: ['send', 'file'], + flags: { ...ctx.flags, file: ctx.args[0] }, + }) +} + +function sendDestination(ctx: CommandContext): string { + const flagValue = stringFlag(ctx.flags, 'to') + const positional = isTextSend(ctx) ? ctx.args[0] : undefined + if (flagValue && positional) throw usage('--to and positional cannot be combined') + const to = flagValue ?? positional + if (!to) throw usage('--to is required') + return to +} + +function isTextSend(ctx: CommandContext): boolean { + return (ctx.commandPath.length === 1 && ctx.commandPath[0] === 'send') || ctx.commandPath[1] === 'text' } async function sendMessage(client: any, options: SendPayload & { chatID: string }): Promise> { - const uploaded = options.file + const uploaded = options.forwardedUpload ?? (options.file ? await client.assets.upload({ file: createReadStream(options.file), fileName: options.fileName, mimeType: options.mimeType, }) - : undefined + : undefined) if (options.file && !uploaded?.uploadID) throw new Error('Upload did not return an uploadID') @@ -1857,6 +3178,12 @@ async function sendMessage(client: any, options: SendPayload & { } } +async function applySendEphemeral(client: any, chatID: string, payload: SendPayload): Promise<{ messageExpirySeconds?: number } | undefined> { + if (payload.messageExpirySeconds === undefined) return payload.ephemeral ? {} : undefined + await client.chats.update(chatID, { messageExpirySeconds: payload.messageExpirySeconds }) + return { messageExpirySeconds: payload.messageExpirySeconds } +} + async function waitForMessage(client: any, chatID: string, pendingMessageID: string, timeoutMs = 30_000): Promise { const started = Date.now() let lastError: unknown @@ -1872,22 +3199,38 @@ async function waitForMessage(client: any, chatID: string, pendingMessageID: str } async function sendReact(ctx: CommandContext): Promise { - const id = stringFlag(ctx.flags, 'id')! - const reaction = stringFlag(ctx.flags, 'reaction')! + const id = reactionMessageID(ctx) + const rawReaction = stringFlag(ctx.flags, 'reaction') ?? '+1' + const reaction = rawReaction || '+1' const transactionID = stringFlag(ctx.flags, 'transaction') - const remove = Boolean(ctx.flags.remove) + const remove = Boolean(ctx.flags.remove) || rawReaction === '' + const to = sendDestination(ctx) + const postSendWait = stringFlag(ctx.flags, 'post-send-wait') + const waitTimeoutMs = postSendWait === undefined ? undefined : parseDurationMs(postSendWait) if (remove && transactionID) throw usage('--transaction cannot be combined with --remove') if (ctx.globalFlags.dryRun) { - return { dry_run: true, op: 'send.react', request: { chat: stringFlag(ctx.flags, 'to'), messageID: id, pick: ctx.flags.pick, reactionKey: reaction, remove, transactionID } } + return { dry_run: true, op: 'send.react', request: { chat: to, messageID: id, pick: ctx.flags.pick, reactionKey: reaction, remove, transactionID, wait: waitTimeoutMs === undefined ? undefined : Boolean(waitTimeoutMs && waitTimeoutMs > 0), waitTimeoutMs } } } const client = await apiClient(ctx) const chatID = await chatIDFromFlag(client, ctx, 'to') - if (remove) return client.chats.messages.reactions.delete(reaction, { chatID, messageID: id }) - return client.chats.messages.reactions.add(id, { chatID, reactionKey: reaction, transactionID }) + const result = remove + ? await client.chats.messages.reactions.delete(reaction, { chatID, messageID: id }) + : await client.chats.messages.reactions.add(id, { chatID, reactionKey: reaction, transactionID }) + if (waitTimeoutMs && waitTimeoutMs > 0) await sleep(waitTimeoutMs) + return result +} + +function reactionMessageID(ctx: CommandContext): string { + const flagID = stringFlag(ctx.flags, 'id') + const positionalID = ctx.args[0] + if (flagID && positionalID) throw usage('--id and positional cannot be combined') + const id = flagID ?? positionalID + if (!id) throw usage('send react requires --id or ') + return id } async function authLogout(ctx: CommandContext): Promise> { - const target = await resolveTarget({ target: ctx.globalFlags.target }) + const target = await resolveTarget({ target: ctx.args[0] ?? ctx.globalFlags.target }) const token = target.auth?.accessToken if (ctx.globalFlags.dryRun) { return { dry_run: true, op: 'auth.logout', request: { baseURL: target.baseURL, hadToken: Boolean(token), revokeToken: Boolean(token), target: target.id } } @@ -1916,6 +3259,12 @@ async function authEmailStart(ctx: CommandContext): Promise { return startEmailSetup(target, email) } +async function login(ctx: CommandContext): Promise { + const email = ctx.args[0] + if (!email) throw usage('login requires email') + return authEmailStart({ ...ctx, flags: { ...ctx.flags, email } }) +} + async function authEmailResponse(ctx: CommandContext): Promise { const target = await resolveTarget({ target: ctx.globalFlags.target }) const code = stringFlag(ctx.flags, 'code')! @@ -1965,38 +3314,70 @@ function jsonBody(ctx: CommandContext): Record { } async function sendPayload(ctx: CommandContext, kind: SendKind): Promise { + const delivery = sendDelivery(ctx) if (kind === 'text') { const message = await messageText(ctx) return { mentions: stringListFlag(ctx.flags, 'mention'), noPreview: Boolean(ctx.flags['no-preview']), + ...sendEphemeral(ctx), replyTo: stringFlag(ctx.flags, 'reply-to'), + replyToSender: stringFlag(ctx.flags, 'reply-to-sender'), text: message, - wait: Boolean(ctx.flags.wait), - waitTimeoutMs: numberFlag(ctx.flags, 'wait-timeout', 30_000), + ...delivery, } } - const file = stringFlag(ctx.flags, 'file')! - const attachmentType: AttachmentType | undefined = kind === 'sticker' ? 'sticker' : kind === 'voice' ? 'voice-note' : undefined + const fileFlag = stringFlag(ctx.flags, 'file') + const positionalFile = ctx.args[0] + if (fileFlag && positionalFile) throw usage('--file and positional cannot be combined') + const file = fileFlag ?? positionalFile + if (!file) throw usage(`${ctx.commandPath.join(' ')} requires --file or `) + const ptt = kind === 'file' && Boolean(ctx.flags.ptt) + if (ptt && stringFlag(ctx.flags, 'caption') !== undefined) throw usage('--caption cannot be combined with --ptt') + const attachmentType: AttachmentType | undefined = kind === 'sticker' ? 'sticker' : kind === 'voice' || ptt ? 'voice-note' : undefined return { attachmentType, duration: kind === 'voice' ? numberFlag(ctx.flags, 'duration', 0) || undefined : undefined, file, fileName: stringFlag(ctx.flags, 'filename'), - mimeType: stringFlag(ctx.flags, 'mime') ?? (kind === 'sticker' ? 'image/webp' : kind === 'voice' ? 'audio/ogg' : undefined), + mimeType: stringFlag(ctx.flags, 'mime') ?? (kind === 'sticker' ? 'image/webp' : kind === 'voice' || ptt ? 'audio/ogg' : undefined), replyTo: stringFlag(ctx.flags, 'reply-to'), - text: kind === 'file' ? stringFlag(ctx.flags, 'caption') ?? '' : '', - wait: Boolean(ctx.flags.wait), - waitTimeoutMs: numberFlag(ctx.flags, 'wait-timeout', 30_000), + replyToSender: stringFlag(ctx.flags, 'reply-to-sender'), + text: kind === 'file' && !ptt ? stringFlag(ctx.flags, 'caption') ?? '' : '', + ...delivery, + } +} + +function sendDelivery(ctx: CommandContext): Pick { + const postSendWait = stringFlag(ctx.flags, 'post-send-wait') + if (postSendWait !== undefined) { + const waitTimeoutMs = parseDurationMs(postSendWait) + return { wait: Boolean(waitTimeoutMs && waitTimeoutMs > 0), waitTimeoutMs } + } + return { wait: Boolean(ctx.flags.wait), waitTimeoutMs: numberFlag(ctx.flags, 'wait-timeout', 30_000) } +} + +function sendEphemeral(ctx: CommandContext): Pick { + const duration = stringFlag(ctx.flags, 'ephemeral-duration') + if (!ctx.flags.ephemeral && duration === undefined) return {} + const messageExpirySeconds = duration === undefined ? undefined : parseDisappearSeconds(duration) + if (messageExpirySeconds === null || messageExpirySeconds === 0) throw usage('--ephemeral-duration must be a positive duration like 24h, 7d, 90d, or 168h') + return { + ephemeral: true, + ephemeralDuration: duration, + messageExpirySeconds: messageExpirySeconds ?? undefined, } } async function messageText(ctx: CommandContext): Promise { const literal = stringFlag(ctx.flags, 'message') const file = stringFlag(ctx.flags, 'message-file') + const positional = isTextSend(ctx) ? ctx.args.slice(1).join(' ') : '' if (literal && file) throw usage('--message and --message-file cannot be combined') + if (positional && (literal !== undefined || file)) throw usage('positional cannot be combined with --message or --message-file') if (file) return file === '-' ? await readStdin() : readFile(file, 'utf8') if (literal !== undefined) return ctx.flags['message-escapes'] ? decodeEscapes(literal) : literal + if (positional) return ctx.flags['message-escapes'] ? decodeEscapes(positional) : positional throw usage('send text requires --message or --message-file') } @@ -2016,11 +3397,12 @@ function decodeEscapes(value: string): string { } async function sendPresence(ctx: CommandContext): Promise { - const state = (stringFlag(ctx.flags, 'state') ?? 'typing') as 'typing' | 'paused' + const fixedState = ctx.commandPath[0] === 'presence' ? ctx.commandPath[1] : undefined + const state = (fixedState ?? stringFlag(ctx.flags, 'state') ?? 'typing') as 'typing' | 'paused' const duration = ctx.flags.duration === undefined ? undefined : numberFlag(ctx.flags, 'duration', 0) if (duration !== undefined && duration <= 0) throw usage('--duration must be a positive integer') if (duration !== undefined && state !== 'typing') throw usage('--duration only applies when --state is typing') - const to = stringFlag(ctx.flags, 'to')! + const to = sendDestination(ctx) if (ctx.globalFlags.dryRun) return { dry_run: true, op: 'send.presence', request: { chat: to, durationSeconds: duration, pick: ctx.flags.pick, state } } const client = await apiClient(ctx) @@ -2037,7 +3419,7 @@ async function sendPresence(ctx: CommandContext): Promise { } async function chatIDFromFlag(client: any, ctx: CommandContext, name: 'chat' | 'to'): Promise { - return resolveChatID(client, stringFlag(ctx.flags, name)!, chatResolutionOptions(ctx)) + return resolveChatID(client, requiredStringFlag(ctx.flags, name), chatResolutionOptions(ctx)) } function chatResolutionOptions(ctx: CommandContext, accountIDs?: string[]): { accountIDs?: string[]; noInput?: boolean; pick?: number } { @@ -2118,8 +3500,7 @@ function forwardWebhook(webhook: WebhookConfig, body: string, events: boolean): process.stderr.write(`warning: webhook queue full (${webhook.max}); dropped event\n`) return } - const signature = webhook.secret ? `sha256=${createHmac('sha256', webhook.secret).update(body).digest('hex')}` : undefined - webhook.queue.push({ body, signature }) + webhook.queue.push({ body, secret: webhook.secret }) void drainWebhook(webhook, events) } @@ -2128,8 +3509,7 @@ async function drainWebhook(webhook: WebhookConfig, events: boolean): Promise = { 'content-type': 'application/json' } - if (item.signature) headers['x-beeper-signature'] = item.signature + const headers = webhookHeaders(item.body, item.secret) const response = await fetch(webhook.url, { body: item.body, headers, method: 'POST', signal: AbortSignal.timeout(10_000) }) if (!response.ok) { if (events) writeEvent('watch.webhook_error', { status: response.status }) @@ -2144,6 +3524,15 @@ async function drainWebhook(webhook: WebhookConfig, events: boolean): Promise { + const headers: Record = { 'content-type': 'application/json' } + if (!secret) return headers + const signature = `sha256=${createHmac('sha256', secret).update(body).digest('hex')}` + headers['x-beeper-signature'] = signature + headers['x-wacli-signature'] = signature + return headers +} + async function chooseBridge(items: Record[]): Promise { const available = items.filter(item => String(item.status ?? 'available') === 'available') if (!available.length) throw usage('No available bridges to connect.') @@ -2180,6 +3569,21 @@ function printAvailableBridges(items: Record[]): void { } } +function printBridgeServicesMarkdown(services: Record[]): void { + output.write('| Bridge ID | Name | Provider | Service | Status | Multiple Accounts |\n') + output.write('| --- | --- | --- | --- | --- | --- |\n') + for (const service of services) { + output.write(`| ${markdownTableCell(service.bridge_id)} | ${markdownTableCell(service.name)} | ${markdownTableCell(service.provider)} | ${markdownTableCell(service.service)} | ${markdownTableCell(service.status)} | ${markdownTableCell(service.supports_multiple_accounts === undefined ? '' : service.supports_multiple_accounts ? 'yes' : 'no')} |\n`) + } +} + +function markdownTableCell(value: unknown): string { + return String(value ?? '') + .replaceAll('|', '\\|') + .replaceAll('\r\n', '
') + .replaceAll('\n', '
') +} + function resolveBridgeChoice(items: Record[], input: string): Record { const keys = (item: Record) => [item.id, item.displayName, item.name, item.network, item.provider, item.type] const normalized = normalizeSelector(input) @@ -2222,6 +3626,14 @@ function ids(items: Record[], preferred: string): string[] { .map(String) } +function accountIDForRow(row: Record): string { + return typeof row.accountID === 'string' && row.accountID + ? row.accountID + : typeof row.id === 'string' && row.id + ? row.id + : '' +} + function resolution(ctx: CommandContext, kind: string, selector: string, candidates: Record[]): Record { if (!candidates.length) { throw new AbortError(`No ${kind} matches "${selector}"`, ExitCodes.NotFound, undefined, 'not_found') @@ -2247,6 +3659,8 @@ function ignorableLookupError(error: unknown): boolean { } function matchesChatFilters(row: Record, ctx: CommandContext): boolean { + const type = stringFlag(ctx.flags, 'type') ?? stringFlag(ctx.flags, 'chat-type') + if (type && type !== 'any' && row.type !== type) return false if (ctx.flags.archived !== undefined && Boolean(row.isArchived) !== ctx.flags.archived) return false if (ctx.flags.pinned !== undefined && Boolean(row.isPinned) !== ctx.flags.pinned) return false if (ctx.flags.muted !== undefined && Boolean(row.isMuted) !== ctx.flags.muted) return false @@ -2258,16 +3672,47 @@ function matchesChatFilters(row: Record, ctx: CommandContext): return true } -async function collectMessages(iterable: AsyncIterable, limit: number, sender?: string): Promise { - if (!sender) return collectPage(iterable, limit) +type MessageListFilter = { + hasMedia: boolean + sender?: string + type?: string +} + +async function collectMessages(iterable: AsyncIterable, limit: number, filter?: MessageListFilter): Promise { + if (!filter || (!filter.sender && !filter.hasMedia && !filter.type)) return collectPage(iterable, limit) const items: unknown[] = [] for await (const item of iterable) { - if (matchesSender(item, sender)) items.push(item) + if (matchesMessageListFilter(item, filter)) items.push(item) if (items.length >= limit) break } return items } +function messageListFilter(ctx: CommandContext): MessageListFilter | undefined { + const sender = messageSenderFilter(ctx) + const type = stringFlag(ctx.flags, 'type') + const hasMedia = Boolean(ctx.flags['has-media']) + return sender || type || hasMedia ? { hasMedia, sender, type } : undefined +} + +function messageSenderFilter(ctx: CommandContext): string | undefined { + const sender = stringFlag(ctx.flags, 'sender') + const fromMe = Boolean(ctx.flags['from-me']) + const fromThem = Boolean(ctx.flags['from-them']) + const count = [Boolean(sender), fromMe, fromThem].filter(Boolean).length + if (count > 1) throw usage('Use only one of --sender, --from-me, or --from-them') + if (fromMe) return 'me' + if (fromThem) return 'others' + return sender +} + +function matchesMessageListFilter(item: unknown, filter: MessageListFilter): boolean { + if (filter.sender && !matchesSender(item, filter.sender)) return false + if (filter.hasMedia && !messageHasMedia(item)) return false + if (filter.type && messageKind(item) !== filter.type) return false + return true +} + function matchesSender(item: unknown, sender: string): boolean { if (!item || typeof item !== 'object') return false const row = item as { isSender?: boolean; senderID?: string } @@ -2276,6 +3721,49 @@ function matchesSender(item: unknown, sender: string): boolean { return row.senderID === sender } +function messageHasMedia(item: unknown): boolean { + const row = apiRecord(item) + const attachments = row.attachments ?? row.files ?? row.media + if (Array.isArray(attachments) && attachments.length > 0) return true + return Boolean(row.attachment || row.file || row.mediaURL || row.mediaUrl || row.thumbnailURL || row.thumbnailUrl) +} + +function messageKind(item: unknown): string { + const row = apiRecord(item) + const explicit = stringValue(row.type) ?? stringValue(row.messageType) ?? stringValue(row.kind) + if (explicit) { + const normalized = explicit.toLowerCase() + if (normalized === 'document') return 'document' + if (normalized === 'file') return 'file' + if (normalized === 'audio' || normalized === 'voice') return 'audio' + if (normalized === 'image' || normalized === 'video' || normalized === 'link' || normalized === 'text') return normalized + } + const attachment = firstAttachment(row) + const attachmentType = stringValue(attachment?.type) ?? stringValue(attachment?.mimeType) + if (attachmentType?.startsWith('image/')) return 'image' + if (attachmentType?.startsWith('video/')) return 'video' + if (attachmentType?.startsWith('audio/')) return 'audio' + if (attachmentType === 'application/pdf' || attachmentType?.startsWith('text/') || attachmentType?.includes('document')) return 'document' + if (attachmentType) return 'file' + return messageHasMedia(row) ? 'file' : 'text' +} + +function firstAttachment(row: Record): Record | undefined { + for (const key of ['attachments', 'files', 'media']) { + const value = row[key] + if (Array.isArray(value) && value[0] && typeof value[0] === 'object') return value[0] as Record + } + for (const key of ['attachment', 'file']) { + const value = row[key] + if (value && typeof value === 'object' && !Array.isArray(value)) return value as Record + } + return undefined +} + +function stringValue(value: unknown): string | undefined { + return typeof value === 'string' && value ? value : undefined +} + function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === 'object' && !Array.isArray(value) } @@ -2284,11 +3772,18 @@ function completionScript(shell: string): string { const command = 'beeper' if (shell === 'bash') { return [ + '#!/usr/bin/env bash', + '', '_beeper_complete() {', + " local IFS=$'\\n'", ' local completions', ' completions=$(beeper __complete --cword "$COMP_CWORD" -- "${COMP_WORDS[@]}")', - ' COMPREPLY=( $completions )', + ' COMPREPLY=()', + ' if [[ -n "$completions" ]]; then', + ' COMPREPLY=( $completions )', + ' fi', '}', + '', `complete -F _beeper_complete ${command}`, '', ].join('\n') @@ -2296,25 +3791,45 @@ function completionScript(shell: string): string { if (shell === 'zsh') { return [ '#compdef beeper', + '', '_beeper() {', ' local -a completions', ' completions=("${(@f)$(beeper __complete --cword "$((CURRENT - 1))" -- "${words[@]}")}")', - ' _describe "values" completions', + " _describe 'values' completions", '}', - '_beeper "$@"', + '', + 'compdef _beeper beeper', '', ].join('\n') } if (shell === 'fish') { - return `complete -c ${command} -f -a '(beeper __complete --cword (commandline -t | wc -w) -- (commandline -opc))'\n` + return [ + 'function __beeper_complete', + ' set -l words (commandline -opc)', + ' set -l cur (commandline -ct)', + '', + ' # Include the current token (partial word being typed) to match bash behavior.', + ' set words $words $cur', + '', + ' # cword points to the last word (the one being completed).', + ' set -l cword (math (count $words) - 1)', + ' beeper __complete --cword $cword -- $words', + 'end', + '', + `complete -c ${command} -f -a "(__beeper_complete)"`, + '', + ].join('\n') } if (shell === 'powershell' || shell === 'pwsh') { return [ - `Register-ArgumentCompleter -Native -CommandName ${command} -ScriptBlock {`, - ' param($wordToComplete, $commandAst, $cursorPosition)', - ' $words = $commandAst.ToString().Split(" ", [System.StringSplitOptions]::RemoveEmptyEntries)', - ' $cword = [Math]::Max(0, $words.Length - 1)', - ' beeper __complete --cword $cword -- $words | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_) }', + `Register-ArgumentCompleter -CommandName ${command} -ScriptBlock {`, + ' param($commandName, $wordToComplete, $cursorPosition, $commandAst, $fakeBoundParameter)', + ' $elements = $commandAst.CommandElements | ForEach-Object { $_.ToString() }', + ' $cword = $elements.Count - 1', + ' $completions = beeper __complete --cword $cword -- $elements', + ' foreach ($completion in $completions) {', + " [System.Management.Automation.CompletionResult]::new($completion, $completion, 'ParameterValue', $completion)", + ' }', '}', '', ].join('\n') @@ -2322,25 +3837,28 @@ function completionScript(shell: string): string { throw usage('completion shell must be one of: bash, zsh, fish, powershell') } -function completeWords(words: string[], cword: number): string[] { +function completeWords(words: string[], cword: number, globalFlags: GlobalFlags): string[] { const index = normalizeCword(cword, words.length) const start = isProgramName(words[0]) ? 1 : 0 if (index < start) return [] const current = index < words.length ? words[index] ?? '' : '' const consumed = words.slice(start, Math.min(index, words.length)) if (consumed.includes('--')) return [] - const node = completionNode(consumed) - if (!node || previousFlagNeedsValue(node.flags, words, index)) return [] + const node = completionNode(consumed, globalFlags) + if (!node) return [] + const valueSuggestions = previousFlagValueSuggestions(node.flagSpecs, words, index, current) + if (valueSuggestions) return valueSuggestions + if (previousFlagNeedsValue(node.flags, words, index)) return [] const flags = node.flags const children = node.children const suggestions = current.startsWith('-') ? matching([...flags], current) - : matching([...children, ...flags], current) - return [...new Set(suggestions)].sort() + : matching([...flags, ...children], current) + return [...new Set(suggestions)] } -function completionNode(consumed: string[]): { children: string[]; command?: CommandSpec; flags: string[] } | undefined { - let candidates = commands.filter(command => !command.hidden) +function completionNode(consumed: string[], globalFlags: GlobalFlags): { children: string[]; command?: CommandSpec; flags: string[]; flagSpecs: FlagSpec[] } | undefined { + let candidates = commands.filter(command => commandVisible(command, globalFlags)) let depth = 0 for (const word of consumed) { if (word.startsWith('-')) continue @@ -2352,39 +3870,132 @@ function completionNode(consumed: string[]): { children: string[]; command?: Com const exact = candidates.find(command => commandPathVariants(command).some(path => path.length === depth)) const children = new Set() for (const command of candidates) { - for (const path of commandPathVariants(command)) { + for (const path of completionChildVariants(command, consumed)) { const part = path[depth] if (part) children.add(part) } } + const flagSpecs = [...(exact?.flags ?? []), ...globalFlagSpecs] return { - children: [...children], + children: [...children].sort((a, b) => completionChildPriority(consumed, a) - completionChildPriority(consumed, b) || a.localeCompare(b)), command: exact, - flags: flagTokens([...(exact?.flags ?? []), ...globalFlagSpecs]), + flags: flagTokens(flagSpecs), + flagSpecs, + } +} + +function completionChildPriority(consumed: string[], child: string): number { + const parent = consumed.filter(part => !part.startsWith('-')).join(' ') + const rootOrder = [ + 'message', + 'ls', + 'search', + 'open', + 'download', + 'upload', + 'login', + 'logout', + 'status', + 'me', + 'whoami', + 'setup', + 'send', + 'chats', + 'groups', + 'messages', + 'accounts', + 'contacts', + 'presence', + 'media', + 'targets', + 'use', + 'remove', + 'resolve', + 'export', + 'watch', + 'doctor', + 'auth', + 'install', + 'api', + 'config', + 'docs', + 'schema', + 'mcp', + 'agent', + 'exit-codes', + 'completion', + 'help', + 'version', + ] + const orders: Record = { + '': rootOrder, + accounts: ['list', 'show', 'add', 'use', 'remove'], + auth: ['add', 'list', 'email', 'logout', 'status'], + chats: ['list', 'show', 'start', 'archive', 'unarchive', 'pin', 'unpin', 'mute', 'unmute', 'read', 'mark-read', 'mark-unread', 'rename', 'description', 'avatar', 'priority', 'draft', 'remind', 'disappear', 'focus', 'notify-anyway'], + config: ['get', 'keys', 'set', 'unset', 'list', 'path'], + contacts: ['list', 'show'], + group: ['list', 'show', 'create', 'rename', 'description'], + groups: ['list', 'show', 'create', 'rename', 'description'], + media: ['download', 'message'], + messages: ['list', 'search', 'context', 'show', 'export', 'edit', 'delete', 'revoke'], + presence: ['typing', 'paused'], + search: ['all'], + send: ['text', 'file', 'voice', 'sticker', 'react', 'presence'], + targets: ['list', 'use', 'add', 'remove', 'logs', 'runtime', 'tunnel'], + 'targets runtime': ['start', 'stop', 'restart'], } + const order = orders[parent] ?? [] + const index = order.indexOf(child) + return index === -1 ? order.length : index } function commandPathVariants(command: CommandSpec): string[][] { return [command.path, ...(command.aliases ?? [])] } +function completionChildVariants(command: CommandSpec, consumed: string[]): string[][] { + const variants = commandPathVariants(command) + if (!consumed.length) return variants + const matching = variants.filter(path => consumed.every((part, index) => path[index] === part)) + return matching.length ? matching : variants.filter(path => path.length > consumed.length) +} + function flagTokens(flags: FlagSpec[]): string[] { return flags.flatMap(flag => [ `--${flag.name}`, flag.short ? `-${flag.short}` : undefined, ...(flag.aliases ?? []).map(alias => `--${alias}`), - flag.type === 'boolean' ? `--no-${flag.name}` : undefined, + shouldCompleteNoFlag(flag) ? `--no-${flag.name}` : undefined, ]).filter((value): value is string => Boolean(value)) } +function shouldCompleteNoFlag(flag: FlagSpec): boolean { + return flag.type === 'boolean' && (flag.default === true || Boolean(flag.env?.length)) +} + function previousFlagNeedsValue(flags: string[], words: string[], cword: number): boolean { const previous = words[cword - 1] if (!previous?.startsWith('-') || previous.includes('=')) return false - const spec = [...globalFlagSpecs, ...commands.flatMap(command => command.flags ?? [])] - .find(flag => [`--${flag.name}`, flag.short ? `-${flag.short}` : undefined, ...(flag.aliases ?? []).map(alias => `--${alias}`)].includes(previous)) + const spec = allFlagSpecs().find(flag => flagSpellings(flag).includes(previous)) return Boolean(spec && spec.type !== 'boolean' && flags.includes(previous)) } +function previousFlagValueSuggestions(flags: FlagSpec[], words: string[], cword: number, current: string): string[] | undefined { + const previous = words[cword - 1] + if (!previous?.startsWith('-') || previous.includes('=')) return undefined + const spec = flags.find(flag => flagSpellings(flag).includes(previous)) + if (!spec || !spec.enum?.length) return undefined + return matching(spec.enum, current) +} + +function allFlagSpecs(): FlagSpec[] { + return [...globalFlagSpecs, ...commands.flatMap(command => command.flags ?? [])] +} + +function flagSpellings(flag: FlagSpec): string[] { + return [`--${flag.name}`, flag.short ? `-${flag.short}` : undefined, ...(flag.aliases ?? []).map(alias => `--${alias}`)].filter((value): value is string => Boolean(value)) +} + function matching(values: string[], prefix: string): string[] { return values.filter(value => value.startsWith(prefix)) } @@ -2402,3 +4013,22 @@ async function packageInfo(): Promise> { const root = dirname(dirname(dirname(fileURLToPath(import.meta.url)))) return JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) as Record } + +function packageInfoSync(): Record { + const root = dirname(dirname(dirname(fileURLToPath(import.meta.url)))) + return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as Record +} + +function buildInfo(): string { + const pkg = packageInfoSync() + return process.env.BEEPER_BUILD_COMMIT || process.env.BEEPER_BUILD_DATE + ? [pkg.version, process.env.BEEPER_BUILD_DATE, process.env.BEEPER_BUILD_COMMIT].filter(Boolean).join('-') + : String(pkg.version ?? '') +} + +function beeperConfigRootInfo(): { path: string; source: string } { + if (process.env.BEEPER_HOME) return { path: process.env.BEEPER_HOME, source: 'BEEPER_HOME' } + if (process.env.BEEPER_STORE_DIR) return { path: process.env.BEEPER_STORE_DIR, source: 'BEEPER_STORE_DIR' } + if (process.env.BEEPER_CLI_CONFIG_DIR) return { path: process.env.BEEPER_CLI_CONFIG_DIR, source: 'BEEPER_CLI_CONFIG_DIR' } + return { path: dirname(configPath()), source: 'default' } +} diff --git a/packages/cli/src/cli/main.ts b/packages/cli/src/cli/main.ts index c2edc1a2..e5c9cb5e 100644 --- a/packages/cli/src/cli/main.ts +++ b/packages/cli/src/cli/main.ts @@ -54,17 +54,34 @@ async function runWithTimeout(run: () => Promise, timeout?: string): Promi function parseDuration(value: string | undefined): number | undefined { if (!value) return undefined - const match = /^(\d+)(ms|s|m|h)?$/.exec(value.trim()) - if (!match) throw usage('--timeout must be a duration like 500ms, 30s, 2m, or 1h') - const amount = Number(match[1]) - if (!Number.isSafeInteger(amount) || amount <= 0) throw usage('--timeout must be greater than 0') - const unit = match[2] ?? 'ms' - const factor = unit === 'h' ? 3_600_000 : unit === 'm' ? 60_000 : unit === 's' ? 1_000 : 1 - return amount * factor + const input = value.trim() + const bareMilliseconds = /^\d+$/.exec(input) + if (bareMilliseconds) { + const ms = Number(input) + if (!Number.isSafeInteger(ms) || ms <= 0) throw usage('--timeout must be greater than 0') + return ms + } + const parts = [...input.matchAll(/(\d+(?:\.\d+)?)(ms|s|m|h)/g)] + if (!parts.length || parts.map(part => part[0]).join('') !== input) throw usage('--timeout must be a duration like 500ms, 30s, 2m, 5m0s, or 1h30m') + const ms = parts.reduce((total, part) => total + Number(part[1]) * durationFactor(part[2]!), 0) + if (!Number.isFinite(ms) || ms <= 0) throw usage('--timeout must be greater than 0') + const rounded = Math.round(ms) + if (!Number.isSafeInteger(rounded)) throw usage('--timeout is too large') + return rounded +} + +function durationFactor(unit: string): number { + if (unit === 'h') return 3_600_000 + if (unit === 'm') return 60_000 + if (unit === 's') return 1_000 + return 1 } function applyGlobalEnvironment(flags: { accessToken?: string; home?: string }): void { - if (flags.home) process.env.BEEPER_CLI_CONFIG_DIR = flags.home + if (flags.home) { + process.env.BEEPER_HOME = flags.home + process.env.BEEPER_CLI_CONFIG_DIR = flags.home + } if (flags.accessToken) process.env.BEEPER_ACCESS_TOKEN = flags.accessToken } diff --git a/packages/cli/src/cli/mcp.ts b/packages/cli/src/cli/mcp.ts index 23736fbb..f65d343d 100644 --- a/packages/cli/src/cli/mcp.ts +++ b/packages/cli/src/cli/mcp.ts @@ -1,124 +1,346 @@ -import type { CommandSpec, FlagSpec, GlobalFlags } from './types.js' +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' +import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js' +import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js' +import { randomUUID } from 'node:crypto' +import { createServer, type IncomingMessage, type ServerResponse } from 'node:http' +import type { AddressInfo } from 'node:net' +import type { Readable, Writable } from 'node:stream' +import { z, type ZodTypeAny } from 'zod' + +import type { ArgSpec, CommandSpec, FlagSpec, GlobalFlags } from './types.js' import { enforcePolicy } from './policy.js' import { wrapUntrusted } from './output.js' import { parseFlagValue, validateCommandInput } from './parse.js' -type JsonRpcRequest = { - id?: number | string - method?: string - params?: Record -} - type McpOptions = { allowTools: string[] allowWrite: boolean + httpHost: string + httpPath: string + httpPort: number listTools: boolean maxOutputBytes: number timeoutSeconds: number + transport: 'http' | 'stdio' } +const curatedMcpCommandPaths = [ + ['status'], + ['me'], + ['targets', 'list'], + ['accounts', 'list'], + ['accounts', 'show'], + ['contacts', 'list'], + ['contacts', 'show'], + ['chats', 'list'], + ['chats', 'show'], + ['resolve', 'account'], + ['resolve', 'chat'], + ['resolve', 'contact'], + ['resolve', 'target'], + ['messages', 'list'], + ['messages', 'context'], + ['messages', 'show'], + ['messages', 'export'], + ['messages', 'search'], + ['send', 'text'], + ['send', 'react'], + ['chats', 'read'], + ['messages', 'edit'], +] as const + export async function serveMcp(commands: CommandSpec[], flags: GlobalFlags, options: McpOptions, version: string): Promise { const tools = mcpCommands(commands, options) if (options.listTools) { - process.stdout.write(`${JSON.stringify(mcpTools(tools), null, 2)}\n`) + process.stdout.write(`${JSON.stringify({ tools: mcpTools(tools) }, null, 2)}\n`) return } - const buffer: string[] = [] - process.stdin.setEncoding('utf8') - for await (const chunk of process.stdin) { - buffer.push(String(chunk)) - let joined = buffer.join('') - let index = joined.indexOf('\n') - while (index !== -1) { - const line = joined.slice(0, index).trim() - joined = joined.slice(index + 1) - if (line) await handleLine(tools, flags, options, version, line) - index = joined.indexOf('\n') - } - buffer.length = 0 - if (joined) buffer.push(joined) + + if (options.transport === 'http') { + await serveHttpMcp(tools, flags, options, version) + return } - const finalLine = buffer.join('').trim() - if (finalLine) await handleLine(tools, flags, options, version, finalLine) + + const server = createMcpServer(tools, flags, options, version) + await server.connect(new BeeperMcpStdioTransport()) } -function mcpTools(commands: CommandSpec[]): Record[] { - return commands - .filter(command => command.mcp) - .map(command => ({ +function createMcpServer(tools: CommandSpec[], flags: GlobalFlags, options: McpOptions, version: string): McpServer { + const server = new McpServer({ name: 'beeper', version }) + for (const command of tools) { + for (const name of toolNames(command)) server.registerTool(name, { + _meta: { + command: command.path.join(' '), + risk: command.risk, + service: command.path[0], + }, + annotations: { + destructiveHint: command.risk === 'destructive', + idempotentHint: command.risk === 'read', + openWorldHint: true, + readOnlyHint: command.risk === 'read', + }, description: command.description, - inputSchema: { - additionalProperties: false, - properties: Object.fromEntries([ - ...(command.args ?? []).map(arg => [arg.name, { description: arg.description, type: 'string' }]), - ...(command.flags ?? []).map(flag => [flag.name, inputSchemaForFlag(flag)]), - ]), - required: [ - ...(command.args ?? []).filter(arg => arg.required).map(arg => arg.name), - ...(command.flags ?? []).filter(flag => flag.required).map(flag => flag.name), - ], - type: 'object', + inputSchema: sdkInputSchema(command), + }, async input => runTool(command, flags, options, input as Record)) + } + return server +} + +async function serveHttpMcp(tools: CommandSpec[], flags: GlobalFlags, options: McpOptions, version: string): Promise { + const transports = new Map() + + const createTransport = async (): Promise => { + const transport = new StreamableHTTPServerTransport({ + enableJsonResponse: true, + onsessionclosed: sessionId => { + transports.delete(sessionId) + }, + onsessioninitialized: sessionId => { + transports.set(sessionId, transport) }, - name: command.path.join('_'), - })) + sessionIdGenerator: randomUUID, + }) + await createMcpServer(tools, flags, options, version).connect(transport) + return transport + } + + const path = normalizeHttpPath(options.httpPath) + const httpServer = createServer((req, res) => { + void handleMcpHttpRequest(transports, createTransport, path, req, res) + }) + + await new Promise((resolve, reject) => { + httpServer.once('error', reject) + httpServer.listen(options.httpPort, options.httpHost, resolve) + }) + + const address = httpServer.address() as AddressInfo + process.stderr.write(`Beeper MCP HTTP server listening on http://${address.address}:${address.port}${path}\n`) + + await new Promise((resolve, reject) => { + httpServer.on('close', resolve) + httpServer.on('error', reject) + }) } -async function handleLine(commands: CommandSpec[], flags: GlobalFlags, options: McpOptions, version: string, line: string): Promise { - let request: JsonRpcRequest = {} +async function handleMcpHttpRequest( + transports: Map, + createTransport: () => Promise, + path: string, + req: IncomingMessage, + res: ServerResponse, +): Promise { try { - request = JSON.parse(line) as JsonRpcRequest - if (request.method === 'initialize') { - respond(request.id, { capabilities: { tools: {} }, protocolVersion: '2024-11-05', serverInfo: { name: 'beeper', version } }) + const url = new URL(req.url ?? '/', `http://${req.headers.host ?? '127.0.0.1'}`) + if (url.pathname !== path) { + res.writeHead(404, { 'content-type': 'application/json' }) + res.end(JSON.stringify({ error: 'not_found', path: url.pathname })) return } - if (request.method === 'tools/list') { - respond(request.id, { tools: mcpTools(commands) }) + const sessionId = headerValue(req.headers['mcp-session-id']) + const transport = sessionId ? transports.get(sessionId) : await createTransport() + if (!transport) { + res.writeHead(404, { 'content-type': 'application/json' }) + res.end(JSON.stringify({ error: 'session_not_found' })) return } - if (request.method === 'tools/call') { - const name = String(request.params?.name ?? '') - const tool = commands.find(command => command.mcp && command.path.join('_') === name) - if (!tool) throw new Error(`unknown MCP tool: ${name}`) - if (tool.risk !== 'read' && !options.allowWrite) throw new Error(`MCP tool "${name}" requires mcp --allow-write`) - const args = request.params?.arguments && typeof request.params.arguments === 'object' - ? request.params.arguments as Record - : {} - const globalFlags = { ...flags, json: true, wrapUntrusted: true } - const positionals = positionalsFor(tool, args) - const toolFlags = flagsFor(tool, args) - validateCommandInput(tool, toolFlags, positionals) - enforcePolicy(tool, globalFlags) - const result = await withTimeout(options.timeoutSeconds, () => tool.run({ args: positionals, commandPath: tool.path, flags: toolFlags, globalFlags })) - const structured = { - exit_code: 0, - risk: tool.risk, - service: tool.path[0], - stdout: wrapUntrusted(result), - stderr: '', - tool: tool.path.join('_'), - } - respond(request.id, { content: [{ text: truncate(JSON.stringify(structured), options.maxOutputBytes), type: 'text' }], structuredContent: structured }) - return - } - if (request.id !== undefined) respond(request.id, {}) + await transport.handleRequest(req, res) } catch (error) { - respondError(request.id, error instanceof Error ? error.message : String(error)) + if (!res.headersSent) { + res.writeHead(500, { 'content-type': 'application/json' }) + } + res.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) })) } } -function mcpCommands(commands: CommandSpec[], options: McpOptions): CommandSpec[] { +function headerValue(value: string | string[] | undefined): string | undefined { + return Array.isArray(value) ? value[0] : value +} + +function normalizeHttpPath(path: string): string { + const trimmed = path.trim() + if (!trimmed) return '/mcp' + return trimmed.startsWith('/') ? trimmed : `/${trimmed}` +} + +class BeeperMcpStdioTransport implements Transport { + onclose?: () => void + onerror?: (error: Error) => void + onmessage?: (message: JSONRPCMessage) => void + + private buffer = '' + private started = false + + constructor( + private readonly stdin: Readable = process.stdin, + private readonly stdout: Writable = process.stdout, + ) {} + + async start(): Promise { + if (this.started) throw new Error('BeeperMcpStdioTransport already started') + this.started = true + this.stdin.setEncoding('utf8') + this.stdin.on('data', this.onData) + this.stdin.on('end', this.onEnd) + this.stdin.on('error', this.onInputError) + } + + async send(message: JSONRPCMessage): Promise { + await new Promise(resolve => { + if (this.stdout.write(`${JSON.stringify(message)}\n`)) resolve() + else this.stdout.once('drain', resolve) + }) + } + + async close(): Promise { + this.stdin.off('data', this.onData) + this.stdin.off('end', this.onEnd) + this.stdin.off('error', this.onInputError) + this.buffer = '' + this.onclose?.() + } + + private readonly onData = (chunk: string | Buffer): void => { + this.buffer += String(chunk) + this.processBuffer(false) + } + + private readonly onEnd = (): void => { + this.processBuffer(true) + } + + private readonly onInputError = (error: Error): void => { + this.onerror?.(error) + } + + private processBuffer(final: boolean): void { + let index = this.buffer.indexOf('\n') + while (index !== -1) { + const line = this.buffer.slice(0, index).trim() + this.buffer = this.buffer.slice(index + 1) + if (line) this.handleLine(line) + index = this.buffer.indexOf('\n') + } + if (final) { + const line = this.buffer.trim() + this.buffer = '' + if (line) this.handleLine(line) + } + } + + private handleLine(line: string): void { + try { + const message = normalizeLegacyInitialize(JSON.parse(line)) as JSONRPCMessage + this.onmessage?.(message) + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + void this.send({ error: { code: -32000, message }, jsonrpc: '2.0' } as JSONRPCMessage) + } + } +} + +function normalizeLegacyInitialize(value: unknown): unknown { + if (!value || typeof value !== 'object') return value + const message = value as { method?: unknown; params?: unknown } + if (message.method !== 'initialize') return value + const params = message.params && typeof message.params === 'object' + ? message.params as Record + : {} + message.params = { + capabilities: {}, + clientInfo: { name: 'beeper-cli-legacy-mcp-client', version: '0' }, + protocolVersion: '2024-11-05', + ...params, + } + return message +} + +function mcpTools(commands: CommandSpec[]): Record[] { return commands - .filter(command => command.mcp) + .flatMap(command => toolNames(command).map(name => ({ + description: command.description, + name, + requirements: requirements(command), + risk: command.risk, + service: command.path[0], + }))) +} + +function requirements(command: CommandSpec): string[] | undefined { + const out: string[] = [] + if (command.risk === 'write') out.push('write') + if (command.risk === 'destructive') out.push('destructive', 'force') + return out.length ? out : undefined +} + +async function runTool(tool: CommandSpec, flags: GlobalFlags, options: McpOptions, args: Record) { + const globalFlags = { ...flags, json: true, wrapUntrusted: true } + const positionals = positionalsFor(tool, args) + const toolFlags = flagsFor(tool, args) + validateCommandInput(tool, toolFlags, positionals) + enforcePolicy(tool, globalFlags) + const result = await withTimeout(options.timeoutSeconds, () => tool.run({ args: positionals, commandPath: tool.path, flags: toolFlags, globalFlags })) + const structured = { + exit_code: 0, + risk: tool.risk, + service: tool.path[0], + stdout: wrapUntrusted(mcpStdout(tool, result)), + stderr: '', + tool: toolName(tool), + } + return { + content: [{ text: truncate(JSON.stringify(structured), options.maxOutputBytes), type: 'text' as const }], + structuredContent: structured, + } +} + +function mcpStdout(command: CommandSpec, result: unknown): unknown { + if (!result || typeof result !== 'object' || Array.isArray(result)) return result + const record = result as Record + if (command.output === 'targets' && Array.isArray(record.targets)) return record.targets + if (command.output === 'auth' && Array.isArray(record.accounts)) return record.accounts + return result +} + +function mcpCommands(commands: CommandSpec[], options: McpOptions): CommandSpec[] { + const allowTools = expandAllowTools(options.allowTools) + return curatedMcpCommands(commands) .filter(command => options.allowWrite || command.risk === 'read') - .filter(command => !options.allowTools.length || options.allowTools.some(pattern => toolMatches(command, pattern))) + .filter(command => !allowTools.length || allowTools.some(pattern => toolMatches(command, pattern))) +} + +function curatedMcpCommands(commands: CommandSpec[]): CommandSpec[] { + return curatedMcpCommandPaths.map(path => { + const command = commands.find(candidate => pathMatches(candidate.path, path)) + if (!command) throw new Error(`Curated MCP command not found: ${path.join(' ')}`) + return command + }) +} + +function pathMatches(path: string[], expected: readonly string[]): boolean { + return path.length === expected.length && path.every((part, index) => part === expected[index]) +} + +function expandAllowTools(patterns: string[]): string[] { + return patterns.flatMap(pattern => pattern.split(',').map(item => item.trim()).filter(Boolean)) } function toolMatches(command: CommandSpec, pattern: string): boolean { const normalized = pattern.trim().toLowerCase().replaceAll(/\s+/g, '.').replaceAll('_', '.') if (!normalized || normalized === '*' || normalized === 'all') return true const dotted = command.path.join('.') - const underscored = command.path.join('_') - return normalized === command.risk || normalized === command.path[0] || normalized === dotted || normalized === underscored || (normalized.endsWith('.*') && dotted.startsWith(normalized.slice(0, -2) + '.')) + const normalizedNames = toolNames(command).map(name => name.toLowerCase().replaceAll('_', '.').replaceAll('-', '.')) + return normalized === command.risk || normalized === command.path[0] || normalized === dotted || normalizedNames.includes(normalized) || (normalized.endsWith('.*') && dotted.startsWith(normalized.slice(0, -2) + '.')) +} + +function toolName(command: CommandSpec): string { + return command.path.join('_').replaceAll('-', '_') +} + +function toolNames(command: CommandSpec): string[] { + return [...new Set([toolName(command), command.path.join('_')])] } async function withTimeout(seconds: number, run: () => Promise): Promise { @@ -141,25 +363,58 @@ function truncate(value: string, maxBytes: number): string { return Buffer.byteLength(value) <= maxBytes ? value : `${value.slice(0, maxBytes)}...` } -function inputSchemaForFlag(flag: FlagSpec): Record { - const schema = { - description: flag.description, - enum: flag.enum, - type: flag.type === 'integer' ? 'integer' : flag.type, +function sdkInputSchema(command: CommandSpec): ZodTypeAny { + const positionalNames = new Set((command.args ?? []).map(arg => arg.name)) + const shape = Object.fromEntries([ + ...(command.args ?? []).map(arg => [arg.name, zodSchemaForArg(arg)]), + ...(command.flags ?? []).filter(flag => !positionalNames.has(flag.name)).map(flag => [flag.name, zodSchemaForFlag(flag)]), + ]) + return z.object(shape).strict() +} + +function zodSchemaForArg(arg: ArgSpec): ZodTypeAny { + let schema = stringSchema(arg.description, arg.enum) + if (arg.variadic) schema = z.array(schema).describe(arg.description ?? '') + return arg.required ? schema : schema.optional() +} + +function zodSchemaForFlag(flag: FlagSpec): ZodTypeAny { + let schema: ZodTypeAny + if (flag.type === 'boolean') { + schema = z.union([z.boolean(), z.string()]).describe(flag.description ?? '') + } else if (flag.type === 'integer') { + schema = z.union([z.number().int(), z.string()]).describe(flag.description ?? '') + } else { + schema = stringSchema(flag.description, flag.enum) } - return flag.multiple ? { description: flag.description, items: schema, type: 'array' } : schema + if (flag.multiple) schema = z.array(schema).describe(flag.description ?? '') + if (flag.default !== undefined) schema = schema.default(flag.default) + return flag.required ? schema : schema.optional() +} + +function stringSchema(description?: string, enumValues?: string[]): ZodTypeAny { + const schema = enumValues?.length + ? z.enum(enumValues as [string, ...string[]]) + : z.string() + return schema.describe(description ?? '') } function positionalsFor(command: CommandSpec, input: Record): string[] { - return (command.args ?? []) - .map(arg => input[arg.name]) - .filter(value => value !== undefined) - .map(String) + const out: string[] = [] + for (const arg of command.args ?? []) { + const value = input[arg.name] + if (value === undefined) continue + if (arg.variadic && Array.isArray(value)) out.push(...value.map(String)) + else out.push(String(value)) + } + return out } function flagsFor(command: CommandSpec, input: Record): Record { const flags: Record = {} + const positionalNames = new Set((command.args ?? []).map(arg => arg.name)) for (const flag of command.flags ?? []) { + if (positionalNames.has(flag.name) && input[flag.name] !== undefined) continue const value = input[flag.name] ?? flag.default if (value === undefined) continue flags[flag.name] = flag.multiple @@ -168,11 +423,3 @@ function flagsFor(command: CommandSpec, input: Record): Record< } return flags } - -function respond(id: JsonRpcRequest['id'], result: unknown): void { - process.stdout.write(`${JSON.stringify({ id, jsonrpc: '2.0', result })}\n`) -} - -function respondError(id: JsonRpcRequest['id'], message: string): void { - process.stdout.write(`${JSON.stringify({ error: { code: -32000, message }, id, jsonrpc: '2.0' })}\n`) -} diff --git a/packages/cli/src/cli/output.ts b/packages/cli/src/cli/output.ts index 7b6db756..e9031616 100644 --- a/packages/cli/src/cli/output.ts +++ b/packages/cli/src/cli/output.ts @@ -12,9 +12,15 @@ type ErrorShape = { export function writeResult(value: unknown, flags: GlobalFlags, command?: CommandSpec): void { if (value === undefined) return if (flags.json) { - const selected = flags.select ? selectFields(value, flags.select) : value - const result = flags.resultsOnly ? primaryResult(selected) : selected - const data = flags.wrapUntrusted ? wrapUntrusted(result) : result + const source = command?.path.join(' ') === 'docs' ? { success: true, data: value, error: null } : value + const transformed = command?.rawJson + ? source + : flags.resultsOnly + ? primaryResult(flags.select ? selectFields(source, flags.select) : source, command) + : flags.select + ? selectFields(source, flags.select) + : source + const data = flags.wrapUntrusted && !command?.rawJson ? wrapUntrusted(transformed) : transformed process.stdout.write(`${JSON.stringify(data, null, 2)}\n`) } else writeText(value, flags.plain, command, flags.full) } @@ -47,12 +53,12 @@ export function writeError(error: unknown, flags: Pick[], preferred: string[]): str function writeStatus(value: Record, plain: boolean): void { const target = isRecord(value.target) ? value.target : {} const auth = isRecord(value.auth) ? value.auth : {} + const config = isRecord(value.config) ? value.config : {} const live = isRecord(value.live) ? value.live : {} const readiness = isRecord(value.readiness) ? value.readiness : {} writeDiagnostic({ @@ -173,21 +227,48 @@ function writeStatus(value: Record, plain: boolean): void { version: live.version, authenticated: auth.authenticated, auth_source: auth.source, + config: config.path, + default_account: config.defaultAccount, readiness: readiness.state, next: readiness.message, }, plain) } function writeDiagnostic(value: Record, plain: boolean): void { + if (plain) { + const flattened = flatPlainMap(value) + if (Object.keys(flattened).length) { + writeKeyValueMap(flattened, true) + return + } + } const rows = Object.entries(value) .filter(([, item]) => item !== undefined) - .map(([key, item]) => ({ key: key.replaceAll('_', ' ').toUpperCase(), value: humanCell(item) })) + .map(([key, item]) => ({ key, label: key.replaceAll('_', ' ').toUpperCase(), value: humanCell(item) })) if (plain) { - for (const row of rows) process.stdout.write(`${row.key}\t${row.value.replaceAll('\n', '\\n').replaceAll('\t', '\\t')}\n`) + for (const row of rows) process.stdout.write(`${row.key}\t${escapePlain(row.value)}\n`) return } - const width = Math.max(4, ...rows.map(row => row.key.length)) + 2 - for (const row of rows) process.stdout.write(`${row.key.padEnd(width)}${row.value}\n`) + const width = Math.max(4, ...rows.map(row => row.label.length)) + 2 + for (const row of rows) process.stdout.write(`${row.label.padEnd(width)}${row.value}\n`) +} + +function writeKeyValueMap(value: Record, plain: boolean): void { + for (const [key, item] of Object.entries(value).filter(([, item]) => item !== undefined)) { + const cell = escapePlain(humanCell(item)) + process.stdout.write(plain ? `${key}\t${cell}\n` : `${key}: ${cell}\n`) + } +} + +function flatPlainMap(value: Record, prefix = ''): Record { + const out: Record = {} + for (const [key, item] of Object.entries(value)) { + if (item === undefined) continue + const path = prefix ? `${prefix}.${key}` : key + if (isRecord(item)) Object.assign(out, flatPlainMap(item, path)) + else out[path] = item + } + return out } function writeTable(rows: Record[], columns: string[], plain: boolean, full: boolean, labels: Record = {}): void { @@ -224,9 +305,13 @@ function humanCell(value: unknown): string { return String(value ?? '') } -function primaryResult(value: unknown): unknown { +function primaryResult(value: unknown, command?: CommandSpec): unknown { if (!isRecord(value)) return value - for (const key of ['data', 'items', 'messages', 'chats', 'accounts', 'contacts', 'target', 'result']) { + const commandPath = command?.path.join(' ') + if (commandPath === 'status' || commandPath === 'auth status') return value + if (commandPath === 'config path' && value.path !== undefined) return value.path + if (commandPath === 'config keys' && value.keys !== undefined) return value.keys + for (const key of ['data', 'items', 'messages', 'chats', 'accounts', 'contacts', 'services', 'targets', 'target', 'result']) { if (value[key] !== undefined) return value[key] } return value diff --git a/packages/cli/src/cli/parse.ts b/packages/cli/src/cli/parse.ts index 65f44891..9d123c49 100644 --- a/packages/cli/src/cli/parse.ts +++ b/packages/cli/src/cli/parse.ts @@ -1,5 +1,6 @@ import type { CommandSpec, FlagSpec, GlobalFlags } from './types.js' import { usage } from './output.js' +import { AbortError, ExitCodes } from '../lib/errors.js' type ParsedCommand = { command?: CommandSpec @@ -10,42 +11,44 @@ type ParsedCommand = { } export const globalFlagSpecs: FlagSpec[] = [ - { name: 'access-token', type: 'string', env: ['BEEPER_ACCESS_TOKEN'], description: 'Use provided access token directly' }, - { name: 'account', aliases: ['acct'], short: 'a', type: 'string', multiple: true, description: 'Account selector for account-aware commands' }, - { name: 'color', type: 'string', enum: ['auto', 'always', 'never'], default: 'auto', description: 'Color output: auto|always|never' }, - { name: 'debug', type: 'boolean', default: false }, - { name: 'disable-commands', type: 'string', description: 'Comma-separated command prefixes to block' }, - { name: 'dry-run', aliases: ['dryrun', 'noop', 'preview'], short: 'n', type: 'boolean', default: false, description: 'Do not make changes; print intended actions' }, - { name: 'enable-commands', type: 'string', description: 'Comma-separated enabled command prefixes' }, - { name: 'enable-commands-exact', type: 'string', description: 'Comma-separated exact enabled commands' }, - { name: 'events', type: 'boolean', default: false }, + { name: 'access-token', type: 'string', env: ['BEEPER_ACCESS_TOKEN'], description: 'Use provided access token directly (bypasses stored target auth)' }, + { name: 'account', aliases: ['acct'], short: 'a', type: 'string', multiple: true, env: ['BEEPER_ACCOUNT'], description: 'Account selector for account-aware commands' }, + { name: 'color', type: 'string', enum: ['auto', 'always', 'never'], default: 'auto', env: ['BEEPER_COLOR'], description: 'Color output: auto|always|never' }, + { name: 'disable-commands', type: 'string', env: ['BEEPER_DISABLE_COMMANDS'], description: 'Comma-separated command prefixes to block; dot paths allowed' }, + { name: 'dry-run', aliases: ['dryrun', 'noop', 'preview'], short: 'n', type: 'boolean', default: false, env: ['BEEPER_DRY_RUN'], description: 'Do not make changes; print intended actions and exit successfully' }, + { name: 'enable-commands', type: 'string', env: ['BEEPER_ENABLE_COMMANDS'], description: 'Comma-separated enabled command prefixes; dot paths allowed' }, + { name: 'enable-commands-exact', type: 'string', env: ['BEEPER_ENABLE_COMMANDS_EXACT'], description: 'Comma-separated exact enabled commands; parent commands do not enable children' }, + { name: 'events', type: 'boolean', default: false, env: ['BEEPER_EVENTS'], description: 'Emit machine-readable NDJSON lifecycle events on stderr' }, { name: 'force', aliases: ['assume-yes', 'yes'], short: 'y', type: 'boolean', default: false, description: 'Skip confirmations for destructive commands' }, { name: 'full', type: 'boolean', default: false, description: 'Disable truncation in human table output' }, - { name: 'home', type: 'string', env: ['BEEPER_CLI_CONFIG_DIR'], description: 'Override Beeper CLI config/data root' }, - { name: 'json', aliases: ['machine'], short: 'j', type: 'boolean', default: false, description: 'Output JSON to stdout' }, - { name: 'no-input', aliases: ['non-interactive', 'noninteractive'], type: 'boolean', default: false, description: 'Never prompt; fail instead' }, - { name: 'plain', aliases: ['tsv'], short: 'p', type: 'boolean', default: false, description: 'Output stable TSV-like text' }, - { name: 'read-only', type: 'boolean', default: false, env: ['BEEPER_READONLY'], description: 'Reject commands that intentionally write' }, + { name: 'help', short: 'h', type: 'boolean', default: false, description: 'Show context-sensitive help' }, + { name: 'home', aliases: ['store'], type: 'string', env: ['BEEPER_HOME', 'BEEPER_STORE_DIR', 'BEEPER_CLI_CONFIG_DIR'], description: 'Override Beeper CLI config/data/state/cache root' }, + { name: 'json', aliases: ['machine'], short: 'j', type: 'boolean', default: false, env: ['BEEPER_JSON'], description: 'Output JSON to stdout (best for scripting)' }, + { name: 'lock-wait', type: 'string', description: 'Accepted for compatibility; Beeper CLI does not use a local store lock' }, + { name: 'no-input', aliases: ['non-interactive', 'noninteractive'], type: 'boolean', default: false, description: 'Never prompt; fail instead (useful for CI)' }, + { name: 'plain', aliases: ['tsv'], short: 'p', type: 'boolean', default: false, env: ['BEEPER_PLAIN'], description: 'Output stable, parseable text to stdout (TSV-like; no colors)' }, + { name: 'read-only', aliases: ['readonly'], type: 'boolean', default: false, env: ['BEEPER_READONLY'], description: 'Reject commands that intentionally write Beeper or local CLI state' }, { name: 'results-only', type: 'boolean', default: false, description: 'In JSON mode, emit only the primary result' }, { name: 'safety-profile', type: 'string', description: 'Safety profile name or YAML path' }, - { name: 'select', aliases: ['fields', 'project'], type: 'string', description: 'Select comma-separated JSON fields' }, - { name: 'target', type: 'string', description: 'Target name or URL' }, - { name: 'timeout', type: 'string', description: 'Command timeout, for example 30s or 2m' }, - { name: 'version', short: 'v', type: 'boolean', default: false, description: 'Print version and exit' }, - { name: 'wrap-untrusted', type: 'boolean', default: false, description: 'Wrap fetched text fields in untrusted-content markers' }, + { name: 'select', aliases: ['fields', 'project'], type: 'string', env: ['BEEPER_SELECT', 'BEEPER_FIELDS', 'BEEPER_PROJECT'], description: 'In JSON mode, select comma-separated fields; dot paths allowed' }, + { name: 'target', type: 'string', env: ['BEEPER_TARGET'], description: 'Target name or URL' }, + { name: 'timeout', type: 'string', env: ['BEEPER_TIMEOUT'], description: 'Command timeout, for example 30s, 2m, 5m0s, or 1h30m' }, + { name: 'verbose', aliases: ['debug'], short: 'v', type: 'boolean', default: false, env: ['BEEPER_DEBUG'], description: 'Enable verbose logging' }, + { name: 'version', type: 'boolean', default: false, description: 'Print version and exit' }, + { name: 'wrap-untrusted', type: 'boolean', default: false, env: ['BEEPER_WRAP_UNTRUSTED'], description: 'In JSON/raw output, wrap fetched text fields in untrusted-content markers' }, ] export function parseCommand(argv: string[], commands: CommandSpec[]): ParsedCommand { const helpRequested = argv.includes('--help') || argv.includes('-h') const global = parseGlobalFlags(argv) const tokens = parseArgv(argv, globalFlagSpecs, { allowUnknownFlags: true }).positionals - if (argv.includes('--version') || argv.includes('-v')) { + if (argv.includes('--version')) { const command = commands.find(item => item.path.join(' ') === 'version') if (command) return { command, flags: {}, globalFlags: global, positionals: [] } } if (argv[0] === '__complete') { const command = commands.find(item => item.path.join(' ') === '__complete') - if (!command) throw usage('unknown command "__complete"') + if (!command) throw commandNotFound('unknown command "__complete"') return { command, flags: { cword: completeCword(argv) }, @@ -59,7 +62,13 @@ export function parseCommand(argv: string[], commands: CommandSpec[]): ParsedCom } const command = findCommand(commands, pathTokens) - if (!command) throw usage(`unknown command "${pathTokens.join(' ')}"`) + if (!command) { + if (helpRequested && hasCommandPrefix(commands, pathTokens)) { + return { command: virtualGroupCommand(pathTokens), flags: { help: true }, globalFlags: global, positionals: [] } + } + const suggestion = suggestCommand(commands, pathTokens) + throw commandNotFound(`unknown command "${pathTokens.join(' ')}"${suggestion ? `, did you mean "${suggestion}"?` : ''}`) + } const pathLength = matchedPathLength(command, pathTokens) const commandArgs = tokens.slice(pathLength) if (helpRequested) return { command, flags: { help: true }, globalFlags: global, positionals: commandArgs } @@ -69,6 +78,10 @@ export function parseCommand(argv: string[], commands: CommandSpec[]): ParsedCom return { command, flags, globalFlags: global, positionals } } +function commandNotFound(message: string): AbortError { + return new AbortError(message, ExitCodes.CommandNotFound, undefined, 'command_not_found') +} + function completeCword(argv: string[]): number { const index = argv.indexOf('--cword') if (index === -1) return -1 @@ -124,44 +137,88 @@ export function parseFlagValue(flag: FlagSpec, value: unknown): boolean | number const parsed = flag.type === 'boolean' ? typeof value === 'boolean' ? value : String(value) !== 'false' : String(value) - if (flag.enum && !flag.enum.includes(String(parsed))) throw usage(`--${flag.name} must be one of: ${flag.enum.join(', ')}`) + if (flag.enum && !flag.enum.includes(String(parsed))) throw usage(`invalid argument "${String(parsed)}" for "--${flag.name}" flag: expected one of: ${flag.enum.join(', ')}`) return parsed } function parseGlobalFlags(argv: string[]): GlobalFlags { const raw = parseArgv(argv, globalFlagSpecs, { allowUnknownFlags: true }).flags - const readOnlyFromEnv = envBool('BEEPER_READONLY') + const color = stringGlobal(raw, argv, 'color', 'BEEPER_COLOR', 'auto') + if (!['auto', 'always', 'never'].includes(color)) throw usage(`invalid argument "${color}" for "--color" flag: expected one of: auto, always, never`) return { - accessToken: typeof raw['access-token'] === 'string' && raw['access-token'] ? raw['access-token'] : undefined, - account: Array.isArray(raw.account) ? raw.account.map(String).filter(Boolean) : typeof raw.account === 'string' && raw.account ? [raw.account] : undefined, - debug: raw.debug === true, - color: raw.color === 'always' || raw.color === 'never' ? raw.color : 'auto', - disableCommands: typeof raw['disable-commands'] === 'string' && raw['disable-commands'] ? raw['disable-commands'] : undefined, - dryRun: raw['dry-run'] === true, - enableCommands: typeof raw['enable-commands'] === 'string' && raw['enable-commands'] ? raw['enable-commands'] : undefined, - enableCommandsExact: typeof raw['enable-commands-exact'] === 'string' && raw['enable-commands-exact'] ? raw['enable-commands-exact'] : undefined, - events: raw.events === true, + accessToken: stringGlobal(raw, argv, 'access-token', 'BEEPER_ACCESS_TOKEN') || undefined, + account: stringListGlobal(raw, argv, 'account', 'BEEPER_ACCOUNT'), + debug: boolGlobalAliases(raw, argv, ['verbose', 'debug'], 'BEEPER_DEBUG'), + color: color as 'auto' | 'always' | 'never', + disableCommands: stringGlobal(raw, argv, 'disable-commands', 'BEEPER_DISABLE_COMMANDS') || undefined, + dryRun: boolGlobal(raw, argv, 'dry-run', 'BEEPER_DRY_RUN'), + enableCommands: stringGlobal(raw, argv, 'enable-commands', 'BEEPER_ENABLE_COMMANDS') || undefined, + enableCommandsExact: stringGlobal(raw, argv, 'enable-commands-exact', 'BEEPER_ENABLE_COMMANDS_EXACT') || undefined, + events: boolGlobal(raw, argv, 'events', 'BEEPER_EVENTS'), force: raw.force === true, full: raw.full === true, - home: typeof raw.home === 'string' && raw.home ? raw.home : undefined, - json: raw.json === true, + home: stringGlobal(raw, argv, 'home', ['BEEPER_HOME', 'BEEPER_STORE_DIR', 'BEEPER_CLI_CONFIG_DIR']) || undefined, + json: boolGlobal(raw, argv, 'json', 'BEEPER_JSON') || autoJSON(argv, raw), + lockWait: typeof raw['lock-wait'] === 'string' && raw['lock-wait'] ? raw['lock-wait'] : undefined, noInput: raw['no-input'] === true, - plain: raw.plain === true, - readOnly: raw['read-only'] === true || (!hasNoFlag(argv, 'read-only') && readOnlyFromEnv), + plain: boolGlobal(raw, argv, 'plain', 'BEEPER_PLAIN'), + readOnly: boolGlobal(raw, argv, 'read-only', 'BEEPER_READONLY'), resultsOnly: raw['results-only'] === true, safetyProfile: typeof raw['safety-profile'] === 'string' && raw['safety-profile'] ? raw['safety-profile'] : undefined, - select: typeof raw.select === 'string' && raw.select ? raw.select : undefined, - target: typeof raw.target === 'string' && raw.target ? raw.target : undefined, - timeout: typeof raw.timeout === 'string' && raw.timeout ? raw.timeout : undefined, - wrapUntrusted: raw['wrap-untrusted'] === true, + select: stringGlobal(raw, argv, 'select', ['BEEPER_SELECT', 'BEEPER_FIELDS', 'BEEPER_PROJECT']) || undefined, + target: stringGlobal(raw, argv, 'target', 'BEEPER_TARGET') || undefined, + timeout: stringGlobal(raw, argv, 'timeout', 'BEEPER_TIMEOUT') || undefined, + wrapUntrusted: boolGlobal(raw, argv, 'wrap-untrusted', 'BEEPER_WRAP_UNTRUSTED'), } } +function boolGlobal(raw: Record, argv: string[], name: string, envName: string): boolean { + return raw[name] === true || (!hasNoFlag(argv, name) && envBool(envName)) +} + +function boolGlobalAliases(raw: Record, argv: string[], names: string[], envName: string): boolean { + return names.some(name => raw[name] === true) || (!names.some(name => hasNoFlag(argv, name)) && envBool(envName)) +} + +function autoJSON(argv: string[], raw: Record): boolean { + if (!envBool('BEEPER_AUTO_JSON')) return false + if (hasNoFlag(argv, 'json') || raw.json === true || raw.plain === true) return false + if (envBool('BEEPER_JSON') || envBool('BEEPER_PLAIN')) return false + return !process.stdout.isTTY +} + +function stringGlobal(raw: Record, argv: string[], name: string, envName: string | string[], fallback = ''): string { + if (typeof raw[name] === 'string' && raw[name]) return raw[name] + if (hasLongFlag(argv, name)) return fallback + for (const candidate of Array.isArray(envName) ? envName : [envName]) { + const fromEnv = process.env[candidate]?.trim() + if (fromEnv) return fromEnv + } + return fallback +} + +function stringListGlobal(raw: Record, argv: string[], name: string, envName: string): string[] | undefined { + const value = raw[name] + if (Array.isArray(value)) return value.map(String).filter(Boolean) + if (typeof value === 'string' && value) return [value] + if (!hasLongFlag(argv, name) && process.env[envName]?.trim()) return splitEnvList(process.env[envName]!) + return undefined +} + +function splitEnvList(value: string): string[] | undefined { + const items = value.split(',').map(item => item.trim()).filter(Boolean) + return items.length ? items : undefined +} + function envBool(name: string): boolean { const value = process.env[name]?.trim().toLowerCase() return value === '1' || value === 'true' || value === 'yes' || value === 'on' } +function hasLongFlag(argv: string[], name: string): boolean { + return argv.some(token => token === `--${name}` || token.startsWith(`--${name}=`) || token === `--no-${name}`) +} + function hasNoFlag(argv: string[], name: string): boolean { return argv.some(token => token === `--no-${name}` || token === `--${name}=false`) } @@ -193,7 +250,7 @@ function parseArgv( const noPrefix = parsed.name.startsWith('no-') ? parsed.name.slice(3) : undefined const spec = byName.get(parsed.name) ?? (noPrefix ? byName.get(noPrefix) : undefined) if (!spec) { - if (!options.allowUnknownFlags) throw usage(`unknown flag --${parsed.name}`) + if (!options.allowUnknownFlags) throw usage(`unknown flag --${parsed.name}`, 'Run with --help to see available flags') positionals.push(token) continue } @@ -203,19 +260,66 @@ function parseArgv( continue } const value = parsed.value ?? argv[index + 1] - if (value === undefined || value.startsWith('-')) throw usage(`--${spec.name} requires a value`) + if (value === undefined || value.startsWith('-')) throw usage(missingFlagValueMessage(spec, value)) setFlag(flags, spec, parseFlagValue(spec, value)) if (parsed.value === undefined) index += 1 } return { flags, positionals } } +function missingFlagValueMessage(spec: FlagSpec, value: string | undefined): string { + const expected = spec.type === 'integer' ? 'integer' : 'string' + if (value === undefined) return `--${spec.name}: expected ${expected} value but got "EOL" ()` + return `--${spec.name}: expected ${expected} value but got "${value}"` +} + function findCommand(commands: CommandSpec[], tokens: string[]): CommandSpec | undefined { return commands .filter(command => commandPaths(command).some(path => path.every((part, index) => tokens[index] === part))) .sort((a, b) => matchedPathLength(b, tokens) - matchedPathLength(a, tokens))[0] } +function hasCommandPrefix(commands: CommandSpec[], tokens: string[]): boolean { + return commands.some(command => commandPaths(command).some(path => tokens.length < path.length && tokens.every((part, index) => path[index] === part))) +} + +function virtualGroupCommand(path: string[]): CommandSpec { + return { + description: path.length === 1 ? rootNamespaceDescription(path[0]!) : `${path.join(' ')} commands`, + path, + risk: 'read', + run: async () => undefined, + } +} + +function rootNamespaceDescription(name: string): string { + const descriptions: Record = { + account: 'Manage connected chat accounts', + accounts: 'Manage connected chat accounts', + api: 'Call raw Beeper Desktop API endpoints', + auth: 'Authenticate and manage stored credentials', + chat: 'List and manage chats', + chats: 'List and manage chats', + config: 'Manage configuration', + contact: 'List and search contacts', + contacts: 'List and search contacts', + group: 'List and manage group chats', + groups: 'List and manage group chats', + install: 'Install Beeper Desktop or Beeper Server', + media: 'Download message media', + messages: 'List, search, edit, and delete messages', + presence: 'Send presence indicators', + remove: 'Remove configured resources', + resolve: 'Resolve Beeper selectors', + search: 'Search Beeper', + send: 'Send messages, files, reactions, and presence', + target: 'Manage Beeper Desktop and Server targets', + targets: 'Manage Beeper Desktop and Server targets', + use: 'Select default resources', + } + return descriptions[name] ?? `${name} commands` +} + function matchedPathLength(command: CommandSpec, tokens: string[]): number { return commandPaths(command) .filter(path => path.every((part, index) => tokens[index] === part)) @@ -226,12 +330,53 @@ function commandPaths(command: CommandSpec): string[][] { return [command.path, ...(command.aliases ?? [])] } +function suggestCommand(commands: CommandSpec[], tokens: string[]): string | undefined { + const parent = tokens.slice(0, -1) + const input = tokens.at(-1) + if (!input) return undefined + const candidates = commands + .flatMap(command => commandPaths(command)) + .filter(path => path.length >= tokens.length) + .filter(path => !parent.length || parent.every((part, index) => path[index] === part)) + .map(path => ({ path, distance: editDistance(input, path[tokens.length - 1] ?? '') })) + .filter(candidate => candidate.distance <= suggestionThreshold(input)) + .sort((a, b) => a.distance - b.distance || a.path.join(' ').localeCompare(b.path.join(' '))) + return candidates[0]?.path.slice(0, tokens.length).join(' ') +} + +function suggestionThreshold(input: string): number { + return Math.max(1, Math.min(2, Math.ceil(input.length / 3))) +} + +function editDistance(a: string, b: string): number { + const previous = Array.from({ length: b.length + 1 }, (_, index) => index) + for (let row = 1; row <= a.length; row += 1) { + const current = [row] + for (let column = 1; column <= b.length; column += 1) { + const cost = a[row - 1] === b[column - 1] ? 0 : 1 + current[column] = Math.min( + current[column - 1]! + 1, + previous[column]! + 1, + previous[column - 1]! + cost, + ) + } + previous.splice(0, previous.length, ...current) + } + return previous[b.length] ?? 0 +} + function validatePositionals(command: CommandSpec, values: string[]): void { const args = command.args ?? [] const required = args.filter(arg => arg.required).length const variadic = args.some(arg => arg.variadic) - if (values.length < required) throw usage(`${command.path.join(' ')} requires ${args[values.length]?.name ?? 'more arguments'}`) - if (!variadic && values.length > args.length) throw usage(`${command.path.join(' ')} got too many arguments`) + if (values.length < required) throw usage(`expected "${formatArgUsage(args[values.length])}"`) + if (!variadic && values.length > args.length) throw usage(`unexpected argument ${values[args.length] ?? values.at(-1) ?? ''}`.trim()) +} + +function formatArgUsage(arg: { name: string; required?: boolean; variadic?: boolean } | undefined): string { + if (!arg) return '' + if (arg.variadic) return arg.required ? `<${arg.name}> ...` : `[<${arg.name}> ...]` + return arg.required ? `<${arg.name}>` : `[<${arg.name}>]` } export function validateCommandInput(command: CommandSpec, flags: Record, positionals: string[]): void { diff --git a/packages/cli/src/cli/policy.ts b/packages/cli/src/cli/policy.ts index 357ab3e0..a9706919 100644 --- a/packages/cli/src/cli/policy.ts +++ b/packages/cli/src/cli/policy.ts @@ -14,6 +14,9 @@ export function enforcePolicy(command: CommandSpec, flags: GlobalFlags): void { if (profile && !matchesPrefix(profile.allow, command.path)) { throw usage(`command "${command.path.join(' ')}" is blocked by safety profile "${profile.name}"`) } + if (command.risk === 'destructive' && !flags.force && !flags.dryRun) { + throw usage(`destructive command "${command.path.join(' ')}" requires --force or --dry-run`, 'Pass --force to confirm, or --dry-run to preview the action.') + } } export function commandVisible(command: CommandSpec, flags: GlobalFlags): boolean { @@ -27,17 +30,16 @@ export function commandVisible(command: CommandSpec, flags: GlobalFlags): boolea function enforceCommandFilters(command: CommandSpec, flags: GlobalFlags): void { if (commandAllowedByFilters(command, flags)) return const path = command.path - if (rulesFromCSV(flags.disableCommands).size && matchesPrefix(rulesFromCSV(flags.disableCommands), path)) throw usage(`command "${path.join(' ')}" is disabled (blocked by --disable-commands)`) + if (rulesFromCSV(flags.disableCommands).size && commandMatchesPrefix(rulesFromCSV(flags.disableCommands), command)) throw usage(`command "${path.join(' ')}" is disabled (blocked by --disable-commands)`) throw usage(`command "${path.join(' ')}" is not enabled (set --enable-commands or --enable-commands-exact to allow it)`) } function commandAllowedByFilters(command: CommandSpec, flags: GlobalFlags): boolean { - const path = command.path const allow = rulesFromCSV(flags.enableCommands) const exactAllow = rulesFromCSV(flags.enableCommandsExact) const deny = rulesFromCSV(flags.disableCommands) - if (deny.size && matchesPrefix(deny, path)) return false - if ((allow.size || exactAllow.size) && !matchesPrefix(allow, path) && !matchesExact(exactAllow, path)) return false + if (deny.size && commandMatchesPrefix(deny, command)) return false + if ((allow.size || exactAllow.size) && !commandMatchesPrefix(allow, command) && !commandMatchesExact(exactAllow, command)) return false return true } @@ -81,6 +83,18 @@ function matchesExact(rules: Set, path: string[]): boolean { return rules.has('*') || rules.has('all') || rules.has(path.join('.')) } +function commandMatchesPrefix(rules: Set, command: CommandSpec): boolean { + return commandPaths(command).some(path => matchesPrefix(rules, path)) +} + +function commandMatchesExact(rules: Set, command: CommandSpec): boolean { + return commandPaths(command).some(path => matchesExact(rules, path)) +} + +function commandPaths(command: CommandSpec): string[][] { + return [command.path, ...(command.aliases ?? [])] +} + function rulesFromCSV(value?: string): Set { const out = new Set() for (const part of (value ?? '').split(',')) { diff --git a/packages/cli/src/cli/schema.ts b/packages/cli/src/cli/schema.ts index c497146d..2207a93f 100644 --- a/packages/cli/src/cli/schema.ts +++ b/packages/cli/src/cli/schema.ts @@ -3,21 +3,28 @@ import { globalFlagSpecs } from './parse.js' import { commandVisible } from './policy.js' type SchemaDoc = { + schema_version: 1 build: string command: SchemaNode - schema_version: 1 } type SchemaNode = { + alias_paths?: string[] aliases?: string[][] flags?: SchemaFlag[] help: string hidden?: boolean + mcp?: boolean name: string output?: string path: string positionals?: SchemaArg[] + primary_result_key?: string + program_alias_paths?: string[] + program_path?: string + raw_json?: boolean requirements?: string[] + risk?: string subcommands?: SchemaNode[] type: 'application' | 'command' usage?: string @@ -28,6 +35,7 @@ type SchemaFlag = { default?: boolean | number | string envs?: string[] enum?: string[] + has_default?: boolean help?: string multiple?: boolean name: string @@ -38,6 +46,7 @@ type SchemaFlag = { } type SchemaArg = { + enum?: string[] help?: string name: string required?: boolean @@ -45,43 +54,344 @@ type SchemaArg = { variadic?: boolean } -export function buildSchema(commands: CommandSpec[], version: string, requested: string[] = [], flags?: GlobalFlags): SchemaDoc { - const visible = commands.filter(command => flags ? commandVisible(command, flags) : !command.hidden) - const filtered = requested.length - ? visible.filter(command => command.path.join('.').startsWith(requested.join('.'))) +export function buildSchema(commands: CommandSpec[], version: string, requested: string[] = [], flags?: GlobalFlags, options: { includeHidden?: boolean } = {}): SchemaDoc { + const visible = commands.filter(command => visibleInSchema(command, flags, Boolean(options.includeHidden))) + const prefix = normalizeRequestedPath(visible, requested) + const prefixHasCanonicalChildren = prefix.length > 0 && visible.some(command => + command.path.length > prefix.length + && command.path.slice(0, prefix.length).every((part, index) => part === prefix[index])) + const includeAliasChildren = !prefixHasCanonicalChildren || prefix[0] === 'auth' + const filtered = prefix.length + ? visible.filter(command => + command.path.slice(0, prefix.length).every((part, index) => part === prefix[index]) + || (includeAliasChildren && (command.aliases ?? []).some(alias => alias.slice(0, prefix.length).every((part, index) => part === prefix[index])))) : visible return { - build: version, - command: nodeFor(filtered, requested, requested.length ? requested.at(-1) ?? 'beeper' : 'beeper'), schema_version: 1, + build: version, + command: nodeFor(filtered, prefix, prefix.length ? prefix.at(-1) ?? 'beeper' : 'beeper'), + } +} + +function visibleInSchema(command: CommandSpec, flags: GlobalFlags | undefined, includeHidden: boolean): boolean { + const candidate = includeHidden ? { ...command, hidden: false } : command + return flags ? commandVisible(candidate, flags) : includeHidden || !command.hidden +} + +function normalizeRequestedPath(commands: CommandSpec[], requested: string[]): string[] { + const parts = requested.flatMap(part => part.trim().split(/\s+/)).filter(Boolean) + if (!parts.length) return [] + const matched = commands + .flatMap(command => [command.path, ...(command.aliases ?? [])].map(path => ({ command, path }))) + .filter(item => item.path.join('.') === parts.join('.')) + .sort((a, b) => b.path.length - a.path.length)[0] + if (matched) return matched.command.path + if (parts.length === 1) { + const canonical = canonicalNamespaceForAlias(parts[0]!) + if (canonical) return [canonical] } + return parts } function nodeFor(commands: CommandSpec[], prefix: string[], name: string): SchemaNode { const exact = commands.find(command => command.path.join('.') === prefix.join('.')) + const exactAlias = exact ? undefined : commands.find(command => (command.aliases ?? []).some(alias => alias.join('.') === prefix.join('.'))) + const nodeCommand = exact ?? exactAlias + const rootAliases = prefix.length === 0 ? rootAliasNodes(commands) : [] + const rootAliasNames = new Set(rootAliases.map(node => node.name)) const childNames = new Set() + const hasCanonicalChildren = prefix.length > 0 && commands.some(command => + command.path.length > prefix.length + && command.path.slice(0, prefix.length).every((part, index) => part === prefix[index])) for (const command of commands) { const child = command.path[prefix.length] + if (prefix.length === 0 && child && rootAliasNames.has(child) && !commands.some(candidate => candidate.path.length === 1 && candidate.path[0] === child)) continue if (child) childNames.add(child) + const commandUnderPrefix = command.path.slice(0, prefix.length).every((part, index) => part === prefix[index]) + if (prefix.length > 0 && !commandUnderPrefix && (!hasCanonicalChildren || prefix[0] === 'auth')) { + const aliasChild = (command.aliases ?? []) + .find(alias => alias.length > prefix.length && alias.slice(0, prefix.length).every((part, index) => part === prefix[index])) + ?.at(prefix.length) + if (aliasChild) childNames.add(aliasChild) + } } const children = [...childNames] - .sort() - .map(child => nodeFor(commands.filter(command => command.path[prefix.length] === child), [...prefix, child], child)) + .sort((a, b) => childPriority(prefix, a) - childPriority(prefix, b) || a.localeCompare(b)) + .map(child => nodeFor(commands.filter(command => + command.path[prefix.length] === child + || (command.aliases ?? []).some(alias => + alias.length > prefix.length + && alias[prefix.length] === child + && alias.slice(0, prefix.length).every((part, index) => part === prefix[index]))), [...prefix, child], child)) + const subcommands = prefix.length === 0 ? sortRootNodes([...children, ...rootAliases]) : children + const namespaceAliasPaths = !nodeCommand && prefix.length === 1 ? namespaceAliases(prefix[0]!, commands).map(alias => [alias]) : undefined + + const aliases = schemaAliasesForNode(prefix, nodeCommand) ?? (namespaceAliasPaths?.length ? namespaceAliasPaths : undefined) return { - aliases: exact?.aliases, - flags: prefix.length === 0 ? schemaFlags(globalFlagSpecs) : schemaFlags(exact?.flags ?? []), - help: exact?.description ?? 'Beeper CLI', - hidden: exact?.hidden || undefined, + alias_paths: schemaAliasPaths(aliases), + aliases, + flags: schemaFlags(flagsForNode(prefix, nodeCommand)), + help: nodeCommand?.description ?? (prefix.length ? `${prefix.join(' ')} commands` : 'Beeper CLI'), + hidden: nodeCommand?.hidden || undefined, + mcp: nodeCommand?.mcp || undefined, name, - output: exact?.output, - path: prefix.join(' '), - positionals: exact?.args?.map(schemaArg), - requirements: exact ? requirements(exact) : undefined, - subcommands: children.length ? children : undefined, + output: nodeCommand?.output, + path: exactAlias ? nodeCommand?.path.join(' ') ?? prefix.join(' ') : prefix.length ? prefix.join(' ') : 'beeper', + positionals: nodeCommand?.args?.map(schemaArg), + primary_result_key: nodeCommand ? primaryResultKey(nodeCommand) : undefined, + program_alias_paths: programAliasPaths(aliases), + program_path: prefix.length ? `beeper ${exactAlias ? nodeCommand?.path.join(' ') ?? prefix.join(' ') : prefix.join(' ')}` : 'beeper', + raw_json: nodeCommand?.rawJson || undefined, + requirements: nodeCommand ? requirements(nodeCommand) : undefined, + risk: nodeCommand?.risk, + subcommands: subcommands.length ? subcommands : undefined, type: prefix.length === 0 ? 'application' : 'command', - usage: exact ? `beeper ${exact.path.join(' ')}` : undefined, + usage: nodeCommand ? usageForNode(nodeCommand, prefix, children, Boolean(exactAlias)) : prefix.length ? namespaceUsage(prefix, commands) : 'beeper [flags]', + } +} + +function rootAliasNodes(commands: CommandSpec[]): SchemaNode[] { + const nodes: SchemaNode[] = [] + const seen = new Set(commands.filter(command => command.path.length === 1).map(command => command.path[0]).filter(Boolean)) + for (const command of commands) { + if (command.path.length <= 1) continue + const aliases = (command.aliases ?? []).filter(alias => alias.length === 1) + if (!aliases.length) continue + const primary = aliases[0]![0]! + if (seen.has(primary)) continue + seen.add(primary) + nodes.push({ + alias_paths: schemaAliasPaths(aliases.slice(1)), + aliases: aliases.slice(1), + flags: schemaFlags(flagsForNode(command.path, command)), + help: `${command.description} (alias for '${command.path.join(' ')}')`, + hidden: command.hidden || undefined, + mcp: command.mcp || undefined, + name: primary, + output: command.output, + path: primary, + positionals: command.args?.map(schemaArg), + primary_result_key: primaryResultKey(command), + program_alias_paths: programAliasPaths(aliases.slice(1)), + program_path: `beeper ${primary}`, + raw_json: command.rawJson || undefined, + requirements: requirements(command), + risk: command.risk, + type: 'command', + usage: `beeper ${primary}${aliases.length > 1 ? ` (${aliases.slice(1).map(alias => alias.join(' ')).join(',')})` : ''}${usageArgs(command.args ?? [])} [flags]`, + }) + } + const me = commands.find(command => command.path.join(' ') === 'me') + const whoamiAliases = me?.aliases?.filter(alias => alias.length === 1 && alias[0]?.startsWith('who')) ?? [] + if (me && whoamiAliases.length) { + const primary = whoamiAliases[0]![0]! + if (!seen.has(primary)) { + seen.add(primary) + nodes.push({ + alias_paths: schemaAliasPaths(whoamiAliases.slice(1)), + aliases: whoamiAliases.slice(1), + flags: schemaFlags(flagsForNode(me.path, me)), + help: `${me.description} (alias for '${me.path.join(' ')}')`, + hidden: me.hidden || undefined, + mcp: me.mcp || undefined, + name: primary, + output: me.output, + path: primary, + positionals: me.args?.map(schemaArg), + primary_result_key: primaryResultKey(me), + program_alias_paths: programAliasPaths(whoamiAliases.slice(1)), + program_path: `beeper ${primary}`, + raw_json: me.rawJson || undefined, + requirements: requirements(me), + risk: me.risk, + type: 'command', + usage: `beeper ${primary}${whoamiAliases.length > 1 ? ` (${whoamiAliases.slice(1).map(alias => alias.join(' ')).join(',')})` : ''}${usageArgs(me.args ?? [])} [flags]`, + }) + } } + return nodes +} + +function sortRootNodes(nodes: SchemaNode[]): SchemaNode[] { + return nodes.sort((a, b) => childPriority([], a.name) - childPriority([], b.name) || a.name.localeCompare(b.name)) +} + +function childPriority(prefix: string[], child: string): number { + const parent = prefix.join(' ') + const rootOrder = [ + 'message', + 'ls', + 'search', + 'open', + 'download', + 'upload', + 'login', + 'logout', + 'status', + 'me', + 'whoami', + 'setup', + 'send', + 'chats', + 'chat', + 'groups', + 'group', + 'messages', + 'accounts', + 'account', + 'contacts', + 'contact', + 'presence', + 'media', + 'targets', + 'target', + 'use', + 'remove', + 'resolve', + 'export', + 'watch', + 'doctor', + 'auth', + 'install', + 'api', + 'config', + 'docs', + 'schema', + 'mcp', + 'agent', + 'exit-codes', + 'completion', + 'help', + 'version', + ] + const orders: Record = { + '': rootOrder, + account: ['list', 'show', 'add', 'use', 'remove'], + accounts: ['list', 'show', 'add', 'use', 'remove'], + auth: ['add', 'list', 'email', 'logout', 'status'], + chat: ['list', 'show', 'start', 'archive', 'unarchive', 'pin', 'unpin', 'mute', 'unmute', 'read', 'mark-read', 'mark-unread', 'rename', 'description', 'avatar', 'priority', 'draft', 'remind', 'disappear', 'focus', 'notify-anyway'], + chats: ['list', 'show', 'start', 'archive', 'unarchive', 'pin', 'unpin', 'mute', 'unmute', 'read', 'mark-read', 'mark-unread', 'rename', 'description', 'avatar', 'priority', 'draft', 'remind', 'disappear', 'focus', 'notify-anyway'], + config: ['get', 'keys', 'set', 'unset', 'list', 'path'], + contact: ['list', 'show'], + contacts: ['list', 'show'], + group: ['list', 'show', 'create', 'rename', 'description'], + groups: ['list', 'show', 'create', 'rename', 'description'], + media: ['download', 'message'], + messages: ['list', 'search', 'context', 'show', 'export', 'forward', 'edit', 'delete', 'revoke'], + presence: ['typing', 'paused'], + search: ['all'], + send: ['text', 'file', 'voice', 'sticker', 'react', 'presence'], + target: ['list', 'use', 'add', 'remove', 'logs', 'runtime', 'tunnel'], + 'target runtime': ['start', 'stop', 'restart'], + targets: ['list', 'use', 'add', 'remove', 'logs', 'runtime', 'tunnel'], + 'targets runtime': ['start', 'stop', 'restart'], + } + const order = orders[parent] ?? [] + const index = order.indexOf(child) + return index === -1 ? order.length : index +} + +function namespaceUsage(prefix: string[], commands: CommandSpec[]): string { + const aliases = prefix.length === 1 ? namespaceAliases(prefix[0]!, commands) : [] + return `beeper ${prefix.join(' ')}${aliases.length ? ` (${aliases.join(',')})` : ''} [flags]` +} + +function namespaceAliases(name: string, commands: CommandSpec[]): string[] { + const allowed = singularNamespaceAliases()[name] ?? [] + const aliases = new Set() + for (const command of commands) { + if (command.path[0] !== name) continue + for (const alias of command.aliases ?? []) { + if (alias.length < 2) continue + const aliasRoot = alias[0] + if (aliasRoot && allowed.includes(aliasRoot)) aliases.add(aliasRoot) + } + } + return [...aliases].sort((a, b) => childPriority([], a) - childPriority([], b) || a.localeCompare(b)) +} + +function canonicalNamespaceForAlias(alias: string): string | undefined { + return Object.entries(singularNamespaceAliases()).find(([, aliases]) => aliases.includes(alias))?.[0] +} + +function singularNamespaceAliases(): Record { + return { + accounts: ['account'], + chats: ['chat'], + contacts: ['contact'], + groups: ['group'], + targets: ['target'], + } +} + +function usageForCommand(command: CommandSpec): string { + const aliases = formatUsageAliases(command) + return `beeper ${[command.path.join(' '), aliases].filter(Boolean).join(' ')}${usageArgs(command.args ?? [])} [flags]` +} + +function usageForNode(command: CommandSpec, prefix: string[], children: SchemaNode[], aliasNode = false): string { + if (children.length && !command.args?.length) return `beeper ${prefix.join(' ')} [flags]` + if (aliasNode) { + const aliases = (command.aliases ?? []) + .filter(alias => alias.join(' ') !== prefix.join(' ')) + .filter(alias => alias.length === prefix.length && alias.slice(0, -1).every((part, index) => part === prefix[index])) + .map(alias => alias.at(-1)!) + return `beeper ${prefix.join(' ')}${aliases.length ? ` (${[...new Set(aliases)].join(',')})` : ''}${usageArgs(command.args ?? [])} [flags]` + } + return usageForCommand(command) +} + +function schemaAliasesForNode(prefix: string[], command: CommandSpec | undefined): string[][] | undefined { + if (!command) return undefined + if (command.path.join(' ') === prefix.join(' ')) return command.aliases + const aliases = (command.aliases ?? []) + .filter(alias => alias.join(' ') !== prefix.join(' ')) + .filter(alias => alias.length === prefix.length && alias.slice(0, -1).every((part, index) => part === prefix[index])) + return aliases.length ? aliases : undefined +} + +function schemaAliasPaths(aliases: string[][] | undefined): string[] | undefined { + const paths = aliases?.map(alias => alias.join(' ')).filter(Boolean) + return paths?.length ? paths : undefined +} + +function programAliasPaths(aliases: string[][] | undefined): string[] | undefined { + const paths = schemaAliasPaths(aliases)?.map(alias => `beeper ${alias}`) + return paths?.length ? paths : undefined +} + +function usageArgs(args: ArgSpec[]): string { + if (!args.length) return '' + return ` ${args.map(formatArgUsage).join(' ')}` +} + +function formatUsageAliases(command: CommandSpec): string { + const canonical = command.path.join(' ') + const aliases = (command.aliases ?? []) + .map(alias => alias.join(' ')) + .filter(alias => alias !== canonical) + return aliases.length ? `(${[...new Set(aliases)].join(',')})` : '' +} + +function formatArgUsage(arg: ArgSpec): string { + if (arg.variadic) return arg.required ? `<${arg.name}> ...` : `[<${arg.name}> ...]` + return arg.required ? `<${arg.name}>` : `[<${arg.name}>]` +} + +function flagsForNode(prefix: string[], exact: CommandSpec | undefined): FlagSpec[] { + if (!prefix.length) return displayFlags(globalFlagSpecs) + return mergeFlags(displayFlags(globalFlagSpecs), exact?.flags ?? []) +} + +function mergeFlags(globalFlags: FlagSpec[], localFlags: FlagSpec[]): FlagSpec[] { + const out = [...globalFlags] + for (const local of localFlags) { + const index = out.findIndex(flag => flag.name === local.name) + if (index === -1) out.push(local) + else out[index] = local + } + return out } function schemaFlags(flags: FlagSpec[]): SchemaFlag[] { @@ -90,18 +400,58 @@ function schemaFlags(flags: FlagSpec[]): SchemaFlag[] { default: flag.default, envs: flag.env, enum: flag.enum, + has_default: flag.default === undefined ? undefined : true, help: flag.description, multiple: flag.multiple, name: flag.name, - placeholder: flag.placeholder, + placeholder: flag.placeholder ?? defaultPlaceholder(flag), required: flag.required, short: flag.short, type: flag.type, })) } +function displayFlags(flags: FlagSpec[]): FlagSpec[] { + const priority = new Map([ + ['help', 0], + ['color', 1], + ['home', 2], + ['account', 3], + ['access-token', 4], + ['enable-commands', 5], + ['enable-commands-exact', 6], + ['disable-commands', 7], + ['json', 8], + ['plain', 9], + ['wrap-untrusted', 10], + ['results-only', 11], + ['select', 12], + ['dry-run', 13], + ['force', 14], + ['no-input', 15], + ['verbose', 16], + ['version', 17], + ['events', 18], + ['full', 19], + ['lock-wait', 20], + ['read-only', 21], + ['safety-profile', 22], + ['target', 23], + ['timeout', 24], + ]) + return [...flags].sort((a, b) => (priority.get(a.name) ?? 100) - (priority.get(b.name) ?? 100) || a.name.localeCompare(b.name)) +} + +function defaultPlaceholder(flag: FlagSpec): string { + if (flag.default !== undefined) return String(flag.default) + if (flag.type === 'integer') return 'INT' + if (flag.type === 'boolean') return 'BOOL' + return 'STRING' +} + function schemaArg(arg: ArgSpec): SchemaArg { return { + enum: arg.enum, help: arg.description, name: arg.name, required: arg.required, @@ -116,3 +466,13 @@ function requirements(command: CommandSpec): string[] | undefined { if (command.risk === 'destructive') out.push('destructive', 'force') return out.length ? out : undefined } + +function primaryResultKey(command: CommandSpec): string | undefined { + const path = command.path.join(' ') + if (path === 'auth list') return 'accounts' + if (path === 'auth services') return 'services' + if (path === 'config keys') return 'keys' + if (path === 'config path') return 'path' + if (path === 'targets list') return 'targets' + return undefined +} diff --git a/packages/cli/src/cli/types.ts b/packages/cli/src/cli/types.ts index 9502485b..f5ca9f46 100644 --- a/packages/cli/src/cli/types.ts +++ b/packages/cli/src/cli/types.ts @@ -15,6 +15,7 @@ export type FlagSpec = { export type ArgSpec = { name: string description?: string + enum?: string[] required?: boolean variadic?: boolean } @@ -36,8 +37,9 @@ export type CommandSpec = { flags?: FlagSpec[] hidden?: boolean mcp?: boolean - output?: 'accounts' | 'chats' | 'contacts' | 'diagnostic' | 'generic' | 'messages' | 'status' | 'targets' + output?: 'accounts' | 'auth' | 'chats' | 'contacts' | 'diagnostic' | 'generic' | 'messages' | 'status' | 'targets' path: string[] + rawJson?: boolean risk: CommandRisk run(ctx: CommandContext): Promise } @@ -56,6 +58,7 @@ export type GlobalFlags = { full: boolean home?: string json: boolean + lockWait?: string noInput: boolean plain: boolean readOnly: boolean diff --git a/packages/cli/src/lib/desktop-auth.ts b/packages/cli/src/lib/desktop-auth.ts index aa8a1626..d67532ce 100644 --- a/packages/cli/src/lib/desktop-auth.ts +++ b/packages/cli/src/lib/desktop-auth.ts @@ -32,7 +32,7 @@ export async function findLocalDesktop(options: { baseURL?: string; scan?: boole } catch { /* fall through */ } } - throw new AbortError(`Could not find a running Beeper Desktop API on ${candidates.join(', ')}.`, ExitCodes.NotReady) + throw new AbortError(`Could not find a running Beeper Desktop API on ${candidates.join(', ')}.`, ExitCodes.NotReady, undefined, 'not_ready') } type AuthorizedTargetToken = TokenResponse & { clientID: string } @@ -46,7 +46,7 @@ export async function authorizeTarget(options: { } = {}): Promise { const desktop = await findLocalDesktop({ baseURL: options.baseURL, scan: options.scan }) if (desktop.status?.state === 'needs-login') { - throw new AbortError('Beeper Desktop is not signed in. Open Beeper Desktop and sign in, then rerun this command.', ExitCodes.AuthRequired) + throw new AbortError('Beeper Desktop is not signed in. Open Beeper Desktop and sign in, then rerun this command.', ExitCodes.AuthRequired, undefined, 'auth_required') } return loginWithPKCE({ diff --git a/packages/cli/src/lib/errors.ts b/packages/cli/src/lib/errors.ts index ccb94244..56030571 100644 --- a/packages/cli/src/lib/errors.ts +++ b/packages/cli/src/lib/errors.ts @@ -3,7 +3,7 @@ * 1 generic runtime error * 2 usage error (parsing, missing required flag/arg, invalid combination) * 3 empty results when --fail-empty/--non-empty is set - * 4 auth required (no stored token; user must authenticate) + * 4 auth required or target not ready (inspect JSON error.code for auth_required vs not_ready) * 5 not found (selector matched nothing) * 6 ambiguous selector (multiple matches; use exact ID or --pick) * 127 user declined a selector suggestion (POSIX "command not found" semantics) diff --git a/packages/cli/src/lib/targets.ts b/packages/cli/src/lib/targets.ts index 814c4b8e..66616c8b 100644 --- a/packages/cli/src/lib/targets.ts +++ b/packages/cli/src/lib/targets.ts @@ -42,7 +42,7 @@ export const builtInDesktopTargetID = 'desktop' const customTargetID = 'custom' export function beeperDir(): string { - return process.env.BEEPER_CLI_CONFIG_DIR ?? join(homedir(), '.beeper') + return process.env.BEEPER_HOME ?? process.env.BEEPER_STORE_DIR ?? process.env.BEEPER_CLI_CONFIG_DIR ?? join(homedir(), '.beeper') } export const configPath = () => join(beeperDir(), 'config.json') diff --git a/packages/cli/test/cli-smoke.ts b/packages/cli/test/cli-smoke.ts index 75afdf2f..3628e928 100644 --- a/packages/cli/test/cli-smoke.ts +++ b/packages/cli/test/cli-smoke.ts @@ -1,21 +1,24 @@ import assert from 'node:assert/strict' import { spawnSync } from 'node:child_process' -import { existsSync, rmSync } from 'node:fs' +import { existsSync, readFileSync, rmSync } from 'node:fs' import { join } from 'node:path' import { fileURLToPath } from 'node:url' const root = fileURLToPath(new URL('..', import.meta.url)) const configDir = '/tmp/beeper-cli-smoke' +const homeConfigDir = '/tmp/beeper-cli-smoke-home' rmSync(configDir, { recursive: true, force: true }) -rmSync('/tmp/beeper-cli-smoke-home', { recursive: true, force: true }) +rmSync(homeConfigDir, { recursive: true, force: true }) const run = (...args: string[]) => spawnSync('bun', ['./bin/dev.js', ...args], { cwd: root, encoding: 'utf8', env: { ...process.env, + BEEPER_HOME: configDir, BEEPER_CLI_CONFIG_DIR: configDir, }, + maxBuffer: 16 * 1024 * 1024, }) const ok = (...args: string[]) => { @@ -24,83 +27,375 @@ const ok = (...args: string[]) => { return result.stdout } +const assertOrderedItems = (text: string, expected: string[]) => { + const items = text.trim().split('\n') + let start = 0 + for (const item of expected) { + const index = items.indexOf(item, start) + assert.notEqual(index, -1, `missing completion item ${item}`) + start = index + 1 + } +} + const runEnv = (env: Record, ...args: string[]) => spawnSync('bun', ['./bin/dev.js', ...args], { cwd: root, encoding: 'utf8', env: { ...process.env, + BEEPER_HOME: configDir, BEEPER_CLI_CONFIG_DIR: configDir, ...env, }, + maxBuffer: 16 * 1024 * 1024, }) -assert.match(ok('--help'), /Usage: beeper /) -assert.match(ok('--help'), /targets add/) -assert.match(ok('--help'), /targets runtime start\s+Start a local target runtime/) -assert.match(ok('--help'), /targets runtime stop\s+Stop a local server runtime/) -assert.match(ok('--help'), /targets runtime restart\s+Restart a local server runtime/) -assert.match(ok('--help'), /targets tunnel/) -assert.match(ok('--help'), /use account\s+Select the default account/) -assert.match(ok('--help'), /use target\s+Select the default target/) -assert.match(ok('--help'), /remove account\s+Remove an account/) -assert.match(ok('--help'), /remove target\s+Remove a target/) -assert.match(ok('--help'), /auth email start\s+Start email sign-in for a target/) -assert.match(ok('--help'), /auth email response\s+Finish email sign-in for a target/) -assert.match(ok('--help'), /install desktop\s+Install Beeper Desktop locally/) -assert.match(ok('--help'), /install server\s+Install Beeper Server locally/) -assert.match(ok('--help'), /accounts list/) -assert.match(ok('--help'), /accounts add/) -assert.match(ok('--help'), /messages list/) -assert.match(ok('--help'), /chats archive\s+Archive or unarchive a chat/) -assert.match(ok('--help'), /chats disappear\s+Set a disappearing-message timer/) -assert.match(ok('--help'), /chats priority\s+Set chat priority/) -assert.match(ok('--help'), /chats focus\s+Focus a chat in Beeper/) -assert.match(ok('--help'), /chats notify-anyway\s+Notify a chat anyway/) -assert.match(ok('--help'), /messages context/) -assert.match(ok('--help'), /messages edit\s+Edit a message/) -assert.match(ok('--help'), /messages delete\s+Delete a message/) -assert.match(ok('--help'), /api request/) -assert.match(ok('--help'), /send text\s+Send a text message/) -assert.match(ok('--help'), /send file\s+Send a file message/) -assert.match(ok('--help'), /send sticker\s+Send a sticker/) -assert.match(ok('--help'), /send voice\s+Send a voice note/) -assert.match(ok('--help'), /send react\s+Send or remove a reaction/) -assert.match(ok('--help'), /send presence\s+Send a typing indicator/) -assert.match(ok('--help'), /resolve account\s+Resolve an account selector/) -assert.match(ok('--help'), /resolve bridge\s+Resolve a bridge selector/) -assert.match(ok('--help'), /resolve chat\s+Resolve a chat selector/) -assert.match(ok('--help'), /resolve contact\s+Resolve a contact selector/) -assert.match(ok('--help'), /resolve target\s+Resolve a target selector/) -assert.match(ok('--help'), /watch/) -assert.match(ok('--help'), /media download/) -assert.match(ok('--help'), /export\s+Export accounts/) -assert.match(ok('--help'), /doctor\s+Run diagnostics/) -assert.match(ok('--help'), /exit-codes\s+Print stable exit codes/) -assert.match(ok('--help'), /Config:\n\n file: /) -assert.match(ok('--help'), /config path\s+Print config file path/) -assert.match(ok('--help'), /config set\s+Set a config value/) -assert.match(ok('--help'), /--full\s+Disable truncation in human table output/) -assert.match(ok('--help'), /--read-only \(\$BEEPER_READONLY\)/) +const rootHelp = ok('--help') +assert.match(rootHelp, /Usage: beeper /) +assert.match(rootHelp, /Build: 0\.6\.2/) +assert.match(rootHelp, /Flags:\n/) +assert.match(rootHelp, /Commands:\n/) +assert.match(rootHelp, /Commands:\n message \(msg\)[\s\S]*\n ls \(list\) \[flags\][\s\S]*\n search \(find\)[\s\S]*\n open \(browse,focus\)[\s\S]*\n download \(dl\)[\s\S]*\n upload \(up,put\)/) +assert.match(rootHelp, /accounts \(account\) \[flags\]\n Manage connected chat accounts/) +assert.match(rootHelp, /targets \(target\) \[flags\]\n Manage Beeper Desktop and Server targets/) +assert.doesNotMatch(rootHelp, /\n use \[flags\]/) +assert.doesNotMatch(rootHelp, /\n remove \[flags\]/) +assert.match(rootHelp, /auth \[flags\]\n Authenticate and manage stored credentials/) +assert.match(rootHelp, /login \(auth add,auth login\) \[flags\]\n Start email sign-in for a target/) +assert.match(rootHelp, /logout \[\] \[flags\]\n Clear stored authentication \(alias for 'auth logout'\)/) +assert.match(rootHelp, /me \(whoami,who-am-i\) \[flags\]\n Show selected account and target identity/) +assert.match(rootHelp, /whoami \(who-am-i\) \[flags\]\n Show selected account and target identity \(alias for 'me'\)/) +assert.match(rootHelp, /install \[flags\]\n Install Beeper Desktop or Beeper Server/) +assert.match(rootHelp, /accounts \(account\) \[flags\]\n Manage connected chat accounts/) +assert.match(rootHelp, /agent \[flags\]\n Agent-friendly helpers/) +assert.match(rootHelp, /help \[ \.\.\.\] \[flags\]\n Show help for a command/) +assert.match(rootHelp, /messages \[flags\]\n List, search, edit, and delete messages/) +assert.match(rootHelp, /chats \(chat\) \[flags\]\n List and manage chats/) +assert.match(rootHelp, /groups \(group\) \[flags\]\n List and manage group chats/) +assert.match(rootHelp, /api \[flags\]\n Call raw Beeper Desktop API endpoints/) +assert.match(rootHelp, /send \[\] \[ \.\.\.\] \[flags\]\n Send a text message/) +assert.match(rootHelp, /message \(msg\) \[\] \[ \.\.\.\] \[flags\]\n Send a text message \(alias for 'send text'\)/) +assert.match(rootHelp, /presence \[flags\]\n Send presence indicators/) +assert.match(rootHelp, /resolve \[flags\]\n Resolve Beeper selectors/) +assert.match(rootHelp, /watch \[flags\]/) +assert.match(rootHelp, /download \(dl\) \[\] \[flags\]\n Download message media \(alias for 'media download'\)/) +assert.match(rootHelp, /upload \(up,put\) \[flags\]\n Send a file message/) +assert.match(rootHelp, /ls \(list\) \[flags\]\n List chats \(alias for 'chats list'\)/) +assert.match(rootHelp, /search \(find\) \[\] \[flags\]\n Search messages across chats \(alias for 'messages search'\)/) +assert.match(rootHelp, /open \(browse,focus\) \[\] \[flags\]\n Focus a chat in Beeper \(alias for 'chats focus'\)/) +assert.match(rootHelp, /export \[flags\]\n Export accounts/) +assert.match(rootHelp, /doctor \(auth doctor\) \[flags\]\n Run diagnostics/) +assert.match(rootHelp, /docs \(help-docs\) \[flags\]\n Print command documentation locations/) +assert.match(rootHelp, /exit-codes \(agent exit-codes,agent exitcodes,agent exit-code,exitcodes\) \[flags\]\n Print stable exit codes/) +assert.doesNotMatch(rootHelp, /targets runtime start \[\] \[flags\]/) +assert.doesNotMatch(rootHelp, /messages delete \[flags\]/) +assert.doesNotMatch(rootHelp, /send text \(message,msg\)/) +assert.doesNotMatch(rootHelp, /\n dl \[flags\]/) +assert.doesNotMatch(rootHelp, /\n find \[\] \[flags\]/) +assert.doesNotMatch(rootHelp, /\n focus \[flags\]/) +assert.doesNotMatch(rootHelp, /\n msg \[\] \[ \.\.\.\] \[flags\]/) +assert.doesNotMatch(rootHelp, /Send a typing indicator \(alias for 'send presence'\)/) +assert.match(rootHelp, /Config:\n\n file: /) +assert.match(rootHelp, / root: \/tmp\/beeper-cli-smoke \(source: BEEPER_HOME\)/) +assert.match(rootHelp, /Flags:\n -h, --help\s+Show context-sensitive help/) +assert.match(rootHelp, /config \[flags\]\n Manage configuration/) +assert.match(rootHelp, /-h, --help\s+Show context-sensitive help/) +assert.match(rootHelp, /--full\s+Disable truncation in human table output/) +assert.match(rootHelp, /--lock-wait=STRING\s+Accepted for compatibility/) +assert.match(rootHelp, /--read-only \(\-\-readonly\).*Reject commands/) +assert.match(rootHelp, /\(\$BEEPER_READONLY\)/) +assert.match(rootHelp, /--home=STRING \(\-\-store\).*Override Beeper CLI/) +assert.match(rootHelp, /\(\$BEEPER_HOME,\$BEEPER_STORE_DIR,\$BEEPER_CLI_CONFIG_DIR\)/) +assert.match(rootHelp, /-v, --verbose \(\-\-debug\).*Enable verbose logging/) +assert.match(rootHelp, /\(\$BEEPER_DEBUG\)/) +assert.match(rootHelp, /--color="auto".*Color output/) +assert.match(rootHelp, /\(\$BEEPER_COLOR\)/) +assert.match(rootHelp, /--select=STRING \(\-\-fields, --project\).*In JSON mode/) +assert.match(rootHelp, /\(\$BEEPER_SELECT,\$BEEPER_FIELDS,\$BEEPER_PROJECT\)/) +assert.match(rootHelp, /--enable-commands=STRING.*Comma-separated enabled command prefixes/) +assert.match(rootHelp, /\(\$BEEPER_ENABLE_COMMANDS\)/) +assert.match(rootHelp, /--json .*?Output JSON to stdout \(best for scripting\)/) +assert.match(rootHelp, /\(\$BEEPER_JSON\)/) +assert.match(rootHelp, /--plain .*?Output stable, parseable text to stdout/) +assert.match(rootHelp, /\(TSV-like; no colors\)/) +assert.match(rootHelp, /\(\$BEEPER_PLAIN\)/) +assert.match(rootHelp, /--events .*?Emit machine-readable NDJSON lifecycle events on stderr/) +assert.match(rootHelp, /--read-only[\s\S]*?Reject commands that intentionally write Beeper or local CLI\s+state/) +assert.match(rootHelp, /--enable-commands=[\s\S]*?dot paths allowed/) +assert.match(rootHelp, /--enable-commands-exact=[\s\S]*?parent commands do not\s+enable children/) +assert.equal(ok('version'), '0.6.2\n') assert.match(ok('--version', '--json'), /"name": "beeper-cli"/) +assert.doesNotMatch(ok('-v'), /^0\.6\.2$/) +assert.equal(ok('--debug', 'version'), '0.6.2\n') assert.match(ok('st'), /READINESS/) assert.match(ok('doctor'), /SELECTED TARGET/) +assert.match(ok('doctor', '--connect'), /SELECTED TARGET/) +assert.match(ok('doctor', '--help'), /--connect\s+Accepted for compatibility/) assert.match(ok('targets', 'ls'), /ID\s+DEFAULT\s+TYPE/) +assert.match(ok('ls', '--help'), /Usage: beeper chats list \(chats ls,chat list,chat ls,ls,list\) \[flags\]/) +const targetsHelp = ok('targets', '--help') +assert.match(targetsHelp, /Usage: beeper targets \[flags\]/) +assert.match(targetsHelp, /Build: 0\.6\.2/) +assert.match(targetsHelp, /list \(ls\) \[flags\]\n List configured Beeper targets/) +assert.match(targetsHelp, /runtime start \[\] \[flags\]\n Start a local target runtime/) +assert.match(targetsHelp, /Commands:\n list \(ls\)[\s\S]*\n use [\s\S]*\n add [\s\S]*\n remove \(rm,del\)[\s\S]*\n logs \[\][\s\S]*\n runtime start[\s\S]*\n runtime stop[\s\S]*\n runtime restart[\s\S]*\n tunnel/) +const sendHelp = ok('send', '--help') +assert.match(sendHelp, /Usage: beeper send \[\] \[ \.\.\.\] \[flags\]/) +assert.match(sendHelp, /Build: 0\.6\.2/) +assert.match(sendHelp, /Arguments:\n \[\]\s+Chat selector/) +assert.match(sendHelp, /--message=STRING\s+Message text to send/) +assert.match(sendHelp, /text \[\] \[ \.\.\.\] \[flags\]\n Send a text message/) +assert.match(sendHelp, /Commands:\n text[\s\S]*\n file[\s\S]*\n voice[\s\S]*\n sticker[\s\S]*\n react[\s\S]*\n presence/) +const messagesHelp = ok('messages', '--help') +assert.match(messagesHelp, /Commands:\n list \(ls\)[\s\S]*\n search \(find\)[\s\S]*\n context \[\] \[flags\]\n Show a message with surrounding context\n\n show \(get,info\) \[\] \[flags\]\n Show one message\n\n export \[flags\]\n Export messages as JSON\n\n forward \[\] \[flags\]\n Forward a message[\s\S]*\n edit \(update,set\) \[\] \[flags\]\n Edit a message\n\n delete \(rm,del,remove\) \[\] \[flags\]\n Delete a message\n\n revoke \[\] \[flags\]\n Delete a sent message for everyone/) +const messagesListHelp = ok('messages', 'list', '--help') +assert.match(messagesListHelp, /--type=STRING\s+Only messages of this kind/) +assert.match(messagesListHelp, /--has-media\s+Only messages with media/) assert.equal(existsSync(join(root, 'docs', 'commands', 'README.md')), true) assert.equal(existsSync(join(root, 'docs', 'commands', 'send-text.md')), true) +const commandIndexDoc = readFileSync(join(root, 'docs', 'commands', 'README.md'), 'utf8') +assert.match(commandIndexDoc, /## Root Commands/) +assert.match(commandIndexDoc, /## Root Commands[\s\S]*\| `beeper message \(msg\)[\s\S]*\| `beeper ls \(list\) \[flags\]`[\s\S]*\| `beeper search \(find\)[\s\S]*\| `beeper open \(browse,focus\)[\s\S]*\| `beeper download \(dl\)[\s\S]*\| `beeper upload \(up,put\)/) +assert.match(commandIndexDoc, /\| `beeper whoami \(who-am-i\) \[flags\]` \| Show selected account and target identity \(alias for 'beeper me'\) \|/) +assert.match(commandIndexDoc, /\| `beeper search-all \(find-all\) \[flags\]` \| Search chats, group participants, and messages together \(alias for 'beeper search all'\) \|/) +assert.match(commandIndexDoc, /\| `beeper targets \(target\) \[flags\]` \| Manage Beeper Desktop and Server targets \|/) +assert.match(commandIndexDoc, /\| `beeper download \(dl\) \[\] \[flags\]` \| Download message media \(alias for 'beeper media download'\) \|/) +assert.match(commandIndexDoc, /\| `beeper message \(msg\) \[\] \[ \.\.\.\] \[flags\]` \| Send a text message \(alias for 'beeper send text'\) \|/) +assert.match(commandIndexDoc, /## Full Command Reference/) +const sendTextDoc = readFileSync(join(root, 'docs', 'commands', 'send-text.md'), 'utf8') +assert.match(sendTextDoc, /beeper send text \(message,msg\) \[\] \[ \.\.\.\] \[flags\]/) +assert.match(sendTextDoc, /`\[\]`/) +assert.match(sendTextDoc, /## Global Flags\n\n\| Name \| Description \|\n\| --- \| --- \|\n\| `-h, --help` \|/) +assert.match(sendTextDoc, /\| `-v, --verbose, --debug` \|[\s\S]*\| `--version` \|/) +assert.doesNotMatch(sendTextDoc, /\| `--version` \|[\s\S]*\| `-v, --verbose, --debug` \|/) +const mediaDownloadDoc = readFileSync(join(root, 'docs', 'commands', 'media-download.md'), 'utf8') +assert.match(mediaDownloadDoc, /beeper media download \(media dl,download,dl\) \[\] \[flags\]/) +const targetsListDoc = readFileSync(join(root, 'docs', 'commands', 'targets-list.md'), 'utf8') +assert.match(targetsListDoc, /## JSON Output[\s\S]*Default JSON output is an object containing the `targets` field\.[\s\S]*`--json --results-only` to emit only `targets`/) +const authListDoc = readFileSync(join(root, 'docs', 'commands', 'auth-list.md'), 'utf8') +assert.match(authListDoc, /## JSON Output[\s\S]*Default JSON output is an object containing the `accounts` field\.[\s\S]*`--json --results-only` to emit only `accounts`/) +assert.match(mediaDownloadDoc, /`--out="\.", --output`/) +const rootCompletion = ok('__complete', '--cword', '1', '--', 'beeper', '') +assert.equal(rootCompletion.split('\n').slice(0, 3).join('\n'), '--access-token\n--account\n-a') +assertOrderedItems(rootCompletion, ['message', 'ls', 'search', 'open', 'download', 'upload']) +assertOrderedItems(rootCompletion, ['status', 'me', 'whoami', 'setup']) +assert.match(rootCompletion, /^me$/m) +const sendCompletion = ok('__complete', '--cword', '2', '--', 'beeper', 'send', '') +assert.equal(sendCompletion.split('\n').slice(0, 4).join('\n'), '--to\n--pick\n--reply-to\n--reply-to-sender') +assertOrderedItems(sendCompletion, ['text', 'file', 'voice', 'sticker', 'react', 'presence']) +const targetsCompletion = ok('__complete', '--cword', '2', '--', 'beeper', 'targets', '') +assertOrderedItems(targetsCompletion, ['list', 'use', 'add', 'remove', 'logs', 'runtime', 'tunnel']) +const accountsCompletion = ok('__complete', '--cword', '2', '--', 'beeper', 'accounts', '') +assertOrderedItems(accountsCompletion, ['list', 'show', 'add', 'use', 'remove']) +const contactsCompletion = ok('__complete', '--cword', '2', '--', 'beeper', 'contacts', '') +assertOrderedItems(contactsCompletion, ['list', 'show']) +const chatsCompletion = ok('__complete', '--cword', '2', '--', 'beeper', 'chats', '') +assertOrderedItems(chatsCompletion, ['list', 'show', 'start', 'archive', 'unarchive', 'pin', 'unpin', 'mute', 'unmute', 'read', 'mark-read', 'mark-unread']) +const configCompletion = ok('__complete', '--cword', '2', '--', 'beeper', 'config', '') +assertOrderedItems(configCompletion, ['get', 'keys', 'set', 'unset', 'list', 'path']) assert.match(ok('__complete', '--cword', '2', '--', 'beeper', 'targets', 'l'), /list/) +assert.match(ok('__complete', '--cword', '2', '--', 'beeper', 'targets', ''), /^rm$/m) +assert.match(ok('__complete', '--cword', '2', '--', 'beeper', 'targets', ''), /^use$/m) +assertOrderedItems(ok('__complete', '--cword', '2', '--', 'beeper', 'presence', ''), ['typing', 'paused']) +assert.doesNotMatch(ok('__complete', '--cword', '2', '--', 'beeper', 'targets', ''), /^target$/m) +assert.match(ok('__complete', '--cword', '2', '--', 'beeper', 'remove', ''), /^target$/m) +assert.match(ok('__complete', '--cword', '1', '--', 'beeper', 'l'), /ls/) +assert.match(ok('__complete', '--cword', '1', '--', 'beeper', 'se'), /search/) +assert.match(ok('__complete', '--cword', '1', '--', 'beeper', '--no-r'), /--no-read-only/) +assert.doesNotMatch(ok('__complete', '--cword', '1', '--', 'beeper', '--no-h'), /--no-help/) +assert.doesNotMatch(ok('__complete', '--cword', '1', '--', 'beeper', '--no-n'), /--no-no-input/) +assert.match(ok('__complete', '--cword', '2', '--', 'beeper', '--color', ''), /always/) +assert.doesNotMatch(ok('__complete', '--cword', '2', '--', 'beeper', '--color', 'ne'), /always/) +assert.match(ok('__complete', '--cword', '4', '--', 'beeper', 'send', 'presence', '--state', ''), /paused/) +assert.match(ok('__complete', '--cword', '4', '--', 'beeper', 'send', 'presence', '--state', 'ty'), /typing/) assert.match(ok('__complete', '--cword', '3', '--', 'beeper', 'send', 'text', '--m'), /--message-file/) -assert.match(ok('completion', 'bash'), /__complete/) +assert.doesNotMatch(ok('--read-only', '__complete', '--cword', '2', '--', 'beeper', 'send', ''), /text/) +assert.doesNotMatch(runEnv({ BEEPER_ENABLE_COMMANDS: 'messages' }, '__complete', '--cword', '2', '--', 'beeper', 'targets', '').stdout, /list/) +const bashCompletion = ok('completion', 'bash') +assert.match(bashCompletion, /^#!\/usr\/bin\/env bash/) +assert.match(bashCompletion, /local IFS=\$'\\n'/) +assert.match(bashCompletion, /COMPREPLY=\(\)/) +assert.match(bashCompletion, /if \[\[ -n "\$completions" \]\]; then/) +assert.match(bashCompletion, /__complete/) +assert.match(ok('completion', '--help'), /Commands:\n bash \[flags\]\n Generate the autocompletion script for bash/) +assert.match(ok('completion', '--help'), /powershell \(pwsh\) \[flags\]\n Generate the autocompletion script for powershell/) +assert.match(ok('completion', 'zsh', '--help'), /--no-descriptions/) +assert.match(ok('completion', 'bash', '--no-descriptions'), /__complete/) +const zshCompletion = ok('completion', 'zsh') +assert.match(zshCompletion, /^#compdef beeper/) +assert.match(zshCompletion, /_describe 'values' completions/) +assert.match(zshCompletion, /compdef _beeper beeper/) +const fishCompletion = ok('completion', 'fish') +assert.match(fishCompletion, /function __beeper_complete/) +assert.match(fishCompletion, /set -l cur \(commandline -ct\)/) +assert.match(fishCompletion, /set words \$words \$cur/) +assert.match(fishCompletion, /set -l cword \(math \(count \$words\) - 1\)/) +assert.match(fishCompletion, /complete -c beeper -f -a "\(__beeper_complete\)"/) +const pwshCompletion = ok('completion', 'pwsh') +assert.match(pwshCompletion, /Register-ArgumentCompleter -CommandName beeper/) +assert.match(pwshCompletion, /\$commandAst\.CommandElements/) +assert.match(pwshCompletion, /\$completions = beeper __complete --cword \$cword -- \$elements/) +assert.match(pwshCompletion, /foreach \(\$completion in \$completions\)/) +assert.match(ok('agent', '--help'), /Agent-friendly helpers/) +assert.match(ok('agent', '--help'), /Commands:\n exit-codes \(exitcodes,exit-code\) \[flags\]\n Print stable exit codes/) +assert.match(ok('--help'), /completion \[flags\]\n Generate shell completion scripts/) +assert.match(ok('help'), /Usage: beeper /) +assert.match(ok('help', 'send', 'text'), /Usage: beeper send text/) +assert.match(ok('help', '--help'), /Arguments:\n \[ \.\.\.\]/) +assert.match(ok('schema', '--help'), /Usage: beeper schema \(help-json,helpjson\) \[ \.\.\.\] \[flags\]/) +assert.match(ok('schema', '--help'), /Optional command path to describe\. Default: entire CLI/) +assert.match(ok('message', '--help'), /Usage: beeper send text \(message,msg\) \[\] \[ \.\.\.\] \[flags\]/) +assert.match(ok('msg', '--help'), /Arguments:\n \[\]\s+Chat selector/) +assert.match(ok('msg', '--help'), /\[ \.\.\.\]\s+Message text/) +assert.match(ok('help', 'targets ls'), /Usage: beeper targets list/) +assert.match(ok('help', 'targets'), /Commands:\n(?:.*\n)* list \(ls\) \[flags\]\n List configured Beeper targets/) +assert.match(ok('targets', '--help'), /Usage: beeper targets \[flags\]/) +assert.match(ok('targets', '--help'), /Manage Beeper Desktop and Server targets/) +assert.match(ok('targets', '--help'), /Commands:\n(?:.*\n)* list \(ls\) \[flags\]\n List configured Beeper targets/) +assert.match(ok('targets', '--help'), /remove \(rm,del\) \[flags\]\n Remove a target/) +assert.match(ok('targets', '--help'), /use \[flags\]\n Select the default target/) +assert.match(ok('targets', 'add', '--help'), /\s+Target name\n \s+Target base URL/) +assert.match(ok('api', 'request', '--help'), /\s+HTTP method: GET, POST, PUT, PATCH, or DELETE\n \s+Desktop API path, for example \/v1\/info/) +assert.match(ok('--help'), /chats \(chat\) \[flags\]/) +assert.match(ok('--help'), /groups \(group\) \[flags\]/) +assert.match(ok('--help'), /accounts \(account\) \[flags\]/) +assert.match(ok('--help'), /contacts \(contact\) \[flags\]/) +assert.match(ok('--help'), /targets \(target\) \[flags\]/) +assert.match(ok('chat', '--help'), /Usage: beeper chat \[flags\]/) +assert.match(ok('account', '--help'), /Commands:\n list \(ls\) \[flags\]\n List connected accounts/) +assert.match(ok('accounts', '--help'), /Commands:\n list \(ls\) \[flags\]\n List connected accounts\n\n show \(get,info\) \[flags\]\n Show one connected account/) +assert.match(ok('accounts', '--help'), /add \(create,new\) \[\] \[flags\]\n Connect a chat account by bridge/) +assert.match(ok('contacts', '--help'), /Commands:\n list \(ls,search,find\) \[\] \[flags\]\n List contacts\n\n show \(get,info\) \[\] \[flags\]\n Show one contact/) +assert.match(ok('contacts', 'search', '--help'), /Usage: beeper contacts list .* \[\] \[flags\]/) +assert.match(ok('contacts', 'show', '--help'), /--jid=STRING\s+Contact JID or user ID/) +assert.match(ok('messages', 'search', '--help'), /--sender=STRING \(--from\)/) +assert.match(ok('messages', 'search', '--help'), /--has-media\s+Only messages with media/) +assert.match(ok('messages', 'search', '--help'), /\[\]\s+Search query\. Optional when a filter/) +const chatsHelp = ok('chats', '--help') +assert.match(chatsHelp, /unarchive \[\] \[flags\]\n Unarchive a chat/) +assert.match(chatsHelp, /mark-unread \[\] \[flags\]\n Mark a chat as unread/) +assert.match(ok('chats', 'list', '--help'), /--limit=50\s+Maximum chats to print/) +assert.match(ok('groups', '--help'), /Commands:\n list \(ls\) \[flags\]\n List group chats\n\n show \(info\) \[\] \[flags\]\n Show group details\n\n create \(add,new\) \[flags\]\n Create a group chat/) +assert.match(ok('groups', 'create', '--help'), /--user=STRING\s+Initial participant user ID/) +assert.match(ok('chats', 'show', '--help'), /--chat=STRING \(--jid\)\s+Chat selector/) +assert.match(ok('chats', 'disappear', '--help'), /--seconds=STRING \(--duration, --ephemeral-duration\)/) +assert.match(ok('config', '--help'), /Commands:\n get \(show\) \[flags\]\n Get a config value\n\n keys \(list-keys,names\) \[flags\]\n List available config keys\n\n set \(add,update\) \[flags\]\n Set a config value/) +assert.doesNotMatch(ok('--read-only', 'config', '--help'), /config set/) +assert.doesNotMatch(ok('--read-only', 'help'), /send text/) assert.match(ok('setup', '--help'), /--remote/) +assert.match(ok('setup', '--help'), /-h, --help\s+Show context-sensitive help/) +assert.doesNotMatch(ok('setup', '--help'), /Global flags:/) +assert.match(ok('schema', '--help'), /--include-hidden/) +assert.doesNotMatch(ok('schema', '--help'), /Global flags:/) +assert.match(ok('mcp', '--help'), /--allow-tool=ALLOW-TOOL,\.\.\. \(\--tool\)\s+Tool or service allowlist/) +assert.match(ok('mcp', '--help'), /--transport="stdio"/) +assert.match(ok('mcp', '--help'), /--http-port=7331/) +assert.match(ok('docs', '--help'), /--url \(--url-only\)\s+Print only the documentation URL/) assert.match(ok('targets', 'tunnel', '--help'), /--url-only/) assert.match(ok('accounts', 'add', '--help'), /--webview-backend/) +assert.match(ok('login', '--help'), /Usage: beeper login \(auth add,auth login\) \[flags\]/) +assert.match(ok('auth', '--help'), /add \(login\) \[flags\]\n Start email sign-in for a target/) +assert.match(ok('auth', '--help'), /list \(ls\) \[flags\]\n List stored target credentials/) +assert.match(ok('auth', '--help'), /services \(bridges\) \[flags\]\n List supported account login services and bridges/) +assert.match(ok('auth', '--help'), /manage \(setup,connect\) \[flags\]\n Make the selected target ready for messaging/) +assert.match(ok('auth', '--help'), /doctor \[flags\]\n Run diagnostics for config, target reachability, auth, and readiness/) +assert.match(ok('auth', '--help'), /logout \(remove,rm,del\) \[\] \[flags\]\n Clear stored authentication/) +assert.match(ok('auth', '--help'), /status \[\] \[flags\]\n Show auth configuration and stored target credential status/) assert.match(ok('watch', '--help'), /--include-type/) +assert.match(ok('watch', '--help'), /--webhook-allow-private\s+Accepted for compatibility/) +assert.match(ok('watch', '--help'), /--webhook-secret=STRING\s+HMAC-SHA256 secret for X-Beeper-Signature and X-Wacli-Signature/) assert.match(ok('send', 'presence', '--help'), /--state/) +assert.match(ok('presence', '--help'), /Usage: beeper presence \[flags\]/) +assert.match(ok('presence', '--help'), /Commands:\n typing \[flags\]\n Send a 'composing' \(typing\) indicator to a chat\n\n paused \[flags\]\n Send a 'paused' indicator \(stop typing\) to a chat/) +assert.match(ok('presence', 'typing', '--help'), /Usage: beeper presence typing \[flags\]/) +assert.match(ok('presence', 'paused', '--help'), /Usage: beeper presence paused \[flags\]/) +assert.match(ok('send', 'text', '--help'), /--post-send-wait=STRING\s+Compatibility alias for waiting after send/) +assert.match(ok('send', 'text', '--help'), /--reply-to-sender=STRING\s+Accepted for compatibility/) +assert.match(ok('send', 'text', '--help'), /--ephemeral\s+Send with this chat's disappearing-message timer/) +assert.match(ok('send', 'text', '--help'), /--ephemeral-duration=STRING\s+Set the chat disappearing-message timer/) assert.match(ok('media', 'download', '--help'), /--out/) +assert.match(ok('media', 'download', '--help'), /--out="\."/) +assert.match(ok('messages', 'search', '--help'), /--limit=50 \(\-\-max\)/) assert.match(ok('export', '--help'), /--no-attachments/) const version = JSON.parse(ok('version', '--json')) assert.equal(version.name, 'beeper-cli') assert.match(version.version, /^\d+\.\d+\.\d+/) +assert.equal(version.commit, '') +assert.equal(version.date, '') + +let exitCodes = JSON.parse(ok('exit-codes', '--json')).exit_codes +assert.equal(exitCodes.auth_required, 4) +assert.equal(exitCodes.not_ready, 4) +const exitCodesText = ok('exit-codes') +assert.match(exitCodesText, /^ok: 0$/m) +assert.match(exitCodesText, /^auth_required: 4$/m) +assert.doesNotMatch(exitCodesText, /^EXIT CODES\s+\{/m) +const exitCodesPlain = ok('exit-codes', '--plain') +assert.match(exitCodesPlain, /^ok\t0$/m) +assert.match(exitCodesPlain, /^auth_required\t4$/m) +assert.doesNotMatch(exitCodesPlain, /^EXIT CODES\t/m) + +exitCodes = JSON.parse(ok('agent', 'exit-code', '--json', '--select=exit_codes.ok')).exit_codes +assert.equal(exitCodes.auth_required, 4) +assert.equal(exitCodes.not_ready, 4) + +const agentPayload = JSON.parse(ok('agent', '--json')) +assert.equal(agentPayload.helpers[0].command, 'agent exit-codes') + +const statusPlain = ok('status', '--plain') +assert.match(statusPlain, /^target\tdesktop$/m) +assert.match(statusPlain, /^auth_source\tnone$/m) +assert.doesNotMatch(statusPlain, /^AUTH SOURCE\t/m) + +let docsPayload = JSON.parse(ok('docs', '--json')) +assert.equal(docsPayload.success, true) +assert.equal(docsPayload.error, null) +assert.equal(docsPayload.data.url, 'https://github.com/beeper/desktop-api-cli/tree/main/packages/cli') +assert.equal(docsPayload.data.relative_commands, 'docs/commands/README.md') +assert.match(docsPayload.data.commands, /docs\/commands\/README\.md$/) +assert.equal(ok('docs'), 'https://github.com/beeper/desktop-api-cli/tree/main/packages/cli\n') +assert.equal(ok('docs', '--plain'), 'https://github.com/beeper/desktop-api-cli/tree/main/packages/cli\n') +assert.equal(ok('docs', '--url'), 'https://github.com/beeper/desktop-api-cli/tree/main/packages/cli\n') +docsPayload = JSON.parse(ok('docs', '--url', '--json')) +assert.equal(docsPayload.success, true) +assert.equal(docsPayload.data, 'https://github.com/beeper/desktop-api-cli/tree/main/packages/cli') + +let envResult = runEnv({ BEEPER_JSON: '1' }, 'version') +assert.equal(envResult.status, 0, envResult.stderr) +assert.equal(JSON.parse(envResult.stdout).name, 'beeper-cli') + +envResult = runEnv({ BEEPER_JSON: '1' }, '--no-json', 'version') +assert.equal(envResult.status, 0, envResult.stderr) +assert.equal(envResult.stdout, '0.6.2\n') + +envResult = runEnv({ BEEPER_AUTO_JSON: '1' }, 'version') +assert.equal(envResult.status, 0, envResult.stderr) +assert.equal(JSON.parse(envResult.stdout).name, 'beeper-cli') + +envResult = runEnv({ BEEPER_AUTO_JSON: '1' }, '--plain', 'version') +assert.equal(envResult.status, 0, envResult.stderr) +assert.equal(envResult.stdout, `${version.version}\n`) + +envResult = runEnv({ BEEPER_AUTO_JSON: '1' }, '--no-json', 'version') +assert.equal(envResult.status, 0, envResult.stderr) +assert.equal(envResult.stdout, '0.6.2\n') + +let payload: Record +envResult = runEnv({ BEEPER_FIELDS: 'name' }, 'version', '--json') +assert.equal(envResult.status, 0, envResult.stderr) +payload = JSON.parse(envResult.stdout) +assert.deepEqual(Object.keys(payload), ['name']) +assert.equal(payload.name, 'beeper-cli') + +envResult = runEnv({ BEEPER_FIELDS: 'name' }, 'version', '--json', '--select=version') +assert.equal(envResult.status, 0, envResult.stderr) +payload = JSON.parse(envResult.stdout) +assert.deepEqual(Object.keys(payload), ['version']) + +envResult = runEnv({ BEEPER_PROJECT: 'name' }, 'version', '--json') +assert.equal(envResult.status, 0, envResult.stderr) +payload = JSON.parse(envResult.stdout) +assert.deepEqual(Object.keys(payload), ['name']) let result = run('version', '--json', '--plain') assert.equal(result.status, 2) @@ -108,12 +403,95 @@ let errorPayload = JSON.parse(result.stderr) assert.equal(errorPayload.error.code, 'usage_error') assert.match(errorPayload.error.message, /cannot combine --json and --plain/) +result = run('schema', '--bogus') +assert.equal(result.status, 2) +assert.match(result.stderr, /unknown flag --bogus/) +assert.match(result.stderr, /Run with --help to see available flags/) +assert.doesNotMatch(result.stderr, /hint: Run with --help to see available flags/) + +result = run('schema', '--bogus', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.equal(errorPayload.error.code, 'usage_error') +assert.equal(errorPayload.error.hint, 'Run with --help to see available flags') + +result = run('targets', 'add', 'onlyname') +assert.equal(result.status, 2) +assert.match(result.stderr, /expected ""/) + +result = run('media', 'download', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.equal(errorPayload.error.code, 'usage_error') +assert.equal(errorPayload.error.message, 'media download requires or --id with --chat') + +payload = JSON.parse(ok('media', 'message', 'm1', '--chat', 'chat', '--index', '2', '--poster', '--out', '/tmp', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'media.download') +assert.equal(payload.request.chat, 'chat') +assert.equal(payload.request.messageID, 'm1') +assert.equal(payload.request.index, 2) +assert.equal(payload.request.poster, true) + +result = run('media', 'message', 'm1', '--chat', 'chat', '--id', 'm2', '--dry-run', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /--id and positional cannot be combined/) + +result = run('targets', 'list', 'extra') +assert.equal(result.status, 2) +assert.match(result.stderr, /unexpected argument extra/) + +result = run('version', 'extra', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.equal(errorPayload.error.code, 'usage_error') +assert.equal(errorPayload.error.message, 'unexpected argument extra') + +result = run('setup', '--server-env', 'nope', '--dry-run') +assert.equal(result.status, 2) +assert.match(result.stderr, /invalid argument "nope" for "--server-env" flag: expected one of: local, dev, staging, prod/) + +result = run('--color', 'nope', 'version', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.equal(errorPayload.error.code, 'usage_error') +assert.equal(errorPayload.error.message, 'invalid argument "nope" for "--color" flag: expected one of: auto, always, never') + +payload = JSON.parse(ok('message', 'chat-selector', 'hello', 'there', '--dry-run', '--json')) +assert.equal(payload.op, 'send.text') +assert.equal(payload.request.chat, 'chat-selector') +assert.equal(payload.request.text, 'hello there') + +payload = JSON.parse(ok('msg', '--to', 'chat-selector', '--message', 'hello', '--dry-run', '--json')) +assert.equal(payload.op, 'send.text') +assert.equal(payload.request.chat, 'chat-selector') +assert.equal(payload.request.text, 'hello') + +result = run('message', '--to', 'chat-selector', 'other-chat', 'hello', '--dry-run', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.equal(errorPayload.error.code, 'usage_error') +assert.match(errorPayload.error.message, /--to and positional cannot be combined/) + result = run('messages', 'list', '--limit', '12abc', '--json') assert.equal(result.status, 2) errorPayload = JSON.parse(result.stderr) assert.equal(errorPayload.error.code, 'usage_error') assert.match(errorPayload.error.message, /--limit must be an integer/) +result = run('messages', 'list', '--limit', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.equal(errorPayload.error.code, 'usage_error') +assert.equal(errorPayload.error.message, '--limit: expected integer value but got "EOL" ()') + +result = run('setup', '--email', '--local', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.equal(errorPayload.error.code, 'usage_error') +assert.equal(errorPayload.error.message, '--email: expected string value but got "--local"') + result = run('messages', 'list', '--limit=', '--json') assert.equal(result.status, 2) errorPayload = JSON.parse(result.stderr) @@ -126,37 +504,133 @@ errorPayload = JSON.parse(result.stderr) assert.equal(errorPayload.error.code, 'usage_error') assert.match(errorPayload.error.message, /--limit must be an integer/) +result = run('send', 'text', '--to', 'chat', '--message', 'hello', '--post-send-wait', 'nope', '--dry-run', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.equal(errorPayload.error.code, 'usage_error') +assert.match(errorPayload.error.message, /Invalid duration "nope"/) + +result = run('contacts', 'search', 'alice', '--query', 'bob', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.equal(errorPayload.error.code, 'usage_error') +assert.match(errorPayload.error.message, /--query and positional cannot be combined/) + +result = run('contacts', 'show', '@user:example.org', '--jid', '@other:example.org', '--dry-run', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.equal(errorPayload.error.code, 'usage_error') +assert.match(errorPayload.error.message, /--jid and positional cannot be combined/) + result = run('version', '--timeout', 'bogus', '--json') assert.equal(result.status, 2) errorPayload = JSON.parse(result.stderr) assert.equal(errorPayload.error.code, 'usage_error') assert.match(errorPayload.error.message, /--timeout must be a duration/) -let payload = JSON.parse(ok('targets', 'list', '--json')) -assert.equal(payload[0].id, 'desktop') +result = runEnv({ BEEPER_TIMEOUT: 'bogus' }, 'version', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.equal(errorPayload.error.code, 'usage_error') +assert.match(errorPayload.error.message, /--timeout must be a duration/) + +payload = JSON.parse(ok('version', '--timeout', '5m0s', '--json')) +assert.equal(payload.name, 'beeper-cli') + +payload = JSON.parse(ok('version', '--timeout', '1.5s', '--json')) +assert.equal(payload.name, 'beeper-cli') + +payload = JSON.parse(ok('--lock-wait', '5s', 'version', '--json')) +assert.equal(payload.name, 'beeper-cli') + +payload = JSON.parse(runEnv({ BEEPER_TIMEOUT: '1m30s' }, 'version', '--json').stdout) +assert.equal(payload.name, 'beeper-cli') + +payload = JSON.parse(ok('targets', 'list', '--json')) +assert.ok((payload.targets as Array<{ id: string }>).some(target => target.id === 'desktop')) +payload = JSON.parse(ok('targets', 'list', '--json', '--results-only')) +assert.ok((payload as Array<{ id: string }>).some(target => target.id === 'desktop')) assert.equal(existsSync(join(configDir, 'config.json')), false) assert.equal(existsSync(join(configDir, 'targets')), false) payload = JSON.parse(ok('--home', '/tmp/beeper-cli-smoke-home', 'targets', 'list', '--json')) -assert.equal(payload[0].id, 'desktop') +assert.ok((payload.targets as Array<{ id: string }>).some(target => target.id === 'desktop')) assert.equal(existsSync('/tmp/beeper-cli-smoke-home'), false) payload = JSON.parse(ok('config', 'path', '--json')) assert.equal(payload.path, join(configDir, 'config.json')) +assert.equal(JSON.parse(ok('config', 'path', '--json', '--results-only')), join(configDir, 'config.json')) +assert.equal(JSON.parse(ok('config', 'where', '--json', '--results-only')), join(configDir, 'config.json')) +assert.equal(ok('config', 'path', '--plain'), `${join(configDir, 'config.json')}\n`) +assert.equal(ok('config', 'where', '--plain'), `${join(configDir, 'config.json')}\n`) + +payload = JSON.parse(runEnv({ BEEPER_HOME: homeConfigDir }, 'config', 'path', '--json').stdout) +assert.equal(payload.path, join(homeConfigDir, 'config.json')) + +payload = JSON.parse(runEnv({ BEEPER_HOME: '', BEEPER_STORE_DIR: homeConfigDir }, 'config', 'path', '--json').stdout) +assert.equal(payload.path, join(homeConfigDir, 'config.json')) + +payload = JSON.parse(runEnv({ BEEPER_HOME: homeConfigDir }, '--home', configDir, 'config', 'path', '--json').stdout) +assert.equal(payload.path, join(configDir, 'config.json')) + +payload = JSON.parse(ok('--store', homeConfigDir, 'config', 'path', '--json')) +assert.equal(payload.path, join(homeConfigDir, 'config.json')) payload = JSON.parse(ok('config', 'keys', '--json')) +assert.deepEqual(payload.keys, ['defaultTarget', 'defaultAccount']) + +payload = JSON.parse(ok('config', 'keys', '--json', '--results-only')) assert.deepEqual(payload, ['defaultTarget', 'defaultAccount']) +assert.equal(ok('config', 'keys', '--plain'), 'defaultTarget\ndefaultAccount\n') + payload = JSON.parse(ok('config', 'set', 'default-target', 'desktop', '--dry-run', '--json')) assert.equal(payload.dry_run, true) assert.equal(payload.op, 'config.set') assert.equal(payload.request.key, 'defaultTarget') +payload = JSON.parse(ok('login', 'person@example.com', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'auth.email.start') +assert.equal(payload.request.email, 'person@example.com') + +payload = JSON.parse(ok('logout', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'auth.logout') + +payload = JSON.parse(ok('logout', 'desktop', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'auth.logout') +assert.equal(payload.request.target, 'desktop') + +payload = JSON.parse(ok('auth', 'list', '--json')) +assert.equal(payload.accounts[0].target, 'desktop') +assert.equal(payload.accounts[0].authenticated, false) +assert.equal(payload.accounts[0].source, 'none') + +payload = JSON.parse(ok('auth', 'list', '--json', '--results-only')) +assert.equal(payload[0].target, 'desktop') +assert.equal(payload[0].authenticated, false) +assert.match(ok('auth', 'ls'), /TARGET\s+DEFAULT\s+AUTHENTICATED\s+SOURCE/) + +payload = JSON.parse(ok('auth', 'services', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'auth.services') + +payload = JSON.parse(ok('auth', 'bridges', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'auth.services') + result = run('--read-only', 'config', 'set', 'default-target', 'desktop', '--json') assert.equal(result.status, 2) errorPayload = JSON.parse(result.stderr) assert.match(errorPayload.error.message, /read-only mode/) +result = run('--readonly', 'config', 'set', 'default-target', 'desktop', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /read-only mode/) + result = runEnv({ BEEPER_READONLY: '1' }, 'config', 'set', 'default-target', 'desktop', '--json') assert.equal(result.status, 2) errorPayload = JSON.parse(result.stderr) @@ -166,21 +640,58 @@ payload = JSON.parse(runEnv({ BEEPER_READONLY: '1' }, '--no-read-only', 'config' assert.equal(payload.dry_run, true) assert.equal(payload.op, 'config.set') +result = runEnv({ BEEPER_ENABLE_COMMANDS: 'messages' }, 'targets', 'list', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /not enabled/) + +result = runEnv({ BEEPER_DISABLE_COMMANDS: 'targets.list' }, 'targets', 'list', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /disabled/) + payload = JSON.parse(ok('use', 'target', 'desktop', '--json')) assert.equal(payload.defaultTarget, 'desktop') payload = JSON.parse(ok('config', 'get', 'defaultTarget', '--json')) assert.equal(payload.key, 'defaultTarget') assert.equal(payload.value, 'desktop') +assert.equal(ok('config', 'get', 'defaultTarget', '--plain'), 'desktop\n') +assert.equal(ok('config', 'show', 'defaultTarget', '--plain'), 'desktop\n') payload = JSON.parse(ok('config', 'list', '--json')) assert.equal(payload.defaultTarget, 'desktop') assert.equal(payload.defaultAccount, null) +payload = JSON.parse(ok('auth', 'status', '--json')) +assert.equal(payload.auth.authenticated, false) +assert.equal(payload.auth.source, 'none') +assert.equal(payload.config.defaultTarget, 'desktop') +assert.equal(payload.config.exists, true) +assert.equal(payload.target.id, 'desktop') +assert.equal(payload.target.default, true) + +payload = JSON.parse(ok('auth', 'status', '--json', '--results-only')) +assert.equal(payload.auth.authenticated, false) +assert.equal(payload.config.defaultTarget, 'desktop') +assert.equal(payload.target.id, 'desktop') + +const authStatusPlain = ok('auth', 'status', '--plain') +assert.match(authStatusPlain, /^auth\.authenticated\tfalse$/m) +assert.match(authStatusPlain, /^auth\.source\tnone$/m) +assert.match(authStatusPlain, /^config\.defaultTarget\tdesktop$/m) +assert.match(authStatusPlain, /^target\.id\tdesktop$/m) + +payload = JSON.parse(ok('auth', 'add', 'user@example.com', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'auth.email.start') +assert.equal(payload.request.email, 'user@example.com') + payload = JSON.parse(ok('config', 'set', 'default-account', 'matrix', '--json')) assert.equal(payload.saved, true) assert.equal(payload.key, 'defaultAccount') assert.equal(payload.value, 'matrix') +assert.equal(ok('config', 'set', 'default-account', 'matrix', '--plain'), 'Set defaultAccount = matrix\n') payload = JSON.parse(ok('config', 'show', 'default_account', '--json')) assert.equal(payload.value, 'matrix') @@ -188,6 +699,9 @@ assert.equal(payload.value, 'matrix') payload = JSON.parse(ok('config', 'rm', 'default-account', '--json')) assert.equal(payload.removed, true) assert.equal(payload.value, null) +payload = JSON.parse(ok('config', 'set', 'default-account', 'matrix', '--json')) +assert.equal(payload.saved, true) +assert.equal(ok('config', 'rm', 'default-account', '--plain'), 'Unset defaultAccount\n') result = run('--safety-profile', 'readonly', 'use', 'target', 'desktop', '--json') assert.equal(result.status, 2) @@ -261,12 +775,51 @@ assert.equal(payload.defaultTarget, 'work') payload = JSON.parse(ok('status', '--json')) assert.equal(payload.auth.authenticated, false) +assert.equal(payload.config.defaultTarget, 'work') +assert.equal(payload.config.exists, true) +assert.equal(payload.target.id, 'work') + +payload = JSON.parse(ok('status', '--json', '--results-only')) +assert.equal(payload.auth.authenticated, false) +assert.equal(payload.config.defaultTarget, 'work') assert.equal(payload.target.id, 'work') +payload = JSON.parse(ok('whoami', '--account', 'matrix', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'me') +assert.equal(payload.request.accounts[0], 'matrix') +assert.equal(payload.request.target, 'work') + +payload = JSON.parse(ok('accounts', 'show', 'matrix', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'accounts.show') +assert.equal(payload.request.selector, 'matrix') + +payload = JSON.parse(ok('contacts', 'show', '@user:example.org', '--account', 'matrix', '--pick', '2', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'contacts.show') +assert.equal(payload.request.accounts[0], 'matrix') +assert.equal(payload.request.pick, 2) +assert.equal(payload.request.selector, '@user:example.org') + +payload = JSON.parse(ok('contacts', 'show', '--jid', '@user:example.org', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'contacts.show') +assert.equal(payload.request.jid, '@user:example.org') +assert.equal(payload.request.selector, '@user:example.org') + payload = JSON.parse(ok('auth', 'logout', '--dry-run', '--json')) assert.equal(payload.dry_run, true) assert.equal(payload.op, 'auth.logout') +payload = JSON.parse(ok('auth', 'remove', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'auth.logout') + +payload = JSON.parse(ok('auth', 'rm', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'auth.logout') + payload = JSON.parse(ok('auth', 'email', 'start', '--email', 'qa@example.invalid', '--dry-run', '--json')) assert.equal(payload.dry_run, true) assert.equal(payload.op, 'auth.email.start') @@ -275,6 +828,16 @@ payload = JSON.parse(ok('auth', 'email', 'response', '--setup-request-id', 'setu assert.equal(payload.dry_run, true) assert.equal(payload.op, 'auth.email.response') +payload = JSON.parse(ok('auth', 'manage', '--local', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'setup') +assert.equal(payload.request.authMode, 'local') + +payload = JSON.parse(ok('auth', 'setup', '--oauth', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'setup') +assert.equal(payload.request.authMode, 'oauth') + payload = JSON.parse(ok('install', 'desktop', '--server-env', 'staging', '--dry-run', '--json')) assert.equal(payload.dry_run, true) assert.equal(payload.op, 'install.desktop') @@ -302,10 +865,21 @@ assert.equal(payload.op, 'targets.runtime.restart') assert.equal(payload.request.target.id, 'work') result = run('targets', 'runtime', 'bogus', '--dry-run', '--json') -assert.equal(result.status, 2) +assert.equal(result.status, 127) errorPayload = JSON.parse(result.stderr) +assert.equal(errorPayload.error.code, 'command_not_found') assert.match(errorPayload.error.message, /unknown command "targets runtime bogus"/) +result = run('targets', 'runtime', 'strat', '--dry-run', '--json') +assert.equal(result.status, 127) +errorPayload = JSON.parse(result.stderr) +assert.equal(errorPayload.error.code, 'command_not_found') +assert.match(errorPayload.error.message, /did you mean "targets runtime start"/) + +result = run('opne') +assert.equal(result.status, 127) +assert.match(result.stderr, /unknown command "opne", did you mean "open"\?/) + payload = JSON.parse(ok('targets', 'tunnel', 'work', '--retries', '1', '--dry-run', '--json')) assert.equal(payload.dry_run, true) assert.equal(payload.op, 'targets.tunnel') @@ -322,6 +896,56 @@ assert.equal(payload.dry_run, true) assert.equal(payload.op, 'remove.target') assert.equal(payload.request.id, 'work') +result = run('targets', 'rm', 'work', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /destructive command "targets remove" requires --force or --dry-run/) + +payload = JSON.parse(ok('chats', 'unarchive', '--chat', 'chat', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'chats.unarchive') +assert.equal(payload.request.isArchived, false) + +payload = JSON.parse(ok('chats', 'archive', 'chat', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'chats.archive') +assert.equal(payload.request.chat, 'chat') +assert.equal(payload.request.isArchived, true) + +payload = JSON.parse(ok('chats', 'unmute', '--chat', 'chat', '--dry-run', '--json')) +assert.equal(payload.request.isMuted, false) + +payload = JSON.parse(ok('chats', 'unpin', '--chat', 'chat', '--dry-run', '--json')) +assert.equal(payload.request.isPinned, false) + +payload = JSON.parse(ok('chats', 'mark-read', '--chat', 'chat', '--message', 'm1', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'chats.read') +assert.equal(payload.request.read, true) +assert.equal(payload.request.messageID, 'm1') + +payload = JSON.parse(ok('chats', 'mark-read', 'chat', '--message', 'm1', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'chats.read') +assert.equal(payload.request.chat, 'chat') +assert.equal(payload.request.messageID, 'm1') + +payload = JSON.parse(ok('chats', 'mark-unread', '--chat', 'chat', '--dry-run', '--json')) +assert.equal(payload.request.read, false) + +payload = JSON.parse(ok('messages', 'export', '--chat', 'chat', '--from-me', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'messages.export') +assert.equal(payload.request.sender, 'me') + +payload = JSON.parse(ok('messages', 'export', '--chat', 'chat', '--from-them', '--dry-run', '--json')) +assert.equal(payload.request.sender, 'others') + +result = run('messages', 'export', '--chat', 'chat', '--sender', 'me', '--from-them', '--dry-run', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /Use only one of --sender, --from-me, or --from-them/) + payload = JSON.parse(ok('api', 'request', 'POST', '/v1/example', '--body', '{"ok":true}', '--dry-run', '--json')) assert.equal(payload.dry_run, true) assert.equal(payload.request.body.ok, true) @@ -335,24 +959,148 @@ assert.equal(payload.dry_run, true) assert.equal(payload.op, 'send.voice') assert.equal(payload.request.chat, 'chat') +payload = JSON.parse(ok('send', 'file', '--to', 'chat', '--file', './note.ogg', '--ptt', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'send.file') +assert.equal(payload.request.attachmentType, 'voice-note') +assert.equal(payload.request.mimeType, 'audio/ogg') +assert.equal(payload.request.text, '') + +result = run('send', 'file', '--to', 'chat', '--file', './note.ogg', '--ptt', '--caption', 'listen', '--dry-run', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /--caption cannot be combined with --ptt/) + payload = JSON.parse(ok('send', 'text', '--to', 'chat', '--message', 'hello', '--mention', 'user1', '--dry-run', '--json')) assert.equal(payload.dry_run, true) assert.equal(payload.op, 'send.text') assert.equal(payload.request.mentions[0], 'user1') +payload = JSON.parse(ok('send', '--to', 'chat', '--message', 'hello', '--mention', 'user1', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'send.text') +assert.equal(payload.request.chat, 'chat') +assert.equal(payload.request.text, 'hello') +assert.equal(payload.request.mentions[0], 'user1') + +payload = JSON.parse(ok('send', 'chat', 'hello', 'there', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'send.text') +assert.equal(payload.request.chat, 'chat') +assert.equal(payload.request.text, 'hello there') + +payload = JSON.parse(ok('send', 'text', '--to', 'chat', '--message', 'hello', '--reply-to', 'm1', '--reply-to-sender', 'user1', '--dry-run', '--json')) +assert.equal(payload.request.replyTo, 'm1') +assert.equal(payload.request.replyToSender, 'user1') + +payload = JSON.parse(ok('send', 'text', '--to', 'chat', '--message', 'hello', '--ephemeral', '--ephemeral-duration', '24h', '--dry-run', '--json')) +assert.equal(payload.request.ephemeral, true) +assert.equal(payload.request.ephemeralDuration, '24h') +assert.equal(payload.request.messageExpirySeconds, 86400) + +result = run('send', 'text', '--to', 'chat', '--message', 'hello', '--ephemeral-duration', 'off', '--dry-run', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /--ephemeral-duration must be a positive duration/) + payload = JSON.parse(ok('send', 'text', '--to', 'chat', '--message', 'hello\\nthere', '--message-escapes', '--dry-run', '--json')) assert.equal(payload.request.text, 'hello\nthere') +payload = JSON.parse(ok('send', 'text', '--to', 'chat', '--message', 'hello', '--post-send-wait', '2s', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'send.text') +assert.equal(payload.request.wait, true) +assert.equal(payload.request.waitTimeoutMs, 2000) + +payload = JSON.parse(ok('send', 'file', '--to', 'chat', '--file', './note.ogg', '--post-send-wait', '0', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'send.file') +assert.equal(payload.request.wait, false) +assert.equal(payload.request.waitTimeoutMs, 0) + +payload = JSON.parse(ok('send', 'file', './note.ogg', '--to', 'chat', '--caption', 'listen', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'send.file') +assert.equal(payload.request.file, './note.ogg') +assert.equal(payload.request.text, 'listen') + +payload = JSON.parse(ok('send', 'voice', './voice.ogg', '--to', 'chat', '--duration', '12', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'send.voice') +assert.equal(payload.request.file, './voice.ogg') +assert.equal(payload.request.duration, 12) +assert.equal(payload.request.attachmentType, 'voice-note') + +payload = JSON.parse(ok('send', 'sticker', './sticker.webp', '--to', 'chat', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'send.sticker') +assert.equal(payload.request.file, './sticker.webp') +assert.equal(payload.request.attachmentType, 'sticker') + +result = run('send', 'file', './note.ogg', '--to', 'chat', '--file', './other.ogg', '--dry-run', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /--file and positional cannot be combined/) + +payload = JSON.parse(ok('upload', './note.ogg', '--to', 'chat', '--caption', 'listen', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'send.file') +assert.equal(payload.request.chat, 'chat') +assert.equal(payload.request.file, './note.ogg') +assert.equal(payload.request.text, 'listen') + +payload = JSON.parse(ok('up', './note.ogg', '--to', 'chat', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'send.file') +assert.equal(payload.request.file, './note.ogg') + payload = JSON.parse(ok('-a', 'matrix', 'chats', 'start', '@u:example.org', '--dry-run', '--json')) assert.equal(payload.dry_run, true) assert.equal(payload.op, 'chats.start') assert.equal(payload.request.account, 'matrix') +payload = JSON.parse(runEnv({ BEEPER_ACCOUNT: 'matrix' }, 'chats', 'start', '@u:example.org', '--dry-run', '--json').stdout) +assert.equal(payload.request.account, 'matrix') + payload = JSON.parse(ok('send', 'react', '--to', 'chat', '--id', 'm1', '--reaction', '+1', '--dry-run', '--json')) assert.equal(payload.dry_run, true) assert.equal(payload.op, 'send.react') assert.equal(payload.request.reactionKey, '+1') +payload = JSON.parse(ok('send', 'react', '--to', 'chat', '--id', 'm1', '--post-send-wait', '2s', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'send.react') +assert.equal(payload.request.wait, true) +assert.equal(payload.request.waitTimeoutMs, 2000) + +payload = JSON.parse(ok('send', 'react', '--to', 'chat', '--id', 'm1', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'send.react') +assert.equal(payload.request.reactionKey, '+1') +assert.equal(payload.request.remove, false) + +payload = JSON.parse(ok('send', 'react', 'm1', '--to', 'chat', '--reaction', 'rocket', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'send.react') +assert.equal(payload.request.messageID, 'm1') +assert.equal(payload.request.reactionKey, 'rocket') + +result = run('send', 'react', 'm1', '--to', 'chat', '--id', 'm2', '--dry-run', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /--id and positional cannot be combined/) + +payload = JSON.parse(ok('send', 'reaction', '--to', 'chat', '--id', 'm1', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'send.react') +assert.equal(payload.request.reactionKey, '+1') + +payload = JSON.parse(ok('send', 'react', '--to', 'chat', '--id', 'm1', '--reaction=', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'send.react') +assert.equal(payload.request.reactionKey, '+1') +assert.equal(payload.request.remove, true) + payload = JSON.parse(ok('chats', 'disappear', '--chat', 'chat', '--seconds', 'off', '--dry-run', '--json')) assert.equal(payload.dry_run, true) assert.equal(payload.request.chat, 'chat') @@ -362,48 +1110,199 @@ payload = JSON.parse(ok('chats', 'disappear', '--chat', 'chat', '--seconds', '36 assert.equal(payload.dry_run, true) assert.equal(payload.request.messageExpirySeconds, 3600) +payload = JSON.parse(ok('chats', 'disappear', '--chat', 'chat', '--duration', '24h', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.request.messageExpirySeconds, 86400) + +payload = JSON.parse(ok('chats', 'disappear', '--chat', 'chat', '--ephemeral-duration', '7d', '--dry-run', '--json')) +assert.equal(payload.request.messageExpirySeconds, 604800) + result = run('chats', 'disappear', '--chat', 'chat', '--seconds', '1e2', '--dry-run', '--json') assert.equal(result.status, 2) errorPayload = JSON.parse(result.stderr) assert.equal(errorPayload.error.code, 'usage_error') -assert.match(errorPayload.error.message, /--seconds must be a positive integer or "off"/) +assert.match(errorPayload.error.message, /--seconds must be a positive integer, a duration like 24h\/7d\/90d, or "off"/) payload = JSON.parse(ok('chats', 'priority', '--chat', 'chat', '--level', 'low', '--dry-run', '--json')) assert.equal(payload.dry_run, true) assert.equal(payload.request.chat, 'chat') +payload = JSON.parse(ok('groups', 'rename', 'group-chat', '--name', 'Planning', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'chats.rename') +assert.equal(payload.request.chat, 'group-chat') +assert.equal(payload.request.title, 'Planning') + +payload = JSON.parse(ok('groups', 'description', 'group-chat', '--description', 'Roadmap', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'chats.description') +assert.equal(payload.request.chat, 'group-chat') +assert.equal(payload.request.description, 'Roadmap') + +result = run('groups', 'rename', 'group-chat', '--jid', 'other', '--name', 'Planning', '--dry-run', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /--jid and positional cannot be combined/) + payload = JSON.parse(ok('chats', 'focus', '--chat', 'chat', '--text', 'draft', '--file', './draft.txt', '--message', 'm1', '--dry-run', '--json')) assert.equal(payload.dry_run, true) assert.equal(payload.op, 'chats.focus') assert.equal(payload.request.draftText, 'draft') assert.equal(payload.request.draftAttachmentPath, './draft.txt') +payload = JSON.parse(ok('open', '--chat', 'chat', '--message', 'm1', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'chats.focus') +assert.equal(payload.request.messageID, 'm1') + +payload = JSON.parse(ok('open', 'chat', '--message', 'm1', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'chats.focus') +assert.equal(payload.request.chat, 'chat') +assert.equal(payload.request.messageID, 'm1') + +payload = JSON.parse(ok('browse', 'chat', '--message', 'm1', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'chats.focus') +assert.equal(payload.request.chat, 'chat') +assert.equal(payload.request.messageID, 'm1') + payload = JSON.parse(ok('chats', 'notify-anyway', '--chat', 'chat', '--dry-run', '--json')) assert.equal(payload.dry_run, true) assert.equal(payload.op, 'chats.notify-anyway') +payload = JSON.parse(ok('chats', 'notify-anyway', 'chat', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'chats.notify-anyway') +assert.equal(payload.request.chat, 'chat') + result = run('chats', 'notify-anyway', '--dry-run', '--json') assert.equal(result.status, 2) errorPayload = JSON.parse(result.stderr) -assert.match(errorPayload.error.message, /--chat is required/) +assert.match(errorPayload.error.message, /chats notify-anyway requires --chat or /) + +result = run('chats', 'notify-anyway', 'chat', '--chat', 'other', '--dry-run', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /--chat and positional cannot be combined/) + +assert.match(ok('search', 'all', '--help'), /Usage: beeper search all \(search-all,find-all\) \[flags\]/) +payload = JSON.parse(ok('search', 'all', 'dinner', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'search.all') +assert.equal(payload.request.query, 'dinner') + +payload = JSON.parse(ok('search-all', 'dinner', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'search.all') +assert.equal(payload.request.query, 'dinner') + +payload = JSON.parse(ok('groups', 'create', '--name', 'Team', '--user', '@a:example.org', '--user', '@b:example.org', '--message', 'hello', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'groups.create') +assert.equal(payload.request.title, 'Team') +assert.deepEqual(payload.request.participantIDs, ['@a:example.org', '@b:example.org']) +assert.equal(payload.request.messageText, 'hello') + +payload = JSON.parse(ok('groups', 'rename', '--jid', 'group-chat', '--name', 'New Team', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'chats.rename') +assert.equal(payload.request.chat, 'group-chat') +assert.equal(payload.request.title, 'New Team') + +payload = JSON.parse(ok('groups', 'description', '--jid', 'group-chat', '--topic', 'Planning', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'chats.description') +assert.equal(payload.request.description, 'Planning') payload = JSON.parse(ok('messages', 'context', '--chat', 'chat', '--id', 'm1', '--dry-run', '--json')) assert.equal(payload.dry_run, true) assert.equal(payload.request.chat, 'chat') assert.equal(payload.request.messageID, 'm1') +payload = JSON.parse(ok('messages', 'context', 'm1', '--chat', 'chat', '--before', '2', '--after', '3', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.request.before, 2) +assert.equal(payload.request.after, 3) +assert.equal(payload.request.messageID, 'm1') + +payload = JSON.parse(ok('messages', 'show', '--chat', 'chat', '--id', 'm1', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'messages.show') +assert.equal(payload.request.after, 0) +assert.equal(payload.request.before, 0) +assert.equal(payload.request.chat, 'chat') +assert.equal(payload.request.messageID, 'm1') + +payload = JSON.parse(ok('messages', 'show', 'm1', '--chat', 'chat', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'messages.show') +assert.equal(payload.request.messageID, 'm1') + +result = run('messages', 'show', 'm1', '--chat', 'chat', '--id', 'm2', '--dry-run', '--json') +assert.equal(result.status, 2) +assert.match(result.stderr, /--id and positional cannot be combined/) + +payload = JSON.parse(ok('messages', 'export', '--chat', 'chat', '--limit', '3', '--output', '/tmp/messages.json', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'messages.export') +assert.equal(payload.request.asc, false) +assert.equal(payload.request.chat, 'chat') +assert.equal(payload.request.limit, 3) +assert.equal(payload.request.output, '/tmp/messages.json') + +payload = JSON.parse(ok('messages', 'forward', '--chat', 'source', '--id', 'm1', '--to', 'dest', '--attachment-index', '2', '--post-send-wait', '0', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'messages.forward') +assert.equal(payload.request.attachmentIndex, 2) +assert.equal(payload.request.chat, 'source') +assert.equal(payload.request.messageID, 'm1') +assert.equal(payload.request.to, 'dest') +assert.equal(payload.request.wait, false) + +payload = JSON.parse(ok('messages', 'forward', 'm1', '--chat', 'source', '--to', 'dest', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'messages.forward') +assert.equal(payload.request.messageID, 'm1') + payload = JSON.parse(ok('messages', 'edit', '--chat', 'chat', '--id', 'm1', '--message', 'edited', '--dry-run', '--json')) assert.equal(payload.dry_run, true) assert.equal(payload.op, 'messages.edit') assert.equal(payload.request.chat, 'chat') assert.equal(payload.request.text, 'edited') +payload = JSON.parse(ok('messages', 'edit', 'm1', '--chat', 'chat', '--message', 'edited', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'messages.edit') +assert.equal(payload.request.messageID, 'm1') + payload = JSON.parse(ok('messages', 'delete', '--chat', 'chat', '--id', 'm1', '--for-everyone', '--dry-run', '--json')) assert.equal(payload.dry_run, true) assert.equal(payload.op, 'messages.delete') assert.equal(payload.request.forEveryone, true) assert.equal(payload.request.messageID, 'm1') +payload = JSON.parse(ok('messages', 'delete', 'm1', '--chat', 'chat', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'messages.delete') +assert.equal(payload.request.messageID, 'm1') + +result = run('messages', 'delete', 'm1', '--chat', 'chat', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.match(errorPayload.error.message, /destructive command "messages delete" requires --force or --dry-run/) + +payload = JSON.parse(ok('messages', 'revoke', '--chat', 'chat', '--id', 'm1', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'messages.revoke') +assert.equal(payload.request.forEveryone, true) +assert.equal(payload.request.messageID, 'm1') + +payload = JSON.parse(ok('messages', 'revoke', 'm1', '--chat', 'chat', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'messages.revoke') +assert.equal(payload.request.messageID, 'm1') + payload = JSON.parse(ok('export', '--chat', 'chat', '--out', '/tmp/beeper-export', '--limit-messages', '10', '--no-attachments', '--dry-run', '--json')) assert.equal(payload.dry_run, true) assert.equal(payload.op, 'export') @@ -414,11 +1313,53 @@ assert.equal(payload.dry_run, true) assert.equal(payload.op, 'send.presence') assert.equal(payload.request.durationSeconds, 1) +payload = JSON.parse(ok('presence', '--to', 'chat', '--state', 'paused', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'send.presence') +assert.equal(payload.request.chat, 'chat') +assert.equal(payload.request.state, 'paused') + +payload = JSON.parse(ok('presence', 'typing', '--to', 'chat', '--duration', '1', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'send.presence') +assert.equal(payload.request.chat, 'chat') +assert.equal(payload.request.durationSeconds, 1) +assert.equal(payload.request.state, 'typing') + +payload = JSON.parse(ok('presence', 'paused', '--to', 'chat', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'send.presence') +assert.equal(payload.request.chat, 'chat') +assert.equal(payload.request.state, 'paused') + +result = run('presence', '--dry-run', '--json') +assert.equal(result.status, 2) +errorPayload = JSON.parse(result.stderr) +assert.equal(errorPayload.error.code, 'usage_error') +assert.match(errorPayload.error.message, /--to is required/) + payload = JSON.parse(ok('media', 'download', 'mxc://server/file', '--out', '/tmp', '--dry-run', '--json')) assert.equal(payload.dry_run, true) assert.equal(payload.op, 'media.download') assert.equal(payload.request.out, '/tmp') +payload = JSON.parse(ok('media', 'dl', 'mxc://server/file', '--out', '/tmp', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'media.download') +assert.equal(payload.request.url, 'mxc://server/file') + +payload = JSON.parse(ok('media', 'message', '--chat', 'chat', '--id', 'm1', '--index', '2', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'media.download') +assert.equal(payload.request.chat, 'chat') +assert.equal(payload.request.index, 2) +assert.equal(payload.request.messageID, 'm1') + +payload = JSON.parse(ok('download', 'mxc://server/file', '--out', '/tmp', '--dry-run', '--json')) +assert.equal(payload.dry_run, true) +assert.equal(payload.op, 'media.download') +assert.equal(payload.request.url, 'mxc://server/file') + payload = JSON.parse(ok('export', '--out', '/tmp/beeper-export', '--limit-chats', '1', '--dry-run', '--json')) assert.equal(payload.dry_run, true) assert.equal(payload.op, 'export') @@ -430,36 +1371,519 @@ assert.equal(payload.kind, 'target') assert.equal(payload.selected.id, 'desktop') payload = JSON.parse(ok('targets', 'list', '--json')) -assert.equal(payload[0].id, 'work') +assert.ok((payload.targets as Array<{ id: string }>).some(target => target.id === 'work')) -const schema = JSON.parse(ok('schema', '--json')) +const schemaText = ok('schema', '--json') +assert.match(schemaText, /^\{\n "schema_version": 1,\n "build":/) +const schema = JSON.parse(schemaText) assert.equal(schema.schema_version, 1) assert.equal(schema.command.type, 'application') +assert.equal(schema.command.path, 'beeper') +assert.equal(schema.command.program_path, 'beeper') +assert.equal(schema.command.usage, 'beeper [flags]') +assert.deepEqual(schema.command.flags.slice(0, 5).map((flag: { name: string }) => flag.name), ['help', 'color', 'home', 'account', 'access-token']) +assert.ok(schema.command.flags.findIndex((flag: { name: string }) => flag.name === 'verbose') < schema.command.flags.findIndex((flag: { name: string }) => flag.name === 'version')) assert.ok(schema.command.flags.some((flag: { name: string; short?: string }) => flag.name === 'json' && flag.short === 'j')) assert.ok(schema.command.flags.some((flag: { name: string; short?: string }) => flag.name === 'account' && flag.short === 'a')) +assert.ok(schema.command.flags.some((flag: { name: string; short?: string }) => flag.name === 'help' && flag.short === 'h')) +assert.ok(schema.command.flags.some((flag: { name: string }) => flag.name === 'lock-wait')) +assert.ok(schema.command.flags.some((flag: { aliases?: string[]; name: string }) => flag.name === 'read-only' && flag.aliases?.includes('readonly'))) assert.ok(schema.command.flags.some((flag: { name: string }) => flag.name === 'full')) +assert.ok(schema.command.flags.some((flag: { has_default?: boolean; name: string; placeholder?: string }) => flag.name === 'json' && flag.has_default === true && flag.placeholder === 'false')) +assert.ok(schema.command.flags.some((flag: { has_default?: boolean; name: string; placeholder?: string }) => flag.name === 'access-token' && flag.has_default === undefined && flag.placeholder === 'STRING')) +assert.ok(schema.command.flags.some((flag: { has_default?: boolean; name: string; placeholder?: string }) => flag.name === 'help' && flag.has_default === true && flag.placeholder === 'false')) +assert.deepEqual(schema.command.subcommands.slice(0, 6).map((command: { name: string }) => command.name), ['message', 'ls', 'search', 'open', 'download', 'upload']) +assert.ok(schema.command.subcommands.some((command: { name: string; path: string; usage: string }) => command.name === 'message' && command.path === 'message' && command.usage === 'beeper message (msg) [] [ ...] [flags]')) +assert.ok(schema.command.subcommands.some((command: { help: string; name: string; path: string; usage: string }) => command.name === 'logout' && command.path === 'logout' && command.usage === 'beeper logout [] [flags]' && command.help.includes("alias for 'auth logout'"))) +assert.ok(schema.command.subcommands.some((command: { aliases?: string[][]; name: string; path: string; usage: string }) => command.name === 'download' && command.path === 'download' && command.usage === 'beeper download (dl) [] [flags]' && command.aliases?.[0]?.[0] === 'dl')) +assert.ok(schema.command.subcommands.some((command: { aliases?: string[][]; name: string; path: string; usage: string }) => command.name === 'upload' && command.path === 'upload' && command.usage === 'beeper upload (up,put) [flags]' && command.aliases?.some(alias => alias[0] === 'up') && command.aliases?.some(alias => alias[0] === 'put'))) +assert.ok(schema.command.subcommands.some((command: { alias_paths?: string[]; name: string }) => command.name === 'upload' && command.alias_paths?.includes('up') && command.alias_paths?.includes('put'))) +assert.ok(schema.command.subcommands.some((command: { aliases?: string[][]; name: string; path: string; usage: string }) => command.name === 'me' && command.path === 'me' && command.usage === 'beeper me (whoami,who-am-i) [flags]' && command.aliases?.some(alias => alias[0] === 'whoami'))) +assert.ok(schema.command.subcommands.some((command: { aliases?: string[][]; help: string; name: string; path: string; usage: string }) => command.name === 'whoami' && command.path === 'whoami' && command.usage === 'beeper whoami (who-am-i) [flags]' && command.help.includes("alias for 'me'") && command.aliases?.some(alias => alias[0] === 'who-am-i'))) +assert.ok(schema.command.subcommands.some((command: { name: string; subcommands?: Array<{ name: string }>; usage: string }) => command.name === 'agent' && command.usage === 'beeper agent [flags]' && command.subcommands?.some(subcommand => subcommand.name === 'exit-codes'))) assert.ok(schema.command.subcommands.some((command: { name: string }) => command.name === 'doctor')) assert.ok(!schema.command.subcommands.some((command: { name: string }) => command.name === '__complete')) +let hiddenSchema = JSON.parse(ok('schema', '--include-hidden', '--json')) +assert.ok(hiddenSchema.command.subcommands.some((command: { name: string }) => command.name === '__complete')) + +let commandSchema = JSON.parse(ok('schema', 'targets ls', '--json')) +assert.equal(commandSchema.command.path, 'targets list') +assert.equal(commandSchema.command.program_path, 'beeper targets list') +assert.equal(commandSchema.command.name, 'list') +assert.equal(commandSchema.command.mcp, true) +assert.equal(commandSchema.command.risk, 'read') +assert.equal(commandSchema.command.primary_result_key, 'targets') +assert.deepEqual(commandSchema.command.program_alias_paths, ['beeper targets ls', 'beeper target list', 'beeper target ls']) +assert.equal(commandSchema.command.usage, 'beeper targets list (targets ls,target list,target ls) [flags]') +assert.ok(commandSchema.command.flags.some((flag: { name: string; short?: string }) => flag.name === 'json' && flag.short === 'j')) + +commandSchema = JSON.parse(ok('schema', 'targets', 'add', '--json')) +assert.equal(commandSchema.command.path, 'targets add') +assert.equal(commandSchema.command.positionals[0].help, 'Target name') +assert.equal(commandSchema.command.positionals[1].help, 'Target base URL') + +commandSchema = JSON.parse(ok('schema', 'api', 'request', '--json')) +assert.equal(commandSchema.command.path, 'api request') +assert.match(commandSchema.command.positionals[0].help, /HTTP method/) +assert.match(commandSchema.command.positionals[1].help, /Desktop API path/) + +commandSchema = JSON.parse(ok('schema', 'ls', '--json')) +assert.equal(commandSchema.command.path, 'chats list') +assert.deepEqual(commandSchema.command.aliases, [['chats', 'ls'], ['chat', 'list'], ['chat', 'ls'], ['ls'], ['list']]) + +commandSchema = JSON.parse(ok('schema', 'upload', '--json')) +assert.equal(commandSchema.command.path, 'upload') +assert.equal(commandSchema.command.usage, 'beeper upload (up,put) [flags]') +assert.deepEqual(commandSchema.command.aliases, [['up'], ['put']]) +assert.ok(commandSchema.command.flags.some((flag: { name: string }) => flag.name === 'caption')) + +commandSchema = JSON.parse(ok('schema', 'open', '--json')) +assert.equal(commandSchema.command.path, 'chats focus') +assert.equal(commandSchema.command.usage, 'beeper chats focus (chat focus,open,browse,focus) [] [flags]') +assert.deepEqual(commandSchema.command.aliases, [['chat', 'focus'], ['open'], ['browse'], ['focus']]) +assert.equal(commandSchema.command.positionals[0].name, 'chat') +assert.ok(commandSchema.command.flags.some((flag: { aliases?: string[]; name: string; required?: boolean }) => flag.name === 'chat' && flag.required === false && flag.aliases?.includes('jid'))) + +commandSchema = JSON.parse(ok('schema', 'search', '--json')) +assert.equal(commandSchema.command.path, 'messages search') +assert.equal(commandSchema.command.usage, 'beeper messages search (messages find,search,find) [] [flags]') +assert.deepEqual(commandSchema.command.aliases, [['messages', 'find'], ['search'], ['find']]) +assert.match(commandSchema.command.positionals[0].help, /Optional when a filter/) +assert.ok(commandSchema.command.flags.some((flag: { has_default?: boolean; name: string; placeholder?: string }) => flag.name === 'limit' && flag.has_default === true && flag.placeholder === '50')) +assert.ok(commandSchema.command.flags.some((flag: { has_default?: boolean; name: string; placeholder?: string }) => flag.name === 'after' && flag.has_default === undefined && flag.placeholder === 'STRING')) +assert.ok(commandSchema.command.flags.some((flag: { aliases?: string[]; name: string }) => flag.name === 'sender' && flag.aliases?.includes('from'))) +assert.ok(commandSchema.command.flags.some((flag: { name: string }) => flag.name === 'has-media')) + +commandSchema = JSON.parse(ok('schema', 'search', 'all', '--json')) +assert.equal(commandSchema.command.path, 'search all') +assert.equal(commandSchema.command.usage, 'beeper search all (search-all,find-all) [flags]') +assert.deepEqual(commandSchema.command.aliases, [['search-all'], ['find-all']]) + +commandSchema = JSON.parse(ok('schema', 'search-all', '--json')) +assert.equal(commandSchema.command.path, 'search all') + +commandSchema = JSON.parse(ok('schema', 'whoami', '--json')) +assert.equal(commandSchema.command.path, 'me') +assert.deepEqual(commandSchema.command.aliases, [['whoami'], ['who-am-i']]) + +commandSchema = JSON.parse(ok('schema', 'msg', '--json')) +assert.equal(commandSchema.command.path, 'send text') +assert.equal(commandSchema.command.usage, 'beeper send text (message,msg) [] [ ...] [flags]') +assert.ok(commandSchema.command.flags.some((flag: { name: string }) => flag.name === 'ephemeral')) +assert.ok(commandSchema.command.flags.some((flag: { name: string }) => flag.name === 'ephemeral-duration')) + +commandSchema = JSON.parse(ok('schema', 'download', '--json')) +assert.equal(commandSchema.command.path, 'media download') +assert.deepEqual(commandSchema.command.aliases, [['media', 'dl'], ['download'], ['dl']]) + +commandSchema = JSON.parse(ok('schema', 'auth', 'status', '--json')) +assert.equal(commandSchema.command.path, 'auth status') +assert.equal(commandSchema.command.usage, 'beeper auth status [] [flags]') + +commandSchema = JSON.parse(ok('schema', 'st', '--json')) +assert.equal(commandSchema.command.path, 'status') +assert.deepEqual(commandSchema.command.aliases, [['st']]) + +commandSchema = JSON.parse(ok('schema', 'auth', 'add', '--json')) +assert.equal(commandSchema.command.path, 'login') +assert.deepEqual(commandSchema.command.aliases, [['auth', 'add'], ['auth', 'login']]) + +commandSchema = JSON.parse(ok('schema', 'auth', '--json')) +assert.deepEqual(commandSchema.command.subcommands.slice(0, 2).map((command: { name: string; usage: string }) => [command.name, command.usage]), [['add', 'beeper auth add (login) [flags]'], ['list', 'beeper auth list (auth ls) [flags]']]) + +commandSchema = JSON.parse(ok('schema', 'auth', 'ls', '--json')) +assert.equal(commandSchema.command.path, 'auth list') +assert.equal(commandSchema.command.output, 'auth') +assert.equal(commandSchema.command.primary_result_key, 'accounts') +assert.deepEqual(commandSchema.command.aliases, [['auth', 'ls']]) + +commandSchema = JSON.parse(ok('schema', 'auth', 'services', '--json')) +assert.equal(commandSchema.command.path, 'auth services') +assert.equal(commandSchema.command.primary_result_key, 'services') +assert.equal(commandSchema.command.usage, 'beeper auth services (auth bridges) [flags]') +assert.deepEqual(commandSchema.command.aliases, [['auth', 'bridges']]) +assert.deepEqual(commandSchema.command.alias_paths, ['auth bridges']) +assert.ok(commandSchema.command.flags.some((flag: { default?: boolean; name: string }) => flag.name === 'markdown' && flag.default === false)) +assert.ok(commandSchema.command.flags.some((flag: { help?: string; name: string }) => flag.name === 'markdown' && flag.help === 'Output a Markdown table')) + +commandSchema = JSON.parse(ok('schema', 'auth', 'bridges', '--json')) +assert.equal(commandSchema.command.path, 'auth services') + +commandSchema = JSON.parse(ok('schema', 'auth', 'manage', '--json')) +assert.equal(commandSchema.command.path, 'auth manage') +assert.equal(commandSchema.command.usage, 'beeper auth manage (auth setup,auth connect) [flags]') +assert.deepEqual(commandSchema.command.aliases, [['auth', 'setup'], ['auth', 'connect']]) +assert.ok(commandSchema.command.flags.some((flag: { name: string }) => flag.name === 'oauth')) + +commandSchema = JSON.parse(ok('schema', 'auth', 'remove', '--json')) +assert.equal(commandSchema.command.path, 'auth logout') +assert.equal(commandSchema.command.usage, 'beeper auth logout (logout,auth remove,auth rm,auth del) [] [flags]') +assert.deepEqual(commandSchema.command.aliases, [['logout'], ['auth', 'remove'], ['auth', 'rm'], ['auth', 'del']]) +assert.equal(commandSchema.command.positionals[0].name, 'target') + +commandSchema = JSON.parse(ok('schema', 'auth', 'del', '--json')) +assert.equal(commandSchema.command.path, 'auth logout') + +commandSchema = JSON.parse(ok('schema', 'doctor', '--json')) +assert.equal(commandSchema.command.path, 'doctor') +assert.deepEqual(commandSchema.command.aliases, [['auth', 'doctor']]) +assert.ok(commandSchema.command.flags.some((flag: { name: string }) => flag.name === 'connect')) + +commandSchema = JSON.parse(ok('schema', 'auth', 'doctor', '--json')) +assert.equal(commandSchema.command.path, 'doctor') +assert.equal(commandSchema.command.usage, 'beeper doctor (auth doctor) [flags]') + +commandSchema = JSON.parse(ok('schema', 'accounts', '--json')) +assert.deepEqual(commandSchema.command.subcommands.map((command: { name: string }) => command.name), ['list', 'show', 'add', 'use', 'remove']) +assert.deepEqual(commandSchema.command.aliases, [['account']]) +assert.equal(commandSchema.command.usage, 'beeper accounts (account) [flags]') + +commandSchema = JSON.parse(ok('schema', 'account', 'ls', '--json')) +assert.equal(commandSchema.command.path, 'accounts list') +assert.deepEqual(commandSchema.command.aliases, [['accounts', 'ls'], ['account', 'list'], ['account', 'ls']]) +assert.equal(commandSchema.command.usage, 'beeper accounts list (accounts ls,account list,account ls) [flags]') + +commandSchema = JSON.parse(ok('schema', 'accounts', 'get', '--json')) +assert.equal(commandSchema.command.path, 'accounts show') +assert.deepEqual(commandSchema.command.aliases, [['accounts', 'get'], ['accounts', 'info'], ['account', 'show'], ['account', 'get'], ['account', 'info']]) +assert.equal(commandSchema.command.usage, 'beeper accounts show (accounts get,accounts info,account show,account get,account info) [flags]') + +commandSchema = JSON.parse(ok('schema', 'account', 'info', '--json')) +assert.equal(commandSchema.command.path, 'accounts show') + +commandSchema = JSON.parse(ok('schema', 'accounts', 'add', '--json')) +assert.equal(commandSchema.command.path, 'accounts add') +assert.deepEqual(commandSchema.command.aliases, [['accounts', 'create'], ['accounts', 'new'], ['account', 'add'], ['account', 'create'], ['account', 'new']]) +assert.equal(commandSchema.command.usage, 'beeper accounts add (accounts create,accounts new,account add,account create,account new) [] [flags]') + +commandSchema = JSON.parse(ok('schema', 'account', 'new', '--json')) +assert.equal(commandSchema.command.path, 'accounts add') + +commandSchema = JSON.parse(ok('schema', 'accounts', 'use', '--json')) +assert.equal(commandSchema.command.path, 'accounts use') +assert.deepEqual(commandSchema.command.aliases, [['use', 'account'], ['account', 'use']]) +assert.equal(commandSchema.command.usage, 'beeper accounts use (use account,account use) [flags]') + +commandSchema = JSON.parse(ok('schema', 'use', 'account', '--json')) +assert.equal(commandSchema.command.path, 'accounts use') + +commandSchema = JSON.parse(ok('schema', 'accounts', 'remove', '--json')) +assert.equal(commandSchema.command.path, 'accounts remove') +assert.deepEqual(commandSchema.command.aliases, [['accounts', 'rm'], ['accounts', 'del'], ['remove', 'account'], ['account', 'remove'], ['account', 'rm'], ['account', 'del']]) +assert.equal(commandSchema.command.usage, 'beeper accounts remove (accounts rm,accounts del,remove account,account remove,account rm,account del) [flags]') + +commandSchema = JSON.parse(ok('schema', 'remove', 'account', '--json')) +assert.equal(commandSchema.command.path, 'accounts remove') + +commandSchema = JSON.parse(ok('schema', 'contacts', '--json')) +assert.deepEqual(commandSchema.command.subcommands.map((command: { name: string }) => command.name), ['list', 'show']) +assert.deepEqual(commandSchema.command.aliases, [['contact']]) + +commandSchema = JSON.parse(ok('schema', 'contacts', 'get', '--json')) +assert.equal(commandSchema.command.path, 'contacts show') +assert.deepEqual(commandSchema.command.aliases, [['contacts', 'get'], ['contacts', 'info'], ['contact', 'show'], ['contact', 'get'], ['contact', 'info']]) +assert.equal(commandSchema.command.usage, 'beeper contacts show (contacts get,contacts info,contact show,contact get,contact info) [] [flags]') + +commandSchema = JSON.parse(ok('schema', 'contact', 'info', '--json')) +assert.equal(commandSchema.command.path, 'contacts show') +assert.equal(commandSchema.command.positionals[0].name, 'selector') +assert.ok(commandSchema.command.flags.some((flag: { name: string }) => flag.name === 'jid')) +assert.ok(commandSchema.command.flags.some((flag: { name: string }) => flag.name === 'pick')) + +commandSchema = JSON.parse(ok('schema', 'contacts', 'search', '--json')) +assert.equal(commandSchema.command.path, 'contacts list') +assert.equal(commandSchema.command.positionals[0].name, 'query') +assert.equal(commandSchema.command.usage, 'beeper contacts list (contacts ls,contacts search,contacts find,contact list,contact ls,contact search,contact find) [] [flags]') + +commandSchema = JSON.parse(ok('schema', 'media', '--json')) +assert.deepEqual(commandSchema.command.subcommands.map((command: { name: string }) => command.name), ['download', 'message']) + +commandSchema = JSON.parse(ok('schema', 'media', 'message', '--json')) +assert.equal(commandSchema.command.path, 'media message') +assert.equal(commandSchema.command.usage, 'beeper media message [] [flags]') +assert.equal(commandSchema.command.positionals[0].name, 'id') + +commandSchema = JSON.parse(ok('schema', 'groups', '--json')) +assert.equal(commandSchema.command.path, 'groups') +assert.deepEqual(commandSchema.command.aliases, [['group']]) +assert.deepEqual(commandSchema.command.subcommands.map((command: { name: string }) => command.name), ['list', 'show', 'create', 'rename', 'description']) + +commandSchema = JSON.parse(ok('schema', 'group', 'ls', '--json')) +assert.equal(commandSchema.command.path, 'groups list') +assert.equal(commandSchema.command.usage, 'beeper groups list (groups ls,group list,group ls) [flags]') +assert.deepEqual(commandSchema.command.aliases, [['groups', 'ls'], ['group', 'list'], ['group', 'ls']]) + +commandSchema = JSON.parse(ok('schema', 'group', 'info', '--json')) +assert.equal(commandSchema.command.path, 'groups show') +assert.equal(commandSchema.command.usage, 'beeper groups show (groups info,group show,group info) [] [flags]') +assert.ok(commandSchema.command.positionals.some((arg: { name: string }) => arg.name === 'jid')) +assert.deepEqual(commandSchema.command.aliases, [['groups', 'info'], ['group', 'show'], ['group', 'info']]) + +commandSchema = JSON.parse(ok('schema', 'groups', 'create', '--json')) +assert.equal(commandSchema.command.path, 'groups create') +assert.deepEqual(commandSchema.command.aliases, [['groups', 'add'], ['groups', 'new'], ['group', 'create'], ['group', 'add'], ['group', 'new']]) +assert.equal(commandSchema.command.usage, 'beeper groups create (groups add,groups new,group create,group add,group new) [flags]') + +commandSchema = JSON.parse(ok('schema', 'group', 'add', '--json')) +assert.equal(commandSchema.command.path, 'groups create') + +commandSchema = JSON.parse(ok('schema', 'groups', 'rename', '--json')) +assert.equal(commandSchema.command.path, 'groups rename') +assert.equal(commandSchema.command.usage, 'beeper groups rename (group rename) [] [flags]') +assert.equal(commandSchema.command.positionals[0].name, 'jid') + +commandSchema = JSON.parse(ok('schema', 'groups', 'description', '--json')) +assert.equal(commandSchema.command.path, 'groups description') +assert.equal(commandSchema.command.usage, 'beeper groups description (groups topic,group description,group topic) [] [flags]') +assert.equal(commandSchema.command.positionals[0].name, 'jid') + +commandSchema = JSON.parse(ok('schema', 'presence', '--json')) +assert.equal(commandSchema.command.path, 'presence') +assert.equal(commandSchema.command.usage, 'beeper presence [flags]') +assert.deepEqual(commandSchema.command.subcommands.map((command: { name: string }) => command.name), ['typing', 'paused']) + +commandSchema = JSON.parse(ok('schema', 'presence', 'typing', '--json')) +assert.equal(commandSchema.command.path, 'presence typing') +assert.equal(commandSchema.command.usage, 'beeper presence typing [flags]') + +commandSchema = JSON.parse(ok('schema', 'send', 'presence', '--json')) +assert.equal(commandSchema.command.path, 'send presence') +assert.equal(commandSchema.command.usage, 'beeper send presence [flags]') +assert.equal(commandSchema.command.risk, 'write') +assert.deepEqual(commandSchema.command.requirements, ['write']) + +commandSchema = JSON.parse(ok('schema', 'messages', '--json')) +assert.deepEqual(commandSchema.command.subcommands.map((command: { name: string }) => command.name), ['list', 'search', 'context', 'show', 'export', 'forward', 'edit', 'delete', 'revoke']) + +commandSchema = JSON.parse(ok('schema', 'messages', 'list', '--json')) +assert.ok(commandSchema.command.flags.some((flag: { name: string }) => flag.name === 'from-me')) +assert.ok(commandSchema.command.flags.some((flag: { name: string }) => flag.name === 'from-them')) + +commandSchema = JSON.parse(ok('schema', 'chats', '--json')) +assert.deepEqual(commandSchema.command.aliases, [['chat']]) +assert.deepEqual(commandSchema.command.subcommands.map((command: { name: string }) => command.name).slice(0, 12), ['list', 'show', 'start', 'archive', 'unarchive', 'pin', 'unpin', 'mute', 'unmute', 'read', 'mark-read', 'mark-unread']) + +commandSchema = JSON.parse(ok('schema', 'chat', 'ls', '--json')) +assert.equal(commandSchema.command.path, 'chats list') +assert.ok(commandSchema.command.aliases.some((alias: string[]) => alias.join(' ') === 'chat ls')) +assert.ok(commandSchema.command.flags.some((flag: { default?: number; name: string; placeholder?: string }) => flag.name === 'limit' && flag.default === 50 && flag.placeholder === '50')) + +commandSchema = JSON.parse(ok('schema', 'chats', 'show', '--json')) +assert.equal(commandSchema.command.path, 'chats show') +assert.equal(commandSchema.command.usage, 'beeper chats show (chats info,chat show,chat info) [] [flags]') +assert.ok(commandSchema.command.positionals.some((arg: { name: string }) => arg.name === 'chat')) +assert.ok(commandSchema.command.flags.some((flag: { aliases?: string[]; name: string }) => flag.name === 'chat' && flag.aliases?.includes('jid'))) + +commandSchema = JSON.parse(ok('schema', 'chat', 'info', '--json')) +assert.equal(commandSchema.command.path, 'chats show') + +commandSchema = JSON.parse(ok('schema', 'chats', 'mark-unread', '--json')) +assert.equal(commandSchema.command.path, 'chats mark-unread') +assert.equal(commandSchema.command.usage, 'beeper chats mark-unread (chat mark-unread) [] [flags]') +assert.equal(commandSchema.command.positionals[0].name, 'chat') +assert.equal(commandSchema.command.risk, 'write') + +commandSchema = JSON.parse(ok('schema', 'messages', 'show', '--json')) +assert.equal(commandSchema.command.path, 'messages show') +assert.equal(commandSchema.command.usage, 'beeper messages show (messages get,messages info) [] [flags]') +assert.equal(commandSchema.command.positionals[0].name, 'id') +assert.deepEqual(commandSchema.command.aliases, [['messages', 'get'], ['messages', 'info']]) +assert.equal(commandSchema.command.risk, 'read') + +commandSchema = JSON.parse(ok('schema', 'messages', 'info', '--json')) +assert.equal(commandSchema.command.path, 'messages show') + +commandSchema = JSON.parse(ok('schema', 'messages', 'export', '--json')) +assert.equal(commandSchema.command.path, 'messages export') +assert.equal(commandSchema.command.usage, 'beeper messages export [flags]') +assert.ok(commandSchema.command.flags.some((flag: { aliases?: string[]; default?: number; name: string }) => flag.name === 'limit' && flag.default === 1000)) +assert.ok(commandSchema.command.flags.some((flag: { aliases?: string[]; name: string }) => flag.name === 'output' && flag.aliases?.includes('out'))) + +commandSchema = JSON.parse(ok('schema', 'messages', 'forward', '--json')) +assert.equal(commandSchema.command.path, 'messages forward') +assert.equal(commandSchema.command.usage, 'beeper messages forward [] [flags]') +assert.equal(commandSchema.command.positionals[0].name, 'id') +assert.equal(commandSchema.command.risk, 'write') +assert.ok(commandSchema.command.flags.some((flag: { default?: string; name: string }) => flag.name === 'post-send-wait' && flag.default === '2s')) + +commandSchema = JSON.parse(ok('schema', 'messages', 'edit', '--json')) +assert.equal(commandSchema.command.path, 'messages edit') +assert.deepEqual(commandSchema.command.aliases, [['messages', 'update'], ['messages', 'set']]) +assert.equal(commandSchema.command.usage, 'beeper messages edit (messages update,messages set) [] [flags]') + +commandSchema = JSON.parse(ok('schema', 'messages', 'set', '--json')) +assert.equal(commandSchema.command.path, 'messages edit') + +commandSchema = JSON.parse(ok('schema', 'messages', 'delete', '--json')) +assert.equal(commandSchema.command.path, 'messages delete') +assert.deepEqual(commandSchema.command.aliases, [['messages', 'rm'], ['messages', 'del'], ['messages', 'remove']]) +assert.equal(commandSchema.command.usage, 'beeper messages delete (messages rm,messages del,messages remove) [] [flags]') + +commandSchema = JSON.parse(ok('schema', 'messages', 'rm', '--json')) +assert.equal(commandSchema.command.path, 'messages delete') + +commandSchema = JSON.parse(ok('schema', 'messages', 'revoke', '--json')) +assert.equal(commandSchema.command.path, 'messages revoke') +assert.equal(commandSchema.command.usage, 'beeper messages revoke [] [flags]') +assert.equal(commandSchema.command.positionals[0].name, 'id') +assert.equal(commandSchema.command.risk, 'destructive') + +commandSchema = JSON.parse(ok('schema', 'watch', '--json')) +assert.equal(commandSchema.command.path, 'watch') +assert.ok(commandSchema.command.flags.some((flag: { default?: boolean; name: string }) => flag.name === 'webhook-allow-private' && flag.default === false)) +assert.ok(commandSchema.command.flags.some((flag: { help?: string; name: string }) => flag.name === 'webhook-secret' && flag.help?.includes('X-Wacli-Signature'))) + +commandSchema = JSON.parse(ok('schema', 'exit-codes', '--json')) +assert.equal(commandSchema.command.path, 'exit-codes') +assert.equal(commandSchema.command.raw_json, true) +assert.equal(commandSchema.command.risk, 'read') + +commandSchema = JSON.parse(ok('schema', 'config', 'show', '--json')) +assert.equal(commandSchema.command.path, 'config get') +assert.equal(commandSchema.command.name, 'get') +assert.equal(commandSchema.command.usage, 'beeper config get (config show) [flags]') + +commandSchema = JSON.parse(ok('schema', 'config', 'keys', '--json')) +assert.equal(commandSchema.command.path, 'config keys') +assert.equal(commandSchema.command.primary_result_key, 'keys') + +commandSchema = JSON.parse(ok('schema', 'config', '--json')) +assert.equal(commandSchema.command.path, 'config') +assert.equal(commandSchema.command.help, 'config commands') +assert.equal(commandSchema.command.usage, 'beeper config [flags]') +assert.ok(commandSchema.command.flags.some((flag: { name: string }) => flag.name === 'read-only')) +assert.deepEqual(commandSchema.command.subcommands.map((command: { name: string }) => command.name), ['get', 'keys', 'set', 'unset', 'list', 'path']) +assert.ok(commandSchema.command.subcommands.some((command: { name: string }) => command.name === 'get')) + +commandSchema = JSON.parse(ok('schema', 'docs', '--json')) +assert.equal(commandSchema.command.path, 'docs') +assert.ok(commandSchema.command.flags.some((flag: { aliases?: string[]; name: string }) => flag.name === 'url' && flag.aliases?.includes('url-only'))) + +commandSchema = JSON.parse(ok('schema', 'completion', 'zsh', '--json')) +assert.equal(commandSchema.command.path, 'completion zsh') +assert.equal(commandSchema.command.name, 'zsh') +assert.equal(commandSchema.command.usage, 'beeper completion zsh [flags]') +assert.ok(commandSchema.command.flags.some((flag: { name: string }) => flag.name === 'no-descriptions')) +assert.ok(commandSchema.command.flags.some((flag: { name: string }) => flag.name === 'json')) + +commandSchema = JSON.parse(ok('schema', 'completion', '--json')) +assert.deepEqual(commandSchema.command.positionals[0].enum, ['bash', 'fish', 'powershell', 'zsh']) + +commandSchema = JSON.parse(ok('schema', 'agent', '--json')) +assert.equal(commandSchema.command.usage, 'beeper agent [flags]') +assert.deepEqual(commandSchema.command.subcommands.map((command: { name: string; usage: string }) => [command.name, command.usage]), [['exit-codes', 'beeper agent exit-codes (exitcodes,exit-code) [flags]']]) + +commandSchema = JSON.parse(ok('schema', 'schema', '--json')) +assert.equal(commandSchema.command.usage, 'beeper schema (help-json,helpjson) [ ...] [flags]') + +commandSchema = JSON.parse(ok('schema', 'mcp', '--json')) +assert.equal(commandSchema.command.help, 'Run a typed, allowlisted MCP server over stdio or HTTP') +assert.ok(commandSchema.command.flags.some((flag: { help?: string; name: string; placeholder?: string }) => flag.name === 'allow-tool' && flag.placeholder === 'ALLOW-TOOL,...' && flag.help?.includes('default: all read-only tools'))) +const mcpHelp = ok('mcp', '--help') +assert.match(mcpHelp, /--allow-tool=ALLOW-TOOL,\.\.\. \(\--tool\)/) +assert.match(mcpHelp, /beeper mcp --allow-tool targets\.\*,messages --list-tools/) + +commandSchema = JSON.parse(ok('schema', 'send', '--json')) +assert.equal(commandSchema.command.path, 'send') +assert.equal(commandSchema.command.usage, 'beeper send [] [ ...] [flags]') +assert.ok(commandSchema.command.flags.some((flag: { name: string }) => flag.name === 'message')) +assert.deepEqual(commandSchema.command.subcommands.map((command: { name: string }) => command.name), ['text', 'file', 'voice', 'sticker', 'react', 'presence']) + +commandSchema = JSON.parse(ok('schema', 'send', 'react', '--json')) +assert.deepEqual(commandSchema.command.aliases, [['send', 'reaction']]) +assert.equal(commandSchema.command.usage, 'beeper send react (send reaction) [] [flags]') +assert.equal(commandSchema.command.positionals[0].name, 'id') +assert.ok(commandSchema.command.flags.some((flag: { default?: string; name: string }) => flag.name === 'reaction' && flag.default === '+1')) + +commandSchema = JSON.parse(ok('schema', 'send', 'reaction', '--json')) +assert.equal(commandSchema.command.path, 'send react') + +commandSchema = JSON.parse(ok('schema', 'send', 'file', '--json')) +assert.ok(commandSchema.command.flags.some((flag: { name: string }) => flag.name === 'ptt')) +assert.ok(commandSchema.command.flags.some((flag: { name: string }) => flag.name === 'post-send-wait')) +assert.ok(commandSchema.command.flags.some((flag: { name: string }) => flag.name === 'reply-to-sender')) + +commandSchema = JSON.parse(ok('schema', 'targets', '--json')) +assert.deepEqual(commandSchema.command.aliases, [['target']]) +assert.equal(commandSchema.command.usage, 'beeper targets (target) [flags]') +assert.deepEqual(commandSchema.command.subcommands.map((command: { name: string }) => command.name), ['list', 'use', 'add', 'remove', 'logs', 'runtime', 'tunnel']) + +commandSchema = JSON.parse(ok('schema', 'targets', 'use', '--json')) +assert.equal(commandSchema.command.path, 'targets use') +assert.deepEqual(commandSchema.command.aliases, [['use', 'target'], ['target', 'use']]) +assert.equal(commandSchema.command.usage, 'beeper targets use (use target,target use) [flags]') + +commandSchema = JSON.parse(ok('schema', 'use', 'target', '--json')) +assert.equal(commandSchema.command.path, 'targets use') + +commandSchema = JSON.parse(ok('schema', 'targets', 'runtime', '--json')) +assert.deepEqual(commandSchema.command.subcommands.map((command: { name: string }) => command.name), ['start', 'stop', 'restart']) + +commandSchema = JSON.parse(ok('schema', 'target', 'runtime', 'restart', '--json')) +assert.equal(commandSchema.command.path, 'targets runtime restart') +assert.equal(commandSchema.command.program_path, 'beeper targets runtime restart') +assert.deepEqual(commandSchema.command.aliases, [['target', 'runtime', 'restart']]) +assert.deepEqual(commandSchema.command.program_alias_paths, ['beeper target runtime restart']) + +commandSchema = JSON.parse(ok('schema', 'target', 'del', '--json')) +assert.equal(commandSchema.command.path, 'targets remove') +assert.deepEqual(commandSchema.command.aliases, [['targets', 'rm'], ['targets', 'del'], ['remove', 'target'], ['target', 'remove'], ['target', 'rm'], ['target', 'del']]) +assert.equal(commandSchema.command.usage, 'beeper targets remove (targets rm,targets del,remove target,target remove,target rm,target del) [flags]') + +commandSchema = JSON.parse(ok('schema', 'remove', 'target', '--json')) +assert.equal(commandSchema.command.path, 'targets remove') + +commandSchema = JSON.parse(ok('schema', 'account', 'del', '--json')) +assert.equal(commandSchema.command.path, 'accounts remove') +assert.deepEqual(commandSchema.command.aliases, [['accounts', 'rm'], ['accounts', 'del'], ['remove', 'account'], ['account', 'remove'], ['account', 'rm'], ['account', 'del']]) +assert.equal(commandSchema.command.usage, 'beeper accounts remove (accounts rm,accounts del,remove account,account remove,account rm,account del) [flags]') + +commandSchema = JSON.parse(ok('schema', '--json', '--select=command.name', '--results-only', '--wrap-untrusted')) +assert.equal(commandSchema.schema_version, 1) +assert.equal(commandSchema.command.name, 'beeper') +assert.ok(Array.isArray(commandSchema.command.flags)) + let filteredHelp = ok('--read-only', '--help') -assert.match(filteredHelp, /targets list/) +assert.match(filteredHelp, /targets \(target\) \[flags\]/) assert.doesNotMatch(filteredHelp, /send text/) +assert.doesNotMatch(filteredHelp, /media download/) +assert.doesNotMatch(filteredHelp, /send presence/) let filteredSchema = JSON.parse(ok('--read-only', 'schema', '--json')) assert.equal(schemaPaths(filteredSchema).includes('send text'), false) assert.equal(schemaPaths(filteredSchema).includes('targets list'), true) +assert.equal(schemaPaths(filteredSchema).includes('media download'), false) +assert.equal(schemaPaths(filteredSchema).includes('chats list'), true) filteredHelp = ok('--enable-commands', 'messages', '--help') assert.match(filteredHelp, /messages search/) assert.doesNotMatch(filteredHelp, /targets list/) +filteredHelp = ok('--enable-commands', 'auth', 'auth', '--help') +assert.match(filteredHelp, /add \(login\) \[flags\]/) +assert.match(filteredHelp, /status \[\] \[flags\]/) +assert.match(filteredHelp, /doctor \[flags\]/) +assert.match(filteredHelp, /services \(bridges\) \[flags\]/) +assert.doesNotMatch(filteredHelp, /targets list/) + +filteredHelp = ok('--enable-commands-exact', 'auth.services', 'auth', '--help') +assert.match(filteredHelp, /services \(bridges\) \[flags\]/) +assert.doesNotMatch(filteredHelp, /add \(login\) /) + filteredSchema = JSON.parse(ok('--disable-commands', 'messages.search', 'schema', '--json')) assert.equal(schemaPaths(filteredSchema).includes('messages search'), false) +filteredSchema = JSON.parse(ok('--enable-commands', 'auth,schema', 'schema', 'auth', '--json')) +assert.equal(schemaPaths(filteredSchema).includes('login'), true) +assert.equal(schemaPaths(filteredSchema).includes('auth status'), true) +assert.equal(schemaPaths(filteredSchema).includes('auth services'), true) + +const docsDiffBefore = spawnSync('git', ['diff', '--', 'packages/cli/docs/commands'], { cwd: join(root, '..', '..'), encoding: 'utf8' }).stdout result = spawnSync('bun', ['scripts/generate-command-docs.ts'], { cwd: root, encoding: 'utf8' }) assert.equal(result.status, 0, result.stderr) -result = spawnSync('git', ['diff', '--quiet', '--', 'packages/cli/docs/commands'], { cwd: join(root, '..', '..'), encoding: 'utf8' }) -assert.equal(result.status, 0, 'generated command docs are out of date') +const docsDiffAfter = spawnSync('git', ['diff', '--', 'packages/cli/docs/commands'], { cwd: join(root, '..', '..'), encoding: 'utf8' }).stdout +assert.equal(docsDiffAfter, docsDiffBefore, 'generated command docs are not idempotent') const mcp = spawnSync('bun', ['./bin/dev.js', 'mcp'], { cwd: root, @@ -473,12 +1897,25 @@ const mcp = spawnSync('bun', ['./bin/dev.js', 'mcp'], { assert.equal(mcp.status, 0, mcp.stderr) payload = JSON.parse(mcp.stdout) assert.ok(payload.result.tools.some((tool: { name: string }) => tool.name === 'targets_list')) +assert.ok(payload.result.tools.some((tool: { name: string }) => tool.name === 'me')) assert.ok(payload.result.tools.some((tool: { name: string }) => tool.name === 'messages_search')) assert.ok(payload.result.tools.some((tool: { name: string }) => tool.name === 'contacts_list')) +assert.ok(payload.result.tools.some((tool: { name: string }) => tool.name === 'accounts_show')) +assert.ok(payload.result.tools.some((tool: { name: string }) => tool.name === 'contacts_show')) +assert.ok(payload.result.tools.some((tool: { _meta?: { command?: string; risk?: string; service?: string }; name: string }) => tool.name === 'me' && tool._meta?.command === 'me' && tool._meta?.risk === 'read' && tool._meta?.service === 'me')) +assert.ok(payload.result.tools.some((tool: { _meta?: { command?: string; risk?: string; service?: string }; name: string }) => tool.name === 'messages_search' && tool._meta?.command === 'messages search' && tool._meta?.risk === 'read' && tool._meta?.service === 'messages')) +assert.ok(payload.result.tools.some((tool: { _meta?: { command?: string; risk?: string; service?: string }; inputSchema?: { properties?: Record }; name: string }) => tool.name === 'accounts_show' && tool._meta?.command === 'accounts show' && tool._meta?.risk === 'read' && tool.inputSchema?.properties?.selector)) +assert.ok(payload.result.tools.some((tool: { _meta?: { command?: string; risk?: string; service?: string }; inputSchema?: { properties?: Record }; name: string }) => tool.name === 'contacts_show' && tool._meta?.command === 'contacts show' && tool._meta?.risk === 'read' && tool.inputSchema?.properties?.selector)) +assert.ok(payload.result.tools.some((tool: { inputSchema?: { properties?: Record }; name: string }) => tool.name === 'messages_search' && tool.inputSchema?.properties?.['chat-type']?.type === 'string' && tool.inputSchema?.properties?.['chat-type']?.enum?.includes('group'))) assert.ok(!payload.result.tools.some((tool: { name: string }) => tool.name === 'api_request')) +assert.ok(!payload.result.tools.some((tool: { name: string }) => tool.name === 'config_get')) +assert.ok(!payload.result.tools.some((tool: { name: string }) => tool.name === 'schema')) +assert.ok(!payload.result.tools.some((tool: { name: string }) => tool.name === 'send_text')) assert.ok(payload.result.tools.some((tool: { name: string }) => tool.name === 'resolve_target')) assert.ok(payload.result.tools.some((tool: { name: string }) => tool.name === 'resolve_chat')) assert.ok(payload.result.tools.some((tool: { name: string }) => tool.name === 'messages_context')) +assert.ok(payload.result.tools.some((tool: { name: string }) => tool.name === 'messages_show')) +assert.ok(payload.result.tools.some((tool: { name: string }) => tool.name === 'messages_export')) const mcpAllowWrite = spawnSync('bun', ['./bin/dev.js', 'mcp', '--allow-write', '--list-tools'], { cwd: root, @@ -490,7 +1927,44 @@ const mcpAllowWrite = spawnSync('bun', ['./bin/dev.js', 'mcp', '--allow-write', }) assert.equal(mcpAllowWrite.status, 0, mcpAllowWrite.stderr) payload = JSON.parse(mcpAllowWrite.stdout) -assert.ok(payload.some((tool: { name: string }) => tool.name === 'api_request')) +assert.ok(Array.isArray(payload.tools)) +assert.ok(payload.tools.some((tool: { name: string }) => tool.name === 'send_text')) +assert.ok(payload.tools.some((tool: { name: string }) => tool.name === 'send_react')) +assert.ok(payload.tools.some((tool: { name: string }) => tool.name === 'chats_read')) +assert.ok(payload.tools.some((tool: { name: string; risk?: string; service?: string }) => tool.name === 'messages_edit' && tool.risk === 'write' && tool.service === 'messages')) +assert.ok(payload.tools.some((tool: { name: string; requirements?: string[] }) => tool.name === 'messages_edit' && tool.requirements?.includes('write'))) +assert.ok(payload.tools.some((tool: { name: string; requirements?: string[] }) => tool.name === 'targets_list' && tool.requirements === undefined)) +assert.ok(!payload.tools.some((tool: { name: string }) => tool.name === 'api_request')) +assert.ok(!payload.tools.some((tool: { name: string }) => tool.name === 'messages_delete')) +assert.ok(!payload.tools.some((tool: { inputSchema?: unknown }) => tool.inputSchema)) + +const mcpAllowComma = spawnSync('bun', ['./bin/dev.js', 'mcp', '--allow-tool', 'targets.*,messages', '--list-tools'], { + cwd: root, + encoding: 'utf8', + env: { + ...process.env, + BEEPER_HOME: configDir, + BEEPER_CLI_CONFIG_DIR: configDir, + }, +}) +assert.equal(mcpAllowComma.status, 0, mcpAllowComma.stderr) +payload = JSON.parse(mcpAllowComma.stdout) +assert.ok(payload.tools.some((tool: { name: string }) => tool.name === 'targets_list')) +assert.ok(payload.tools.some((tool: { name: string }) => tool.name === 'messages_search')) +assert.ok(!payload.tools.some((tool: { name: string }) => tool.name === 'contacts_list')) + +const mcpAllowSnake = spawnSync('bun', ['./bin/dev.js', 'mcp', '--allow-tool', 'messages_search', '--list-tools'], { + cwd: root, + encoding: 'utf8', + env: { + ...process.env, + BEEPER_HOME: configDir, + BEEPER_CLI_CONFIG_DIR: configDir, + }, +}) +assert.equal(mcpAllowSnake.status, 0, mcpAllowSnake.stderr) +payload = JSON.parse(mcpAllowSnake.stdout) +assert.deepEqual(payload.tools.map((tool: { name: string }) => tool.name), ['messages_search']) const mcpInitialize = spawnSync('bun', ['./bin/dev.js', 'mcp'], { cwd: root, @@ -499,7 +1973,7 @@ const mcpInitialize = spawnSync('bun', ['./bin/dev.js', 'mcp'], { ...process.env, BEEPER_CLI_CONFIG_DIR: configDir, }, - input: '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n', + input: '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}\n', }) assert.equal(mcpInitialize.status, 0, mcpInitialize.stderr) payload = JSON.parse(mcpInitialize.stdout) @@ -513,14 +1987,28 @@ const mcpCall = spawnSync('bun', ['./bin/dev.js', 'mcp'], { ...process.env, BEEPER_CLI_CONFIG_DIR: configDir, }, - input: '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"version","arguments":{}}}\n', + input: '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"targets_list","arguments":{}}}\n', }) assert.equal(mcpCall.status, 0, mcpCall.stderr) payload = JSON.parse(mcpCall.stdout) -const mcpVersion = JSON.parse(payload.result.content[0].text) -assert.equal(mcpVersion.exit_code, 0) -assert.match(mcpVersion.stdout.name, /beeper-cli/) -assert.equal(mcpVersion.stdout.version, version.version) +const mcpTargets = JSON.parse(payload.result.content[0].text) +assert.equal(mcpTargets.exit_code, 0) +assert.equal(mcpTargets.tool, 'targets_list') +assert.ok(Array.isArray(mcpTargets.stdout)) + +const mcpHiddenWriteCall = spawnSync('bun', ['./bin/dev.js', 'mcp'], { + cwd: root, + encoding: 'utf8', + env: { + ...process.env, + BEEPER_CLI_CONFIG_DIR: configDir, + }, + input: '{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"send_text","arguments":{}}}\n', +}) +assert.equal(mcpHiddenWriteCall.status, 0, mcpHiddenWriteCall.stderr) +payload = JSON.parse(mcpHiddenWriteCall.stdout) +assert.equal(payload.result.isError, true) +assert.match(payload.result.content[0].text, /Tool send_text not found/) const mcpEOFCall = spawnSync('bun', ['./bin/dev.js', 'mcp'], { cwd: root, @@ -529,12 +2017,12 @@ const mcpEOFCall = spawnSync('bun', ['./bin/dev.js', 'mcp'], { ...process.env, BEEPER_CLI_CONFIG_DIR: configDir, }, - input: '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"version","arguments":{}}}', + input: '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"targets_list","arguments":{}}}', }) assert.equal(mcpEOFCall.status, 0, mcpEOFCall.stderr) payload = JSON.parse(mcpEOFCall.stdout) assert.equal(payload.id, 4) -assert.equal(JSON.parse(payload.result.content[0].text).stdout.version, version.version) +assert.equal(JSON.parse(payload.result.content[0].text).tool, 'targets_list') const mcpDryRunCall = spawnSync('bun', ['./bin/dev.js', '--dry-run', 'mcp'], { cwd: root, @@ -552,6 +2040,20 @@ assert.equal(mcpContext.stdout.dry_run, true) assert.equal(mcpContext.stdout.request.after, 3) assert.equal(mcpContext.stdout.request.before, 4) +const mcpStrictInputCall = spawnSync('bun', ['./bin/dev.js', 'mcp'], { + cwd: root, + encoding: 'utf8', + env: { + ...process.env, + BEEPER_CLI_CONFIG_DIR: configDir, + }, + input: '{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"targets_list","arguments":{"unexpected":true}}}\n', +}) +assert.equal(mcpStrictInputCall.status, 0, mcpStrictInputCall.stderr) +payload = JSON.parse(mcpStrictInputCall.stdout) +assert.equal(payload.result.isError, true) +assert.match(payload.result.content[0].text, /Unrecognized key/) + const mcpInvalidJSON = spawnSync('bun', ['./bin/dev.js', 'mcp'], { cwd: root, encoding: 'utf8', diff --git a/packages/cli/test/messages-search-validation.test.ts b/packages/cli/test/messages-search-validation.test.ts index 0adc3c40..3bd1dc20 100644 --- a/packages/cli/test/messages-search-validation.test.ts +++ b/packages/cli/test/messages-search-validation.test.ts @@ -32,4 +32,9 @@ describe('messages search query-or-filter requirement', () => { const result = run('messages', 'search', '--sender', 'me', '--help') expect(result.status).toBe(0) }) + + it('accepts the wacli-style --from alias and --has-media filter', () => { + const result = run('messages', 'search', '--from', 'me', '--has-media', '--help') + expect(result.status).toBe(0) + }) }) diff --git a/packages/cli/test/output.test.ts b/packages/cli/test/output.test.ts new file mode 100644 index 00000000..690f97fc --- /dev/null +++ b/packages/cli/test/output.test.ts @@ -0,0 +1,126 @@ +import { afterEach, describe, expect, it, spyOn } from 'bun:test' +import { writeResult } from '../src/cli/output.js' +import type { GlobalFlags } from '../src/cli/types.js' + +const baseFlags: GlobalFlags = { + account: [], + color: 'auto', + debug: false, + dryRun: false, + events: false, + force: false, + full: false, + json: true, + noInput: false, + plain: false, + readOnly: false, + resultsOnly: true, + wrapUntrusted: false, +} + +describe('writeResult', () => { + afterEach(() => { + process.stdout.write = originalWrite + }) + + const originalWrite = process.stdout.write + + it('treats services as a primary JSON result envelope', () => { + let stdout = '' + spyOn(process.stdout, 'write').mockImplementation((chunk: string | Uint8Array) => { + stdout += String(chunk) + return true + }) + + writeResult({ services: [{ bridge_id: 'imessage', name: 'iMessage' }] }, baseFlags) + + expect(JSON.parse(stdout)).toEqual([{ bridge_id: 'imessage', name: 'iMessage' }]) + }) + + it('unwraps config path for JSON results-only', () => { + let stdout = '' + spyOn(process.stdout, 'write').mockImplementation((chunk: string | Uint8Array) => { + stdout += String(chunk) + return true + }) + + writeResult({ path: '/tmp/beeper/config.json' }, baseFlags, { + description: 'Print config file path', + path: ['config', 'path'], + risk: 'read', + run: async () => undefined, + }) + + expect(JSON.parse(stdout)).toBe('/tmp/beeper/config.json') + }) + + it('unwraps config keys for JSON results-only', () => { + let stdout = '' + spyOn(process.stdout, 'write').mockImplementation((chunk: string | Uint8Array) => { + stdout += String(chunk) + return true + }) + + writeResult({ keys: ['defaultTarget', 'defaultAccount'] }, baseFlags, { + description: 'List available config keys', + path: ['config', 'keys'], + risk: 'read', + run: async () => undefined, + }) + + expect(JSON.parse(stdout)).toEqual(['defaultTarget', 'defaultAccount']) + }) + + it('unwraps auth accounts for JSON results-only', () => { + let stdout = '' + spyOn(process.stdout, 'write').mockImplementation((chunk: string | Uint8Array) => { + stdout += String(chunk) + return true + }) + + writeResult({ accounts: [{ target: 'desktop', authenticated: false }] }, baseFlags, { + description: 'List stored target credentials', + output: 'auth', + path: ['auth', 'list'], + risk: 'read', + run: async () => undefined, + }) + + expect(JSON.parse(stdout)).toEqual([{ target: 'desktop', authenticated: false }]) + }) + + it('unwraps targets for JSON results-only', () => { + let stdout = '' + spyOn(process.stdout, 'write').mockImplementation((chunk: string | Uint8Array) => { + stdout += String(chunk) + return true + }) + + writeResult({ targets: [{ id: 'desktop', type: 'desktop' }] }, baseFlags, { + description: 'List configured Beeper targets', + output: 'targets', + path: ['targets', 'list'], + risk: 'read', + run: async () => undefined, + }) + + expect(JSON.parse(stdout)).toEqual([{ id: 'desktop', type: 'desktop' }]) + }) + + it('does not unwrap status target fields for JSON results-only', () => { + let stdout = '' + spyOn(process.stdout, 'write').mockImplementation((chunk: string | Uint8Array) => { + stdout += String(chunk) + return true + }) + + writeResult({ auth: { authenticated: false }, target: { id: 'desktop' } }, baseFlags, { + description: 'Show selected target and setup readiness', + path: ['status'], + risk: 'read', + run: async () => undefined, + }) + + expect(JSON.parse(stdout)).toEqual({ auth: { authenticated: false }, target: { id: 'desktop' } }) + }) +}) diff --git a/packages/cli/test/webhook-headers.test.ts b/packages/cli/test/webhook-headers.test.ts new file mode 100644 index 00000000..3c7b8a8a --- /dev/null +++ b/packages/cli/test/webhook-headers.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'bun:test' +import { createHmac } from 'node:crypto' +import { webhookHeaders } from '../src/cli/commands.js' + +describe('webhookHeaders', () => { + it('includes Beeper and wacli-compatible signatures when a secret is set', () => { + const body = '{"type":"message","messageID":"m1"}' + const expected = `sha256=${createHmac('sha256', 'secret').update(body).digest('hex')}` + + expect(webhookHeaders(body, 'secret')).toEqual({ + 'content-type': 'application/json', + 'x-beeper-signature': expected, + 'x-wacli-signature': expected, + }) + }) + + it('omits signature headers when no secret is set', () => { + expect(webhookHeaders('{}')).toEqual({ 'content-type': 'application/json' }) + }) +})