#!/usr/bin/env node
import React, { useEffect, useRef, useState } from "react";
import { Box, render, Text, useApp, useInput } from "ink";
import { marked, type Token, type Tokens } from "marked";
import {
configureAuthProvider,
listAuthProviderTools,
shouldDiscoverToolsAfterAuth,
} from "./auth/configure.js";
import { startNgrokTunnel } from "./auth/ngrok.js";
import { formatAuthProviderList, runOAuthAuth } from "./auth/oauth.js";
import { ensureCodeModeRepoSetup, runCodeModeConnectors } from "./code-mode.js";
import {
commandEmitsTelemetry,
helpContent,
isDevelopmentMode,
parseCommand,
shouldRunNonInteractively,
type CliCommand,
type HelpRow,
type OpenWikiRunMode,
} from "./commands.js";
import { resolveStartupCommand } from "./startup.js";
import {
InitSetup,
needsCredentialSetup,
type InitSetupResult,
} from "./credentials.js";
import {
getCredentialDiagnostics,
getShellEnvValue,
loadOpenWikiEnv,
saveOpenWikiEnv,
type CredentialDiagnostic,
} from "./env.js";
import { createOpenWikiThreadId, runOpenWikiAgent } from "./agent/index.js";
import { formatChatGptAccountFromEnv } from "./agent/openai-chatgpt-oauth.js";
import {
getErrorMessage,
isAuthError,
isSecretLikeKey,
sanitizeDiagnosticText,
} from "./diagnostics.js";
import { stripHtmlTags } from "./utils.js";
import {
type OpenWikiRunEvent,
type OpenWikiRunResult,
} from "./agent/types.js";
import {
runOpenWikiIngestion,
type OpenWikiIngestionResult,
} from "./ingestion.js";
import {
readOpenWikiOnboardingConfig,
saveOpenWikiOnboardingConfig,
} from "./onboarding.js";
import { openWikiLocalWikiDir } from "./openwiki-home.js";
import {
deleteConnectorSchedules,
getSavedPowerScheduleStatus,
listConnectorSchedules,
pauseConnectorSchedules,
resumeConnectorSchedules,
type ConnectorScheduleStatus,
type PowerScheduleStatus,
type ScheduleMutationResult,
} from "./schedules.js";
import {
getDefaultModelId,
getMissingProviderEnvKey,
getProviderApiKeyEnvKey,
getProviderCredentialHint,
getProviderLabel,
getProviderModelOptions,
getProviderProjectEnvKey,
isValidModelId,
normalizeModelId,
normalizeProvider,
OPENWIKI_PROVIDER_ENV_KEY,
OPENWIKI_MODEL_ID_ENV_KEY,
providerUsesAwsSdkCredentials,
resolveConfiguredProvider,
SELECTABLE_OPENWIKI_PROVIDERS,
OPENWIKI_VERSION,
type OpenWikiProvider,
} from "./constants.js";
import type { OpenWikiCommand, OpenWikiOutputMode } from "./agent/types.js";
import {
firstRunNoticePending,
FIRST_RUN_NOTICE_BODY,
FIRST_RUN_NOTICE_OPT_OUT,
FIRST_RUN_NOTICE_VERIFY,
} from "./telemetry/index.js";
type RunState =
| { status: "idle" }
| { status: "setup-complete-exit"; result: InitSetupResult }
| { status: "init-setup-saved"; result: InitSetupResult }
| {
status: "ingestion-running";
log: RunLogItem[];
credentialDiagnostics?: CredentialDiagnostic[];
}
| {
status: "ingestion-success";
result: OpenWikiIngestionResult;
log: RunLogItem[];
credentialDiagnostics?: CredentialDiagnostic[];
}
| {
status: "running";
command: OpenWikiCommand;
log: RunLogItem[];
credentialDiagnostics?: CredentialDiagnostic[];
}
| {
status: "success";
result: OpenWikiRunResult;
log: RunLogItem[];
credentialDiagnostics?: CredentialDiagnostic[];
}
| {
status: "error";
message: string;
credentialDiagnostics?: CredentialDiagnostic[];
errorDiagnostics?: ErrorDiagnostic[];
authFix?: AuthFix;
};
type RunLogItem = {
actionCount?: number;
activeToolCallIds?: string[];
call?: string;
doneContent?: string;
errorCount?: number;
id: number;
latestDoneContent?: string;
status?: "done" | "error" | "running";
toolCallId?: string;
toolName?: string;
type: "debug" | "text" | "tool";
content: string;
};
type CompletedRun = {
id: number;
command: OpenWikiCommand;
credentialDiagnostics?: CredentialDiagnostic[];
log: RunLogItem[];
message: string | null;
result: OpenWikiRunResult;
};
type ErrorDiagnostic = {
label: string;
value: string;
};
/**
* What the "How to fix" panel needs after an auth failure. Key names only, no
* secret values: {@link AuthFix.keyFromShell} reflects only whether the failing
* provider's API key was sourced from a shell export (which shadows saved
* config), so the panel can tell the user to unset it.
*/
type AuthFix = {
apiKeyEnvKey: string | undefined;
keyFromShell: boolean;
provider: OpenWikiProvider;
};
type AppProps = {
command: CliCommand;
};
const OPENWIKI_LOGO_LINES = [
" ___ __ ___ _ _ ",
" / _ \\ _ __ ___ _ __ \\ \\ / (_) | _(_)",
"| | | | '_ \\ / _ \\ '_ \\ \\ \\ /\\ / /| | |/ / |",
"| |_| | |_) | __/ | | | \\ V V / | | <| |",
" \\___/| .__/ \\___|_| |_| \\_/\\_/ |_|_|\\_\\_|",
" |_|",
];
const OPENWIKI_LOGO_WIDTH = Math.max(
...OPENWIKI_LOGO_LINES.map((line) => line.length),
);
/** Frame/wrap width for the plain-text (print/non-TTY) first-run disclosure. */
const FIRST_RUN_NOTICE_WIDTH = 64;
/** Greedy word-wrap to `width` columns. Input carries no existing newlines. */
function wrapText(text: string, width: number): string[] {
const lines: string[] = [];
let line = "";
for (const word of text.split(/\s+/)) {
if (line.length === 0) {
line = word;
} else if (line.length + 1 + word.length <= width) {
line += ` ${word}`;
} else {
lines.push(line);
line = word;
}
}
if (line.length > 0) {
lines.push(line);
}
return lines;
}
/**
* The plain-text first-run disclosure for print/non-TTY output: the same copy as
* the interactive box (single-sourced in telemetry/config.ts), framed with light
* rules and wrapped to a fixed width. Rendered gray when stderr is a TTY, plain
* when redirected so a captured log stays free of escape codes.
*/
function renderFirstRunNoticeText(color: boolean): string {
const label = "OpenWiki telemetry";
const width = FIRST_RUN_NOTICE_WIDTH;
const topRule = `─── ${label} ${"─".repeat(Math.max(3, width - label.length - 5))}`;
const block = [
"",
topRule,
"",
...wrapText(FIRST_RUN_NOTICE_BODY, width),
"",
...wrapText(FIRST_RUN_NOTICE_OPT_OUT, width),
"",
...wrapText(FIRST_RUN_NOTICE_VERIFY, width),
"─".repeat(width),
"",
].join("\n");
return color ? `\u001b[90m${block}\u001b[39m` : block;
}
/**
* The one-time telemetry disclosure, rendered as a box so it sits inline with
* the rest of the TUI (mirrors SetupHeader's rounded style). The copy is
* single-sourced in telemetry/config.ts; the print/non-TTY path renders the
* same wording as plain text via renderFirstRunNoticeText.
*/
function FirstRunNotice() {
return (
OpenWiki
{" "}
telemetry
{FIRST_RUN_NOTICE_BODY}
{FIRST_RUN_NOTICE_OPT_OUT}
{FIRST_RUN_NOTICE_VERIFY}
);
}
function App({ command }: AppProps) {
const app = useApp();
const startupModelId = command.kind === "run" ? command.modelId : null;
const startupRunMode = command.kind === "run" ? command.mode : "personal";
const [runMode, setRunMode] = useState(startupRunMode);
const [codeRuntimeCwd, setCodeRuntimeCwd] = useState(process.cwd());
const runtimeCwd = getRunModeCwd(runMode, codeRuntimeCwd);
const runtimeOutputMode = getRunModeOutputMode(runMode);
const startupProvider = resolveConfiguredProvider();
const autoExitOnSuccess = shouldAutoExitStartupRun(command);
const [sessionProvider, setSessionProvider] =
useState(startupProvider);
const [sessionModelId, setSessionModelId] = useState(
startupModelId,
);
const activeRunId = useRef(0);
const sessionThreadId = useRef(createOpenWikiThreadId(runtimeCwd));
const sessionThreadMode = useRef(runMode);
const mountedRef = useRef(false);
const nextLogId = useRef(1);
const nextCompletedRunId = useRef(1);
const activeRunCredentialDiagnostics = useRef<
CredentialDiagnostic[] | undefined
>(undefined);
const activeRunLog = useRef([]);
const [runState, setRunState] = useState({ status: "idle" });
const [completedRuns, setCompletedRuns] = useState([]);
const [activeUserMessage, setActiveUserMessage] = useState(
command.kind === "run" ? command.userMessage : null,
);
const [activeMessageIsFollowup, setActiveMessageIsFollowup] = useState(
command.kind === "run" && command.command === "chat",
);
const shouldOpenSetupForExplicitModeChat =
command.kind === "run" &&
!command.dryRun &&
!command.shouldStart &&
command.modeSource !== "default" &&
process.stdin.isTTY &&
needsCredentialSetup(sessionModelId, runMode);
const [resolvedCommand, setResolvedCommand] =
useState(
command.kind === "run" &&
(command.shouldStart || shouldOpenSetupForExplicitModeChat)
? command.command
: null,
);
// `--init` always opens the full setup walk, even when everything is already
// configured, so you can review or change any step. Consumed once the walk
// finishes so it does not re-open when the run later returns to idle.
const [initWizardConsumed, setInitWizardConsumed] = useState(false);
const isInitCommand = command.kind === "run" && command.command === "init";
const shouldRunInteractiveCredentialSetup =
command.kind === "run" &&
resolvedCommand !== null &&
!command.dryRun &&
process.stdin.isTTY &&
runState.status === "idle" &&
(needsCredentialSetup(sessionModelId, runMode) ||
(isInitCommand && !initWizardConsumed));
const displayModelId = sessionModelId ?? startupModelId;
function submitChatMessage(message: string) {
if (isExitMessage(message)) {
process.exitCode = 0;
app.exit();
return;
}
setActiveUserMessage(message);
setActiveMessageIsFollowup(true);
setResolvedCommand("chat");
setRunState({ status: "idle" });
}
function submitCommandRun(
nextCommand: Extract,
message: string | null,
) {
setActiveUserMessage(message);
setActiveMessageIsFollowup(false);
setResolvedCommand(nextCommand);
setRunState({ status: "idle" });
}
function startIngestionRun(modelId: string | null) {
const runId = activeRunId.current + 1;
activeRunId.current = runId;
activeRunCredentialDiagnostics.current = undefined;
activeRunLog.current = [];
setResolvedCommand(null);
setActiveUserMessage(
"Run source-specific OpenWiki ingestion for configured sources.",
);
setActiveMessageIsFollowup(false);
setRunState({
status: "ingestion-running",
log: [],
});
void runOpenWikiIngestion(process.cwd(), {
debug: isDebugMode(),
modelId,
target: "all",
onEvent: (event) => {
if (!mountedRef.current || activeRunId.current !== runId) {
return;
}
activeRunLog.current = appendRunLogEvent(
activeRunLog.current,
event,
nextLogId,
);
setRunState((currentState) =>
currentState.status === "ingestion-running"
? {
...currentState,
log: activeRunLog.current,
}
: currentState,
);
},
})
.then((result) => {
if (!mountedRef.current || activeRunId.current !== runId) {
return;
}
if (
result.results.some((sourceResult) => sourceResult.status === "error")
) {
process.exitCode = 1;
}
setRunState({
status: "ingestion-success",
result,
log: activeRunLog.current,
credentialDiagnostics: activeRunCredentialDiagnostics.current,
});
})
.catch((error: unknown) => {
if (!mountedRef.current || activeRunId.current !== runId) {
return;
}
const errorDiagnostics = getErrorDiagnostics(error);
const message = getErrorMessage(error);
const authFix = getAuthFix(error, message, sessionProvider);
// The full credential dump is opt-in (--debug); by default show only the
// concise message, allowlisted error fields, and the how-to-fix panel.
if (!shouldShowCredentialDiagnostics()) {
setRunState({
status: "error",
message,
errorDiagnostics,
authFix,
});
return;
}
void getCredentialDiagnostics()
.catch(() => undefined)
.then((credentialDiagnostics) => {
if (!mountedRef.current || activeRunId.current !== runId) {
return;
}
setRunState({
status: "error",
message,
credentialDiagnostics,
errorDiagnostics,
authFix,
});
});
});
}
function clearSession() {
activeRunId.current += 1;
sessionThreadId.current = createOpenWikiThreadId(runtimeCwd);
activeRunCredentialDiagnostics.current = undefined;
activeRunLog.current = [];
nextLogId.current = 1;
nextCompletedRunId.current = 1;
setCompletedRuns([]);
setActiveUserMessage(null);
setActiveMessageIsFollowup(false);
setResolvedCommand(null);
setRunState({ status: "idle" });
}
async function selectModel(modelId: string) {
await saveOpenWikiEnv({
[OPENWIKI_MODEL_ID_ENV_KEY]: modelId,
});
setSessionModelId(modelId);
}
async function selectProvider(provider: OpenWikiProvider) {
const modelId =
getProviderModelOptions(provider).length > 0
? getDefaultModelId(provider)
: null;
await saveOpenWikiEnv({
[OPENWIKI_PROVIDER_ENV_KEY]: provider,
...(modelId ? { [OPENWIKI_MODEL_ID_ENV_KEY]: modelId } : {}),
});
setSessionProvider(provider);
setSessionModelId(modelId);
}
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
}, []);
useEffect(() => {
if (sessionThreadMode.current === runMode) {
return;
}
sessionThreadId.current = createOpenWikiThreadId(runtimeCwd);
sessionThreadMode.current = runMode;
}, [runMode, runtimeCwd]);
useEffect(() => {
if (command.kind === "help" || command.kind === "error") {
process.exitCode = command.exitCode;
app.exit();
return;
}
if (command.kind === "auth") {
process.exitCode = command.exitCode;
app.exit();
return;
}
if (command.kind === "run" && command.dryRun) {
process.exitCode = 0;
app.exit();
return;
}
if (command.kind !== "run") {
return;
}
if (resolvedCommand === null) {
return;
}
const missingEnvKey = getMissingProviderEnvKey(sessionProvider);
if (missingEnvKey && !process.stdin.isTTY) {
const hint = getProviderCredentialHint(sessionProvider);
setRunState({
status: "error",
message: `${missingEnvKey} is required. Run openwiki in an interactive terminal to save credentials.${
hint ? ` ${hint}` : ""
}`,
});
return;
}
if (shouldRunInteractiveCredentialSetup) {
return;
}
if (runState.status !== "idle" && runState.status !== "init-setup-saved") {
return;
}
const runId = activeRunId.current + 1;
const runMessage = activeUserMessage;
activeRunId.current = runId;
activeRunCredentialDiagnostics.current = undefined;
activeRunLog.current = [];
setRunState({
status: "running",
command: resolvedCommand,
log: [],
});
if (shouldShowCredentialDiagnostics()) {
void getCredentialDiagnostics()
.catch(() => undefined)
.then((credentialDiagnostics) => {
if (
!mountedRef.current ||
activeRunId.current !== runId ||
!credentialDiagnostics
) {
return;
}
setRunState((currentState) =>
updateRunningCredentialDiagnostics(
currentState,
credentialDiagnostics,
activeRunCredentialDiagnostics,
),
);
});
}
const setupPromise =
runMode === "code"
? ensureCodeModeRepoSetup(runtimeCwd, {
createWorkflow: resolvedCommand === "init",
})
: Promise.resolve();
setupPromise
.then(async () => {
const handleRunEvent = (event: OpenWikiRunEvent): void => {
if (!mountedRef.current || activeRunId.current !== runId) {
return;
}
activeRunLog.current = appendRunLogEvent(
activeRunLog.current,
event,
nextLogId,
);
setRunState((currentState) =>
currentState.status === "running"
? {
...currentState,
log: activeRunLog.current,
}
: currentState,
);
};
// Code-mode connectors pull their evidence and augment the agent message
// before the run, matching the --print path exactly. They emit progress
// into the same run log so the pull is visible rather than a silent gap.
const userMessage =
runMode === "code" && resolvedCommand !== "chat"
? await runCodeModeConnectors(
runtimeCwd,
activeUserMessage ?? undefined,
handleRunEvent,
)
: activeUserMessage;
return runOpenWikiAgent(resolvedCommand, runtimeCwd, {
debug: isDebugMode(),
isFollowup: activeMessageIsFollowup,
language: command.language,
modelId: sessionModelId,
outputMode: runtimeOutputMode,
threadId: sessionThreadId.current,
userMessage,
telemetryFile: command.telemetryFile ?? undefined,
onEvent: handleRunEvent,
});
})
.then((result) => {
if (!mountedRef.current || activeRunId.current !== runId) {
return;
}
setRunState({
status: "success",
result,
log: activeRunLog.current,
credentialDiagnostics: activeRunCredentialDiagnostics.current,
});
setCompletedRuns((runs) => [
...runs,
{
id: nextCompletedRunId.current,
command: result.command,
credentialDiagnostics: activeRunCredentialDiagnostics.current,
log: activeRunLog.current,
message: runMessage,
result,
},
]);
nextCompletedRunId.current += 1;
})
.catch((error: unknown) => {
if (!mountedRef.current || activeRunId.current !== runId) {
return;
}
const errorDiagnostics = getErrorDiagnostics(error);
const message = getErrorMessage(error);
const authFix = getAuthFix(error, message, sessionProvider);
// The full credential dump is opt-in (--debug); by default show only the
// concise message, allowlisted error fields, and the how-to-fix panel.
if (!shouldShowCredentialDiagnostics()) {
setRunState({
status: "error",
message,
errorDiagnostics,
authFix,
});
return;
}
void getCredentialDiagnostics()
.catch(() => undefined)
.then((credentialDiagnostics) => {
if (!mountedRef.current || activeRunId.current !== runId) {
return;
}
setRunState({
status: "error",
message,
credentialDiagnostics,
errorDiagnostics,
authFix,
});
});
});
}, [
app,
command,
activeMessageIsFollowup,
activeUserMessage,
resolvedCommand,
runMode,
runState.status,
runtimeCwd,
runtimeOutputMode,
sessionModelId,
sessionProvider,
shouldRunInteractiveCredentialSetup,
]);
useEffect(() => {
if (runState.status === "error") {
process.exitCode = 1;
app.exit();
return;
}
if (runState.status === "success" && autoExitOnSuccess) {
process.exitCode = 0;
app.exit();
return;
}
if (runState.status === "ingestion-success" && autoExitOnSuccess) {
process.exitCode = runState.result.results.some(
(sourceResult) => sourceResult.status === "error",
)
? 1
: 0;
app.exit();
}
}, [app, autoExitOnSuccess, runState]);
if (command.kind === "help") {
return ;
}
if (command.kind === "error") {
return (
);
}
if (command.kind === "run" && command.dryRun) {
return (
);
}
if (shouldRunInteractiveCredentialSetup) {
return (
{
setInitWizardConsumed(true);
const nextCodeRuntimeCwd = result.repoRoot ?? codeRuntimeCwd;
if (result.repoRoot) {
setCodeRuntimeCwd(result.repoRoot);
}
if (result.mode !== runMode) {
const nextRuntimeCwd = getRunModeCwd(
result.mode,
nextCodeRuntimeCwd,
);
sessionThreadId.current = createOpenWikiThreadId(nextRuntimeCwd);
sessionThreadMode.current = result.mode;
setRunMode(result.mode);
} else if (result.repoRoot) {
sessionThreadId.current = createOpenWikiThreadId(result.repoRoot);
sessionThreadMode.current = result.mode;
}
if (result.modelId) {
setSessionModelId(result.modelId);
}
if (result.provider) {
setSessionProvider(result.provider);
}
if (!result.shouldContinueToRun) {
activeRunId.current += 1;
setResolvedCommand(null);
setActiveUserMessage(null);
setActiveMessageIsFollowup(false);
setRunState({ status: "idle" });
return;
}
if (result.runIngestionNow && result.mode === "code") {
if (command.kind === "run" && !command.shouldStart) {
setResolvedCommand("init");
}
setActiveMessageIsFollowup(false);
setRunState({ status: "init-setup-saved", result });
return;
}
if (result.runIngestionNow) {
startIngestionRun(result.modelId ?? sessionModelId);
return;
}
setRunState({ status: "init-setup-saved", result });
}}
onError={(message) => {
setRunState({ status: "error", message });
}}
/>
);
}
if (runState.status === "init-setup-saved") {
return (
{runState.result.savedApiKey ||
runState.result.savedProvider ||
runState.result.savedBaseUrl ||
runState.result.savedRegion ||
runState.result.savedSecretKey ||
runState.result.savedGcpProject ||
runState.result.savedGcpLocation ||
runState.result.savedModelId ||
runState.result.savedLangSmithKey ? (
) : null}
{runState.result.provider ? (
) : null}
{runState.result.modelId ? (
) : null}
);
}
if (runState.status === "setup-complete-exit") {
return (
);
}
if (runState.status === "running") {
return (
);
}
if (runState.status === "ingestion-running") {
return (
);
}
if (runState.status === "ingestion-success") {
return (
);
}
if (runState.status === "success") {
if (autoExitOnSuccess) {
return (
);
}
return (
);
}
if (runState.status === "idle" && completedRuns.length > 0) {
return (
{activeUserMessage ? : null}
);
}
if (runState.status === "error") {
return (
{runState.authFix ? : null}
{runState.credentialDiagnostics ? (
) : null}
{runState.errorDiagnostics && runState.errorDiagnostics.length > 0 ? (
) : null}
);
}
return (
);
}
function HelpView() {
return (
{helpContent.usage.map((line) => (
{line}
))}
{isDevelopmentMode() ? (
) : null}
{helpContent.examples.map((line) => (
{line}
))}
{isDevelopmentMode()
? helpContent.developmentExamples.map((line) => (
{line}
))
: null}
);
}
function DryRunView({
command,
modelId,
shouldStart,
userMessage,
}: {
command: OpenWikiCommand;
modelId: string | null;
shouldStart: boolean;
userMessage: string | null;
}) {
return (
{userMessage ? (
) : null}
);
}
function CredentialDiagnosticsPanel({
diagnostics,
}: {
diagnostics: CredentialDiagnostic[];
}) {
return (
Raw secret values are intentionally not printed.
{diagnostics.map((diagnostic) => (
{diagnostic.key}{" "}
source={diagnostic.source}
length={diagnostic.length ?? "unset"} preview={diagnostic.preview}
0 ? "yellow" : "gray"}>
warnings=
{diagnostic.warnings.length > 0
? diagnostic.warnings.join(", ")
: "none"}
))}
);
}
/**
* The ordered "how to fix" steps for an auth failure. Shared by the interactive
* panel and the --print stderr path so they stay in sync. Names env keys only,
* never secret values.
*/
function getAuthFixSteps(authFix: AuthFix): string[] {
const steps: string[] = [];
if (providerUsesAwsSdkCredentials(authFix.provider)) {
steps.push(
"Verify the selected AWS identity with `aws sts get-caller-identity` in the same environment.",
"Configure the AWS SDK credential chain (OIDC/workload role, AWS_PROFILE/SSO, or standard AWS credentials) and a Bedrock region, then retry.",
"Unset AWS_BEARER_TOKEN_BEDROCK for OIDC/IAM runs; a bearer token takes precedence when present.",
"A complete BEDROCK_AWS_ACCESS_KEY_ID/BEDROCK_AWS_SECRET_ACCESS_KEY pair takes precedence; unset both in the shell and remove both from ~/.openwiki/.env to use ambient AWS credentials.",
);
return steps;
}
if (authFix.keyFromShell && authFix.apiKeyEnvKey) {
steps.push(
`${authFix.apiKeyEnvKey} came from your shell, not ~/.openwiki/.env. ` +
`Unset it (unset ${authFix.apiKeyEnvKey}) or fix it, then retry.`,
);
}
steps.push(
"Re-enter your key: re-run openwiki --init, or edit ~/.openwiki/.env.",
);
return steps;
}
function AuthFixPanel({ authFix }: { authFix: AuthFix }) {
const steps = getAuthFixSteps(authFix);
return (
Your provider rejected the credentials for this run.
{steps.map((step, index) => (
{index + 1}.
{step}
))}
For full detail, re-run with --debug.
);
}
function ErrorDiagnosticsPanel({
diagnostics,
}: {
diagnostics: ErrorDiagnostic[];
}) {
return (
OPENWIKI_DEBUG=1 is enabled. Only allowlisted, non-secret error fields
are shown.
{diagnostics.map((diagnostic) => (
{diagnostic.label} {diagnostic.value}
))}
);
}
function Header({
compact = false,
modelId,
showLogo = true,
subtitle,
}: {
compact?: boolean;
modelId?: string | null;
showLogo?: boolean;
subtitle: string;
}) {
const terminalColumns = process.stdout.columns ?? 80;
const displayModelId = sanitizeHeaderValue(
modelId ??
process.env[OPENWIKI_MODEL_ID_ENV_KEY] ??
getDefaultModelId(resolveConfiguredProvider()),
Math.max(8, terminalColumns - 12),
);
const configuredProvider = resolveConfiguredProvider();
const displayProvider = getProviderLabel(configuredProvider);
const chatGptAccount =
configuredProvider === "openai-chatgpt"
? formatChatGptAccountFromEnv()
: null;
const displayDirectory = sanitizeHeaderValue(
formatCwd(process.cwd()),
Math.max(8, terminalColumns - 17),
);
const shouldShowLogo = showLogo && terminalColumns > OPENWIKI_LOGO_WIDTH;
const tracingEnabled =
process.env.LANGCHAIN_TRACING_V2 === "true" &&
Boolean(process.env.LANGSMITH_API_KEY);
if (compact) {
return (
{">_ "}
OpenWiki{" "}
v{OPENWIKI_VERSION}{" "}
provider:
{displayProvider}{" "}
{chatGptAccount ? (
<>
account:
{chatGptAccount}{" "}
>
) : null}
model:
{displayModelId}
{tracingEnabled ? "* " : "- "}
LangSmith tracing {tracingEnabled ? "enabled" : "disabled"}
-
{subtitle}
);
}
return (
{shouldShowLogo ? (
{OPENWIKI_LOGO_LINES.map((line) => (
{line}
))}
) : null}
{">_ "}
OpenWiki{" "}
v{OPENWIKI_VERSION}{" "}
agent docs for codebases
provider:
{displayProvider}
{chatGptAccount ? (
account:
{chatGptAccount}
) : null}
model:
{displayModelId}
directory:
{displayDirectory}
{tracingEnabled ? "* " : "- "}
LangSmith tracing {tracingEnabled ? "enabled" : "disabled"}
-
{subtitle}
Tip: ask for a docs change, or use /exit when you are done.
);
}
type StatusLineProps = {
tone: "active" | "error" | "muted" | "success";
label: string;
value: string;
};
function StatusLine({ tone, label, value }: StatusLineProps) {
const color =
tone === "success"
? "green"
: tone === "error"
? "red"
: tone === "active"
? "yellow"
: "gray";
return (
*
{label}
{" "}
{value}
);
}
function IngestionSummary({ result }: { result: OpenWikiIngestionResult }) {
return (
{result.results.map((sourceResult) => (
))}
);
}
type RunViewProps = {
command: OpenWikiCommand;
credentialDiagnostics?: CredentialDiagnostic[];
log: RunLogItem[];
done?: boolean;
message?: string | null;
modelId?: string | null;
};
function RunView({
command,
credentialDiagnostics,
log,
done = false,
message = null,
modelId = null,
}: RunViewProps) {
const [animationFrame, setAnimationFrame] = useState(0);
const activeRunningToolId = getActiveRunningToolLogId(log);
const hasRunningTool = activeRunningToolId !== null;
useEffect(() => {
if (done || !hasRunningTool) {
return;
}
const interval = setInterval(() => {
setAnimationFrame((frame) => frame + 1);
}, 140);
return () => {
clearInterval(interval);
};
}, [done, hasRunningTool]);
return (
{message ? : null}
*
{done ? "Complete" : "Working"}{" "}
openwiki {command}
{!done ? - streaming : null}
{log.length > 0 ? (
log.map((item) => (
))
) : (
Waiting for model output...
)}
{credentialDiagnostics ? (
) : null}
);
}
function RunLogLine({
activeRunningToolId = null,
animationFrame = 0,
item,
}: {
activeRunningToolId?: number | null;
animationFrame?: number;
item: RunLogItem;
}) {
if (item.type === "tool") {
if (item.status === "running") {
const isActive = item.id === activeRunningToolId;
return (
{isActive ? `${getSpinnerFrame(animationFrame)} ` : "* "}
{item.content}
{isActive && item.call ? (
{truncateLogOutput(item.call, "")}
) : null}
);
}
if (item.status === "error") {
return (
{"!! "}
{item.content}
);
}
return (
{"* "}
{item.content}
);
}
if (item.type === "debug") {
return (
-
{item.content}
);
}
return (
*
);
}
function getActiveRunningToolLogId(log: RunLogItem[]): number | null {
for (let index = log.length - 1; index >= 0; index -= 1) {
const item = log[index];
if (item.type === "tool" && item.status === "running") {
return item.id;
}
}
return null;
}
function getSpinnerFrame(frame: number): string {
const frames = ["-", "\\", "|", "/"];
return frames[frame % frames.length] ?? "-";
}
function MarkdownText({ markdown }: { markdown: string }) {
const tokens = marked.lexer(markdown, {
async: false,
gfm: true,
});
return (
{tokens.map((token, index) => (
))}
);
}
function MarkdownBlock({ index, token }: { index: number; token: Token }) {
if (token.type === "space" || token.type === "def" || token.type === "hr") {
return null;
}
if (token.type === "paragraph") {
return (
);
}
if (token.type === "heading") {
return (
);
}
if (token.type === "list") {
return (
{(token as Tokens.List).items.map((item, itemIndex) => (
{(token as Tokens.List).ordered
? `${Number((token as Tokens.List).start || 1) + itemIndex}. `
: "- "}
))}
);
}
if (token.type === "code") {
return {token.text};
}
if (token.type === "blockquote") {
return (
|
);
}
if (token.type === "table") {
return {renderPlainTable(token as Tokens.Table)};
}
if (token.type === "html") {
return {renderHtmlToken(token)};
}
if (token.type === "text") {
return (
);
}
return {token.raw};
}
function InlineMarkdown({ tokens }: { tokens: Token[] }) {
return (
<>
{tokens.map((token, index) => (
))}
>
);
}
function InlineMarkdownToken({ token }: { token: Token }) {
if (token.type === "text" || token.type === "escape") {
return <>{token.text}>;
}
if (token.type === "strong") {
return (
);
}
if (token.type === "em") {
return (
);
}
if (token.type === "link") {
return (
);
}
if (token.type === "codespan") {
return {token.text};
}
if (token.type === "br") {
return <>{"\n"}>;
}
if (token.type === "del") {
return (
);
}
if (token.type === "html") {
return <>{renderHtmlToken(token)}>;
}
if ("tokens" in token && Array.isArray(token.tokens)) {
return ;
}
return <>{token.raw}>;
}
function getTokenChildren(token: Token): Token[] {
return "tokens" in token && Array.isArray(token.tokens) ? token.tokens : [];
}
function renderPlainTable(token: Tokens.Table): string {
const header = token.header.map((cell) => cell.text).join(" | ");
const rows = token.rows.map((row) =>
row.map((cell) => cell.text).join(" | "),
);
return [header, ...rows].filter(Boolean).join("\n");
}
function renderHtmlToken(token: Token): React.ReactNode {
const text =
"text" in token && typeof token.text === "string" ? token.text : token.raw;
const underlineMatch = text.match(/^(.*)<\/u>$/isu);
if (underlineMatch) {
return {underlineMatch[1]};
}
return stripHtmlTags(text);
}
function ChatHistory({ runs }: { runs: CompletedRun[] }) {
if (runs.length === 0) {
return null;
}
return (
{runs.map((run) => (
{run.message ? : null}
*
Complete{" "}
openwiki {run.command} - {run.result.model}
{run.log.length > 0 ? (
run.log.map((item) => )
) : (
No assistant output captured.
)}
))}
);
}
type ChatInputProps = {
currentModelId: string;
currentProvider: OpenWikiProvider;
onClear: () => void;
onCommandRun: (
command: Extract,
message: string | null,
) => void;
onModelSelect: (modelId: string) => Promise;
onProviderSelect: (provider: OpenWikiProvider) => Promise;
onSubmit: (message: string) => void;
};
function ChatInput({
currentModelId,
currentProvider,
onClear,
onCommandRun,
onModelSelect,
onProviderSelect,
onSubmit,
}: ChatInputProps) {
const [inputState, setInputState] = useState({
cursorPosition: 0,
value: "",
});
const [menuState, setMenuState] = useState({
kind: "none",
});
const [error, setError] = useState(null);
const [notice, setNotice] = useState(null);
const [isSaving, setIsSaving] = useState(false);
const [secretInputMode, setSecretInputMode] =
useState(null);
const input = inputState.value;
const cursorPosition = inputState.cursorPosition;
useEffect(() => {
if (secretInputMode !== null) {
return;
}
setMenuState((currentState) =>
syncMenuStateForInput(
input,
currentState,
currentModelId,
currentProvider,
),
);
}, [currentModelId, currentProvider, input, secretInputMode]);
useInput((inputValue, key) => {
if (isSaving) {
return;
}
if (secretInputMode !== null) {
if (isEscapeInput(inputValue, key)) {
resetInput();
setSecretInputMode(null);
setNotice("Credential update canceled.");
return;
}
if (key.return) {
void saveSecretInput();
return;
}
if (key.backspace || isRawBackspaceInput(inputValue)) {
setInputState(deleteBeforeInputCursor);
return;
}
if (key.delete) {
setInputState(
inputValue.length === 0
? deleteBeforeInputCursor
: deleteAtInputCursor,
);
return;
}
if (inputValue && !key.ctrl && !key.meta) {
setError(null);
setNotice(null);
setInputState((state) => applyRawInputValue(state, inputValue));
}
return;
}
if (isMenuUpInput(inputValue, key) && menuState.kind !== "none") {
setMenuState((state) =>
moveMenuSelection(state, -1, currentModelId, currentProvider),
);
return;
}
if (isMenuDownInput(inputValue, key) && menuState.kind !== "none") {
setMenuState((state) =>
moveMenuSelection(state, 1, currentModelId, currentProvider),
);
return;
}
if (key.return) {
void submitInput();
return;
}
if (isEscapeInput(inputValue, key) && menuState.kind !== "none") {
resetInput();
return;
}
if (key.leftArrow) {
setInputState((state) => moveInputCursor(state, -1));
return;
}
if (key.rightArrow) {
setInputState((state) => moveInputCursor(state, 1));
return;
}
if ((key.ctrl && inputValue === "a") || inputValue === "\u0001") {
setInputState((state) => ({
...state,
cursorPosition: 0,
}));
return;
}
if ((key.ctrl && inputValue === "e") || inputValue === "\u0005") {
setInputState((state) => ({
...state,
cursorPosition: state.value.length,
}));
return;
}
if (key.backspace || isRawBackspaceInput(inputValue)) {
setInputState(deleteBeforeInputCursor);
return;
}
if (key.delete) {
setInputState(
inputValue.length === 0 ? deleteBeforeInputCursor : deleteAtInputCursor,
);
return;
}
if (inputValue && !key.ctrl && !key.meta) {
setError(null);
setNotice(null);
setInputState((state) => applyRawInputValue(state, inputValue));
}
});
async function submitInput() {
const message = input.trim();
if (message.length === 0) {
setError("Enter a follow-up message.");
return;
}
if (message.startsWith("/")) {
await submitSlashInput(message);
return;
}
resetInput();
onSubmit(message);
}
async function submitSlashInput(message: string) {
if (message === "/" && menuState.kind === "commands") {
await runSlashCommand(slashCommandOptions[menuState.selectedIndex]);
return;
}
if (message === "/model" && menuState.kind === "model") {
await selectModelMenuOption(menuState.selectedIndex);
return;
}
if (message === "/provider" && menuState.kind === "provider") {
await selectProviderMenuOption(menuState.selectedIndex);
return;
}
const parsedCommand = parseSlashInput(message);
if (parsedCommand === null) {
setError(`Unknown command: ${message}`);
return;
}
await runSlashCommand(
parsedCommand.option,
parsedCommand.args.length > 0 ? parsedCommand.args : null,
);
}
async function runSlashCommand(
option: SlashCommandOption | undefined,
args: string | null = null,
) {
if (!option) {
setError("Select a slash command.");
return;
}
if (option.id === "model") {
if (args && args.length > 0) {
await saveModelSelection(args);
return;
}
setError(null);
setNotice("Choose a model, or type /model .");
setInputValue("/model");
setMenuState({
kind: "model",
selectedIndex: getCurrentModelOptionIndex(
currentModelId,
currentProvider,
),
});
return;
}
if (option.id === "provider") {
if (args && args.length > 0) {
await saveProviderSelection(args);
return;
}
setError(null);
setNotice("Choose a provider, or type /provider .");
setInputValue("/provider");
setMenuState({
kind: "provider",
selectedIndex: getCurrentProviderOptionIndex(currentProvider),
});
return;
}
if (option.id === "api-key") {
if (args && args.length > 0) {
setError(
"Use the masked prompt for API keys; do not pass keys inline.",
);
return;
}
if (providerUsesAwsSdkCredentials(currentProvider)) {
setError(
`${getProviderLabel(currentProvider)} uses the AWS SDK credential chain; /api-key cannot safely configure an access-key pair. ${getProviderCredentialHint(currentProvider) ?? ""} Legacy BEDROCK_AWS_ACCESS_KEY_ID and BEDROCK_AWS_SECRET_ACCESS_KEY values must be configured or removed together in the shell and ~/.openwiki/.env.`.trim(),
);
return;
}
const apiKeyEnvKey = getProviderApiKeyEnvKey(currentProvider);
if (!apiKeyEnvKey) {
const hint = getProviderCredentialHint(currentProvider);
setError(
`${getProviderLabel(currentProvider)} does not use an API key.${
hint ? ` ${hint}` : ""
}`,
);
return;
}
setError(null);
setNotice(`Paste your ${getProviderLabel(currentProvider)} API key.`);
setSecretInputMode({
envKey: apiKeyEnvKey,
kind: "api-key",
label: `${getProviderLabel(currentProvider)} API key`,
provider: currentProvider,
});
setInputState({ cursorPosition: 0, value: "" });
setMenuState({ kind: "none" });
return;
}
if (option.id === "langsmith-key") {
if (args && args.length > 0) {
setError(
"Use the masked prompt for LangSmith keys; do not pass keys inline.",
);
return;
}
setError(null);
setNotice("Paste your LangSmith API key, or press Enter empty to clear.");
setSecretInputMode({
envKey: "LANGSMITH_API_KEY",
kind: "langsmith-key",
label: "LangSmith API key",
});
setInputState({ cursorPosition: 0, value: "" });
setMenuState({ kind: "none" });
return;
}
if (option.id === "init" || option.id === "update") {
resetInput();
onCommandRun(option.id, args);
return;
}
if (option.id === "clear") {
resetInput();
onClear();
setNotice("Started a new chat thread.");
return;
}
if (option.id === "help") {
resetInput();
setNotice(
"Slash commands: /provider, /model, /api-key, /langsmith-key, /init, /update, /clear, /help, /exit. Use arrows to select.",
);
return;
}
resetInput();
onSubmit("/exit");
}
async function selectModelMenuOption(selectedIndex: number) {
const option = getModelMenuOptions(currentModelId, currentProvider)[
selectedIndex
];
if (!option) {
setError("Select a model.");
return;
}
if (option.kind === "custom") {
setError(null);
setNotice("Type a custom model ID after /model.");
setInputValue("/model ");
return;
}
await saveModelSelection(option.modelId);
}
async function saveModelSelection(rawModelId: string) {
const modelId = normalizeModelId(rawModelId);
if (!isValidModelId(modelId)) {
setError("Enter a valid model ID.");
return;
}
setIsSaving(true);
setError(null);
setNotice(null);
try {
await onModelSelect(modelId);
resetInput();
setNotice(`Model switched to ${modelId}.`);
} catch (saveError) {
setError(
saveError instanceof Error
? saveError.message
: "Failed to save model selection.",
);
} finally {
setIsSaving(false);
}
}
async function selectProviderMenuOption(selectedIndex: number) {
const provider = SELECTABLE_OPENWIKI_PROVIDERS[selectedIndex];
if (!provider) {
setError("Select a provider.");
return;
}
await saveProviderSelection(provider);
}
async function saveProviderSelection(rawProvider: string) {
const provider = normalizeProvider(rawProvider);
if (provider === null) {
setError(
`Enter a valid provider: ${SELECTABLE_OPENWIKI_PROVIDERS.join(", ")}.`,
);
return;
}
setIsSaving(true);
setError(null);
setNotice(null);
try {
await onProviderSelect(provider);
resetInput();
const apiKeyEnvKey = getProviderApiKeyEnvKey(provider);
const requirement = providerUsesAwsSdkCredentials(provider)
? (getProviderCredentialHint(provider) ??
"Configure AWS SDK credentials.")
: apiKeyEnvKey
? `Ensure ${apiKeyEnvKey} is set.`
: `Ensure ${getProviderProjectEnvKey(provider)} is set. ${getProviderCredentialHint(provider) ?? ""}`.trim();
const modelNotice =
getProviderModelOptions(provider).length > 0
? ` with model ${getDefaultModelId(provider)}`
: ". Set a model with /model";
setNotice(
`Provider switched to ${getProviderLabel(provider)}${modelNotice}. ${requirement}`,
);
} catch (saveError) {
setError(
saveError instanceof Error
? saveError.message
: "Failed to save provider selection.",
);
} finally {
setIsSaving(false);
}
}
async function saveSecretInput() {
if (secretInputMode === null) {
return;
}
const nextValue = input.trim();
if (secretInputMode.kind === "api-key" && nextValue.length === 0) {
setError(`${secretInputMode.envKey} is required.`);
return;
}
setIsSaving(true);
setError(null);
setNotice(null);
try {
if (secretInputMode.kind === "langsmith-key") {
await saveOpenWikiEnv({
LANGCHAIN_PROJECT: nextValue.length > 0 ? "openwiki" : "",
LANGCHAIN_TRACING_V2: nextValue.length > 0 ? "true" : "false",
LANGSMITH_API_KEY: nextValue,
});
} else {
await saveOpenWikiEnv({
[secretInputMode.envKey]: nextValue,
});
}
const savedLabel = secretInputMode.label;
resetInput();
setSecretInputMode(null);
setNotice(`${savedLabel} saved.`);
} catch (saveError) {
setError(
saveError instanceof Error
? saveError.message
: "Failed to save credential.",
);
} finally {
setIsSaving(false);
}
}
function resetInput() {
setInputState({ cursorPosition: 0, value: "" });
setMenuState({ kind: "none" });
setError(null);
}
function setInputValue(value: string) {
setInputState({
cursorPosition: value.length,
value,
});
}
const beforeCursor = input.slice(0, cursorPosition);
const afterCursor = input.slice(cursorPosition);
return (
{">"}{" "}
{secretInputMode !== null ? (
<>
{secretInputMode.envKey}=
{formatSecretInputSummary(input)}
>
) : input.length > 0 ? (
<>
{beforeCursor}
{afterCursor}
>
) : (
<>
Ask a follow-up...
>
)}
{secretInputMode !== null
? "enter to save - esc to cancel - input is masked"
: `enter to send - / for commands - /exit to quit - cwd ${formatCwd(
process.cwd(),
)}`}
{secretInputMode !== null ? (
{secretInputMode.label}
Saving to {secretInputMode.envKey}
{secretInputMode.kind === "langsmith-key" ? (
Press Enter empty to clear LangSmith.
) : null}
) : menuState.kind !== "none" ? (
) : null}
{notice ? {notice} : null}
{isSaving ? Saving selection... : null}
{error ? {error} : null}
);
}
type ChatInputState = {
cursorPosition: number;
value: string;
};
type SecretInputMode = {
envKey: string;
kind: "api-key" | "langsmith-key";
label: string;
provider?: OpenWikiProvider;
};
type ChatInputMenuState =
| { kind: "commands"; selectedIndex: number }
| { kind: "model"; selectedIndex: number }
| { kind: "provider"; selectedIndex: number }
| { kind: "none" };
type SlashCommandId =
| "api-key"
| "clear"
| "exit"
| "help"
| "init"
| "langsmith-key"
| "model"
| "provider"
| "update";
type SlashCommandOption = {
description: string;
id: SlashCommandId;
label: string;
};
type ModelMenuOption =
| {
kind: "model";
label: string;
modelId: string;
}
| {
kind: "custom";
label: string;
};
const slashCommandOptions: SlashCommandOption[] = [
{
description: "Switch the model provider",
id: "provider",
label: "/provider",
},
{
description: "Switch the current provider model",
id: "model",
label: "/model",
},
{
description: "Set the API key for the current provider",
id: "api-key",
label: "/api-key",
},
{
description: "Set or clear the LangSmith API key",
id: "langsmith-key",
label: "/langsmith-key",
},
{
description: "Run an initial OpenWiki documentation pass",
id: "init",
label: "/init",
},
{
description: "Update existing OpenWiki documentation",
id: "update",
label: "/update",
},
{
description: "Start a fresh thread and clear chat history",
id: "clear",
label: "/clear",
},
{
description: "Show slash command help",
id: "help",
label: "/help",
},
{
description: "Exit OpenWiki",
id: "exit",
label: "/exit",
},
];
function SlashMenu({
currentModelId,
currentProvider,
input,
menuState,
}: {
currentModelId: string;
currentProvider: OpenWikiProvider;
input: string;
menuState: Exclude;
}) {
if (menuState.kind === "model") {
const modelOptions = getModelMenuOptions(currentModelId, currentProvider);
return (
Models for {getProviderLabel(currentProvider)}
{modelOptions.map((option, index) => (
"
: ""
}
isSelected={index === menuState.selectedIndex}
key={option.label}
label={option.label}
/>
))}
{input.startsWith("/model ") ? (
Press enter to save the custom model ID.
) : (
Use arrows, enter to select, esc to cancel.
)}
);
}
if (menuState.kind === "provider") {
return (
Providers
{SELECTABLE_OPENWIKI_PROVIDERS.map((provider, index) => (
))}
Use arrows, enter to select, esc to cancel.
);
}
return (
Commands
{slashCommandOptions.map((option, index) => (
))}
Use arrows, enter to select, esc to cancel.
);
}
function MenuRow({
description,
isSelected,
label,
}: {
description: string;
isSelected: boolean;
label: string;
}) {
return (
{isSelected ? ">" : " "}{" "}
{label.padEnd(28)}
{description}
);
}
function moveInputCursor(
state: ChatInputState,
offset: number,
): ChatInputState {
return {
...state,
cursorPosition: clampCursorPosition(
state.cursorPosition + offset,
state.value,
),
};
}
function deleteBeforeInputCursor(state: ChatInputState): ChatInputState {
if (state.cursorPosition === 0) {
return state;
}
return {
cursorPosition: state.cursorPosition - 1,
value: `${state.value.slice(0, state.cursorPosition - 1)}${state.value.slice(
state.cursorPosition,
)}`,
};
}
function deleteAtInputCursor(state: ChatInputState): ChatInputState {
if (state.cursorPosition >= state.value.length) {
return state;
}
return {
...state,
value: `${state.value.slice(0, state.cursorPosition)}${state.value.slice(
state.cursorPosition + 1,
)}`,
};
}
function applyRawInputValue(
state: ChatInputState,
inputValue: string,
): ChatInputState {
let nextState = state;
for (let index = 0; index < inputValue.length; index += 1) {
if (inputValue.startsWith("\u001b[D", index)) {
nextState = moveInputCursor(nextState, -1);
index += 2;
continue;
}
if (inputValue.startsWith("\u001b[C", index)) {
nextState = moveInputCursor(nextState, 1);
index += 2;
continue;
}
if (inputValue.startsWith("\u001b[3~", index)) {
nextState = deleteAtInputCursor(nextState);
index += 3;
continue;
}
if (
inputValue.startsWith("\u007f", index) ||
inputValue.startsWith("\b", index)
) {
nextState = deleteBeforeInputCursor(nextState);
continue;
}
if (
inputValue.startsWith("\u001b[A", index) ||
inputValue.startsWith("\u001b[B", index)
) {
index += 2;
continue;
}
const character = inputValue[index];
if (isControlCharacter(character)) {
continue;
}
nextState = insertAtInputCursor(nextState, character);
}
return nextState;
}
function insertAtInputCursor(
state: ChatInputState,
character: string,
): ChatInputState {
return {
cursorPosition: state.cursorPosition + character.length,
value: `${state.value.slice(0, state.cursorPosition)}${character}${state.value.slice(
state.cursorPosition,
)}`,
};
}
function clampCursorPosition(position: number, value: string): number {
return Math.max(0, Math.min(value.length, position));
}
function isControlCharacter(character: string): boolean {
const codePoint = character.codePointAt(0);
return codePoint !== undefined && codePoint < 32;
}
function isRawBackspaceInput(inputValue: string): boolean {
return inputValue === "\u007f" || inputValue === "\b";
}
function isEscapeInput(
inputValue: string,
key: Parameters[0]>[1],
): boolean {
return key.escape || inputValue === "\u001b";
}
function syncMenuStateForInput(
input: string,
currentState: ChatInputMenuState,
currentModelId: string,
currentProvider: OpenWikiProvider,
): ChatInputMenuState {
if (input.startsWith("/provider")) {
const selectedIndex =
currentState.kind === "provider"
? currentState.selectedIndex
: getCurrentProviderOptionIndex(currentProvider);
return {
kind: "provider",
selectedIndex: clampMenuIndex(
selectedIndex,
SELECTABLE_OPENWIKI_PROVIDERS.length,
),
};
}
if (input.startsWith("/model")) {
const selectedIndex =
currentState.kind === "model"
? currentState.selectedIndex
: getCurrentModelOptionIndex(currentModelId, currentProvider);
return {
kind: "model",
selectedIndex: clampMenuIndex(
selectedIndex,
getModelMenuOptions(currentModelId, currentProvider).length,
),
};
}
if (input.startsWith("/")) {
const selectedIndex =
currentState.kind === "commands"
? currentState.selectedIndex
: getCommandOptionIndex(input);
return {
kind: "commands",
selectedIndex: clampMenuIndex(selectedIndex, slashCommandOptions.length),
};
}
return { kind: "none" };
}
function moveMenuSelection(
menuState: ChatInputMenuState,
offset: number,
currentModelId: string,
currentProvider: OpenWikiProvider,
): ChatInputMenuState {
if (menuState.kind === "none") {
return menuState;
}
const itemCount =
menuState.kind === "model"
? getModelMenuOptions(currentModelId, currentProvider).length
: menuState.kind === "provider"
? SELECTABLE_OPENWIKI_PROVIDERS.length
: slashCommandOptions.length;
return {
...menuState,
selectedIndex: wrapMenuIndex(menuState.selectedIndex + offset, itemCount),
};
}
function getCommandOptionIndex(input: string): number {
const matchingIndex = slashCommandOptions.findIndex((option) =>
option.label.startsWith(input),
);
return matchingIndex === -1 ? 0 : matchingIndex;
}
function getCurrentModelOptionIndex(
currentModelId: string,
currentProvider: OpenWikiProvider,
): number {
const matchingIndex = getModelMenuOptions(
currentModelId,
currentProvider,
).findIndex(
(option) => option.kind === "model" && option.modelId === currentModelId,
);
return matchingIndex === -1 ? 0 : matchingIndex;
}
function getCurrentProviderOptionIndex(
currentProvider: OpenWikiProvider,
): number {
const matchingIndex = SELECTABLE_OPENWIKI_PROVIDERS.findIndex(
(provider) => provider === currentProvider,
);
return matchingIndex === -1 ? 0 : matchingIndex;
}
function getModelMenuOptions(
currentModelId: string,
currentProvider: OpenWikiProvider,
): ModelMenuOption[] {
const modelIds = Array.from(
new Set(
[
currentModelId,
...getProviderModelOptions(currentProvider).map((model) => model.id),
].filter(Boolean),
),
);
return [
...modelIds.map((modelId) => {
const preset = getProviderModelOptions(currentProvider).find(
(model) => model.id === modelId,
);
return {
kind: "model" as const,
label: preset ? `${preset.label} ${modelId}` : modelId,
modelId,
};
}),
{
kind: "custom" as const,
label: "Custom model ID",
},
];
}
function parseSlashInput(
input: string,
): { args: string; option: SlashCommandOption } | null {
const trimmedInput = input.trim();
const [commandName = "", ...args] = trimmedInput.split(/\s+/u);
const option = slashCommandOptions.find(
(commandOption) => commandOption.label === commandName,
);
return option ? { args: args.join(" "), option } : null;
}
function isMenuUpInput(
inputValue: string,
key: Parameters[0]>[1],
): boolean {
return key.upArrow || inputValue === "\u001b[A";
}
function isMenuDownInput(
inputValue: string,
key: Parameters[0]>[1],
): boolean {
return key.downArrow || inputValue === "\u001b[B";
}
function clampMenuIndex(index: number, itemCount: number): number {
return Math.max(0, Math.min(Math.max(0, itemCount - 1), index));
}
function wrapMenuIndex(index: number, itemCount: number): number {
if (itemCount <= 0) {
return 0;
}
return ((index % itemCount) + itemCount) % itemCount;
}
function InputCursor() {
return |;
}
function formatSecretInputSummary(value: string): string {
return value.length === 0 ? "[empty]" : `[hidden, ${value.length} chars]`;
}
function PromptBlock({ message }: { message: string }) {
return (
{" "}
{">"} {message}
);
}
function updateRunningCredentialDiagnostics(
state: RunState,
credentialDiagnostics: CredentialDiagnostic[],
credentialDiagnosticsRef: React.MutableRefObject<
CredentialDiagnostic[] | undefined
>,
): RunState {
credentialDiagnosticsRef.current = credentialDiagnostics;
return state.status === "running"
? {
...state,
credentialDiagnostics,
}
: state;
}
function appendRunLogEvent(
log: RunLogItem[],
event: OpenWikiRunEvent,
nextLogId: React.MutableRefObject,
): RunLogItem[] {
if (event.type === "text" && event.source === "subgraph") {
return log;
}
if (event.type === "text" && event.text.length === 0) {
return log;
}
if (event.type === "tool_start") {
return appendToolStartLogItem(log, event, nextLogId);
}
if (event.type === "tool_end") {
return completeToolLogItem(log, event);
}
const nextLog = [...log];
const content = event.type === "text" ? event.text : event.message;
const previous = nextLog.at(-1);
if (event.type === "text" && previous?.type === "text") {
nextLog[nextLog.length - 1] = {
...previous,
content: `${previous.content}${content}`,
};
} else {
nextLog.push({
id: nextLogId.current,
type: event.type,
content,
});
nextLogId.current += 1;
}
return nextLog;
}
function appendToolStartLogItem(
log: RunLogItem[],
event: Extract,
nextLogId: React.MutableRefObject,
): RunLogItem[] {
const toolDisplay = createToolDisplay(event);
const nextLog = [...log];
const previous = nextLog.at(-1);
if (previous?.type === "tool") {
const actionCount = (previous.actionCount ?? 1) + 1;
const errorCount = previous.errorCount ?? 0;
const latestDoneContent = toolDisplay.done;
nextLog[nextLog.length - 1] = {
...previous,
actionCount,
activeToolCallIds: [...getActiveToolCallIds(previous), event.id],
call: toolDisplay.showDetail ? event.call : undefined,
content: formatToolGroupRunning(actionCount, toolDisplay.running),
doneContent: formatToolGroupDone(
actionCount,
errorCount,
latestDoneContent,
),
errorCount,
latestDoneContent,
status: "running",
toolCallId: event.id,
toolName: event.name,
};
return nextLog;
}
return [
...log,
{
actionCount: 1,
activeToolCallIds: [event.id],
call: toolDisplay.showDetail ? event.call : undefined,
content: toolDisplay.running,
doneContent: toolDisplay.done,
errorCount: 0,
id: nextLogId.current++,
latestDoneContent: toolDisplay.done,
status: "running",
toolCallId: event.id,
toolName: event.name,
type: "tool",
},
];
}
function completeToolLogItem(
log: RunLogItem[],
event: Extract,
): RunLogItem[] {
const matchingIndex = findLastToolLogItemIndex(log, event.id);
if (matchingIndex === -1) {
return log;
}
return log.map((item, index) =>
index === matchingIndex ? completeToolGroupItem(item, event) : item,
);
}
function completeToolGroupItem(
item: RunLogItem,
event: Extract,
): RunLogItem {
const actionCount = item.actionCount ?? 1;
const activeToolCallIds = getActiveToolCallIds(item).filter(
(id) => id !== event.id,
);
const errorCount =
(item.errorCount ?? 0) + (event.status === "error" ? 1 : 0);
const latestDoneContent = item.latestDoneContent ?? item.doneContent;
if (activeToolCallIds.length > 0) {
return {
...item,
activeToolCallIds,
call: undefined,
content: formatToolGroupRunning(actionCount, null),
doneContent: formatToolGroupDone(
actionCount,
errorCount,
latestDoneContent,
),
errorCount,
status: "running",
};
}
return {
...item,
activeToolCallIds,
call: undefined,
content: formatToolGroupDone(actionCount, errorCount, latestDoneContent),
doneContent: formatToolGroupDone(
actionCount,
errorCount,
latestDoneContent,
),
errorCount,
status: errorCount > 0 ? "error" : "done",
};
}
function findLastToolLogItemIndex(
log: RunLogItem[],
toolCallId: string,
): number {
for (let index = log.length - 1; index >= 0; index -= 1) {
const item = log[index];
if (
item.type === "tool" &&
item.status === "running" &&
getActiveToolCallIds(item).includes(toolCallId)
) {
return index;
}
}
return -1;
}
function getActiveToolCallIds(item: RunLogItem): string[] {
if (item.activeToolCallIds) {
return item.activeToolCallIds;
}
if (item.status === "running" && item.toolCallId) {
return [item.toolCallId];
}
return [];
}
function formatToolGroupRunning(
actionCount: number,
currentAction: string | null,
): string {
if (actionCount <= 1) {
return currentAction ?? "Running 1 action";
}
if (currentAction) {
return `Running ${formatCount(actionCount, "action", "actions")}: ${currentAction}`;
}
return `Running ${formatCount(actionCount, "action", "actions")}`;
}
function formatToolGroupDone(
actionCount: number,
errorCount: number,
latestDoneContent?: string,
): string {
if (actionCount <= 1 && errorCount === 0) {
return latestDoneContent ?? "Ran 1 action";
}
if (errorCount > 0) {
return `Ran ${formatCount(actionCount, "action", "actions")} with ${formatCount(
errorCount,
"failure",
"failures",
)}`;
}
return `Ran ${formatCount(actionCount, "action", "actions")}`;
}
type ToolDisplay = {
done: string;
running: string;
showDetail: boolean;
};
function createToolDisplay(
event: Extract,
): ToolDisplay {
const input = parseToolInput(event.input);
const variantIndex = pickVariantIndex(
`${event.id}:${event.name}:${event.call}`,
);
switch (event.name) {
case "read_file": {
const count = countToolTargets(input, ["path", "paths", "file", "files"]);
return pickToolDisplay(
variantIndex,
[
`Reading ${formatCount(count, "file", "files")}`,
`Examining ${formatCount(count, "file", "files")}`,
`Taking a look at ${formatCount(count, "file", "files")}`,
],
[
`Read ${formatCount(count, "file", "files")}`,
`Examined ${formatCount(count, "file", "files")}`,
`Looked at ${formatCount(count, "file", "files")}`,
],
);
}
case "edit_file": {
const count = countToolTargets(input, ["path", "paths", "file", "files"]);
return pickToolDisplay(
variantIndex,
[
`Editing ${formatCount(count, "file", "files")}`,
`Updating ${formatCount(count, "file", "files")}`,
`Applying changes to ${formatCount(count, "file", "files")}`,
],
[
`Edited ${formatCount(count, "file", "files")}`,
`Updated ${formatCount(count, "file", "files")}`,
`Applied changes to ${formatCount(count, "file", "files")}`,
],
false,
);
}
case "write_file": {
const count = countToolTargets(input, ["path", "paths", "file", "files"]);
return pickToolDisplay(
variantIndex,
[
`Writing ${formatCount(count, "file", "files")}`,
`Creating ${formatCount(count, "file", "files")}`,
`Saving ${formatCount(count, "file", "files")}`,
],
[
`Wrote ${formatCount(count, "file", "files")}`,
`Created ${formatCount(count, "file", "files")}`,
`Saved ${formatCount(count, "file", "files")}`,
],
false,
);
}
case "ls":
return pickToolDisplay(
variantIndex,
["Listing files", "Scanning a directory", "Checking the file tree"],
["Listed files", "Scanned a directory", "Checked the file tree"],
);
case "glob":
return pickToolDisplay(
variantIndex,
[
"Finding matching files",
"Searching file paths",
"Scanning for matches",
],
["Found matching files", "Searched file paths", "Scanned for matches"],
);
case "grep":
return pickToolDisplay(
variantIndex,
[
"Searching file contents",
"Grepping the codebase",
"Looking for matches",
],
[
"Searched file contents",
"Grepped the codebase",
"Looked for matches",
],
);
case "write_todos": {
const count = countTodoItems(input);
return pickToolDisplay(
variantIndex,
[
`Updating ${formatCount(count, "todo", "todos")}`,
`Organizing ${formatCount(count, "todo", "todos")}`,
`Refreshing ${formatCount(count, "todo", "todos")}`,
],
[
`Updated ${formatCount(count, "todo", "todos")}`,
`Organized ${formatCount(count, "todo", "todos")}`,
`Refreshed ${formatCount(count, "todo", "todos")}`,
],
);
}
case "task": {
const count = countToolTargets(input, [
"tasks",
"subagents",
"agents",
"items",
]);
return pickToolDisplay(
variantIndex,
[
`Spinning up ${formatCount(count, "subagent", "subagents")}`,
`Starting ${formatCount(count, "subagent", "subagents")}`,
`Delegating to ${formatCount(count, "subagent", "subagents")}`,
],
[
`Finished ${formatCount(count, "subagent", "subagents")}`,
`Completed ${formatCount(count, "subagent", "subagents")}`,
`Wrapped up ${formatCount(count, "subagent", "subagents")}`,
],
);
}
default:
return {
done: event.call,
running: event.call,
showDetail: false,
};
}
}
function pickToolDisplay(
variantIndex: number,
running: string[],
done: string[],
showDetail = true,
): ToolDisplay {
const index = variantIndex % Math.min(running.length, done.length);
return {
done: done[index],
running: running[index],
showDetail,
};
}
function parseToolInput(input: unknown): unknown {
if (typeof input !== "string") {
return input;
}
try {
return JSON.parse(input);
} catch {
return input;
}
}
function countToolTargets(input: unknown, keys: string[]): number {
if (Array.isArray(input)) {
return Math.max(input.length, 1);
}
if (!isRecord(input)) {
return 1;
}
for (const key of keys) {
const value = input[key];
if (Array.isArray(value)) {
return Math.max(value.length, 1);
}
if (typeof value === "string" && value.trim().length > 0) {
return 1;
}
}
return 1;
}
function countTodoItems(input: unknown): number {
if (!isRecord(input)) {
return 1;
}
const todos = input.todos ?? input.items;
return Array.isArray(todos) ? Math.max(todos.length, 1) : 1;
}
function formatCount(count: number, singular: string, plural: string): string {
return `${count} ${count === 1 ? singular : plural}`;
}
function pickVariantIndex(seed: string): number {
let hash = 0;
for (let index = 0; index < seed.length; index += 1) {
hash = (hash * 31 + seed.charCodeAt(index)) >>> 0;
}
return hash;
}
function isExitMessage(message: string): boolean {
const normalizedMessage = message.trim().toLowerCase();
return normalizedMessage === "/exit";
}
function truncateLogOutput(content: string, label: string): string {
const terminalColumns = process.stdout.columns ?? 80;
const availableColumns = Math.max(24, terminalColumns - label.length - 7);
return truncateToDisplayLines(content, 2, availableColumns);
}
function truncateToDisplayLines(
content: string,
maxLines: number,
maxColumns: number,
): string {
const normalizedContent = content.replace(/\s+/gu, " ").trim();
if (normalizedContent.length <= maxColumns) {
return normalizedContent;
}
const lines: string[] = [];
let remaining = normalizedContent;
while (remaining.length > 0 && lines.length < maxLines) {
lines.push(remaining.slice(0, maxColumns));
remaining = remaining.slice(maxColumns);
}
if (remaining.length > 0 && lines.length > 0) {
const lastLine = lines[lines.length - 1];
lines[lines.length - 1] =
lastLine.length > 3 ? `${lastLine.slice(0, -3)}...` : "...";
}
return lines.join("\n");
}
function formatCwd(cwd: string): string {
const home = process.env.HOME;
if (home && cwd.startsWith(home)) {
return `~${cwd.slice(home.length)}`;
}
return cwd;
}
function isDebugMode(): boolean {
return process.env.OPENWIKI_DEBUG === "1";
}
function shouldShowCredentialDiagnostics(): boolean {
return isDebugMode() || process.env.OPENWIKI_DEBUG_CREDENTIALS === "1";
}
function getDisplayModelId(modelId: string | null): string {
return (
modelId ??
process.env[OPENWIKI_MODEL_ID_ENV_KEY] ??
getDefaultModelId(resolveConfiguredProvider())
);
}
/**
* The auth "how to fix" context for a failure, or undefined when it does not
* look like an auth error. Names the failing provider's API key env var and
* flags whether it came from the shell (a shell export shadows saved config, so
* the fix is to unset it). Existence check only, never reads the value.
*/
function getAuthFix(
error: unknown,
message: string,
provider: OpenWikiProvider,
): AuthFix | undefined {
if (!isAuthError(error, message)) {
return undefined;
}
const apiKeyEnvKey = getProviderApiKeyEnvKey(provider);
return {
apiKeyEnvKey,
keyFromShell:
apiKeyEnvKey !== undefined &&
getShellEnvValue(apiKeyEnvKey) !== undefined,
provider,
};
}
function getErrorDiagnostics(error: unknown): ErrorDiagnostic[] {
const diagnostics: ErrorDiagnostic[] = [];
const debugMode = isDebugMode();
if (debugMode && error instanceof Error) {
diagnostics.push(
{ label: "name", value: error.name },
{ label: "message", value: sanitizeDiagnosticText(error.message) },
);
const messageStatus = error.message.match(/\b([45]\d{2})\b/)?.[1];
if (messageStatus) {
diagnostics.push({
label: "httpStatusFromMessage",
value: messageStatus,
});
}
}
if (!isRecord(error)) {
return diagnostics;
}
addOpenRouterMetadataDiagnostics(diagnostics, error, "");
addAttachedDebugDiagnostics(diagnostics, error, "");
if (debugMode) {
addSafeObjectDiagnostics(diagnostics, error, "");
addSafeNestedDiagnostics(diagnostics, error, "cause");
addSafeNestedDiagnostics(diagnostics, error, "error");
addSafeNestedDiagnostics(diagnostics, error, "response");
}
return dedupeDiagnostics(diagnostics);
}
function addSafeNestedDiagnostics(
diagnostics: ErrorDiagnostic[],
value: Record,
key: string,
): void {
const nested = value[key];
if (!isRecord(nested)) {
return;
}
addSafeObjectDiagnostics(diagnostics, nested, key);
addOpenRouterMetadataDiagnostics(diagnostics, nested, key);
addAttachedDebugDiagnostics(diagnostics, nested, key);
}
function addSafeObjectDiagnostics(
diagnostics: ErrorDiagnostic[],
value: Record,
prefix: string,
): void {
for (const key of [
"status",
"statusCode",
"statusText",
"code",
"type",
"param",
"request_id",
"requestID",
"lc_error_code",
]) {
const property = value[key];
if (isDiagnosticValue(property)) {
diagnostics.push({
label: prefix ? `${prefix}.${key}` : key,
value: sanitizeDiagnosticText(String(property)),
});
}
}
addSafeHeaderDiagnostics(diagnostics, value.headers, prefix);
}
function addAttachedDebugDiagnostics(
diagnostics: ErrorDiagnostic[],
value: Record,
prefix: string,
): void {
const debugValue = value.openRouterDebug;
if (debugValue === undefined || debugValue === null) {
return;
}
diagnostics.push({
label: prefix ? `${prefix}.openRouterDebug` : "openRouterDebug",
value: formatDiagnosticMetadataValue(debugValue),
});
}
function addOpenRouterMetadataDiagnostics(
diagnostics: ErrorDiagnostic[],
value: Record,
prefix: string,
): void {
const metadata = value.metadata;
if (!isRecord(metadata)) {
return;
}
for (const key of ["provider_name", "is_byok", "finish_reason"]) {
const property = metadata[key];
if (isDiagnosticValue(property)) {
diagnostics.push({
label: prefix ? `${prefix}.metadata.${key}` : `metadata.${key}`,
value: sanitizeDiagnosticText(String(property)),
});
}
}
addMetadataValueDiagnostic(diagnostics, metadata, "raw", prefix);
addPreviousErrorDiagnostics(diagnostics, metadata.previous_errors, prefix);
}
function addMetadataValueDiagnostic(
diagnostics: ErrorDiagnostic[],
metadata: Record,
key: string,
prefix: string,
): void {
const value = metadata[key];
if (value === undefined || value === null) {
return;
}
diagnostics.push({
label: prefix ? `${prefix}.metadata.${key}` : `metadata.${key}`,
value: formatDiagnosticMetadataValue(value),
});
}
function addPreviousErrorDiagnostics(
diagnostics: ErrorDiagnostic[],
previousErrors: unknown,
prefix: string,
): void {
if (!Array.isArray(previousErrors)) {
return;
}
previousErrors.slice(0, 5).forEach((previousError, index) => {
diagnostics.push({
label: prefix
? `${prefix}.metadata.previous_errors.${index}`
: `metadata.previous_errors.${index}`,
value: formatDiagnosticMetadataValue(previousError),
});
});
if (previousErrors.length > 5) {
diagnostics.push({
label: prefix
? `${prefix}.metadata.previous_errors.more`
: "metadata.previous_errors.more",
value: `${previousErrors.length - 5} more previous provider errors`,
});
}
}
function formatDiagnosticMetadataValue(value: unknown): string {
if (isDiagnosticValue(value)) {
return truncateDiagnosticValue(sanitizeDiagnosticText(String(value)));
}
return truncateDiagnosticValue(sanitizeDiagnosticText(safeStringify(value)));
}
function safeStringify(value: unknown): string {
try {
return JSON.stringify(value, createDiagnosticJsonReplacer(), 2);
} catch {
return String(value);
}
}
function createDiagnosticJsonReplacer() {
const seen = new WeakSet