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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Bolt is built for human developers writing TypeScript. This CLI is built for cod

- **No interactive prompts, no ASCII art.** Every successful response is a single line of JSON on stdout. Errors are JSON on stdout *and* a human-readable line on stderr.
- **Exhaustive surface.** The agent has access to the full Slack Web API — not a hand-picked subset. Capability boundaries are enforced through **bot token scopes**, not through code.
- **Bot tokens only.** Uses `SLACK_BOT_TOKEN` exclusively. There is no user-OAuth flow because there is no human in the loop.
- **Two transports, one interface.** Direct mode calls Slack with `SLACK_BOT_TOKEN`; proxy mode routes the same `{method, args}` calls through a Nori Sessions broker using `NORI_SLACK_PROXY_URL` + `NORI_SLACK_CONTEXT_TOKEN`, so managed sessions never hold a raw bot token. There is no user-OAuth flow because there is no human in the loop.
- **Self-locating errors.** Every error response includes a `source` field with the on-disk path to the CLI, so an agent can read the source code to debug.
- **Install from npm.** `npm install -g nori-slack-cli` puts `nori-slack` on your `PATH`. Cloning and building from source is also supported for contributors.

Expand All @@ -32,10 +32,15 @@ npm run build
npm link # makes `nori-slack` available globally
```

Then set your bot token:
Then set credentials for one of the two transports (see [Authentication](#authentication)):

```bash
# Direct mode
export SLACK_BOT_TOKEN=xoxb-...

