From f71201c73410830f92b7a47189202a1361f33306 Mon Sep 17 00:00:00 2001 From: harry-harish <22562634+harry-harish@users.noreply.github.com> Date: Thu, 9 Jul 2026 12:22:50 +0530 Subject: [PATCH 1/2] feat(peek-cli): add --local to peek connect add Sugar for registering a locally-built connector: --local resolves to an absolute path and desugars to --command node --args=. Mutual exclusion enforced: --local + --command or --local + --args prints a clear error and returns 1. USAGE updated; connector-slack README gains a local-build section. Changeset: @peekdev/cli minor. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: harry-harish <22562634+harry-harish@users.noreply.github.com> --- .changeset/peek-cli-connect-add-local.md | 5 ++ packages/connector-slack/README.md | 35 ++++++++++++ .../peek-cli/src/commands/connect.test.ts | 57 +++++++++++++++++++ packages/peek-cli/src/commands/connect.ts | 42 ++++++++++++-- 4 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 .changeset/peek-cli-connect-add-local.md diff --git a/.changeset/peek-cli-connect-add-local.md b/.changeset/peek-cli-connect-add-local.md new file mode 100644 index 0000000..24034f4 --- /dev/null +++ b/.changeset/peek-cli-connect-add-local.md @@ -0,0 +1,5 @@ +--- +'@peekdev/cli': minor +--- + +Add `peek connect add --local ` to register a locally-built connector (sugar for `--command node --args=`). diff --git a/packages/connector-slack/README.md b/packages/connector-slack/README.md index dfc98cd..4c66521 100644 --- a/packages/connector-slack/README.md +++ b/packages/connector-slack/README.md @@ -37,3 +37,38 @@ adapter.onMessage(async (msg) => { await adapter.start(); ``` + +## Run from a local build (no npm publish) + +`@peekdev/connector-slack` is not published to npm — it runs from a local +build, spawned by the `peek connect` daemon. + +1. Build the connector: + + ```sh + pnpm --filter @peekdev/connector-slack build + ``` + +2. Register it with peek using the `--local` shorthand (resolves the path to + an absolute path and sets `command=node`): + + ```sh + peek connect add slack --local /packages/connector-slack/dist/index.js + ``` + +3. Start the daemon: + + ```sh + peek connect start + ``` + +**Notes:** + +- Slack tokens (`SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN`) are captured + interactively on first run and stored in the system keychain (SP6b-1); you + do not need to set them manually after the first interactive run. +- The daemon inherits `PEEK_LLM_*` and `PEEK_MCP_COMMAND` from the shell + that ran `peek connect start` — set those in your shell profile before + starting the daemon. +- Pairing with the browser extension happens via the extension side panel + after the daemon is running. diff --git a/packages/peek-cli/src/commands/connect.test.ts b/packages/peek-cli/src/commands/connect.test.ts index a796a51..a9c34db 100644 --- a/packages/peek-cli/src/commands/connect.test.ts +++ b/packages/peek-cli/src/commands/connect.test.ts @@ -102,6 +102,63 @@ describe('peek connect add', () => { const entry = Object.values(file.connectors)[0]; expect(entry?.args).toEqual(['--token', 'xoxb-test']); }); + + it('--local resolves to absolute path, sets command=node and args=[absPath]', async () => { + silenced(); + const relPath = 'packages/connector-slack/dist/index.js'; + const code = await runConnect(['add', 'slack', `--local=${relPath}`]); + expect(code).toBe(0); + + const file = readConnectors(); + const entry = Object.values(file.connectors)[0]; + expect(entry?.command).toBe('node'); + // args must contain exactly the absolute resolved path + expect(entry?.args).toHaveLength(1); + const absPath = entry?.args?.[0]; + expect(absPath).toBeDefined(); + // must be absolute + expect(absPath?.startsWith('/')).toBe(true); + // must end with the relative tail + expect(absPath?.endsWith(relPath)).toBe(true); + }); + + it('--local + --command together returns 1 and prints conflict message', async () => { + const { err } = silenced(); + const code = await runConnect([ + 'add', + 'slack', + '--local=packages/connector-slack/dist/index.js', + '--command=my-bin', + ]); + expect(code).toBe(1); + expect(err.join('')).toMatch(/--local.*--command|--command.*--local|conflict/i); + }); + + it('--local + --args together returns 1 and prints conflict message', async () => { + const { err } = silenced(); + const code = await runConnect([ + 'add', + 'slack', + '--local=packages/connector-slack/dist/index.js', + '--args=--foo', + ]); + expect(code).toBe(1); + expect(err.join('')).toMatch(/--local.*--args|--args.*--local|conflict/i); + }); + + it('plain add slack (no --local) still works unchanged', async () => { + silenced(); + const code = await runConnect(['add', 'slack']); + expect(code).toBe(0); + + const file = readConnectors(); + const entry = Object.values(file.connectors)[0]; + // no command or args injected — descriptor default only + expect(entry?.surface).toBe('slack'); + expect(entry?.enabled).toBe(true); + expect(entry?.command).toBeUndefined(); + expect(entry?.args).toBeUndefined(); + }); }); // ── list ─────────────────────────────────────────────────────────────────── diff --git a/packages/peek-cli/src/commands/connect.ts b/packages/peek-cli/src/commands/connect.ts index 3ea4dc6..18bf343 100644 --- a/packages/peek-cli/src/commands/connect.ts +++ b/packages/peek-cli/src/commands/connect.ts @@ -6,7 +6,7 @@ import { spawn as _realSpawn } from 'node:child_process'; import { closeSync, mkdirSync, openSync } from 'node:fs'; -import { dirname, join } from 'node:path'; +import { dirname, join, resolve } from 'node:path'; import { parseArgs } from 'node:util'; import { getDescriptor, resolveSpawn } from '../lib/connect/descriptors.js'; import { @@ -31,6 +31,8 @@ const USAGE = `Usage: peek connect [options] Subcommands: add [--name ] [--command ] [--args=] (repeatable) Register a connector for a surface + add --local Run a locally-built connector: sugar for + --command node --args= list List all configured connectors remove Remove a connector from the registry start Start the connector supervisor daemon @@ -568,6 +570,7 @@ const ADD_FLAGS = { name: { type: 'string' }, command: { type: 'string' }, args: { type: 'string', multiple: true }, + local: { type: 'string' }, help: { type: 'boolean' }, } as const; @@ -583,6 +586,7 @@ function runAdd(rest: string[]): number { name?: string; command?: string; args?: string[]; + local?: string; help?: boolean; }; try { @@ -596,22 +600,50 @@ function runAdd(rest: string[]): number { return 0; } + // Mutual exclusion: --local cannot be combined with --command or --args. + if (values.local !== undefined) { + if (values.command !== undefined) { + process.stderr.write( + 'peek connect add: --local and --command conflict — use one or the other\n', + ); + return 1; + } + if (values.args !== undefined && values.args.length > 0) { + process.stderr.write( + 'peek connect add: --local and --args conflict — use one or the other\n', + ); + return 1; + } + } + const descriptor = getDescriptor(surface); - if (descriptor === undefined && values.command === undefined) { + if (descriptor === undefined && values.command === undefined && values.local === undefined) { process.stderr.write( - `peek connect add: unknown surface '${surface}' — pass --command to use a custom connector binary\n`, + `peek connect add: unknown surface '${surface}' — pass --command or --local to use a custom connector binary\n`, ); return 1; } const name = values.name ?? surface; + // --local : desugar to command=node, args=[absPath]. + let effectiveCommand: string | undefined; + let effectiveArgs: string[] | undefined; + + if (values.local !== undefined) { + effectiveCommand = 'node'; + effectiveArgs = [resolve(process.cwd(), values.local)]; + } else { + effectiveCommand = values.command; + effectiveArgs = values.args !== undefined && values.args.length > 0 ? values.args : undefined; + } + // Build entry conditionally to satisfy exactOptionalPropertyTypes. const entry = { surface, enabled: true, - ...(values.command !== undefined ? { command: values.command } : {}), - ...(values.args !== undefined && values.args.length > 0 ? { args: values.args } : {}), + ...(effectiveCommand !== undefined ? { command: effectiveCommand } : {}), + ...(effectiveArgs !== undefined ? { args: effectiveArgs } : {}), }; addConnector(name, entry); From 4e57df4ef2668c8359e8ef85c0bb454c9f83c84e Mon Sep 17 00:00:00 2001 From: harry-harish <22562634+harry-harish@users.noreply.github.com> Date: Thu, 9 Jul 2026 12:53:10 +0530 Subject: [PATCH 2/2] test(peek-cli): make --local path assertions Windows-safe Replace POSIX-only startsWith('/') and endsWith(relPath) checks with isAbsolute() and resolve(cwd, relPath) equality, which are correct on both POSIX and Windows (drive-letter paths + backslash separators). Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: harry-harish <22562634+harry-harish@users.noreply.github.com> --- packages/peek-cli/src/commands/connect.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/peek-cli/src/commands/connect.test.ts b/packages/peek-cli/src/commands/connect.test.ts index a9c34db..2dcaae9 100644 --- a/packages/peek-cli/src/commands/connect.test.ts +++ b/packages/peek-cli/src/commands/connect.test.ts @@ -6,7 +6,7 @@ import { mkdtempSync, rmSync } from 'node:fs'; import { mkdirSync as fsMkdirSync, writeFileSync as fsWriteFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; -import { join } from 'node:path'; +import { isAbsolute, join, resolve } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { run } from '../index.js'; import { readConnectors } from '../lib/connect/registry.js'; @@ -117,9 +117,9 @@ describe('peek connect add', () => { const absPath = entry?.args?.[0]; expect(absPath).toBeDefined(); // must be absolute - expect(absPath?.startsWith('/')).toBe(true); - // must end with the relative tail - expect(absPath?.endsWith(relPath)).toBe(true); + expect(isAbsolute(absPath ?? '')).toBe(true); + // must end with the relative tail (separator-agnostic) + expect(absPath).toBe(resolve(process.cwd(), relPath)); }); it('--local + --command together returns 1 and prints conflict message', async () => {