From df1d709b98be55f712661735ac7718f2500a8a55 Mon Sep 17 00:00:00 2001 From: Davi Rodrigues Date: Mon, 9 Mar 2026 23:12:54 -0300 Subject: [PATCH 01/11] feat(MORG-13): add Claude CLI AI provider Adds a `claude-cli` backend that calls the local `claude -p` binary instead of hitting the Anthropic API, so users with a Claude subscription but no API key can use AI features (standup, PR descriptions, PR review). - New `ClaudeCLIProvider` implementing `AIProvider` via `claude --print` - `aiProvider` field in `GlobalConfigSchema` (`anthropic-api` | `claude-cli`) - Registry `ai()` returns `ClaudeCLIProvider` when `aiProvider === 'claude-cli'` - Config wizard gains an AI provider `select` prompt; `--show` displays it - Standup error message updated to be provider-agnostic - 9 integration tests for `ClaudeCLIProvider` --- src/commands/config.ts | 11 ++ src/commands/standup.ts | 4 +- src/config/schemas.ts | 1 + .../implementations/claude-cli-ai-provider.ts | 34 ++++++ src/services/registry.ts | 2 + tests/integrations/claude-cli.test.ts | 100 ++++++++++++++++++ 6 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 src/integrations/providers/ai/implementations/claude-cli-ai-provider.ts create mode 100644 tests/integrations/claude-cli.test.ts diff --git a/src/commands/config.ts b/src/commands/config.ts index 657312d..62c709b 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -20,6 +20,15 @@ async function runConfigWizard(existing: GlobalConfig | undefined): Promise { console.log(` githubUsername: ${theme.primary(config.githubUsername)}`); if (config.anthropicApiKey) console.log(` anthropicApiKey: ${theme.muted(redact(config.anthropicApiKey))}`); + if (config.aiProvider) console.log(` aiProvider: ${theme.primary(config.aiProvider)}`); console.log(` autoStash: ${theme.primary(config.autoStash)}`); console.log(` autoDeleteMerged: ${theme.primary(config.autoDeleteMerged)}`); console.log(` autoUpdateTicketStatus: ${theme.primary(config.autoUpdateTicketStatus)}`); diff --git a/src/commands/standup.ts b/src/commands/standup.ts index 656c1d3..1a62bc3 100644 --- a/src/commands/standup.ts +++ b/src/commands/standup.ts @@ -27,8 +27,8 @@ async function runStandup(options: { post?: boolean; channel?: string }): Promis const ai = await registry.ai(); if (!ai) { console.error( - theme.error('Anthropic API key is required for standup.'), - theme.muted('Run: morg config'), + theme.error('An AI provider is required for standup.'), + theme.muted('Run: morg config — set an Anthropic API key or enable Claude CLI'), ); process.exit(1); } diff --git a/src/config/schemas.ts b/src/config/schemas.ts index 2ab6cbf..2a37900 100644 --- a/src/config/schemas.ts +++ b/src/config/schemas.ts @@ -30,6 +30,7 @@ export const GlobalConfigSchema = z.object({ version: z.literal(1), githubUsername: z.string().min(1), anthropicApiKey: z.string().min(1).optional(), + aiProvider: z.enum(['anthropic-api', 'claude-cli']).optional(), autoStash: z.enum(['always', 'ask', 'never']).default('ask'), lastStashChoice: z.enum(['stash', 'skip']).optional(), autoDeleteMerged: z.enum(['always', 'ask', 'never']).default('ask'), diff --git a/src/integrations/providers/ai/implementations/claude-cli-ai-provider.ts b/src/integrations/providers/ai/implementations/claude-cli-ai-provider.ts new file mode 100644 index 0000000..2584a07 --- /dev/null +++ b/src/integrations/providers/ai/implementations/claude-cli-ai-provider.ts @@ -0,0 +1,34 @@ +import { execa } from 'execa'; +import type { AIProvider } from '../ai-provider'; +import { IntegrationError } from '../../../../utils/errors'; + +export class ClaudeCLIProvider implements AIProvider { + async complete(prompt: string, systemPrompt?: string): Promise { + const args = ['--print', '--tools', '', '--no-session-persistence', prompt]; + if (systemPrompt) args.unshift('--system-prompt', systemPrompt); + + let result; + try { + result = await execa('claude', args, { reject: false }); + } catch (err: unknown) { + if ((err as NodeJS.ErrnoException).code === 'ENOENT') { + throw new IntegrationError( + 'Claude CLI binary not found.', + 'claude-cli', + 'Install the Claude CLI: https://claude.ai/download', + ); + } + throw err; + } + + if (result.exitCode !== 0) { + throw new IntegrationError( + `Claude CLI exited with code ${result.exitCode}: ${result.stderr}`, + 'claude-cli', + 'Ensure the claude CLI is installed and authenticated. Run: claude --version', + ); + } + + return result.stdout.trim(); + } +} diff --git a/src/services/registry.ts b/src/services/registry.ts index ad49700..826d41d 100644 --- a/src/services/registry.ts +++ b/src/services/registry.ts @@ -4,6 +4,7 @@ import { GhClient } from '../integrations/providers/github/github-client'; import { JiraClient } from '../integrations/providers/tickets/implementations/jira-tickets-provider'; import { NotionClient } from '../integrations/providers/tickets/implementations/notion-tickets-provider'; import { ClaudeClient } from '../integrations/providers/ai/implementations/claude-ai-provider'; +import { ClaudeCLIProvider } from '../integrations/providers/ai/implementations/claude-cli-ai-provider'; import { SlackClient } from '../integrations/providers/messaging/implementations/slack-messaging-provider'; import type { TicketsProvider } from '../integrations/providers/tickets/tickets-provider'; import type { AIProvider } from '../integrations/providers/ai/ai-provider'; @@ -41,6 +42,7 @@ class Registry { async ai(): Promise { const globalConfig = await configManager.getGlobalConfig(await this.pid()); + if (globalConfig.aiProvider === 'claude-cli') return new ClaudeCLIProvider(); return globalConfig.anthropicApiKey ? new ClaudeClient(globalConfig.anthropicApiKey) : null; } diff --git a/tests/integrations/claude-cli.test.ts b/tests/integrations/claude-cli.test.ts new file mode 100644 index 0000000..e70500a --- /dev/null +++ b/tests/integrations/claude-cli.test.ts @@ -0,0 +1,100 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { execa } from 'execa'; +import { ClaudeCLIProvider } from '../../src/integrations/providers/ai/implementations/claude-cli-ai-provider'; +import { IntegrationError } from '../../src/utils/errors'; + +vi.mock('execa'); + +const mockExeca = execa as unknown as ReturnType; + +describe('ClaudeCLIProvider', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('returns trimmed stdout on success', async () => { + mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: ' hello world\n', stderr: '' }); + + const provider = new ClaudeCLIProvider(); + const result = await provider.complete('Say hello'); + + expect(result).toBe('hello world'); + }); + + it('always passes --print, --tools, --no-session-persistence', async () => { + mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: 'ok', stderr: '' }); + + const provider = new ClaudeCLIProvider(); + await provider.complete('test prompt'); + + const [cmd, args] = mockExeca.mock.calls[0] as [string, string[]]; + expect(cmd).toBe('claude'); + expect(args).toContain('--print'); + expect(args).toContain('--tools'); + expect(args).toContain('--no-session-persistence'); + }); + + it('includes prompt as last arg', async () => { + mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: 'ok', stderr: '' }); + + const provider = new ClaudeCLIProvider(); + await provider.complete('my prompt'); + + const [, args] = mockExeca.mock.calls[0] as [string, string[]]; + expect(args[args.length - 1]).toBe('my prompt'); + }); + + it('passes --system-prompt arg when systemPrompt is provided', async () => { + mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: 'ok', stderr: '' }); + + const provider = new ClaudeCLIProvider(); + await provider.complete('user prompt', 'system instructions'); + + const [, args] = mockExeca.mock.calls[0] as [string, string[]]; + const sysIdx = args.indexOf('--system-prompt'); + expect(sysIdx).toBeGreaterThanOrEqual(0); + expect(args[sysIdx + 1]).toBe('system instructions'); + }); + + it('omits --system-prompt when no systemPrompt given', async () => { + mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: 'ok', stderr: '' }); + + const provider = new ClaudeCLIProvider(); + await provider.complete('user prompt'); + + const [, args] = mockExeca.mock.calls[0] as [string, string[]]; + expect(args).not.toContain('--system-prompt'); + }); + + it('throws IntegrationError on non-zero exit code', async () => { + mockExeca.mockResolvedValueOnce({ exitCode: 1, stdout: '', stderr: 'auth error' }); + + const provider = new ClaudeCLIProvider(); + await expect(provider.complete('test')).rejects.toThrow(/exited with code 1/); + }); + + it('throws IntegrationError type on non-zero exit code', async () => { + mockExeca.mockResolvedValueOnce({ exitCode: 1, stdout: '', stderr: 'auth error' }); + + const provider = new ClaudeCLIProvider(); + await expect(provider.complete('test')).rejects.toThrow(IntegrationError); + }); + + it('throws IntegrationError when binary not found (ENOENT)', async () => { + const err = new Error('spawn claude ENOENT') as NodeJS.ErrnoException; + err.code = 'ENOENT'; + mockExeca.mockRejectedValueOnce(err); + + const provider = new ClaudeCLIProvider(); + await expect(provider.complete('test')).rejects.toThrow(/not found/); + }); + + it('throws IntegrationError type when binary not found (ENOENT)', async () => { + const err = new Error('spawn claude ENOENT') as NodeJS.ErrnoException; + err.code = 'ENOENT'; + mockExeca.mockRejectedValueOnce(err); + + const provider = new ClaudeCLIProvider(); + await expect(provider.complete('test')).rejects.toThrow(IntegrationError); + }); +}); From 898707067247334803243e207573d2ed00b53e07 Mon Sep 17 00:00:00 2001 From: Davi Rodrigues Date: Tue, 10 Mar 2026 00:19:54 -0300 Subject: [PATCH 02/11] fix: add missing null-coalescing in password() prompt wrapper clack v1.1 password() can return undefined for empty input; add ?? '' fallback (matching text()) to prevent "Cannot read properties of undefined (reading 'trim')" errors in the config wizard. --- src/ui/prompts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/prompts.ts b/src/ui/prompts.ts index 1e95002..a2ac973 100644 --- a/src/ui/prompts.ts +++ b/src/ui/prompts.ts @@ -31,7 +31,7 @@ export async function password(opts: { clack.cancel('Operation cancelled.'); process.exit(0); } - return result; + return result ?? ''; } export async function confirm(opts: { message: string; initialValue?: boolean }): Promise { From bd7eee1c274fe0dcf66755338916c1cdd2b1f2c9 Mon Sep 17 00:00:00 2001 From: Davi Rodrigues Date: Tue, 10 Mar 2026 00:23:47 -0300 Subject: [PATCH 03/11] fix: remove unsupported --tools flag from claude CLI invocation --tools is not available in all claude CLI versions; drop it since --print alone is sufficient for text-only completions. --- .../providers/ai/implementations/claude-cli-ai-provider.ts | 2 +- tests/integrations/claude-cli.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/integrations/providers/ai/implementations/claude-cli-ai-provider.ts b/src/integrations/providers/ai/implementations/claude-cli-ai-provider.ts index 2584a07..e9e54bd 100644 --- a/src/integrations/providers/ai/implementations/claude-cli-ai-provider.ts +++ b/src/integrations/providers/ai/implementations/claude-cli-ai-provider.ts @@ -4,7 +4,7 @@ import { IntegrationError } from '../../../../utils/errors'; export class ClaudeCLIProvider implements AIProvider { async complete(prompt: string, systemPrompt?: string): Promise { - const args = ['--print', '--tools', '', '--no-session-persistence', prompt]; + const args = ['--print', '--no-session-persistence', prompt]; if (systemPrompt) args.unshift('--system-prompt', systemPrompt); let result; diff --git a/tests/integrations/claude-cli.test.ts b/tests/integrations/claude-cli.test.ts index e70500a..b27e34b 100644 --- a/tests/integrations/claude-cli.test.ts +++ b/tests/integrations/claude-cli.test.ts @@ -21,7 +21,7 @@ describe('ClaudeCLIProvider', () => { expect(result).toBe('hello world'); }); - it('always passes --print, --tools, --no-session-persistence', async () => { + it('always passes --print and --no-session-persistence', async () => { mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: 'ok', stderr: '' }); const provider = new ClaudeCLIProvider(); @@ -30,7 +30,7 @@ describe('ClaudeCLIProvider', () => { const [cmd, args] = mockExeca.mock.calls[0] as [string, string[]]; expect(cmd).toBe('claude'); expect(args).toContain('--print'); - expect(args).toContain('--tools'); + expect(args).not.toContain('--tools'); expect(args).toContain('--no-session-persistence'); }); From ea07fad36cf4d56acdedc470e5baf869dedc6b78 Mon Sep 17 00:00:00 2001 From: Davi Rodrigues Date: Tue, 10 Mar 2026 00:26:42 -0300 Subject: [PATCH 04/11] fix: drop --no-session-persistence flag for wider claude CLI compatibility --- .../providers/ai/implementations/claude-cli-ai-provider.ts | 2 +- tests/integrations/claude-cli.test.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/integrations/providers/ai/implementations/claude-cli-ai-provider.ts b/src/integrations/providers/ai/implementations/claude-cli-ai-provider.ts index e9e54bd..df412aa 100644 --- a/src/integrations/providers/ai/implementations/claude-cli-ai-provider.ts +++ b/src/integrations/providers/ai/implementations/claude-cli-ai-provider.ts @@ -4,7 +4,7 @@ import { IntegrationError } from '../../../../utils/errors'; export class ClaudeCLIProvider implements AIProvider { async complete(prompt: string, systemPrompt?: string): Promise { - const args = ['--print', '--no-session-persistence', prompt]; + const args = ['--print', prompt]; if (systemPrompt) args.unshift('--system-prompt', systemPrompt); let result; diff --git a/tests/integrations/claude-cli.test.ts b/tests/integrations/claude-cli.test.ts index b27e34b..2f87598 100644 --- a/tests/integrations/claude-cli.test.ts +++ b/tests/integrations/claude-cli.test.ts @@ -21,7 +21,7 @@ describe('ClaudeCLIProvider', () => { expect(result).toBe('hello world'); }); - it('always passes --print and --no-session-persistence', async () => { + it('always passes --print', async () => { mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: 'ok', stderr: '' }); const provider = new ClaudeCLIProvider(); @@ -30,8 +30,6 @@ describe('ClaudeCLIProvider', () => { const [cmd, args] = mockExeca.mock.calls[0] as [string, string[]]; expect(cmd).toBe('claude'); expect(args).toContain('--print'); - expect(args).not.toContain('--tools'); - expect(args).toContain('--no-session-persistence'); }); it('includes prompt as last arg', async () => { From ad76ae1dcd5ccc13d9c254584caafe40ea566e92 Mon Sep 17 00:00:00 2001 From: Davi Rodrigues Date: Tue, 10 Mar 2026 00:32:07 -0300 Subject: [PATCH 05/11] fix: pipe prompt via stdin instead of positional arg Large prompts (e.g. PR diffs) stall when passed as a CLI argument; piping via stdin handles arbitrary-length input cleanly. --- .../providers/ai/implementations/claude-cli-ai-provider.ts | 6 +++--- tests/integrations/claude-cli.test.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/integrations/providers/ai/implementations/claude-cli-ai-provider.ts b/src/integrations/providers/ai/implementations/claude-cli-ai-provider.ts index df412aa..d6a9e07 100644 --- a/src/integrations/providers/ai/implementations/claude-cli-ai-provider.ts +++ b/src/integrations/providers/ai/implementations/claude-cli-ai-provider.ts @@ -4,12 +4,12 @@ import { IntegrationError } from '../../../../utils/errors'; export class ClaudeCLIProvider implements AIProvider { async complete(prompt: string, systemPrompt?: string): Promise { - const args = ['--print', prompt]; - if (systemPrompt) args.unshift('--system-prompt', systemPrompt); + const args: string[] = ['--print']; + if (systemPrompt) args.push('--system-prompt', systemPrompt); let result; try { - result = await execa('claude', args, { reject: false }); + result = await execa('claude', args, { input: prompt, reject: false }); } catch (err: unknown) { if ((err as NodeJS.ErrnoException).code === 'ENOENT') { throw new IntegrationError( diff --git a/tests/integrations/claude-cli.test.ts b/tests/integrations/claude-cli.test.ts index 2f87598..f03ffa8 100644 --- a/tests/integrations/claude-cli.test.ts +++ b/tests/integrations/claude-cli.test.ts @@ -32,14 +32,14 @@ describe('ClaudeCLIProvider', () => { expect(args).toContain('--print'); }); - it('includes prompt as last arg', async () => { + it('passes prompt via stdin input option', async () => { mockExeca.mockResolvedValueOnce({ exitCode: 0, stdout: 'ok', stderr: '' }); const provider = new ClaudeCLIProvider(); await provider.complete('my prompt'); - const [, args] = mockExeca.mock.calls[0] as [string, string[]]; - expect(args[args.length - 1]).toBe('my prompt'); + const [, , opts] = mockExeca.mock.calls[0] as [string, string[], { input: string }]; + expect(opts.input).toBe('my prompt'); }); it('passes --system-prompt arg when systemPrompt is provided', async () => { From a6340c9aeb712f7c0af6716856d2a643d391a468 Mon Sep 17 00:00:00 2001 From: Davi Rodrigues Date: Tue, 10 Mar 2026 01:39:32 -0300 Subject: [PATCH 06/11] fix: unset CLAUDECODE env var when spawning claude CLI When morg is invoked from within Claude Code, the CLAUDECODE env var is set. The claude binary refuses to run nested sessions and exits with an error. Strip it (and CLAUDE_CODE_ENTRYPOINT) from the child process environment so morg can call claude --print from any context. --- .../providers/ai/implementations/claude-cli-ai-provider.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/integrations/providers/ai/implementations/claude-cli-ai-provider.ts b/src/integrations/providers/ai/implementations/claude-cli-ai-provider.ts index d6a9e07..d49c935 100644 --- a/src/integrations/providers/ai/implementations/claude-cli-ai-provider.ts +++ b/src/integrations/providers/ai/implementations/claude-cli-ai-provider.ts @@ -7,9 +7,13 @@ export class ClaudeCLIProvider implements AIProvider { const args: string[] = ['--print']; if (systemPrompt) args.push('--system-prompt', systemPrompt); + const env = { ...process.env }; + delete env['CLAUDECODE']; + delete env['CLAUDE_CODE_ENTRYPOINT']; + let result; try { - result = await execa('claude', args, { input: prompt, reject: false }); + result = await execa('claude', args, { input: prompt, reject: false, env }); } catch (err: unknown) { if ((err as NodeJS.ErrnoException).code === 'ENOENT') { throw new IntegrationError( From 85f77a847ea0f1fa3dea8189473b3da46499c984 Mon Sep 17 00:00:00 2001 From: Davi Rodrigues Date: Tue, 10 Mar 2026 02:02:33 -0300 Subject: [PATCH 07/11] feat: multi-line PR body editor using @inquirer/editor Replace the single-line text() prompt for PR body with an @inquirer/editor prompt that opens $EDITOR, allowing full multi-line editing of AI-generated PR descriptions. --- package.json | 1 + pnpm-lock.yaml | 131 +++++++++++++++++++++++++++++++++++++++++++++ src/commands/pr.ts | 7 ++- src/ui/prompts.ts | 5 ++ 4 files changed, 140 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 1fb45d7..2caefb8 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "dependencies": { "@anthropic-ai/sdk": "^0.78.0", "@clack/prompts": "^1.1.0", + "@inquirer/editor": "^5.0.8", "@notionhq/client": "^5.11.1", "@slack/web-api": "^7.14.1", "boxen": "^8.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 992fde7..da2d4bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@clack/prompts': specifier: ^1.1.0 version: 1.1.0 + '@inquirer/editor': + specifier: ^5.0.8 + version: 5.0.8(@types/node@22.19.13) '@notionhq/client': specifier: ^5.11.1 version: 5.11.1 @@ -303,6 +306,50 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@inquirer/ansi@2.0.3': + resolution: {integrity: sha512-g44zhR3NIKVs0zUesa4iMzExmZpLUdTLRMCStqX3GE5NT6VkPcxQGJ+uC8tDgBUC/vB1rUhUd55cOf++4NZcmw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + + '@inquirer/core@11.1.5': + resolution: {integrity: sha512-QQPAX+lka8GyLcZ7u7Nb1h6q72iZ/oy0blilC3IB2nSt1Qqxp7akt94Jqhi/DzARuN3Eo9QwJRvtl4tmVe4T5A==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@5.0.8': + resolution: {integrity: sha512-sLcpbb9B3XqUEGrj1N66KwhDhEckzZ4nI/W6SvLXyBX8Wic3LDLENlWRvkOGpCPoserabe+MxQkpiMoI8irvyA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@2.0.3': + resolution: {integrity: sha512-LgyI7Agbda74/cL5MvA88iDpvdXI2KuMBCGRkbCl2Dg1vzHeOgs+s0SDcXV7b+WZJrv2+ERpWSM65Fpi9VfY3w==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@2.0.3': + resolution: {integrity: sha512-y09iGt3JKoOCBQ3w4YrSJdokcD8ciSlMIWsD+auPu+OZpfxLuyz+gICAQ6GCBOmJJt4KEQGHuZSVff2jiNOy7g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + + '@inquirer/type@4.0.3': + resolution: {integrity: sha512-cKZN7qcXOpj1h+1eTTcGDVLaBIHNMT1Rz9JqJP5MnEJ0JhgVWllx7H/tahUp5YEK1qaByH2Itb8wLG/iScD5kw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -669,6 +716,9 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + check-error@2.1.3: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} @@ -693,6 +743,10 @@ packages: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} engines: {node: 10.* || >= 12.*} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -865,6 +919,15 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-wrap-ansi@0.2.0: + resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -960,6 +1023,10 @@ packages: resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} engines: {node: '>=18.18.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1092,6 +1159,10 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -1254,6 +1325,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} @@ -1676,6 +1750,41 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@inquirer/ansi@2.0.3': {} + + '@inquirer/core@11.1.5(@types/node@22.19.13)': + dependencies: + '@inquirer/ansi': 2.0.3 + '@inquirer/figures': 2.0.3 + '@inquirer/type': 4.0.3(@types/node@22.19.13) + cli-width: 4.1.0 + fast-wrap-ansi: 0.2.0 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + optionalDependencies: + '@types/node': 22.19.13 + + '@inquirer/editor@5.0.8(@types/node@22.19.13)': + dependencies: + '@inquirer/core': 11.1.5(@types/node@22.19.13) + '@inquirer/external-editor': 2.0.3(@types/node@22.19.13) + '@inquirer/type': 4.0.3(@types/node@22.19.13) + optionalDependencies: + '@types/node': 22.19.13 + + '@inquirer/external-editor@2.0.3(@types/node@22.19.13)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 22.19.13 + + '@inquirer/figures@2.0.3': {} + + '@inquirer/type@4.0.3(@types/node@22.19.13)': + optionalDependencies: + '@types/node': 22.19.13 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2026,6 +2135,8 @@ snapshots: chalk@5.6.2: {} + chardet@2.1.1: {} + check-error@2.1.3: {} chokidar@4.0.3: @@ -2046,6 +2157,8 @@ snapshots: optionalDependencies: '@colors/colors': 1.5.0 + cli-width@4.1.0: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -2242,6 +2355,16 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-wrap-ansi@0.2.0: + dependencies: + fast-string-width: 3.0.2 + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -2334,6 +2457,10 @@ snapshots: human-signals@8.0.1: {} + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} ignore@7.0.5: {} @@ -2432,6 +2559,8 @@ snapshots: ms@2.1.3: {} + mute-stream@3.0.0: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -2595,6 +2724,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 + safer-buffer@2.1.2: {} + semver@7.7.4: {} shebang-command@2.0.0: diff --git a/src/commands/pr.ts b/src/commands/pr.ts index 828c9eb..a5a653c 100644 --- a/src/commands/pr.ts +++ b/src/commands/pr.ts @@ -13,7 +13,7 @@ import { requireTrackedRepo } from '../utils/detect'; import { findBranchCaseInsensitive } from '../utils/ticket'; import { theme, symbols } from '../ui/theme'; import { withSpinner } from '../ui/spinner'; -import { intro, outro, text } from '../ui/prompts'; +import { intro, outro, text, editor } from '../ui/prompts'; import { registry } from '../services/registry'; async function runPrCreate(options: { @@ -80,10 +80,9 @@ async function runPrCreate(options: { const body = options.yes ? bodyDefault - : await text({ - message: 'PR body (optional)', + : await editor({ + message: 'PR body (Markdown)', initialValue: bodyDefault, - placeholder: 'Leave blank to skip', }); const remoteHasBase = diff --git a/src/ui/prompts.ts b/src/ui/prompts.ts index a2ac973..3776d53 100644 --- a/src/ui/prompts.ts +++ b/src/ui/prompts.ts @@ -1,4 +1,5 @@ import * as clack from '@clack/prompts'; +import inquirerEditor from '@inquirer/editor'; export function intro(title: string): void { clack.intro(title); @@ -58,4 +59,8 @@ export async function select(opts: { return result as T; } +export async function editor(opts: { message: string; initialValue?: string }): Promise { + return inquirerEditor({ message: opts.message, default: opts.initialValue }); +} + export { clack }; From 3fcd70a2940fa597b916aa2f9eeb687d0f7de93c Mon Sep 17 00:00:00 2001 From: Davi Rodrigues Date: Tue, 10 Mar 2026 02:02:55 -0300 Subject: [PATCH 08/11] fix: restore --tools and --no-session-persistence flags These are supported by claude CLI >=2.1.69. The flags were removed during debugging but work correctly with the current version. --- .../providers/ai/implementations/claude-cli-ai-provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/integrations/providers/ai/implementations/claude-cli-ai-provider.ts b/src/integrations/providers/ai/implementations/claude-cli-ai-provider.ts index d49c935..454f697 100644 --- a/src/integrations/providers/ai/implementations/claude-cli-ai-provider.ts +++ b/src/integrations/providers/ai/implementations/claude-cli-ai-provider.ts @@ -4,7 +4,7 @@ import { IntegrationError } from '../../../../utils/errors'; export class ClaudeCLIProvider implements AIProvider { async complete(prompt: string, systemPrompt?: string): Promise { - const args: string[] = ['--print']; + const args: string[] = ['--print', '--tools', '', '--no-session-persistence']; if (systemPrompt) args.push('--system-prompt', systemPrompt); const env = { ...process.env }; From d9e289ff21d631765f6de0c8f137059688240cfb Mon Sep 17 00:00:00 2001 From: Davi Rodrigues Date: Tue, 10 Mar 2026 02:08:27 -0300 Subject: [PATCH 09/11] feat: replace @inquirer/editor with $EDITOR-based multi-line prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the 700KB+ inquirer dependency. The editor() prompt writes initial content to a temp file, opens $VISUAL/$EDITOR/vi with inherited stdio, and reads back the result — same UX, zero new deps. --- package.json | 1 - pnpm-lock.yaml | 131 ---------------------------------------------- src/ui/prompts.ts | 22 +++++++- 3 files changed, 20 insertions(+), 134 deletions(-) diff --git a/package.json b/package.json index 2caefb8..1fb45d7 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "dependencies": { "@anthropic-ai/sdk": "^0.78.0", "@clack/prompts": "^1.1.0", - "@inquirer/editor": "^5.0.8", "@notionhq/client": "^5.11.1", "@slack/web-api": "^7.14.1", "boxen": "^8.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da2d4bd..992fde7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,9 +14,6 @@ importers: '@clack/prompts': specifier: ^1.1.0 version: 1.1.0 - '@inquirer/editor': - specifier: ^5.0.8 - version: 5.0.8(@types/node@22.19.13) '@notionhq/client': specifier: ^5.11.1 version: 5.11.1 @@ -306,50 +303,6 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@inquirer/ansi@2.0.3': - resolution: {integrity: sha512-g44zhR3NIKVs0zUesa4iMzExmZpLUdTLRMCStqX3GE5NT6VkPcxQGJ+uC8tDgBUC/vB1rUhUd55cOf++4NZcmw==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - - '@inquirer/core@11.1.5': - resolution: {integrity: sha512-QQPAX+lka8GyLcZ7u7Nb1h6q72iZ/oy0blilC3IB2nSt1Qqxp7akt94Jqhi/DzARuN3Eo9QwJRvtl4tmVe4T5A==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/editor@5.0.8': - resolution: {integrity: sha512-sLcpbb9B3XqUEGrj1N66KwhDhEckzZ4nI/W6SvLXyBX8Wic3LDLENlWRvkOGpCPoserabe+MxQkpiMoI8irvyA==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/external-editor@2.0.3': - resolution: {integrity: sha512-LgyI7Agbda74/cL5MvA88iDpvdXI2KuMBCGRkbCl2Dg1vzHeOgs+s0SDcXV7b+WZJrv2+ERpWSM65Fpi9VfY3w==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/figures@2.0.3': - resolution: {integrity: sha512-y09iGt3JKoOCBQ3w4YrSJdokcD8ciSlMIWsD+auPu+OZpfxLuyz+gICAQ6GCBOmJJt4KEQGHuZSVff2jiNOy7g==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - - '@inquirer/type@4.0.3': - resolution: {integrity: sha512-cKZN7qcXOpj1h+1eTTcGDVLaBIHNMT1Rz9JqJP5MnEJ0JhgVWllx7H/tahUp5YEK1qaByH2Itb8wLG/iScD5kw==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -716,9 +669,6 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - chardet@2.1.1: - resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} - check-error@2.1.3: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} @@ -743,10 +693,6 @@ packages: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} engines: {node: 10.* || >= 12.*} - cli-width@4.1.0: - resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} - engines: {node: '>= 12'} - combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -919,15 +865,6 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-string-truncated-width@3.0.3: - resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} - - fast-string-width@3.0.2: - resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} - - fast-wrap-ansi@0.2.0: - resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1023,10 +960,6 @@ packages: resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} engines: {node: '>=18.18.0'} - iconv-lite@0.7.2: - resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} - engines: {node: '>=0.10.0'} - ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1159,10 +1092,6 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - mute-stream@3.0.0: - resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} - engines: {node: ^20.17.0 || >=22.9.0} - mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -1325,9 +1254,6 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} @@ -1750,41 +1676,6 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@inquirer/ansi@2.0.3': {} - - '@inquirer/core@11.1.5(@types/node@22.19.13)': - dependencies: - '@inquirer/ansi': 2.0.3 - '@inquirer/figures': 2.0.3 - '@inquirer/type': 4.0.3(@types/node@22.19.13) - cli-width: 4.1.0 - fast-wrap-ansi: 0.2.0 - mute-stream: 3.0.0 - signal-exit: 4.1.0 - optionalDependencies: - '@types/node': 22.19.13 - - '@inquirer/editor@5.0.8(@types/node@22.19.13)': - dependencies: - '@inquirer/core': 11.1.5(@types/node@22.19.13) - '@inquirer/external-editor': 2.0.3(@types/node@22.19.13) - '@inquirer/type': 4.0.3(@types/node@22.19.13) - optionalDependencies: - '@types/node': 22.19.13 - - '@inquirer/external-editor@2.0.3(@types/node@22.19.13)': - dependencies: - chardet: 2.1.1 - iconv-lite: 0.7.2 - optionalDependencies: - '@types/node': 22.19.13 - - '@inquirer/figures@2.0.3': {} - - '@inquirer/type@4.0.3(@types/node@22.19.13)': - optionalDependencies: - '@types/node': 22.19.13 - '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2135,8 +2026,6 @@ snapshots: chalk@5.6.2: {} - chardet@2.1.1: {} - check-error@2.1.3: {} chokidar@4.0.3: @@ -2157,8 +2046,6 @@ snapshots: optionalDependencies: '@colors/colors': 1.5.0 - cli-width@4.1.0: {} - combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -2355,16 +2242,6 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-string-truncated-width@3.0.3: {} - - fast-string-width@3.0.2: - dependencies: - fast-string-truncated-width: 3.0.3 - - fast-wrap-ansi@0.2.0: - dependencies: - fast-string-width: 3.0.2 - fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -2457,10 +2334,6 @@ snapshots: human-signals@8.0.1: {} - iconv-lite@0.7.2: - dependencies: - safer-buffer: 2.1.2 - ignore@5.3.2: {} ignore@7.0.5: {} @@ -2559,8 +2432,6 @@ snapshots: ms@2.1.3: {} - mute-stream@3.0.0: {} - mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -2724,8 +2595,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 - safer-buffer@2.1.2: {} - semver@7.7.4: {} shebang-command@2.0.0: diff --git a/src/ui/prompts.ts b/src/ui/prompts.ts index 3776d53..2e4848b 100644 --- a/src/ui/prompts.ts +++ b/src/ui/prompts.ts @@ -1,5 +1,8 @@ +import { writeFileSync, readFileSync, unlinkSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { execa } from 'execa'; import * as clack from '@clack/prompts'; -import inquirerEditor from '@inquirer/editor'; export function intro(title: string): void { clack.intro(title); @@ -60,7 +63,22 @@ export async function select(opts: { } export async function editor(opts: { message: string; initialValue?: string }): Promise { - return inquirerEditor({ message: opts.message, default: opts.initialValue }); + const tmpFile = join(tmpdir(), `morg-${Date.now()}.md`); + writeFileSync(tmpFile, opts.initialValue ?? '', 'utf-8'); + + const editorCmd = process.env.VISUAL ?? process.env.EDITOR ?? 'vi'; + clack.log.step(`${opts.message} — opening ${editorCmd}`); + + try { + await execa(editorCmd, [tmpFile], { stdio: 'inherit', reject: false }); + return readFileSync(tmpFile, 'utf-8').trim(); + } finally { + try { + unlinkSync(tmpFile); + } catch { + // ignore cleanup errors + } + } } export { clack }; From 65b638a7d6b0ccbff210f3768b50990daa21b88d Mon Sep 17 00:00:00 2001 From: Davi Rodrigues Date: Tue, 10 Mar 2026 02:15:30 -0300 Subject: [PATCH 10/11] fix: ask AI provider before API key, add 'none' option - Move AI provider select before the API key prompt - Only show the Anthropic API key prompt when user picks 'anthropic-api' - Add 'None (disable AI features)' as a first-class option --- src/commands/config.ts | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/commands/config.ts b/src/commands/config.ts index 62c709b..5d2bbe4 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -12,22 +12,26 @@ async function runConfigWizard(existing: GlobalConfig | undefined): Promise (v?.trim() ? undefined : 'Required'), }); - const anthropicApiKeyRaw = await text({ - message: 'Anthropic API key (sk-ant-...) — leave blank to skip', - initialValue: existing?.anthropicApiKey ?? '', - validate: (v) => - !v?.trim() || v.startsWith('sk-ant-') ? undefined : 'Must start with sk-ant-', - }); - const anthropicApiKey = anthropicApiKeyRaw.trim() || undefined; - const aiProvider = (await select({ message: 'AI provider', options: [ - { value: 'anthropic-api', label: 'Anthropic API (use API key above)' }, - { value: 'claude-cli', label: 'Claude CLI (use local claude binary)' }, + { value: 'none', label: 'None (disable AI features)' }, + { value: 'anthropic-api', label: 'Anthropic API (API key)' }, + { value: 'claude-cli', label: 'Claude CLI (local claude binary)' }, ], - initialValue: existing?.aiProvider ?? 'anthropic-api', - })) as GlobalConfig['aiProvider']; + initialValue: (existing?.aiProvider ?? existing?.anthropicApiKey) ? 'anthropic-api' : 'none', + })) as GlobalConfig['aiProvider'] | 'none'; + + let anthropicApiKey: string | undefined; + if (aiProvider === 'anthropic-api') { + const raw = await text({ + message: 'Anthropic API key (sk-ant-...)', + initialValue: existing?.anthropicApiKey ?? '', + validate: (v) => + !v?.trim() || v.startsWith('sk-ant-') ? undefined : 'Must start with sk-ant-', + }); + anthropicApiKey = raw.trim() || undefined; + } const autoStash = await select({ message: 'Auto-stash dirty working tree on branch switch?', @@ -134,7 +138,7 @@ async function runConfigWizard(existing: GlobalConfig | undefined): Promise Date: Tue, 10 Mar 2026 02:20:18 -0300 Subject: [PATCH 11/11] feat: replace integration confirm prompts with multiselect Single 'Integrations to enable' multiselect (Jira / Notion / Slack) replaces three separate yes/no prompts. Credentials are only asked for selected providers. Jira and Notion are grouped with a 'tickets provider' hint so users know only one can be active per project. --- src/commands/config.ts | 57 +++++++++++++++++++++--------------------- src/ui/prompts.ts | 15 +++++++++++ 2 files changed, 44 insertions(+), 28 deletions(-) diff --git a/src/commands/config.ts b/src/commands/config.ts index 5d2bbe4..a9f0c92 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -2,7 +2,7 @@ import type { Command } from 'commander'; import { configManager } from '../config/manager'; import type { GlobalConfig } from '../config/schemas'; import { theme, symbols } from '../ui/theme'; -import { intro, outro, text, password, confirm, select } from '../ui/prompts'; +import { intro, outro, text, password, select, multiselect } from '../ui/prompts'; import { requireTrackedRepo } from '../utils/detect'; async function runConfigWizard(existing: GlobalConfig | undefined): Promise { @@ -63,13 +63,24 @@ async function runConfigWizard(existing: GlobalConfig | undefined): Promise({ + message: 'Integrations to enable (space to toggle)', + options: [ + { value: 'jira', label: 'Jira', hint: 'tickets provider' }, + { value: 'notion', label: 'Notion', hint: 'tickets provider' }, + { value: 'slack', label: 'Slack', hint: 'messaging / standup' }, + ], + initialValues: currentIntegrations, }); let jiraConfig: GlobalConfig['integrations']['jira'] = undefined; - if (enableJira) { + if (enabledIntegrations.includes('jira')) { const baseUrl = await text({ message: 'Jira base URL (e.g. https://yourorg.atlassian.net)', initialValue: existing?.integrations.jira?.baseUrl, @@ -91,13 +102,21 @@ async function runConfigWizard(existing: GlobalConfig | undefined): Promise (!v?.trim() && !existingNotionToken ? 'Required' : undefined), + }); + const apiToken = notionApiTokenRaw.trim() || existingNotionToken!; + notionConfig = { enabled: true, apiToken }; + } let slackConfig: GlobalConfig['integrations']['slack'] = undefined; - if (enableSlack) { + if (enabledIntegrations.includes('slack')) { const existingSlackToken = existing?.integrations.slack?.apiToken; const slackApiTokenRaw = await password({ message: existingSlackToken @@ -116,24 +135,6 @@ async function runConfigWizard(existing: GlobalConfig | undefined): Promise (!v?.trim() && !existingNotionToken ? 'Required' : undefined), - }); - const apiToken = notionApiTokenRaw.trim() || existingNotionToken!; - notionConfig = { enabled: true, apiToken }; - } - return { version: 1, githubUsername, diff --git a/src/ui/prompts.ts b/src/ui/prompts.ts index 2e4848b..2a52c57 100644 --- a/src/ui/prompts.ts +++ b/src/ui/prompts.ts @@ -62,6 +62,21 @@ export async function select(opts: { return result as T; } +export async function multiselect(opts: { + message: string; + options: { value: T; label: string; hint?: string }[]; + initialValues?: T[]; + required?: boolean; +}): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await clack.multiselect({ required: false, ...opts } as any); + if (clack.isCancel(result)) { + clack.cancel('Operation cancelled.'); + process.exit(0); + } + return result as T[]; +} + export async function editor(opts: { message: string; initialValue?: string }): Promise { const tmpFile = join(tmpdir(), `morg-${Date.now()}.md`); writeFileSync(tmpFile, opts.initialValue ?? '', 'utf-8');