diff --git a/.changeset/bundled-slack-connector.md b/.changeset/bundled-slack-connector.md new file mode 100644 index 00000000..d4e3fe7c --- /dev/null +++ b/.changeset/bundled-slack-connector.md @@ -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`. diff --git a/packages/connector-slack/README.md b/packages/connector-slack/README.md index 4c665215..a2fa4d28 100644 --- a/packages/connector-slack/README.md +++ b/packages/connector-slack/README.md @@ -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` @@ -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 /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: diff --git a/packages/peek-cli/package.json b/packages/peek-cli/package.json index cc1fd402..0efea638 100644 --- a/packages/peek-cli/package.json +++ b/packages/peek-cli/package.json @@ -31,11 +31,12 @@ }, "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", @@ -43,7 +44,8 @@ }, "devDependencies": { "@types/better-sqlite3": "^7.6.13", - "@types/node": "^22.10.0" + "@types/node": "^22.10.0", + "esbuild": "^0.25.12" }, "publishConfig": { "access": "public", diff --git a/packages/peek-cli/scripts/bundle-connectors.mjs b/packages/peek-cli/scripts/bundle-connectors.mjs new file mode 100644 index 00000000..ca611056 --- /dev/null +++ b/packages/peek-cli/scripts/bundle-connectors.mjs @@ -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 /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`); +} diff --git a/packages/peek-cli/src/lib/connect/descriptors.test.ts b/packages/peek-cli/src/lib/connect/descriptors.test.ts index 539abd19..9340aeb0 100644 --- a/packages/peek-cli/src/lib/connect/descriptors.test.ts +++ b/packages/peek-cli/src/lib/connect/descriptors.test.ts @@ -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', () => { @@ -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', () => { diff --git a/packages/peek-cli/src/lib/connect/descriptors.ts b/packages/peek-cli/src/lib/connect/descriptors.ts index 1cd04b5a..829c68d7 100644 --- a/packages/peek-cli/src/lib/connect/descriptors.ts +++ b/packages/peek-cli/src/lib/connect/descriptors.ts @@ -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 { @@ -20,8 +28,8 @@ export const DESCRIPTORS: Record = { slack: { surface: 'slack', displayName: 'Slack', - defaultCommand: 'peek-connector-slack', - defaultArgs: [], + defaultCommand: process.execPath, + defaultArgs: [BUNDLED_SLACK], }, }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d35eef41..e6314ab3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -195,6 +195,9 @@ importers: packages/peek-cli: dependencies: + '@napi-rs/keyring': + specifier: ^1.3.0 + version: 1.3.0 '@peekdev/mcp': specifier: workspace:* version: link:../peek-mcp @@ -214,6 +217,9 @@ importers: '@types/node': specifier: ^22.10.0 version: 22.19.19 + esbuild: + specifier: ^0.25.12 + version: 0.25.12 packages/peek-extension: dependencies: