Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .claude/skills/ably-codebase-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ Launch these agents **in parallel**. Each agent gets a focused mandate and uses
- `chalk.red("✗")` used as visual indicators (not error handling) is exempt
- Component strings must be camelCase for consistency in verbose logs and JSON envelopes

**Error hints (`src/utils/errors.ts`):**
7. **Grep** for double-quoted CLI commands inside hint strings (e.g., `"ably login"`) — must use single quotes to avoid `\"` in JSON output
8. **Check** that long hints use `\n` for manual line breaks — oclif auto-wraps at awkward positions
9. **Verify** that `this.fail()` in base-command.ts strips `\n` from hints in JSON output (`.replaceAll("\n", " ")`)

### Agent 3: Output Formatting Sweep

**Goal:** Verify all human output uses the correct format helpers and is JSON-guarded.
Expand Down
17 changes: 17 additions & 0 deletions .claude/skills/ably-new-command/references/patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -780,3 +780,20 @@ This emits two NDJSON lines in `--json` mode:
| Collection result (list/get-all) | Plural noun | `keys`, `apps`, `rules`, `cursors` |

The key name should match the SDK/domain terminology, not be generic. Use `message` not `data`, `cursor` not `item`.

## Error Hints

Add actionable hints for known Ably error codes in `src/utils/errors.ts`. `this.fail()` automatically looks up and appends these via `getFriendlyAblyErrorHint(code)`.

**Formatting rules:**
- Use `\n` to control line wrapping in terminal output — oclif auto-wraps long lines at awkward positions, so manual `\n` gives precise control
- `\n` is automatically stripped (replaced with space) for `--json` / `--pretty-json` output, keeping hints as clean single-line strings
- Use single quotes for CLI command references (e.g., `'ably apps rules list'`) — double quotes become `\"` in JSON

```typescript
// src/utils/errors.ts
const hints: Record<number, string> = {
93002:
"This channel requires mutableMessages to be enabled.\nRun 'ably apps rules list' to check your channel rules,\nor enable it with 'ably apps rules create' or 'ably apps rules update'.",
};
```
6 changes: 6 additions & 0 deletions .claude/skills/ably-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ Apply the full checklist from the `ably-new-command` skill. These deserve the mo
2. **Grep** for new helper functions and check naming conventions (`format*` prefix for output helpers)
3. **Grep** for `this\.error\(` — should only be used inside `fail()`, not directly

### For changed error hints (`src/utils/errors.ts`)

1. **Grep** for `"` (double quotes) inside hint strings — must use single quotes for CLI command references (double quotes become `\"` in JSON output)
2. **Check** that long hints use `\n` for line breaks — oclif auto-wraps at awkward positions, so `\n` gives control over terminal line wrapping
3. **Verify** that `this.fail()` in `src/base-command.ts` strips `\n` from hints in JSON output (`.replaceAll("\n", " ")`)

## Step 4: Check for missing test coverage

**Glob** for each new or modified command file and check if a corresponding test file exists at `test/unit/commands/`. If a command was added but no test was added, flag it.
Expand Down
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,20 @@ this.error() ← oclif exit (ONLY inside fail, nowhere else)
- **`requireAppId`** returns `Promise<string>` (not nullable) — calls `this.fail()` internally if no app found.
- **`runControlCommand<T>`** returns `Promise<T>` (not nullable) — calls `this.fail()` internally on error.

### Error hints (`src/utils/errors.ts`)

`getFriendlyAblyErrorHint(code)` maps Ably error codes to actionable CLI hints. `this.fail()` automatically looks up and appends these hints.

**Line breaks in hints:** Use `\n` to control where hint text wraps in non-JSON terminal output — oclif auto-wraps long lines at awkward positions, so manual `\n` gives us control. The `\n` is automatically stripped (replaced with a space) for JSON output so the hint is a clean single-line string. Always use single quotes for CLI command references (e.g., `'ably apps rules list'`) — double quotes become `\"` in JSON output.

```typescript
// In src/utils/errors.ts
const hints: Record<number, string> = {
93002:
"This channel requires mutableMessages to be enabled.\nRun 'ably apps rules list' to check your channel rules,\nor enable it with 'ably apps rules create' or 'ably apps rules update'.",
};
```

### Additional output patterns (direct chalk, not helpers)
- **No app error**: `'No app specified. Use --app flag or select an app with "ably apps switch"'`

Expand Down
44 changes: 29 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ $ npm install -g @ably/cli
$ ably COMMAND
running command...
$ ably (--version)
@ably/cli/0.17.0 darwin-arm64 node-v25.3.0
@ably/cli/0.17.0 darwin-arm64 node-v24.4.1
$ ably --help [COMMAND]
USAGE
$ ably COMMAND
Expand Down Expand Up @@ -1976,25 +1976,30 @@ Publish a message to an Ably channel