# Proxy mode (set automatically inside Nori Sessions)
export NORI_SLACK_PROXY_URL=https://broker.example.com/api/slack-proxy
export NORI_SLACK_CONTEXT_TOKEN=...
```

## Usage
Expand Down Expand Up @@ -83,12 +88,21 @@ nori-slack describe chat.postMessage
### Exit codes

- `0` — success
- `1` — Slack API error or missing token
- `1` — Slack API error, proxy error, or missing credentials
- `2` — bad CLI usage (missing args, invalid stdin JSON)

## Authentication

Set `SLACK_BOT_TOKEN` in the environment. The CLI does not read tokens from any other source. To control what the agent can do, scope the bot token in the Slack app's OAuth & Permissions page — the CLI itself imposes no method-level restrictions.
The CLI supports two transports, selected from the environment:

| Mode | Environment | Behavior |
| --- | --- | --- |
| **Proxy** | `NORI_SLACK_PROXY_URL` + `NORI_SLACK_CONTEXT_TOKEN` | POSTs `{method, args}` to `<url>/method` with the context token as a bearer token. Used inside Nori Sessions, where the broker enforces a per-session access grant and the raw bot token never reaches the machine. |
| **Direct** | `SLACK_BOT_TOKEN` | Calls the Slack Web API directly via `@slack/web-api`. |

When both are configured, **proxy mode wins**. All CLI features (`--json-input`, `--paginate`, `--dry-run`, kebab-case conversion, type coercion, error suggestions) behave identically in both modes. `--dry-run` reports which transport would be used via the `transport` field (`proxy`, `direct`, or `none`).

In direct mode, capability boundaries come from the bot token's OAuth scopes. In proxy mode, the broker additionally restricts methods and channels to the session's access grant — requests outside the grant fail with a structured `proxy_error`.

## License

Expand Down
13 changes: 7 additions & 6 deletions docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,25 @@ Path: @/
### Overview
- A TypeScript CLI that exposes the entire Slack Web API as a single command: `nori-slack <method> [--param value ...]`
- Designed for coding agents: all output is JSON on stdout, human-readable errors go to stderr
- Uses `@slack/web-api` WebClient for dynamic dispatch -- the CLI is not limited to a fixed set of methods
- Supports two transports, resolved in [src/transport.ts](src/transport.ts): direct calls to Slack via the `@slack/web-api` WebClient, or a Nori Sessions broker proxy. Dispatch is dynamic in both modes -- the CLI is not limited to a fixed set of methods
- Supports automatic cursor pagination via `--paginate`, which fetches all pages and returns a single merged JSON response
- Supports `--dry-run` to preview resolved API requests without sending them -- designed as a safety net for coding agents to validate parameter resolution
- Supports `describe <method>` to look up parameter documentation for any Slack API method without requiring a token -- the metadata map covers all methods in `KNOWN_METHODS`, so agents always get full parameter documentation rather than a fallback

### How it fits into the larger codebase
- Standalone repository (was originally imported from the `nori-integrations` monorepo and now lives on its own). Distributed via the public npm registry as `nori-slack-cli`
- The canonical install path is `npm install -g nori-slack-cli`, which places the `nori-slack` binary on `PATH`; `npm link` from a local clone is retained for contributors
- Authentication is bot-token-only via `SLACK_BOT_TOKEN` environment variable (no user OAuth flows)
- Two credential modes, no user OAuth flows: direct mode via the `SLACK_BOT_TOKEN` environment variable, and proxy mode via `NORI_SLACK_PROXY_URL` + `NORI_SLACK_CONTEXT_TOKEN` (both must be set; Nori session machines export them). Proxy mode takes precedence when both credential sets are present
- Proxy mode exists so Nori Sessions can route Slack calls through its broker's scoped access grants. It replaced a separate hand-rolled proxy client script in the sessions repo, consolidating two diverging implementations of the same command behind this one CLI
- The CLI is a thin wrapper -- it does not contain business logic, scheduling, or state management; it translates CLI flags into Slack API calls and returns the raw JSON response
- The pagination merge logic in [src/paginate.ts](src/paginate.ts) is a pure function decoupled from the Slack SDK -- it operates on any `AsyncIterable` of page objects
- The pagination logic in [src/paginate.ts](src/paginate.ts) is decoupled from the Slack SDK -- the cursor loop talks only to the `Transport` interface, and the merge step operates on any `AsyncIterable` of page objects
- User-facing installation and usage documentation lives in [README.md](README.md)

### Core Implementation
- Entry point is [src/index.ts](src/index.ts), which uses Commander.js with `allowUnknownOption()` so arbitrary `--flag value` pairs pass through without Commander rejecting them
- The dynamic handler has three code paths: `--dry-run` short-circuits after param resolution (no token required, no API call), `--paginate` triggers `WebClient.paginate()` + `mergePages()`, and the default path uses `WebClient.apiCall()`
- The dynamic handler has three code paths: `--dry-run` short-circuits after param resolution (no credentials required, no API call, reports which transport would be used), `--paginate` runs the generic cursor loop `paginatePages()` + `mergePages()` from [src/paginate.ts](src/paginate.ts), and the default path makes a single `transport.call()`. The transport is resolved once per invocation and both API paths route through it, so behavior (including pagination) is identical in proxy and direct mode
- Two input modes: CLI flags (`--channel C123 --text "hi"`) and piped JSON via `--json-input`; when both are provided, CLI flags override stdin values
- Two discovery subcommands that do not require `SLACK_BOT_TOKEN`: `list-methods` outputs known method names as JSON (supports `--namespace` filtering and `--descriptions` to include method descriptions), and `describe <method>` returns structured parameter documentation
- Two discovery subcommands that do not require credentials: `list-methods` outputs known method names as JSON (supports `--namespace` filtering and `--descriptions` to include method descriptions), and `describe <method>` returns structured parameter documentation
- `describe` uses [src/method-metadata.ts](src/method-metadata.ts), a hand-curated static map covering every method in `KNOWN_METHODS` -- this is static because `@slack/web-api` erases parameter type information at compile time, so runtime introspection is not possible
- For unknown methods (not in `KNOWN_METHODS`), `getMethodMetadata()` returns a fallback entry with empty params and a generated docs URL, so `describe` never errors; the `known` field in the output distinguishes curated entries from fallbacks
- When an unknown method is used, [src/suggest.ts](src/suggest.ts) provides fuzzy matching via Levenshtein distance against `KNOWN_METHODS`, surfacing "Did you mean?" suggestions; suggestions are non-blocking -- unknown methods still proceed to the API
Expand Down Expand Up @@ -50,7 +51,7 @@ dist/ (gitignored)
- Flag parsing in [src/parse-args.ts](src/parse-args.ts) converts `--kebab-case` to `snake_case` because the Slack API uses snake_case parameter names
- Type coercion in `coerceValue` handles booleans (`"true"`/`"false"`), numbers (but preserves leading-zero strings like `"007"`), and inline JSON arrays/objects
- A standalone `--flag` with no following value (or followed by another `--flag`) is treated as boolean `true`
- Error formatting in [src/errors.ts](src/errors.ts) maps Slack error codes to actionable suggestions (e.g., `channel_not_found` suggests running `conversations.list`); unknown errors get a generic suggestion pointing to the source directory
- Error formatting in [src/errors.ts](src/errors.ts) maps Slack error codes to actionable suggestions (e.g., `channel_not_found` suggests running `conversations.list`); unknown errors get a generic suggestion pointing to the source directory. Broker errors from proxy mode are normalized into the same envelope, including extracting Slack platform codes embedded in broker messages
- Every error response includes a `source` field with the filesystem path to the CLI, so agents can locate the source code for debugging
- The method metadata in [src/method-metadata.ts](src/method-metadata.ts) marks `files.upload` as deprecated with a pointer to the two-step `files.getUploadURLExternal` + `files.completeUploadExternal` flow
- The CLI version string is currently duplicated: once in [package.json](package.json) `version` and once as a hardcoded argument to Commander's `.version()` call in [src/index.ts](src/index.ts). Both must be bumped together on release
Expand Down
19 changes: 3 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nori-slack-cli",
"version": "0.1.1",
"version": "0.2.0",
"description": "CLI for interacting with the Slack Web API, designed for coding agents",
"type": "module",
"bin": {
Expand All @@ -18,8 +18,7 @@
},
"dependencies": {
"@slack/web-api": "^7.0.0",
"commander": "^13.0.0",
"nori-slack-cli": "^0.1.0"
"commander": "^13.0.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
Expand Down
29 changes: 21 additions & 8 deletions src/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,35 @@
Path: @/src

### Overview
- Contains all source modules for the CLI: entry point, argument parsing, error formatting, pagination merging, fuzzy method suggestion, the known-methods catalog, and the method metadata registry
- Contains all source modules for the CLI: entry point, argument parsing, transport selection, error formatting, pagination, fuzzy method suggestion, the known-methods catalog, and the method metadata registry
- Compiles from `src/` to `dist/` via TypeScript (ES2022 target, Node16 module resolution)

### How it fits into the larger codebase
- [index.ts](index.ts) is the CLI entry point (shebang `#!/usr/bin/env node`), compiled to `dist/index.js` and exposed as the `nori-slack` binary via the `bin` field in [../package.json](../package.json). The compiled `dist/` directory is produced at pack time by the `prepare` script and shipped to the npm registry via the `files` allowlist -- see [../docs.md](../docs.md) for the full packaging chain
- [parse-args.ts](parse-args.ts), [errors.ts](errors.ts), and [paginate.ts](paginate.ts) are pure utility modules with no side effects -- they are independently testable and tested in [@/test](../test/)
- [transport.ts](transport.ts) is the only module that knows how to reach Slack. It selects between proxy mode (a Nori Sessions broker, configured by `NORI_SLACK_PROXY_URL` + `NORI_SLACK_CONTEXT_TOKEN`) and direct mode (`SLACK_BOT_TOKEN` via `@slack/web-api`); everything downstream works against its `Transport` interface
- [parse-args.ts](parse-args.ts) and [errors.ts](errors.ts) are pure utility modules with no side effects; [paginate.ts](paginate.ts) is transport-generic (no Slack SDK dependency). All are independently testable and tested in [@/test](../test/)
- [methods.ts](methods.ts) is a static data file; it is only used by the `list-methods` subcommand and has no effect on which methods the CLI can actually call

