diff --git a/README.md b/README.md index b52c0b63..6bc7bd72 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,13 @@ Show help: openwiki --help ``` +Reconfigure your saved provider, API key, model, and optional LangSmith key +without starting a documentation run: + +```sh +openwiki --config +``` + `openwiki` creates initial documentation in `openwiki/` when no wiki exists. If `openwiki/` already exists, it refreshes that documentation from repository changes. By default, the CLI stays open after each run so you can send follow-up messages. Use `-p` or `--print` for a one-shot non-interactive run that prints the final assistant output. `openwiki` will automatically append prompting to your `AGENTS.md` and/or `CLAUDE.md` files to instruct your coding agent to reference it when searching for context. If the file does not already exist in your repository, OpenWiki will create it for you. diff --git a/openwiki/cli/usage.md b/openwiki/cli/usage.md index 26f8f585..043f081f 100644 --- a/openwiki/cli/usage.md +++ b/openwiki/cli/usage.md @@ -10,6 +10,7 @@ From `src/commands.ts` and `README.md`, the supported entry patterns are: - `openwiki "message"` — send a chat message immediately, then stay open. - `openwiki --init [message]` — generate initial OpenWiki documentation. - `openwiki --update [message]` — refresh existing OpenWiki documentation. +- `openwiki --config` — re-run provider, API key, model, and optional LangSmith setup without starting an agent run. - `openwiki -p, --print` — run once and print the final assistant output (non-interactive). - `openwiki --modelId ` / `--model-id ` — choose a model ID for the run. - `openwiki --help` / `-h` — print usage, options, and examples. diff --git a/src/cli.tsx b/src/cli.tsx index a4723292..e81f2def 100644 --- a/src/cli.tsx +++ b/src/cli.tsx @@ -153,6 +153,8 @@ function App({ command }: AppProps) { process.stdin.isTTY && runState.status === "idle" && needsCredentialSetup(sessionModelId); + const shouldRunConfigSetup = + command.kind === "config" && runState.status === "idle"; const displayModelId = sessionModelId ?? startupModelId; function submitChatMessage(message: string) { @@ -225,13 +227,13 @@ function App({ command }: AppProps) { return; } - if (command.dryRun) { - process.exitCode = 0; - app.exit(); + if (command.kind !== "run") { return; } - if (command.kind !== "run") { + if (command.dryRun) { + process.exitCode = 0; + app.exit(); return; } @@ -383,11 +385,17 @@ function App({ command }: AppProps) { return; } + if (runState.status === "init-setup-saved" && command.kind === "config") { + process.exitCode = 0; + app.exit(); + return; + } + if (runState.status === "success" && autoExitOnSuccess) { process.exitCode = 0; app.exit(); } - }, [app, autoExitOnSuccess, runState.status]); + }, [app, autoExitOnSuccess, command.kind, runState.status]); if (command.kind === "help") { return ; @@ -403,6 +411,27 @@ function App({ command }: AppProps) { ); } + if (shouldRunConfigSetup) { + return ( + { + if (result.modelId) { + setSessionModelId(result.modelId); + } + if (result.provider) { + setSessionProvider(result.provider); + } + + setRunState({ status: "init-setup-saved", result }); + }} + onError={(message) => { + setRunState({ status: "error", message }); + }} + /> + ); + } + if (command.kind === "run" && command.dryRun) { return ( ) : null} - + ); } @@ -3023,7 +3056,10 @@ function Rows({ rows }: RowsProps) { const argv = process.argv.slice(2); const parsedCommand = parseCommand(argv); -if (parsedCommand.kind === "run" && !parsedCommand.dryRun) { +if ( + (parsedCommand.kind === "run" && !parsedCommand.dryRun) || + parsedCommand.kind === "config" +) { await loadOpenWikiEnv(); } @@ -3113,6 +3149,14 @@ function writePrintErrorDiagnostics(error: unknown): void { } function resolveStartupCommand(command: CliCommand): CliCommand { + if (command.kind === "config" && !process.stdin.isTTY) { + return { + kind: "error", + exitCode: 1, + message: "openwiki --config requires an interactive terminal.", + }; + } + if ( command.kind === "run" && !command.dryRun && diff --git a/src/commands.ts b/src/commands.ts index 9840293b..ea03f506 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -18,6 +18,7 @@ export type HelpContent = { }; export type CliCommand = + | { kind: "config"; exitCode: 0 } | { kind: "help"; exitCode: 0 } | { kind: "run"; @@ -40,6 +41,18 @@ export function parseCommand(argv: string[]): CliCommand { return { kind: "help", exitCode: 0 }; } + if (argv.includes("--config")) { + if (argv.length > 1) { + return { + kind: "error", + exitCode: 1, + message: "--config cannot be combined with other options or messages.", + }; + } + + return { kind: "config", exitCode: 0 }; + } + let dryRun = false; let modelId: string | null = null; let print = false; @@ -176,6 +189,7 @@ export const helpContent: HelpContent = { usage: [ "openwiki [--modelId ]", "openwiki [--modelId ] [message]", + "openwiki --config", "openwiki --init [message]", "openwiki --update [message]", ], @@ -186,6 +200,10 @@ export const helpContent: HelpContent = { }, ], options: [ + { + label: "--config", + description: "Configure or update saved provider credentials.", + }, { label: "--init", description: "Generate initial OpenWiki documentation.", @@ -211,6 +229,7 @@ export const helpContent: HelpContent = { ], examples: [ "openwiki", + "openwiki --config", "openwiki --init", "openwiki --update", 'openwiki "What can you do?"', diff --git a/src/credentials.tsx b/src/credentials.tsx index 16f9125e..3c3954a1 100644 --- a/src/credentials.tsx +++ b/src/credentials.tsx @@ -30,6 +30,7 @@ export type InitSetupResult = { }; type InitSetupProps = { + forceReconfigure?: boolean; modelIdOverride?: string | null; onComplete: (result: InitSetupResult) => void; onError: (message: string) => void; @@ -68,6 +69,7 @@ function isBaseUrlConfigured(provider: OpenWikiProvider): boolean { } export function InitSetup({ + forceReconfigure = false, modelIdOverride = null, onComplete, onError, @@ -96,7 +98,11 @@ export function InitSetup({ const [isSaving, setIsSaving] = useState(false); useEffect(() => { - const initialStep = getInitialStep(modelIdOverride, initialProvider); + const initialStep = getInitialStep( + modelIdOverride, + initialProvider, + forceReconfigure, + ); if (initialStep === null) { onComplete({ @@ -127,7 +133,7 @@ export function InitSetup({ shouldStartWithCustomModelInput(initialProvider), ); setStep(initialStep); - }, [initialProvider, modelIdOverride, onComplete]); + }, [forceReconfigure, initialProvider, modelIdOverride, onComplete]); useInput((inputValue, key) => { if (isSaving || step === null) { @@ -211,6 +217,7 @@ export function InitSetup({ const nextStep = getNextStepAfterProvider( selectedProvider, modelIdOverride, + forceReconfigure, ); if (nextStep) { @@ -242,7 +249,11 @@ export function InitSetup({ setApiKey(trimmedInput); setInput(""); - const nextStep = getNextStepAfterApiKey(provider, modelIdOverride); + const nextStep = getNextStepAfterApiKey( + provider, + modelIdOverride, + forceReconfigure, + ); if (nextStep) { setIsCustomModelInput( @@ -279,7 +290,11 @@ export function InitSetup({ setBaseUrl(trimmedInput); setInput(""); - const nextStep = getNextStepAfterBaseUrl(provider, modelIdOverride); + const nextStep = getNextStepAfterBaseUrl( + provider, + modelIdOverride, + forceReconfigure, + ); if (nextStep) { setIsCustomModelInput( @@ -322,7 +337,7 @@ export function InitSetup({ setInput(""); setIsCustomModelInput(false); - if (process.env.LANGSMITH_API_KEY === undefined) { + if (forceReconfigure || process.env.LANGSMITH_API_KEY === undefined) { setStep("langsmith"); return; } @@ -446,10 +461,10 @@ export function InitSetup({ - {needsCredentialPrompt ? ( + {forceReconfigure || needsCredentialPrompt ? ( Secrets are masked and saved only after setup. ) : null} @@ -745,7 +760,12 @@ function SelectionMarker({ isSelected }: { isSelected: boolean }) { function getInitialStep( modelIdOverride: string | null, provider: OpenWikiProvider, + forceReconfigure = false, ): PromptStep | null { + if (forceReconfigure) { + return "provider"; + } + if (process.env[OPENWIKI_PROVIDER_ENV_KEY] === undefined) { return "provider"; } @@ -775,29 +795,40 @@ function getInitialStep( function getNextStepAfterProvider( provider: OpenWikiProvider, modelIdOverride: string | null, + forceReconfigure = false, ): PromptStep | null { + if (forceReconfigure) { + return "api-key"; + } + if (!process.env[getProviderApiKeyEnvKey(provider)]) { return "api-key"; } - return getNextStepAfterApiKey(provider, modelIdOverride); + return getNextStepAfterApiKey(provider, modelIdOverride, forceReconfigure); } function getNextStepAfterApiKey( provider: OpenWikiProvider, modelIdOverride: string | null, + forceReconfigure = false, ): PromptStep | null { if (needsBaseUrlStep(provider)) { return "base-url"; } - return getNextStepAfterBaseUrl(provider, modelIdOverride); + return getNextStepAfterBaseUrl(provider, modelIdOverride, forceReconfigure); } function getNextStepAfterBaseUrl( provider: OpenWikiProvider, modelIdOverride: string | null, + forceReconfigure = false, ): PromptStep | null { + if (forceReconfigure) { + return "model"; + } + if ( modelIdOverride === null && process.env[OPENWIKI_MODEL_ID_ENV_KEY] === undefined