```
USAGE
$ ably channels publish CHANNEL MESSAGE [-v] [--json | --pretty-json] [--client-id <value>] [-c <value>] [-d
<value>] [-e <value>] [-n <value>] [--transport rest|realtime]
$ ably channels publish CHANNEL MESSAGE [-v] [--json | --pretty-json] [--client-id <value>] [--token-size <value>
--token-streaming] [-c <value>] [-d <value>] [-e <value>] [-n <value>] [--stream-duration <value> ] [--transport
rest|realtime]

ARGUMENTS
CHANNEL The channel name to publish to
MESSAGE The message to publish (JSON format or plain text)

FLAGS
-c, --count=<value> [default: 1] Number of messages to publish (default: 1)
-d, --delay=<value> [default: 40] Delay between messages in milliseconds (default: 40ms, max 25 msgs/sec)
-e, --encoding=<value> The encoding for the message
-n, --name=<value> The event name (if not specified in the message JSON)
-v, --verbose Output verbose logs
--client-id=<value> Overrides any default client ID when using API authentication. Use "none" to explicitly set
no client ID. Not applicable when using token authentication.
--json Output in JSON format
--pretty-json Output in colorized JSON format
--transport=<option> Transport method to use for publishing (rest or realtime)
<options: rest|realtime>
-c, --count=<value> [default: 1] Number of messages to publish (default: 1)
-d, --delay=<value> [default: 40] Delay between messages in milliseconds (default: 40ms, max 25 msgs/sec)
-e, --encoding=<value> The encoding for the message
-n, --name=<value> The event name (if not specified in the message JSON)
-v, --verbose Output verbose logs
--client-id=<value> Overrides any default client ID when using API authentication. Use "none" to explicitly
set no client ID. Not applicable when using token authentication.
--json Output in JSON format
--pretty-json Output in colorized JSON format
--stream-duration=<value> [default: 10] Total duration in seconds over which to stream tokens
--token-size=<value> [default: 4] Approximate characters per token
--token-streaming Enable token streaming: publish initial message then stream remaining text as appends
(message-per-response pattern)
--transport=<option> Transport method to use for publishing (rest or realtime)
<options: rest|realtime>

DESCRIPTION
Publish a message to an Ably channel
Expand All @@ -2020,6 +2025,10 @@ EXAMPLES

$ ably channels publish my-channel '{"data":"Push notification","extras":{"push":{"notification":{"title":"Hello","body":"World"}}}}'

$ ably channels publish my-channel "The quick brown fox jumps over the lazy dog" --token-streaming --stream-duration 5

$ ably channels publish --name ai-response my-channel "The quick brown fox" --token-streaming

$ ABLY_API_KEY="YOUR_API_KEY" ably channels publish my-channel '{"data":"Simple message"}'
```

Expand All @@ -2033,7 +2042,7 @@ Subscribe to messages published on one or more Ably channels
USAGE
$ ably channels subscribe CHANNELS... [-v] [--json | --pretty-json] [--client-id <value>] [-D <value>] [--rewind
<value>] [--cipher-algorithm <value>] [--cipher-key <value>] [--cipher-key-length <value>] [--cipher-mode <value>]
[--delta] [--sequence-numbers]
[--delta] [--sequence-numbers] [--token-streaming]

ARGUMENTS
CHANNELS... Channel name(s) to subscribe to
Expand All @@ -2052,6 +2061,9 @@ FLAGS
--pretty-json Output in colorized JSON format
--rewind=<value> Number of messages to rewind when subscribing (default: 0)
--sequence-numbers Include sequence numbers in output
--token-streaming Enable token streaming mode: accumulates message.append data for the same serial,
displaying the growing response in-place (requires message interactions enabled on
the channel)

DESCRIPTION
Subscribe to messages published on one or more Ably channels
Expand All @@ -2073,6 +2085,8 @@ EXAMPLES

$ ably channels subscribe my-channel --duration 30

$ ably channels subscribe --token-streaming my-channel

$ ABLY_API_KEY="YOUR_API_KEY" ably channels subscribe my-channel
```

Expand Down
3 changes: 2 additions & 1 deletion docs/Project-Structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ This document outlines the directory structure of the Ably CLI project.
│ └── utils/
│ ├── channel-rule-display.ts # Channel rule human-readable display
│ ├── chat-constants.ts # Shared Chat SDK constants (REACTION_TYPE_MAP)
│ ├── errors.ts # Error utilities (errorMessage)
│ ├── errors.ts # Error utilities (errorMessage, getFriendlyAblyErrorHint)
│ ├── interrupt-feedback.ts # Ctrl+C feedback messages
│ ├── json-formatter.ts # JSON output formatting (formatJson, formatMessageData)
│ ├── key-display.ts # Key capability formatting
Expand All @@ -94,6 +94,7 @@ This document outlines the directory structure of the Ably CLI project.
│ ├── sigint-exit.ts # SIGINT/Ctrl+C handling (exit code 130)
│ ├── string-distance.ts # Levenshtein distance for fuzzy matching
│ ├── terminal-diagnostics.ts # Terminal capability detection
│ ├── text-chunker.ts # Text chunking for token streaming (chunkText)
│ ├── test-mode.ts # isTestMode() helper
│ ├── version.ts # Version string utilities
│ └── web-mode.ts # Web CLI mode detection
Expand Down
79 changes: 28 additions & 51 deletions src/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -872,7 +872,6 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand {

protected getClientOptions(flags: BaseFlags): Ably.ClientOptions {
const options: Ably.ClientOptions = {};
const isJsonMode = this.shouldOutputJson(flags);

// Handle authentication: ABLY_TOKEN env → flags["api-key"] (set by ensureAppAndKey) → ABLY_API_KEY env → config
if (process.env.ABLY_TOKEN) {
Expand Down Expand Up @@ -920,54 +919,22 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand {
options.tls = flags.tls === "true";
}

// Always add a log handler to control SDK output formatting and destination
// SDK log handler: only surface logs in --verbose mode.
// Most SDK errors (NACKs, auth, connection) also propagate via promise
// rejections or state changes, so fail() / setupChannelStateLogging()
// handle them with structured output + actionable hints.
// A few SDK errors (presence/annotation decode failures) only surface here,
// but those are rare edge cases — users can use --verbose to diagnose them.
options.logHandler = (message: string, level: number) => {
if (isJsonMode) {
// JSON Mode Handling
if (flags.verbose && level <= 2) {
// Verbose JSON: Log ALL SDK messages via logCliEvent
const logData = { sdkLogLevel: level, sdkMessage: message };
this.logCliEvent(
flags,
"ablySdk",
`LogLevel-${level}`,
message,
logData,
);
} else if (level <= 1) {
// Standard JSON: Log only SDK ERRORS (level <= 1) to stderr as JSON
const errorData = {
level,
logType: "sdkError",
message,
timestamp: new Date().toISOString(),
};
// Log to stderr with standard JSON envelope for consistency
this.logToStderr(
this.formatJsonRecord(JsonRecordType.Log, errorData, flags),
);
}
// If not verbose JSON and level > 1, suppress non-error SDK logs
} else {
// Non-JSON Mode Handling
if (flags.verbose && level <= 2) {
// Verbose Non-JSON: Log ALL SDK messages via logCliEvent (human-readable)
const logData = { sdkLogLevel: level, sdkMessage: message };
// logCliEvent handles non-JSON formatting when verbose is true
this.logCliEvent(
flags,
"ablySdk",
`LogLevel-${level}`,
message,
logData,
);
} else if (level <= 1) {
// SDK errors are handled by setupChannelStateLogging() and fail()
// Only show raw SDK errors in verbose mode (handled above)
// In non-verbose mode, log to stderr for debugging without polluting stdout
this.logToStderr(`${chalk.red.bold(`[AblySDK Error]`)} ${message}`);
}
// If not verbose non-JSON and level > 1, suppress non-error SDK logs
if (flags.verbose && level <= 2) {
const logData = { sdkLogLevel: level, sdkMessage: message };
this.logCliEvent(
flags,
"ablySdk",
`LogLevel-${level}`,
message,
logData,
);
}
};

Expand Down Expand Up @@ -1363,6 +1330,16 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand {
const connectionStateHandler = (
stateChange: Ably.ConnectionStateChange,
) => {
// Skip closing/closed — these are CLI-initiated teardown, not actionable.
// The SDK attaches an _ErrorInfo reason even on normal close, which looks
// alarming in verbose output.
if (
stateChange.current === "closing" ||
stateChange.current === "closed"
) {
return;
}

this.logCliEvent(
flags,
component,
Expand All @@ -1379,7 +1356,7 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand {
break;
}
case "disconnected": {
this.log(formatWarning("Disconnected from Ably"));
this.log(formatWarning("Disconnected from Ably."));
break;
}
case "failed": {
Expand All @@ -1391,7 +1368,7 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand {
break;
}
case "suspended": {
this.log(formatWarning("Connection suspended"));
this.log(formatWarning("Connection suspended."));
break;
}
case "connecting": {
Expand Down Expand Up @@ -1549,7 +1526,7 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand {
if (this.shouldOutputJson(flags)) {
const jsonData = cmdError.toJsonData();
if (friendlyHint) {
jsonData.hint = friendlyHint;
jsonData.hint = friendlyHint.replaceAll("\n", " ");
}
this.log(this.formatJsonRecord(JsonRecordType.Error, jsonData, flags));
this.exit(1);
Expand Down
2 changes: 1 addition & 1 deletion src/commands/channels/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export default class ChannelsHistory extends AblyBaseCommand {
channel: channelName,
clientId: message.clientId,
data: message.data,
event: message.name || "(none)",
...(message.name ? { name: message.name } : {}),
id: message.id,
indexPrefix: `${formatIndex(index + 1)} ${formatTimestamp(formatMessageTimestamp(ts))}`,
serial: message.serial,
Expand Down
Loading
Loading