Skip to content
Open
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
17 changes: 17 additions & 0 deletions .changeset/bundled-slack-connector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@peekdev/cli": minor
---

peek connect: the Slack connector now ships pre-bundled inside the CLI, so
`peek connect add slack` works with no monorepo clone and no `--local` build.

`pnpm build` bundles `@peekdev/connector-slack` (from source, with
`@peekdev/connector-core` aliased to source too) into
`dist/connectors/slack.js` via esbuild. The slack descriptor now spawns that
bundle with the same Node binary running the CLI. The native keychain module
`@napi-rs/keyring` is the only external — it is now a runtime dependency of
`@peekdev/cli`, so npm installs its platform binary and the bundle resolves it
at spawn time (tokens still go to the OS keychain).

End-to-end setup drops to: `npm i -g @peekdev/cli && peek connect add slack &&
peek connect start`.
36 changes: 31 additions & 5 deletions packages/connector-slack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,21 @@ slash commands to the connector core.

### Required scopes

Bot token scopes the connector actually uses:

| Scope | Why |
|---|---|
| `assistant:write` | Required to register the app as an AI assistant in Slack |
| `chat:write` | Required to post messages and set the "thinking…" status |
| `assistant:write` | Register the app as an AI assistant + set suggested prompts |
| `chat:write` | Post replies, consent cards, journey summaries, and the "thinking…" status |
| `files:write` | `files.uploadV2` — `share_session` + journey artifacts |
| `files:read` | `files.info` — resolve the session-journey canvas permalink |
| `canvases:write` | `conversations.canvases.create` — session journey as a Slack canvas |
| `im:history` | Read the user's messages in the assistant thread / DM |
| `app_mentions:read` | `@peek` in a channel (team debugging) |
| `commands` | Receive the `/peek` slash command |

Event subscriptions: `assistant_thread_started`, `assistant_thread_context_changed`,
`message.im`, `app_mention`.

### Slack app scope — `chat:write`