### Core Implementation

**Entry point (`index.ts`)**
- Sets up Commander with three subcommands: `list-methods`, `describe`, and the default dynamic method handler
- The dynamic handler: optionally reads JSON from stdin, parses CLI flags, merges params (CLI flags win over stdin), then branches into three paths:
1. `--dry-run`: short-circuits immediately after param resolution -- outputs a JSON preview with `ok`, `dry_run`, `method`, `params`, `token_present`, `paginate`, and optionally a `warning` for unknown methods. Does not require a token. Always exits 0.
2. `--paginate`: validates token, then calls `client.paginate()` + `mergePages()`
3. Default: validates token, then calls `client.apiCall()`
1. `--dry-run`: short-circuits immediately after param resolution -- outputs a JSON preview with `ok`, `dry_run`, `method`, `params`, `transport`, `token_present`, `paginate`, and optionally a `warning` for unknown methods. Does not require credentials. Always exits 0.
2. `--paginate`: resolves the transport via `resolveTransport()`, then runs `mergePages(paginatePages(transport, method, params))`
3. Default: resolves the transport, then makes a single `transport.call(method, params)`
- If `resolveTransport()` returns null (no credentials in either mode), the handler emits the `no_token` error envelope and exits 1 before any API path runs
- When no arguments are provided (`process.argv.length <= 2`), help text and error go to stderr and the process exits with code 2
- The `list-methods` subcommand supports two options that compose together: `--namespace <ns>` filters the method list to those starting with the given prefix (e.g., `chat.`), and `--descriptions` changes the output shape from `string[]` to `Array<{ method, description }>` by pulling descriptions from `getMethodMetadata()`. When `--namespace` is provided, a `namespace` field is added to the response JSON.

