You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: add Nori Sessions broker proxy transport (0.2.0) (#3)
## Summary
- Adds a transport seam (`src/transport.ts`): direct mode
(`SLACK_BOT_TOKEN` via `@slack/web-api`) and proxy mode
(`NORI_SLACK_PROXY_URL` + `NORI_SLACK_CONTEXT_TOKEN`, POSTing `{method,
args}` to the Nori Sessions broker's `/method` endpoint). Proxy wins
when both are configured, matching the sessions bash wrapper it
replaces.
- All CLI features now work identically in both modes: `--json-input`,
`--paginate` (now a single transport-generic cursor loop), `--dry-run`
(reports the selected `transport`), kebab→snake conversion, type
coercion, and error suggestions. Broker errors are normalized into the
standard error envelope, including extracting Slack platform codes from
broker messages.
- Removes the accidental self-dependency (`nori-slack-cli@^0.1.0`) from
package.json. Bumps to 0.2.0.
## Why
Nori Sessions currently ships a separate hand-rolled 74-line proxy
client next to this CLI, selected by a branching bash wrapper. The two
implementations diverged: in proxy mode (the managed-session default)
`--json-input` fails outright, kebab-case flags are silently sent
unconverted, and there is no coercion/pagination/discovery. This PR
makes the real CLI speak the proxy wire protocol so the sessions repo
can delete the duplicate (companion PR in nori-sessions).
## Test plan
- [x] 13 new blackbox subprocess tests in `test/proxy-mode.test.ts`
against a real local HTTP fake broker (wire format, precedence,
trailing-slash URLs, `--json-input` pass-through, coercion, error
mapping incl. 401, cursor pagination, dry-run transport reporting,
no-credentials envelope) — written red-first
- [x] Test helpers are hermetic: Slack credential env vars are stripped
so suites pass identically on dev machines and inside Nori sessions
(which export the proxy vars)
- [x] Full suite green: 69 tests across 9 files, `tsc --noEmit` clean
- [ ] After merge: publish `nori-slack-cli@0.2.0` to npm (@amol), then
land the companion sessions PR that deletes the hand-rolled client and
bumps base.lock.json
Co-authored-by: Sprite <noreply@sprites.dev>
Co-authored-by: Nori <contact@tilework.tech>
Copy file name to clipboardExpand all lines: README.md
+18-4Lines changed: 18 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -10,7 +10,7 @@ Bolt is built for human developers writing TypeScript. This CLI is built for cod
10
10
11
11
-**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.
12
12
-**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.
13
-
-**Bot tokens only.**Uses `SLACK_BOT_TOKEN` exclusively. There is no user-OAuth flow because there is no human in the loop.
13
+
-**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.
14
14
-**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.
15
15
-**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.
16
16
@@ -32,10 +32,15 @@ npm run build
32
32
npm link # makes `nori-slack` available globally
33
33
```
34
34
35
-
Then set your bot token:
35
+
Then set credentials for one of the two transports (see [Authentication](#authentication)):
36
36
37
37
```bash
38
+
# Direct mode
38
39
export SLACK_BOT_TOKEN=xoxb-...
40
+
41
+
# Proxy mode (set automatically inside Nori Sessions)
-`1` — Slack API error, proxy error, or missing credentials
87
92
-`2` — bad CLI usage (missing args, invalid stdin JSON)
88
93
89
94
## Authentication
90
95
91
-
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.
96
+
The CLI supports two transports, selected from the environment:
97
+
98
+
| Mode | Environment | Behavior |
99
+
| --- | --- | --- |
100
+
|**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. |
101
+
|**Direct**|`SLACK_BOT_TOKEN`| Calls the Slack Web API directly via `@slack/web-api`. |
102
+
103
+
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`).
104
+
105
+
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`.
Copy file name to clipboardExpand all lines: docs.md
+7-6Lines changed: 7 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -5,24 +5,25 @@ Path: @/
5
5
### Overview
6
6
- A TypeScript CLI that exposes the entire Slack Web API as a single command: `nori-slack <method> [--param value ...]`
7
7
- Designed for coding agents: all output is JSON on stdout, human-readable errors go to stderr
8
-
-Uses `@slack/web-api` WebClient for dynamic dispatch -- the CLI is not limited to a fixed set of methods
8
+
-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
9
9
- Supports automatic cursor pagination via `--paginate`, which fetches all pages and returns a single merged JSON response
10
10
- Supports `--dry-run` to preview resolved API requests without sending them -- designed as a safety net for coding agents to validate parameter resolution
11
11
- 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
12
12
13
13
### How it fits into the larger codebase
14
14
- 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`
15
15
- 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
16
-
- Authentication is bot-token-only via `SLACK_BOT_TOKEN` environment variable (no user OAuth flows)
16
+
- 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
17
+
- 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
17
18
- 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
18
-
- 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
19
+
- 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
19
20
- User-facing installation and usage documentation lives in [README.md](README.md)
20
21
21
22
### Core Implementation
22
23
- 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
23
-
- 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()`
24
+
- 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
24
25
- Two input modes: CLI flags (`--channel C123 --text "hi"`) and piped JSON via `--json-input`; when both are provided, CLI flags override stdin values
25
-
- 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
26
+
- 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
26
27
-`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
27
28
- 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
28
29
- 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
@@ -50,7 +51,7 @@ dist/ (gitignored)
50
51
- 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
51
52
- Type coercion in `coerceValue` handles booleans (`"true"`/`"false"`), numbers (but preserves leading-zero strings like `"007"`), and inline JSON arrays/objects
52
53
- A standalone `--flag` with no following value (or followed by another `--flag`) is treated as boolean `true`
53
-
- 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
54
+
- 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
54
55
- Every error response includes a `source` field with the filesystem path to the CLI, so agents can locate the source code for debugging
55
56
- 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
56
57
- 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
Copy file name to clipboardExpand all lines: src/docs.md
+21-8Lines changed: 21 additions & 8 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -3,26 +3,35 @@
3
3
Path: @/src
4
4
5
5
### Overview
6
-
- 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
6
+
- 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
7
7
- Compiles from `src/` to `dist/` via TypeScript (ES2022 target, Node16 module resolution)
8
8
9
9
### How it fits into the larger codebase
10
10
-[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
11
-
-[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/)
11
+
-[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
12
+
-[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/)
12
13
-[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
13
14
14
15
### Core Implementation
15
16
16
17
**Entry point (`index.ts`)**
17
18
- Sets up Commander with three subcommands: `list-methods`, `describe`, and the default dynamic method handler
18
19
- The dynamic handler: optionally reads JSON from stdin, parses CLI flags, merges params (CLI flags win over stdin), then branches into three paths:
19
-
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.
20
-
2.`--paginate`: validates token, then calls `client.paginate()` + `mergePages()`
21
-
3. Default: validates token, then calls `client.apiCall()`
20
+
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.
21
+
2.`--paginate`: resolves the transport via `resolveTransport()`, then runs `mergePages(paginatePages(transport, method, params))`
22
+
3. Default: resolves the transport, then makes a single `transport.call(method, params)`
23
+
- 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
22
24
- When no arguments are provided (`process.argv.length <= 2`), help text and error go to stderr and the process exits with code 2
23
25
- 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.
24
26
25
-
**Pagination merging (`paginate.ts`)**
27
+
**Transport selection (`transport.ts`)**
28
+
-`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
29
+
-`resolveTransport(env)` returns a `Transport` (`{ mode, call(method, params) }`) or `null` when no credentials are available
30
+
- 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
31
+
- Direct `call` wraps `WebClient.apiCall` from `@slack/web-api`
32
+
33
+
**Pagination (`paginate.ts`)**
34
+
-`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
26
35
-`mergePages(pages)` takes an `AsyncIterable` of page objects and returns a single merged object
27
36
- Array-valued keys are concatenated across pages; scalar/metadata keys (`ok`, `response_metadata`, `headers`, `warning`) are overwritten with the last page's value
28
37
- 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`)
@@ -34,8 +43,10 @@ Path: @/src
34
43
35
44
**Error formatting (`errors.ts`)**
36
45
-`formatError(error, sourceDir)` returns a `CliError` object with fields: `ok`, `error`, `message`, `suggestion`, `source`
37
-
- 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`
38
-
- 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.
46
+
- 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`
47
+
- 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
- The `SUGGESTIONS` map provides agent-friendly remediation text for common Slack platform errors like `channel_not_found`, `not_in_channel`, `rate_limited`, etc.
39
50
40
51
**Fuzzy method suggestion (`suggest.ts`)**
41
52
-`findSimilarMethods(input, methods?, maxResults?)` returns up to 3 similar method names from `KNOWN_METHODS` for typo correction
@@ -60,5 +71,7 @@ Path: @/src
60
71
- When both stdin JSON and CLI flags provide the same key, the CLI flag value wins due to spread order: `{ ...stdinParams, ...cliParams }`
61
72
- Non-flag arguments (tokens not starting with `--`) are silently skipped by `parseArgs` -- they do not cause errors
62
73
- Rate limit errors extract `retryAfter` from the `@slack/web-api` error object and include the retry duration in the message
74
+
- 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)
75
+
- 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
0 commit comments