From 437be9f3e46f417ed2ccb181c0e858c00c6cfac0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 21:35:01 +0000 Subject: [PATCH 1/2] feat: first-person Polylane voice across CLI output Apply the approved Direction A persona to every human-facing string: the CLI speaks as "I" (calm senior SRE register), errors lead with what to do next, success stays quietly pleased, and no em dashes per the house copy rules. Unknown commands now suggest the closest match ("Unknown command: polylane scna. Closest match: polylane scan."), straight from the approved design mock. Machine-readable output (JSON shapes, exit codes, flags, command and option names) is unchanged. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RpvmDmwuwen1nmgCQbvD4n --- ERRORS.md | 6 ++-- src/auth/oauth.ts | 36 +++++++++++++++------- src/auth/refresh.ts | 4 +-- src/auth/resolver.ts | 4 +-- src/auth/setup.ts | 8 ++--- src/commands/artifact/delete.ts | 2 +- src/commands/auth/login.ts | 10 ++++-- src/commands/auth/logout.ts | 2 +- src/commands/auth/signup.ts | 14 ++++----- src/commands/autofix/watch.ts | 14 ++++----- src/commands/cloud/connect.ts | 8 ++--- src/commands/cloud/disconnect.ts | 2 +- src/commands/help.ts | 4 +-- src/commands/helpers.ts | 2 +- src/commands/integration/connect.ts | 18 +++++------ src/commands/integration/disconnect.ts | 2 +- src/commands/memory/delete.ts | 2 +- src/commands/note/delete.ts | 2 +- src/commands/note/global-clear.ts | 2 +- src/commands/scan.ts | 16 +++++----- src/commands/setup.ts | 6 ++-- src/commands/thread/ask.ts | 6 ++-- src/commands/thread/continue.ts | 6 ++-- src/commands/update.ts | 4 +-- src/errors/api.ts | 4 +-- src/errors/handler.ts | 2 +- src/main.ts | 3 +- src/registry.ts | 42 +++++++++++++++++++++++++- test/errors.test.ts | 2 +- test/registry.test.ts | 27 ++++++++++++++++- test/resolve.test.ts | 2 +- test/scan.test.ts | 2 +- 32 files changed, 174 insertions(+), 90 deletions(-) diff --git a/ERRORS.md b/ERRORS.md index 2e54f98..bdbb555 100644 --- a/ERRORS.md +++ b/ERRORS.md @@ -62,9 +62,9 @@ Every command inherits these. Specific messages are subject to change, but the * | Scenario | Typical message | |---|---| | No credentials | `Not signed in.` with a hint listing `auth login` variants | -| HTTP 401 | `Not authenticated` | +| HTTP 401 | `Not signed in.` | | HTTP 403 | `Permission denied` | -| OAuth refresh failed | `Token refresh failed` with a hint to re-authenticate | +| OAuth refresh failed | `Couldn't refresh your session ()` with a hint to sign in again | | WebSocket upgrade rejected (for streaming commands) | `WebSocket upgrade rejected ()` | ### Rate limit / plan (exit `4`) @@ -78,7 +78,7 @@ Every command inherits these. Specific messages are subject to change, but the * | Scenario | Typical message | |---|---| -| Unknown command | `Unknown command: polylane ` with a hint pointing at `polylane --help` | +| Unknown command | `Unknown command: polylane `, plus `Closest match: polylane ` when one is close, with a hint pointing at `polylane --help` | | Unknown flag | `Unknown flag: ` | | Flag requires a value | `Flag requires a value` | | Flag expects a number | `Flag expects a number, got ""` | diff --git a/src/auth/oauth.ts b/src/auth/oauth.ts index 517d072..150ade4 100644 --- a/src/auth/oauth.ts +++ b/src/auth/oauth.ts @@ -104,8 +104,9 @@ async function fetchOIDCConfig(domain: string): Promise { const res = await fetch(url); if (!res.ok) { throw new CLIError( - `Failed to fetch OIDC config from ${url}: ${res.status}`, - ExitCode.NETWORK + `Couldn't fetch the sign-in configuration from ${url} (${res.status})`, + ExitCode.NETWORK, + 'Check your network and the --domain flag, then try again' ); } return (await res.json()) as OIDCConfig; @@ -246,11 +247,11 @@ function renderErrorHtml(error: string): string { return map[c] ?? c; }); return renderShell( - 'Authentication failed', + 'Sign-in did not complete', // Red-500 from Tailwind default — high-contrast destructive accent '#ef4444', - 'Authentication failed', - safe, + 'Sign-in did not complete', + `Close this tab and run polylane auth login again from your terminal. (${safe})`, ALERT_ICON ); } @@ -307,12 +308,20 @@ async function startCallbackServer(expectedState: string, timeoutMs: number): Pr }); server.on('error', (err) => { - reject(new CLIError(`Failed to start callback server: ${err.message}`, ExitCode.GENERAL)); + reject( + new CLIError( + `Couldn't start the local sign-in listener: ${err.message}`, + ExitCode.GENERAL, + `Check that port ${CALLBACK_PORT} is free, then run \`polylane auth login\` again` + ) + ); }); setTimeout(() => { server.close(); - reject(new CLIError('OAuth flow timed out', ExitCode.TIMEOUT)); + reject( + new CLIError('Sign-in timed out', ExitCode.TIMEOUT, 'Run `polylane auth login` to try again') + ); }, timeoutMs); }); } @@ -391,7 +400,11 @@ export async function oauthBrowserFlow( if (!tokenRes.ok) { const body = await tokenRes.text(); - throw new CLIError(`Token exchange failed: ${tokenRes.status} ${body}`, ExitCode.AUTH); + throw new CLIError( + `Sign-in did not complete: the token exchange returned ${tokenRes.status} ${body}`, + ExitCode.AUTH, + 'Run `polylane auth login` to try again' + ); } return (await tokenRes.json()) as OAuthTokenResponse; } @@ -463,12 +476,13 @@ export async function oauthDeviceCodeFlow(config: Config): Promise { diff --git a/src/auth/refresh.ts b/src/auth/refresh.ts index 8b004a6..598ff5e 100644 --- a/src/auth/refresh.ts +++ b/src/auth/refresh.ts @@ -31,9 +31,9 @@ export async function refreshToken( if (!res.ok) { throw new CLIError( - `Token refresh failed: ${res.status}`, + `Couldn't refresh your session (${res.status})`, ExitCode.AUTH, - 'Run `polylane auth login` to re-authenticate' + 'Run `polylane auth login` to sign in again' ); } diff --git a/src/auth/resolver.ts b/src/auth/resolver.ts index 632f57b..5165bae 100644 --- a/src/auth/resolver.ts +++ b/src/auth/resolver.ts @@ -38,9 +38,9 @@ export async function resolveCredential(config: Config): Promise { } throw new CLIError( - 'Not authenticated', + 'Not signed in.', ExitCode.AUTH, - 'Run `polylane auth login` to authenticate' + 'Run `polylane auth login`' ); } diff --git a/src/auth/setup.ts b/src/auth/setup.ts index 64b80ed..8a0dae9 100644 --- a/src/auth/setup.ts +++ b/src/auth/setup.ts @@ -10,15 +10,15 @@ export async function ensureAuth(config: Config): Promise { if (!isInteractive(config.nonInteractive)) { throw new CLIError( - 'Not authenticated', + 'Not signed in.', ExitCode.AUTH, - 'Run `polylane auth login` to authenticate' + 'Run `polylane auth login`' ); } throw new CLIError( - 'Not authenticated', + 'Not signed in.', ExitCode.AUTH, - 'Run `polylane auth login` to authenticate' + 'Run `polylane auth login`' ); } diff --git a/src/commands/artifact/delete.ts b/src/commands/artifact/delete.ts index f5b2c5c..1c92101 100644 --- a/src/commands/artifact/delete.ts +++ b/src/commands/artifact/delete.ts @@ -26,7 +26,7 @@ export const artifactDeleteCommand: Command = { false ); if (!ok) { - process.stderr.write('Cancelled\n'); + process.stderr.write('Cancelled. Nothing changed.\n'); return; } } diff --git a/src/commands/auth/login.ts b/src/commands/auth/login.ts index a7dcd3a..9465650 100644 --- a/src/commands/auth/login.ts +++ b/src/commands/auth/login.ts @@ -27,7 +27,7 @@ interface WorkspaceItem { } async function validateApiKey(config: Config, key: string): Promise { - const spinner = new Spinner('Validating API key…'); + const spinner = new Spinner('Checking the API key…'); spinner.start(); try { const res = await request( @@ -41,7 +41,11 @@ async function validateApiKey(config: Config, key: string): Promise { - const spinner = new Spinner('Fetching workspaces…'); + const spinner = new Spinner('Finding your workspaces…'); spinner.start(); try { const list = await requestJson<{ items: WorkspaceItem[]; count: number }>( diff --git a/src/commands/auth/logout.ts b/src/commands/auth/logout.ts index f711078..408d544 100644 --- a/src/commands/auth/logout.ts +++ b/src/commands/auth/logout.ts @@ -24,7 +24,7 @@ export const authLogoutCommand: Command = { true ); if (!ok) { - process.stderr.write('Cancelled\n'); + process.stderr.write('Cancelled. Nothing changed.\n'); return; } } diff --git a/src/commands/auth/signup.ts b/src/commands/auth/signup.ts index 418a46a..6d73b5e 100644 --- a/src/commands/auth/signup.ts +++ b/src/commands/auth/signup.ts @@ -79,15 +79,15 @@ function workspaceStep(landing?: Landing): string[] { case 'created': return [ landing.workspaceSlug - ? ` 1. Your first workspace ("${landing.workspaceSlug}") was created — set it as the default` - : ` 1. Your first workspace was created — set it as the default`, + ? ` 1. Your first workspace ("${landing.workspaceSlug}") was created: set it as the default` + : ` 1. Your first workspace was created: set it as the default`, ...setDefault, ]; case 'joined': return [ landing.workspaceSlug - ? ` 1. You joined the "${landing.workspaceSlug}" workspace — set it as the default` - : ` 1. You joined an existing workspace — set it as the default`, + ? ` 1. You joined the "${landing.workspaceSlug}" workspace: set it as the default` + : ` 1. You joined an existing workspace: set it as the default`, ...setDefault, ]; case 'existing': @@ -159,7 +159,7 @@ async function verifyEmail(config: Config, email: string, code: string): Promise const json = (await res.json()) as VerifyEmailEnvelope; if (res.status === 400) return null; if (!res.ok || !json.success) { - throw new CLIError(json.error?.detail ?? json.error?.message ?? 'Email verification failed', ExitCode.GENERAL); + throw new CLIError(json.error?.detail ?? json.error?.message ?? 'Email verification did not complete', ExitCode.GENERAL); } const expiresAt = parseSessionExpiresAt(res.headers.get('set-cookie')) ?? @@ -233,7 +233,7 @@ async function emailSignup(config: Config, args: Record): Promi }); const json = (await res.json()) as SignupEnvelope; if (!res.ok || !json.success) { - throw new CLIError(json.error?.detail ?? json.error?.message ?? 'Signup failed', ExitCode.GENERAL); + throw new CLIError(json.error?.detail ?? json.error?.message ?? 'Signup did not complete', ExitCode.GENERAL); } const { user, token } = json.result; if (!user) { @@ -300,7 +300,7 @@ async function emailSignup(config: Config, args: Record): Promi } } throw new CLIError( - 'Email verification failed', + 'Email verification did not complete', ExitCode.GENERAL, `Re-run \`polylane auth signup\` for a fresh code, or finish later with: polylane auth signup --email ${email} --code ` ); diff --git a/src/commands/autofix/watch.ts b/src/commands/autofix/watch.ts index 8765572..b7a2405 100644 --- a/src/commands/autofix/watch.ts +++ b/src/commands/autofix/watch.ts @@ -44,7 +44,7 @@ function printOutcome(row: AutofixRow): void { } const reason = row.skippedReason || row.failureReason; if (reason) { - process.stdout.write(`\n${label} (${repoLabel(row)})\n Polylane held off: ${reason}\n`); + process.stdout.write(`\n${label} (${repoLabel(row)})\n I held off: ${reason}\n`); return; } process.stdout.write(`\n${label} (${repoLabel(row)}): ${row.status ?? 'unknown'}\n`); @@ -52,7 +52,7 @@ function printOutcome(row: AutofixRow): void { export const autofixWatchCommand: Command = { name: 'autofix watch', - description: 'Watch pull requests Polylane is opening, live', + description: "Watch the pull requests I'm opening, live", options: [ { flag: '--id ', description: 'Watch one autofix instead of the connect-time pair', type: 'string' }, { flag: '--timeout ', description: `Stop waiting after this many seconds (default ${DEFAULT_TIMEOUT_SECONDS})`, type: 'number' }, @@ -80,7 +80,7 @@ export const autofixWatchCommand: Command = { } if (watched.size === 0) { - note('No pull request activity to watch yet. Connect a GitHub repository and Polylane opens its first pull request within minutes.'); + note("No pull request activity to watch yet. Connect a GitHub repository and I'll open a first pull request within minutes."); return; } @@ -96,7 +96,7 @@ export const autofixWatchCommand: Command = { } if ([...watched.values()].every((row) => isTerminal(row))) return; - const spinner = new Spinner('Watching Polylane work'); + const spinner = new Spinner('Working on the pull requests…'); const socketRef: { current: WorkspaceSocket | null } = { current: null }; const finished = new Promise((resolve) => { @@ -109,7 +109,7 @@ export const autofixWatchCommand: Command = { const timer = setTimeout(() => { spinner.stop(); - note("Still working. You'll get an email when the pull request opens, and it will be in your console."); + note("I'm still working. I'll email you when the pull request opens, and it will be in your console."); finish(); }, timeoutSeconds * 1000); timer.unref?.(); @@ -125,7 +125,7 @@ export const autofixWatchCommand: Command = { const onSigint = () => { clearTimeout(timer); spinner.stop(); - note('Stopped watching. Polylane keeps working; check your console or GitHub for the pull requests.'); + note('Stopped watching. I keep working; check your console or GitHub for the pull requests.'); finish(); }; process.once('SIGINT', onSigint); @@ -172,7 +172,7 @@ export const autofixWatchCommand: Command = { .catch(() => { clearTimeout(timer); spinner.stop(); - note("Live updates are unavailable right now. You'll get an email when the pull request opens."); + note("Live updates are unavailable right now. I'll email you when the pull request opens."); finish(); }); }); diff --git a/src/commands/cloud/connect.ts b/src/commands/cloud/connect.ts index 25aaf0a..8a66762 100644 --- a/src/commands/cloud/connect.ts +++ b/src/commands/cloud/connect.ts @@ -115,7 +115,7 @@ async function confirmBrowserConnect( process.stderr.write(`✓ Connected: ${account.alias || account.account}${detail ? ` (${detail})` : ''}\n`); } } else { - process.stderr.write('Not seeing the connection yet — check with `polylane cloud list`.\n'); + process.stderr.write("I don't see the connection yet. Check with `polylane cloud list`.\n"); } } @@ -131,7 +131,7 @@ function printConnectSuccess(config: Config, result: ConnectResult): void { process.stderr.write(`✓ Connected: ${account.alias || account.account}${detail ? ` (${detail})` : ''}\n`); } for (const failure of result.failures) { - process.stderr.write(`Failed to connect ${failure.account}: ${failure.message}\n`); + process.stderr.write(`I couldn't connect ${failure.account}: ${failure.message}\n`); } } @@ -238,7 +238,7 @@ async function connectProvider( } const answer = await promptConfirmOrBack( ctx, - 'Investigate alarms? (Polylane subscribes to all CloudWatch alarms in this account and investigates them when they fire)', + 'Investigate alarms? (I subscribe to all CloudWatch alarms in this account and investigate them when they fire)', true ); if (answer === BACK) return BACK; @@ -268,7 +268,7 @@ async function connectProvider( { message: 'Cloudflare API token', instructions: - 'Create an account-owned API token: Manage Account > Account API Tokens (you must be a Super Administrator on the account). The docs page has links that pre-fill the exact permissions Polylane needs; leave the pre-filled set as it is, or use the read-only variant. New account tokens start with cfat_ and are shown only once.', + 'Create an account-owned API token: Manage Account > Account API Tokens (you must be a Super Administrator on the account). The docs page has links that pre-fill the exact permissions I need; leave the pre-filled set as it is, or use the read-only variant. New account tokens start with cfat_ and are shown only once.', link: 'https://docs.polylane.com/integrations/cloudflare', linkLabel: 'How to create the token', }, diff --git a/src/commands/cloud/disconnect.ts b/src/commands/cloud/disconnect.ts index 6519370..b866eb9 100644 --- a/src/commands/cloud/disconnect.ts +++ b/src/commands/cloud/disconnect.ts @@ -28,7 +28,7 @@ export const cloudDisconnectCommand: Command = { false ); if (!confirmed) { - process.stderr.write('Cancelled\n'); + process.stderr.write('Cancelled. Nothing changed.\n'); return; } } diff --git a/src/commands/help.ts b/src/commands/help.ts index b397cee..cfd66dc 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -1,6 +1,6 @@ import type { Command } from '../command'; import type { Config } from '../config/schema'; -import { registry, renderHelp } from '../registry'; +import { registry, renderHelp, unknownCommandMessage } from '../registry'; import { CLIError } from '../errors/base'; import { ExitCode } from '../errors/codes'; @@ -16,7 +16,7 @@ export const helpCommand: Command = { const resolved = registry.resolve(positional); if (!resolved) { throw new CLIError( - `Unknown command: polylane ${positional.join(' ')}`, + unknownCommandMessage(positional, registry.suggestCommand(positional)), ExitCode.USAGE, 'Run `polylane --help` to list commands' ); diff --git a/src/commands/helpers.ts b/src/commands/helpers.ts index f286cb7..3025a30 100644 --- a/src/commands/helpers.ts +++ b/src/commands/helpers.ts @@ -290,7 +290,7 @@ export async function waitForBrowserCompletion( } const spinner = new Spinner(`Waiting for ${opts.waitingFor}… (Ctrl+C to stop waiting)`); const onSigint = (): void => { - spinner.stop(`Stopped waiting — the browser setup continues on its own. ${opts.interruptHint}`); + spinner.stop(`Stopped waiting. The browser setup continues on its own. ${opts.interruptHint}`); process.exit(0); }; spinner.start(); diff --git a/src/commands/integration/connect.ts b/src/commands/integration/connect.ts index 0e86ac4..abdc200 100644 --- a/src/commands/integration/connect.ts +++ b/src/commands/integration/connect.ts @@ -52,9 +52,9 @@ const TYPE_OPTIONS: Array<{ value: ConnectableType; label: string; hint: string { value: 'honeycomb', label: 'Honeycomb', hint: 'configuration API key' }, { value: 'axiom', label: 'Axiom', hint: 'API token' }, { value: 'betterstack', label: 'Better Stack', hint: 'global, Uptime and Telemetry tokens' }, - { value: 'devin', label: 'Devin', hint: 'API key — coding agent' }, - { value: 'cursor', label: 'Cursor', hint: 'API key — coding agent' }, - { value: 'factory', label: 'Factory', hint: 'API key — coding agent' }, + { value: 'devin', label: 'Devin', hint: 'API key · coding agent' }, + { value: 'cursor', label: 'Cursor', hint: 'API key · coding agent' }, + { value: 'factory', label: 'Factory', hint: 'API key · coding agent' }, { value: 'mcp', label: 'MCP server', hint: 'any MCP server by URL' }, ]; @@ -134,7 +134,7 @@ async function confirmBrowserConnect( if (found) { process.stderr.write(`✓ ${label} connected: ${found.name}\n`); } else { - process.stderr.write('Not seeing the connection yet — check with `polylane integration list`.\n'); + process.stderr.write("I don't see the connection yet. Check with `polylane integration list`.\n"); } } @@ -213,7 +213,7 @@ async function connectMcp( async () => { if (authMethod !== 'bearer' || bearerToken !== undefined) return SKIPPED; note( - 'Paste the bare token, without the "Bearer " prefix. Polylane adds that.\nGet it from whoever runs this MCP server. MCP defines no standard console or scope names.\nThe token needs whatever the server requires for tools/list and tools/call. Polylane calls nothing else.', + 'Paste the bare token, without the "Bearer " prefix. I add that.\nGet it from whoever runs this MCP server. MCP defines no standard console or scope names.\nThe token needs whatever the server requires for tools/list and tools/call. I call nothing else.', 'Bearer token' ); const token = await promptPasswordOrBack(ctx, 'Bearer token'); @@ -277,7 +277,7 @@ async function connectWithCredentials( let apiKey = ''; let appKey = ''; const ok = await runSteps([ - choiceStep(config, args, 'site', '--site', 'Datadog site — the one in your Datadog URL', DATADOG_SITES, (v) => { + choiceStep(config, args, 'site', '--site', 'Datadog site: the one in your Datadog URL', DATADOG_SITES, (v) => { site = v; }), secretStep( @@ -324,7 +324,7 @@ async function connectWithCredentials( args, 'region', '--region', - 'Honeycomb region — the one in your Honeycomb URL', + 'Honeycomb region: the one in your Honeycomb URL', [ { value: 'us', label: 'US (api.honeycomb.io)' }, { value: 'eu', label: 'EU (api.eu1.honeycomb.io)' }, @@ -362,7 +362,7 @@ async function connectWithCredentials( args, 'region', '--region', - 'Axiom edge deployment region — see your organization settings (https://app.axiom.co/settings/org)', + 'Axiom edge deployment region: see your organization settings (https://app.axiom.co/settings/org)', [ { value: 'us-east-1', label: 'US East 1' }, { value: 'eu-central-1', label: 'EU Central 1' }, @@ -475,7 +475,7 @@ async function connectWithCredentials( } const answer = await promptConfirmOrBack( { nonInteractive: config.nonInteractive }, - `Use ${agent.name} for all autofixes? (instead of the Polylane executor — you can change this later)`, + `Use ${agent.name} for all autofixes? (instead of the Polylane executor; you can change this later)`, true ); if (answer === BACK) return BACK; diff --git a/src/commands/integration/disconnect.ts b/src/commands/integration/disconnect.ts index c833d3e..b7b3288 100644 --- a/src/commands/integration/disconnect.ts +++ b/src/commands/integration/disconnect.ts @@ -28,7 +28,7 @@ export const integrationDisconnectCommand: Command = { false ); if (!confirmed) { - process.stderr.write('Cancelled\n'); + process.stderr.write('Cancelled. Nothing changed.\n'); return; } } diff --git a/src/commands/memory/delete.ts b/src/commands/memory/delete.ts index 9a4e9ed..8e2e005 100644 --- a/src/commands/memory/delete.ts +++ b/src/commands/memory/delete.ts @@ -22,7 +22,7 @@ export const memoryDeleteCommand: Command = { false ); if (!ok) { - process.stderr.write('Cancelled\n'); + process.stderr.write('Cancelled. Nothing changed.\n'); return; } } diff --git a/src/commands/note/delete.ts b/src/commands/note/delete.ts index 91257d0..2715381 100644 --- a/src/commands/note/delete.ts +++ b/src/commands/note/delete.ts @@ -22,7 +22,7 @@ export const noteDeleteCommand: Command = { false ); if (!ok) { - process.stderr.write('Cancelled\n'); + process.stderr.write('Cancelled. Nothing changed.\n'); return; } } diff --git a/src/commands/note/global-clear.ts b/src/commands/note/global-clear.ts index 4f97b1d..46eeaa8 100644 --- a/src/commands/note/global-clear.ts +++ b/src/commands/note/global-clear.ts @@ -20,7 +20,7 @@ export const noteGlobalClearCommand: Command = { false ); if (!ok) { - process.stderr.write('Cancelled\n'); + process.stderr.write('Cancelled. Nothing changed.\n'); return; } } diff --git a/src/commands/scan.ts b/src/commands/scan.ts index 9624e40..3ada5dd 100644 --- a/src/commands/scan.ts +++ b/src/commands/scan.ts @@ -143,7 +143,7 @@ export function renderRiskLines( limit = MAX_RISK_LINES ): string[] { if (ranked.length === 0) { - return ['No key risks found.']; + return ['I found no key risks.']; } const lines = [color(`Key risks (${ranked.length})`, '1', useColor)]; for (const risk of ranked.slice(0, limit)) { @@ -296,7 +296,7 @@ async function runRiskNavigator( if (options.length === 0) return; const choice = await promptSelectOrBack( ctx, - 'Investigate a risk (Enter creates an issue; Polylane investigates in the background)', + 'Investigate a risk (Enter creates an issue; I investigate in the background)', options, 'Done' ); @@ -310,7 +310,7 @@ async function runRiskNavigator( ? issueConsoleUrl(risk.reportHtmlUrl ?? scanConsoleUrl, knownIssueId) : null; note( - 'An issue is already open for this risk and Polylane is investigating it.' + + "An issue is already open for this risk and I'm investigating it." + (url ? `\n\nView the issue in the console:\n ${url}` : ''), 'Already under investigation' ); @@ -333,7 +333,7 @@ async function runRiskNavigator( spinner.stop(); const message = err instanceof Error ? err.message : String(err); note( - `The issue was not created (${message}).\nPick the risk again to retry, or investigate it from the console.`, + `I couldn't create the issue (${message}).\nPick the risk again to retry, or investigate it from the console.`, 'Nothing changed' ); continue; @@ -345,7 +345,7 @@ async function runRiskNavigator( : null; note( `Issue created for "${risk.title}".\n` + - 'Polylane is investigating this risk in the background and will post what it finds on the issue. You can keep working; nothing else is needed from you.' + + "I'm investigating this risk in the background and will post what I find on the issue. You can keep working; nothing else is needed from you." + (url ? `\n\nView the issue in the console:\n ${url}` : ''), 'Investigation started' ); @@ -409,7 +409,7 @@ export const scanCommand: Command = { outputJson({ workspaceId, targets: 0, reports: [], risks: [], consoleUrl: null }); return; } - say('Nothing to scan — no cloud accounts or integrations are connected.'); + say('Nothing to scan yet: no cloud accounts or integrations are connected.'); say('Connect one with `polylane cloud connect` or `polylane integration connect`.'); return; } @@ -439,12 +439,12 @@ export const scanCommand: Command = { const consoleUrl = resolveConsoleUrl(results) ?? (await scansFallbackUrl(cfg, api, workspaceId)); for (const f of failed) { - say(color(`Scan failed for ${f.target.label}${f.error ? ` (${f.error})` : ''}`, '33', useColor)); + say(color(`I couldn't finish the ${f.target.label} scan${f.error ? ` (${f.error})` : ''}.`, '33', useColor)); } if (timedOut.length > 0) { say( color( - `${timedOut.length} scan${timedOut.length === 1 ? ' is' : 's are'} still running — view progress in the console.`, + `I'm still running ${timedOut.length} scan${timedOut.length === 1 ? '' : 's'} in the background; view progress in the console.`, '2', useColor ) diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 25e89f0..42bf30b 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -443,7 +443,7 @@ export const setupCommand: Command = { : AGENTS.filter((a) => a.detect(home)); if (selected.length === 0) { - say('No coding agents detected.'); + say("I didn't find any coding agents on this machine."); say(`Configure one anyway with --agent (${AGENTS.map((a) => a.id).join(', ')}).`); return; } @@ -455,7 +455,7 @@ export const setupCommand: Command = { let outcomes: WriteOutcome[]; if (project) { if (!agent.project) { - say(`${agent.id}: skipped — no project-level convention; run without --project`); + say(`${agent.id}: skipped, no project-level convention; run without --project`); continue; } outcomes = agent.project(process.cwd(), config.dryRun); @@ -471,7 +471,7 @@ export const setupCommand: Command = { const detail = outcome.detail ? ` (${outcome.detail})` : ''; say(`${agent.id}: ${outcome.label} ${state}: ${outcome.path}${detail}`); if (outcome.needsManualStep) { - say(`${agent.id}: register it manually — see https://docs.polylane.com/coding-agents/platform-mcp`); + say(`${agent.id}: register it manually: https://docs.polylane.com/coding-agents/platform-mcp`); } } } diff --git a/src/commands/thread/ask.ts b/src/commands/thread/ask.ts index 1359897..db13069 100644 --- a/src/commands/thread/ask.ts +++ b/src/commands/thread/ask.ts @@ -74,7 +74,7 @@ export const threadAskCommand: Command = { process.stderr.write(`Thread: ${url}\n\n`); } - const spinner = !config.quiet && textMode ? new Spinner('Waiting for the reply…') : null; + const spinner = !config.quiet && textMode ? new Spinner('Working on the reply…') : null; if (spinner) spinner.start(); let spinnerStopped = false; const stopSpinner = (): void => { @@ -111,13 +111,13 @@ export const threadAskCommand: Command = { if (result.status === 'timeout') { if (!config.quiet) { - process.stderr.write(`The reply is still being generated. View it at:\n ${url}\n`); + process.stderr.write(`I'm still writing the reply. View it at:\n ${url}\n`); } return; } if (result.text.length === 0 && !config.quiet) { - process.stderr.write(`The agent finished without a text reply. View the thread at:\n ${url}\n`); + process.stderr.write(`I finished without a text reply. View the thread at:\n ${url}\n`); } }, }; diff --git a/src/commands/thread/continue.ts b/src/commands/thread/continue.ts index fa18cda..538a5b6 100644 --- a/src/commands/thread/continue.ts +++ b/src/commands/thread/continue.ts @@ -55,7 +55,7 @@ export const threadContinueCommand: Command = { process.stderr.write(`Thread: ${url}\n\n`); } - const spinner = !config.quiet && textMode ? new Spinner('Waiting for the reply…') : null; + const spinner = !config.quiet && textMode ? new Spinner('Working on the reply…') : null; if (spinner) spinner.start(); let spinnerStopped = false; const stopSpinner = (): void => { @@ -93,13 +93,13 @@ export const threadContinueCommand: Command = { if (result.status === 'timeout') { if (!config.quiet) { - process.stderr.write(`The reply is still being generated. View it at:\n ${url}\n`); + process.stderr.write(`I'm still writing the reply. View it at:\n ${url}\n`); } return; } if (result.text.length === 0 && !config.quiet) { - process.stderr.write(`The agent finished without a text reply. View the thread at:\n ${url}\n`); + process.stderr.write(`I finished without a text reply. View the thread at:\n ${url}\n`); } }, }; diff --git a/src/commands/update.ts b/src/commands/update.ts index 91aa626..9182582 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -50,7 +50,7 @@ export const updateCommand: Command = { const latest = await fetchLatest(); if (!latest) { - process.stderr.write("Couldn't reach npm to check for updates\n"); + process.stderr.write("I couldn't reach npm to check for updates. Try again later.\n"); return; } @@ -61,7 +61,7 @@ export const updateCommand: Command = { process.stderr.write(`Run: npm install -g @coreplane/polylane@${latest}\n`); process.stderr.write(` or: brew upgrade polylane\n`); } else { - process.stderr.write(`Up to date\n`); + process.stderr.write(`You're up to date.\n`); } }, }; diff --git a/src/errors/api.ts b/src/errors/api.ts index 38cbc67..523e4d9 100644 --- a/src/errors/api.ts +++ b/src/errors/api.ts @@ -8,14 +8,14 @@ export interface ApiErrorPayload { export function mapApiError(status: number, error: ApiErrorPayload | null): CLIError { const detail = error?.detail; - const message = error?.message ?? 'Unknown error'; + const message = error?.message ?? `The request did not succeed (${status})`; switch (status) { case 400: return new CLIError(detail || 'Bad request', ExitCode.USAGE); case 401: return new CLIError( - detail || 'Not authenticated', + detail || 'Not signed in.', ExitCode.AUTH, 'Run `polylane auth login`' ); diff --git a/src/errors/handler.ts b/src/errors/handler.ts index b0a81ce..e17ff3e 100644 --- a/src/errors/handler.ts +++ b/src/errors/handler.ts @@ -23,7 +23,7 @@ function wrapUnknown(err: unknown): CLIError { } if (hasCode(err, 'ENOTFOUND')) { return new CLIError( - `DNS lookup failed: ${msg}`, + `Couldn't resolve the API host: ${msg}`, ExitCode.NETWORK, 'Check your network connection and the --domain flag' ); diff --git a/src/main.ts b/src/main.ts index ff1abcc..a3148ff 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,6 +12,7 @@ import { renderGroupHelp, renderCommandHelp, buildStatusLine, + unknownCommandMessage, } from './registry'; import type { Config } from './config/schema'; import type { Credential } from './auth/types'; @@ -103,7 +104,7 @@ async function run(): Promise { const config = loadConfig(flags as GlobalFlags); handleError( new CLIError( - `Unknown command: polylane ${commandPath.join(' ')}`, + unknownCommandMessage(commandPath, registry.suggestCommand(commandPath)), ExitCode.USAGE, 'Run `polylane --help` to list commands' ), diff --git a/src/registry.ts b/src/registry.ts index 582104c..2be66aa 100644 --- a/src/registry.ts +++ b/src/registry.ts @@ -107,6 +107,22 @@ export class CommandRegistry { return [...this.all]; } + suggestCommand(path: string[]): string | null { + const input = path[0]; + if (!input) return null; + const candidates = new Set(); + for (const cmd of this.all) { + candidates.add(cmd.name.split(/\s+/)[0]!); + } + let best: { name: string; distance: number } | null = null; + for (const candidate of candidates) { + const distance = editDistance(input.toLowerCase(), candidate.toLowerCase()); + if (distance > 2 || distance * 2 > candidate.length) continue; + if (!best || distance < best.distance) best = { name: candidate, distance }; + } + return best ? best.name : null; + } + getResourceGroups(): Array<{ resource: string; commands: Command[]; meta: ResourceGroup }> { const byResource = new Map(); for (const cmd of this.all) { @@ -129,6 +145,30 @@ export class CommandRegistry { } } +function editDistance(a: string, b: string): number { + const prev = new Array(b.length + 1); + for (let j = 0; j <= b.length; j++) prev[j] = j; + for (let i = 1; i <= a.length; i++) { + let diagonal = prev[0]!; + prev[0] = i; + for (let j = 1; j <= b.length; j++) { + const next = Math.min( + prev[j]! + 1, + prev[j - 1]! + 1, + diagonal + (a[i - 1] === b[j - 1] ? 0 : 1) + ); + diagonal = prev[j]!; + prev[j] = next; + } + } + return prev[b.length]!; +} + +export function unknownCommandMessage(path: string[], suggestion: string | null): string { + const base = `Unknown command: polylane ${path.join(' ')}`; + return suggestion ? `${base}. Closest match: polylane ${suggestion}.` : base; +} + export const registry = new CommandRegistry(); function color(s: string, code: string, useColor: boolean): string { @@ -183,7 +223,7 @@ export function renderRootHelp( lines.push(dim(statusMessage)); lines.push(''); } - lines.push(`${bold('polylane')} — CLI for the Polylane platform`); + lines.push(`${bold('polylane')}: investigate production issues from your terminal`); lines.push(''); lines.push(`${bold('Usage:')} polylane [options]`); lines.push(''); diff --git a/test/errors.test.ts b/test/errors.test.ts index 3464dc5..602116a 100644 --- a/test/errors.test.ts +++ b/test/errors.test.ts @@ -30,7 +30,7 @@ describe('mapApiError', () => { it('maps 401 -> AUTH with hint', () => { const err = mapApiError(401, { message: 'Unauthorized' }); assert.equal(err.exitCode, ExitCode.AUTH); - assert.equal(err.message, 'Not authenticated'); + assert.equal(err.message, 'Not signed in.'); assert.ok(err.hint?.includes('polylane auth login')); }); diff --git a/test/registry.test.ts b/test/registry.test.ts index 72a6ea5..574c592 100644 --- a/test/registry.test.ts +++ b/test/registry.test.ts @@ -1,6 +1,6 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; -import { CommandRegistry } from '../src/registry'; +import { CommandRegistry, unknownCommandMessage } from '../src/registry'; import type { Command } from '../src/command'; const makeCmd = (name: string): Command => ({ @@ -44,6 +44,31 @@ describe('CommandRegistry', () => { assert.equal(resolved.command.name, 'only sub'); }); + it('suggests the closest command for a typo', () => { + const r = new CommandRegistry(); + r.register(makeCmd('scan')); + r.register(makeCmd('setup')); + r.register(makeCmd('workspace list')); + assert.equal(r.suggestCommand(['scna']), 'scan'); + assert.equal(r.suggestCommand(['workspce', 'list']), 'workspace'); + }); + + it('suggests nothing when no command is close', () => { + const r = new CommandRegistry(); + r.register(makeCmd('scan')); + r.register(makeCmd('workspace list')); + assert.equal(r.suggestCommand(['frobnicate']), null); + assert.equal(r.suggestCommand([]), null); + }); + + it('renders the unknown-command message with and without a suggestion', () => { + assert.equal( + unknownCommandMessage(['scna'], 'scan'), + 'Unknown command: polylane scna. Closest match: polylane scan.' + ); + assert.equal(unknownCommandMessage(['frobnicate'], null), 'Unknown command: polylane frobnicate'); + }); + it('groups commands by first path segment', () => { const r = new CommandRegistry(); r.register(makeCmd('workspace list')); diff --git a/test/resolve.test.ts b/test/resolve.test.ts index b27421b..e840408 100644 --- a/test/resolve.test.ts +++ b/test/resolve.test.ts @@ -91,7 +91,7 @@ describe('unknown command handling', () => { helpCommand.execute(mockConfig(), {}, { _: ['nope', 'sub'] }), (err: unknown) => { assert.ok(err instanceof CLIError); - assert.equal(err.message, 'Unknown command: polylane nope sub'); + assert.equal(err.message, 'Unknown command: polylane nope sub. Closest match: polylane note.'); assert.equal(err.exitCode, ExitCode.USAGE); assert.ok(err.hint?.includes('polylane --help')); return true; diff --git a/test/scan.test.ts b/test/scan.test.ts index 78c0d56..8fb5108 100644 --- a/test/scan.test.ts +++ b/test/scan.test.ts @@ -243,7 +243,7 @@ describe('renderRiskLines', () => { }); it('renders a single line when there are no risks', () => { - assert.deepEqual(renderRiskLines([], true), ['No key risks found.']); + assert.deepEqual(renderRiskLines([], true), ['I found no key risks.']); }); it('colors severity tags when enabled', () => { From b2be15634c35f5a1fc3a70be5f48f4bb63fc0bc1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 22:34:37 +0000 Subject: [PATCH 2/2] fix: drop first-person voice, keep the neutral copy improvements Per review on #30: revert every string where the CLI speaks as "I" back to main's wording or a neutral equivalent, while keeping the closest-match suggestions, "Not signed in.", "Cancelled. Nothing changed.", em-dash fixes, no-"failed" phrasing, and next-step hints. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RpvmDmwuwen1nmgCQbvD4n --- src/commands/autofix/watch.ts | 14 +++++++------- src/commands/cloud/connect.ts | 8 ++++---- src/commands/integration/connect.ts | 4 ++-- src/commands/scan.ts | 14 +++++++------- src/commands/setup.ts | 2 +- src/commands/thread/ask.ts | 4 ++-- src/commands/thread/continue.ts | 4 ++-- src/commands/update.ts | 2 +- test/scan.test.ts | 2 +- 9 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/commands/autofix/watch.ts b/src/commands/autofix/watch.ts index b7a2405..8765572 100644 --- a/src/commands/autofix/watch.ts +++ b/src/commands/autofix/watch.ts @@ -44,7 +44,7 @@ function printOutcome(row: AutofixRow): void { } const reason = row.skippedReason || row.failureReason; if (reason) { - process.stdout.write(`\n${label} (${repoLabel(row)})\n I held off: ${reason}\n`); + process.stdout.write(`\n${label} (${repoLabel(row)})\n Polylane held off: ${reason}\n`); return; } process.stdout.write(`\n${label} (${repoLabel(row)}): ${row.status ?? 'unknown'}\n`); @@ -52,7 +52,7 @@ function printOutcome(row: AutofixRow): void { export const autofixWatchCommand: Command = { name: 'autofix watch', - description: "Watch the pull requests I'm opening, live", + description: 'Watch pull requests Polylane is opening, live', options: [ { flag: '--id ', description: 'Watch one autofix instead of the connect-time pair', type: 'string' }, { flag: '--timeout ', description: `Stop waiting after this many seconds (default ${DEFAULT_TIMEOUT_SECONDS})`, type: 'number' }, @@ -80,7 +80,7 @@ export const autofixWatchCommand: Command = { } if (watched.size === 0) { - note("No pull request activity to watch yet. Connect a GitHub repository and I'll open a first pull request within minutes."); + note('No pull request activity to watch yet. Connect a GitHub repository and Polylane opens its first pull request within minutes.'); return; } @@ -96,7 +96,7 @@ export const autofixWatchCommand: Command = { } if ([...watched.values()].every((row) => isTerminal(row))) return; - const spinner = new Spinner('Working on the pull requests…'); + const spinner = new Spinner('Watching Polylane work'); const socketRef: { current: WorkspaceSocket | null } = { current: null }; const finished = new Promise((resolve) => { @@ -109,7 +109,7 @@ export const autofixWatchCommand: Command = { const timer = setTimeout(() => { spinner.stop(); - note("I'm still working. I'll email you when the pull request opens, and it will be in your console."); + note("Still working. You'll get an email when the pull request opens, and it will be in your console."); finish(); }, timeoutSeconds * 1000); timer.unref?.(); @@ -125,7 +125,7 @@ export const autofixWatchCommand: Command = { const onSigint = () => { clearTimeout(timer); spinner.stop(); - note('Stopped watching. I keep working; check your console or GitHub for the pull requests.'); + note('Stopped watching. Polylane keeps working; check your console or GitHub for the pull requests.'); finish(); }; process.once('SIGINT', onSigint); @@ -172,7 +172,7 @@ export const autofixWatchCommand: Command = { .catch(() => { clearTimeout(timer); spinner.stop(); - note("Live updates are unavailable right now. I'll email you when the pull request opens."); + note("Live updates are unavailable right now. You'll get an email when the pull request opens."); finish(); }); }); diff --git a/src/commands/cloud/connect.ts b/src/commands/cloud/connect.ts index 8a66762..6e3b62e 100644 --- a/src/commands/cloud/connect.ts +++ b/src/commands/cloud/connect.ts @@ -115,7 +115,7 @@ async function confirmBrowserConnect( process.stderr.write(`✓ Connected: ${account.alias || account.account}${detail ? ` (${detail})` : ''}\n`); } } else { - process.stderr.write("I don't see the connection yet. Check with `polylane cloud list`.\n"); + process.stderr.write('Not seeing the connection yet. Check with `polylane cloud list`.\n'); } } @@ -131,7 +131,7 @@ function printConnectSuccess(config: Config, result: ConnectResult): void { process.stderr.write(`✓ Connected: ${account.alias || account.account}${detail ? ` (${detail})` : ''}\n`); } for (const failure of result.failures) { - process.stderr.write(`I couldn't connect ${failure.account}: ${failure.message}\n`); + process.stderr.write(`Couldn't connect ${failure.account}: ${failure.message}\n`); } } @@ -238,7 +238,7 @@ async function connectProvider( } const answer = await promptConfirmOrBack( ctx, - 'Investigate alarms? (I subscribe to all CloudWatch alarms in this account and investigate them when they fire)', + 'Investigate alarms? (Polylane subscribes to all CloudWatch alarms in this account and investigates them when they fire)', true ); if (answer === BACK) return BACK; @@ -268,7 +268,7 @@ async function connectProvider( { message: 'Cloudflare API token', instructions: - 'Create an account-owned API token: Manage Account > Account API Tokens (you must be a Super Administrator on the account). The docs page has links that pre-fill the exact permissions I need; leave the pre-filled set as it is, or use the read-only variant. New account tokens start with cfat_ and are shown only once.', + 'Create an account-owned API token: Manage Account > Account API Tokens (you must be a Super Administrator on the account). The docs page has links that pre-fill the exact permissions Polylane needs; leave the pre-filled set as it is, or use the read-only variant. New account tokens start with cfat_ and are shown only once.', link: 'https://docs.polylane.com/integrations/cloudflare', linkLabel: 'How to create the token', }, diff --git a/src/commands/integration/connect.ts b/src/commands/integration/connect.ts index abdc200..64a0c0d 100644 --- a/src/commands/integration/connect.ts +++ b/src/commands/integration/connect.ts @@ -134,7 +134,7 @@ async function confirmBrowserConnect( if (found) { process.stderr.write(`✓ ${label} connected: ${found.name}\n`); } else { - process.stderr.write("I don't see the connection yet. Check with `polylane integration list`.\n"); + process.stderr.write('Not seeing the connection yet. Check with `polylane integration list`.\n'); } } @@ -213,7 +213,7 @@ async function connectMcp( async () => { if (authMethod !== 'bearer' || bearerToken !== undefined) return SKIPPED; note( - 'Paste the bare token, without the "Bearer " prefix. I add that.\nGet it from whoever runs this MCP server. MCP defines no standard console or scope names.\nThe token needs whatever the server requires for tools/list and tools/call. I call nothing else.', + 'Paste the bare token, without the "Bearer " prefix. Polylane adds that.\nGet it from whoever runs this MCP server. MCP defines no standard console or scope names.\nThe token needs whatever the server requires for tools/list and tools/call. Polylane calls nothing else.', 'Bearer token' ); const token = await promptPasswordOrBack(ctx, 'Bearer token'); diff --git a/src/commands/scan.ts b/src/commands/scan.ts index 3ada5dd..2aa16e7 100644 --- a/src/commands/scan.ts +++ b/src/commands/scan.ts @@ -143,7 +143,7 @@ export function renderRiskLines( limit = MAX_RISK_LINES ): string[] { if (ranked.length === 0) { - return ['I found no key risks.']; + return ['No key risks found.']; } const lines = [color(`Key risks (${ranked.length})`, '1', useColor)]; for (const risk of ranked.slice(0, limit)) { @@ -296,7 +296,7 @@ async function runRiskNavigator( if (options.length === 0) return; const choice = await promptSelectOrBack( ctx, - 'Investigate a risk (Enter creates an issue; I investigate in the background)', + 'Investigate a risk (Enter creates an issue; Polylane investigates in the background)', options, 'Done' ); @@ -310,7 +310,7 @@ async function runRiskNavigator( ? issueConsoleUrl(risk.reportHtmlUrl ?? scanConsoleUrl, knownIssueId) : null; note( - "An issue is already open for this risk and I'm investigating it." + + 'An issue is already open for this risk and Polylane is investigating it.' + (url ? `\n\nView the issue in the console:\n ${url}` : ''), 'Already under investigation' ); @@ -333,7 +333,7 @@ async function runRiskNavigator( spinner.stop(); const message = err instanceof Error ? err.message : String(err); note( - `I couldn't create the issue (${message}).\nPick the risk again to retry, or investigate it from the console.`, + `The issue was not created (${message}).\nPick the risk again to retry, or investigate it from the console.`, 'Nothing changed' ); continue; @@ -345,7 +345,7 @@ async function runRiskNavigator( : null; note( `Issue created for "${risk.title}".\n` + - "I'm investigating this risk in the background and will post what I find on the issue. You can keep working; nothing else is needed from you." + + 'Polylane is investigating this risk in the background and will post what it finds on the issue. You can keep working; nothing else is needed from you.' + (url ? `\n\nView the issue in the console:\n ${url}` : ''), 'Investigation started' ); @@ -439,12 +439,12 @@ export const scanCommand: Command = { const consoleUrl = resolveConsoleUrl(results) ?? (await scansFallbackUrl(cfg, api, workspaceId)); for (const f of failed) { - say(color(`I couldn't finish the ${f.target.label} scan${f.error ? ` (${f.error})` : ''}.`, '33', useColor)); + say(color(`The ${f.target.label} scan didn't finish${f.error ? ` (${f.error})` : ''}.`, '33', useColor)); } if (timedOut.length > 0) { say( color( - `I'm still running ${timedOut.length} scan${timedOut.length === 1 ? '' : 's'} in the background; view progress in the console.`, + `${timedOut.length} scan${timedOut.length === 1 ? ' is' : 's are'} still running; view progress in the console.`, '2', useColor ) diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 42bf30b..95951ae 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -443,7 +443,7 @@ export const setupCommand: Command = { : AGENTS.filter((a) => a.detect(home)); if (selected.length === 0) { - say("I didn't find any coding agents on this machine."); + say('No coding agents detected.'); say(`Configure one anyway with --agent (${AGENTS.map((a) => a.id).join(', ')}).`); return; } diff --git a/src/commands/thread/ask.ts b/src/commands/thread/ask.ts index db13069..6c45b9f 100644 --- a/src/commands/thread/ask.ts +++ b/src/commands/thread/ask.ts @@ -111,13 +111,13 @@ export const threadAskCommand: Command = { if (result.status === 'timeout') { if (!config.quiet) { - process.stderr.write(`I'm still writing the reply. View it at:\n ${url}\n`); + process.stderr.write(`The reply is still being generated. View it at:\n ${url}\n`); } return; } if (result.text.length === 0 && !config.quiet) { - process.stderr.write(`I finished without a text reply. View the thread at:\n ${url}\n`); + process.stderr.write(`The agent finished without a text reply. View the thread at:\n ${url}\n`); } }, }; diff --git a/src/commands/thread/continue.ts b/src/commands/thread/continue.ts index 538a5b6..b83f0f8 100644 --- a/src/commands/thread/continue.ts +++ b/src/commands/thread/continue.ts @@ -93,13 +93,13 @@ export const threadContinueCommand: Command = { if (result.status === 'timeout') { if (!config.quiet) { - process.stderr.write(`I'm still writing the reply. View it at:\n ${url}\n`); + process.stderr.write(`The reply is still being generated. View it at:\n ${url}\n`); } return; } if (result.text.length === 0 && !config.quiet) { - process.stderr.write(`I finished without a text reply. View the thread at:\n ${url}\n`); + process.stderr.write(`The agent finished without a text reply. View the thread at:\n ${url}\n`); } }, }; diff --git a/src/commands/update.ts b/src/commands/update.ts index 9182582..c0876b6 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -50,7 +50,7 @@ export const updateCommand: Command = { const latest = await fetchLatest(); if (!latest) { - process.stderr.write("I couldn't reach npm to check for updates. Try again later.\n"); + process.stderr.write("Couldn't reach npm to check for updates. Try again later.\n"); return; } diff --git a/test/scan.test.ts b/test/scan.test.ts index 8fb5108..78c0d56 100644 --- a/test/scan.test.ts +++ b/test/scan.test.ts @@ -243,7 +243,7 @@ describe('renderRiskLines', () => { }); it('renders a single line when there are no risks', () => { - assert.deepEqual(renderRiskLines([], true), ['I found no key risks.']); + assert.deepEqual(renderRiskLines([], true), ['No key risks found.']); }); it('colors severity tags when enabled', () => {