**Pagination merging (`paginate.ts`)**
**Transport selection (`transport.ts`)**
- `detectTransportMode(env)` returns `'proxy' | 'direct' | 'none'`: proxy when both `NORI_SLACK_PROXY_URL` and `NORI_SLACK_CONTEXT_TOKEN` are non-empty, otherwise direct when `SLACK_BOT_TOKEN` is set, otherwise none. Proxy takes precedence over a bot token
- `resolveTransport(env)` returns a `Transport` (`{ mode, call(method, params) }`) or `null` when no credentials are available
- Proxy `call` POSTs `{ method, args }` as JSON to `<proxy-url>/method` (trailing slashes are stripped from the configured URL first) with an `authorization: Bearer <context token>` header. A 2xx response is returned as the raw Slack JSON body; a non-2xx response throws `ProxyError` (code `nori_slack_proxy_error`) carrying the HTTP status and the broker's `error` message
- Direct `call` wraps `WebClient.apiCall` from `@slack/web-api`

**Pagination (`paginate.ts`)**
- `paginatePages(transport, method, params)` is an async generator that repeatedly calls the transport, following `response_metadata.next_cursor` and terminating when the cursor is empty or missing. It replaces the old `WebClient.paginate()` path so pagination works identically over both transports
- `mergePages(pages)` takes an `AsyncIterable` of page objects and returns a single merged object
- Array-valued keys are concatenated across pages; scalar/metadata keys (`ok`, `response_metadata`, `headers`, `warning`) are overwritten with the last page's value
- This design means the function works generically with any Slack method's response shape -- it does not need to know which key holds the data (e.g., `channels`, `members`, `messages`)
Expand All @@ -34,8 +43,10 @@ Path: @/src

**Error formatting (`errors.ts`)**
- `formatError(error, sourceDir)` returns a `CliError` object with fields: `ok`, `error`, `message`, `suggestion`, `source`
- Handles four specific `@slack/web-api` error codes: `slack_webapi_platform_error`, `slack_webapi_rate_limited_error`, `slack_webapi_request_error`, and the custom `no_token`
- The `SUGGESTIONS` map provides agent-friendly remediation text for common Slack platform errors like `channel_not_found`, `not_in_channel`, `invalid_auth`, `rate_limited`, etc.
- Handles five specific error codes: the `@slack/web-api` codes `slack_webapi_platform_error`, `slack_webapi_rate_limited_error`, and `slack_webapi_request_error`, plus the custom `no_token` and the proxy transport's `nori_slack_proxy_error`
- For `nori_slack_proxy_error`: broker messages of the form "An API error occurred: \<code\>" have the Slack platform code extracted and mapped through the same `SUGGESTIONS` table as direct-mode platform errors; HTTP 401 maps to `proxy_unauthorized` with a context-token rotation suggestion; any other status maps to `proxy_error` with a suggestion about the session's access grant
- Broker wire contract behind those mappings: 200 returns raw Slack JSON; error statuses (e.g., 401, 403, 404) return `{ error: message }`
- The `SUGGESTIONS` map provides agent-friendly remediation text for common Slack platform errors like `channel_not_found`, `not_in_channel`, `rate_limited`, etc.

**Fuzzy method suggestion (`suggest.ts`)**
- `findSimilarMethods(input, methods?, maxResults?)` returns up to 3 similar method names from `KNOWN_METHODS` for typo correction
Expand All @@ -60,5 +71,7 @@ Path: @/src
- When both stdin JSON and CLI flags provide the same key, the CLI flag value wins due to spread order: `{ ...stdinParams, ...cliParams }`
- Non-flag arguments (tokens not starting with `--`) are silently skipped by `parseArgs` -- they do not cause errors
- Rate limit errors extract `retryAfter` from the `@slack/web-api` error object and include the retry duration in the message
- The `--dry-run` output's `token_present` field only reflects `SLACK_BOT_TOKEN`; the `transport` field is the authoritative indicator of which mode would be used (proxy wins when both credential sets are present)
- The missing-credentials error keeps the error code `no_token` for backward compatibility, but its message ("No Slack credentials provided.") and suggestion cover both credential sets

Created and maintained by Nori.
Loading
Loading