Expand All @@ -38,10 +49,25 @@ adapter.onMessage(async (msg) => {
await adapter.start();
```

## Run from a local build (no npm publish)
## Run it (recommended: via the peek CLI)

This connector ships **pre-bundled inside `@peekdev/cli`**, so the fastest path
needs no clone and no build:

```sh
npm i -g @peekdev/cli
peek connect add slack # resolves the bundled connector — no --local needed
peek connect start
```

`peek connect add slack` spawns `node <cli>/dist/connectors/slack.js` (a single
esbuild bundle of this package). The only external is the native keychain module
`@napi-rs/keyring`, which `@peekdev/cli` installs as a dependency.

## Run from a local build (development)

`@peekdev/connector-slack` is not published to npm — it runs from a local
build, spawned by the `peek connect` daemon.
For working on the connector itself, run it from a local build, spawned by the
`peek connect` daemon.

1. Build the connector:

Expand Down
6 changes: 4 additions & 2 deletions packages/peek-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,21 @@
},
"files": ["dist", "NOTICE", "README.md"],
"scripts": {
"build": "tsc -p tsconfig.json && node ./scripts/postbuild.mjs",
"build": "tsc -p tsconfig.json && node ./scripts/bundle-connectors.mjs && node ./scripts/postbuild.mjs",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "vitest run --passWithNoTests"
},
"dependencies": {
"@napi-rs/keyring": "^1.3.0",
"@peekdev/mcp": "workspace:*",
"better-sqlite3": "^12.11.1",
"tar": "^7.4.3",
"zod": "^3.25.76"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^22.10.0"
"@types/node": "^22.10.0",
"esbuild": "^0.25.12"
},
"publishConfig": {
"access": "public",
Expand Down
52 changes: 52 additions & 0 deletions packages/peek-cli/scripts/bundle-connectors.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env node
// Bundle the surface connectors into this package's dist/ so the published CLI
// can spawn them directly — no separate npm install, no monorepo clone, no
// `--local` build. `peek connect add slack` then resolves to
// `node <cli>/dist/connectors/slack.js` (see src/lib/connect/descriptors.ts).
//
// We bundle from SOURCE and alias the workspace connector packages to their
// src entry, so the private `@peekdev/connector-{core,slack}` packages never
// need to be published or pre-built. Only the native keychain module
// (`@napi-rs/keyring`) stays external — it can't be inlined; it's a runtime
// dependency of `@peekdev/cli`, so npm installs its platform binary and the
// bundle resolves it from the CLI's node_modules at spawn time.

import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { build } from 'esbuild';

const here = dirname(fileURLToPath(import.meta.url)); // packages/peek-cli/scripts
const cliRoot = dirname(here); // packages/peek-cli
const pkgs = resolve(cliRoot, '..'); // packages/

const connectors = [
{
surface: 'slack',
entry: resolve(pkgs, 'connector-slack/src/index.ts'),
outfile: resolve(cliRoot, 'dist/connectors/slack.js'),
},
];

for (const c of connectors) {
await build({
entryPoints: [c.entry],
outfile: c.outfile,
bundle: true,
platform: 'node',
format: 'esm',
target: 'node22',
// Native module — cannot be inlined. Declared as a runtime dep of @peekdev/cli.
external: ['@napi-rs/keyring'],
// Pull the workspace connector core from source so no dist/publish is needed.
alias: {
'@peekdev/connector-core': resolve(pkgs, 'connector-core/src/index.ts'),
},
// ESM output for node: provide `require` for any bundled CJS dep interop.
banner: {
js: "import { createRequire as __peekCreateRequire } from 'node:module'; const require = __peekCreateRequire(import.meta.url);",
},
legalComments: 'none',
logLevel: 'info',
});
console.log(`bundle-connectors: wrote dist/connectors/${c.surface}.js`);
}
23 changes: 12 additions & 11 deletions packages/peek-cli/src/lib/connect/descriptors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import { getDescriptor, resolveSpawn } from './descriptors.js';
import type { ConnectorEntry } from './registry.js';

describe('getDescriptor', () => {
it('returns the slack descriptor for known surface', () => {
it('spawns the pre-bundled slack connector with the running Node binary', () => {
const desc = getDescriptor('slack');
expect(desc).toEqual({
surface: 'slack',
displayName: 'Slack',
defaultCommand: 'peek-connector-slack',
defaultArgs: [],
});
if (!desc) throw new Error('slack descriptor missing');
expect(desc.surface).toBe('slack');
expect(desc.displayName).toBe('Slack');
// Bundled connector is run via the same Node that runs the CLI.
expect(desc.defaultCommand).toBe(process.execPath);
expect(desc.defaultArgs).toHaveLength(1);
expect((desc.defaultArgs[0] ?? '').replace(/\\/g, '/')).toMatch(/\/connectors\/slack\.js$/);
});

it('returns undefined for unknown surface', () => {
Expand All @@ -21,10 +22,10 @@ describe('getDescriptor', () => {
describe('resolveSpawn', () => {
it('uses descriptor defaults when entry has no overrides', () => {
const entry: ConnectorEntry = { surface: 'slack', enabled: true };
expect(resolveSpawn(entry)).toEqual({
command: 'peek-connector-slack',
args: [],
});
const { command, args } = resolveSpawn(entry);
expect(command).toBe(process.execPath);
expect(args).toHaveLength(1);
expect((args[0] ?? '').replace(/\\/g, '/')).toMatch(/\/connectors\/slack\.js$/);
});

it('uses entry overrides when command and args are set', () => {
Expand Down
12 changes: 10 additions & 2 deletions packages/peek-cli/src/lib/connect/descriptors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@
// each surface's connector as a subprocess. `resolveSpawn` merges the
// per-entry overrides from connectors.json with the defaults defined here.

import { fileURLToPath } from 'node:url';
import type { ConnectorEntry } from './registry.js';

// The Slack connector ships pre-bundled inside this package at
// dist/connectors/slack.js (built from source by scripts/bundle-connectors.mjs).
// We spawn it with the same Node binary that runs the CLI, so `peek connect add
// slack` needs no separate npm install or local build. The path is resolved
// relative to this module so it works from the installed dist regardless of cwd.
const BUNDLED_SLACK = fileURLToPath(new URL('../../connectors/slack.js', import.meta.url));

// ── Types ──────────────────────────────────────────────────────────────────

export interface ConnectorDescriptor {
Expand All @@ -20,8 +28,8 @@ export const DESCRIPTORS: Record<string, ConnectorDescriptor> = {
slack: {
surface: 'slack',
displayName: 'Slack',
defaultCommand: 'peek-connector-slack',
defaultArgs: [],
defaultCommand: process.execPath,
defaultArgs: [BUNDLED_SLACK],
},
};

Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

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

Loading