4222 lines · 110.6 KB · TypeScript
RawDownload
#!/usr/bin/env nodeimport 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 (    <Box      borderStyle="round"      borderColor="cyan"      flexDirection="column"      marginBottom={1}      paddingX={1}    >      <Text>        <Text bold color="cyan">          OpenWiki        </Text>{" "}        <Text color="gray">telemetry</Text>      </Text>      <Text color="white">{FIRST_RUN_NOTICE_BODY}</Text>      <Text color="white">{FIRST_RUN_NOTICE_OPT_OUT}</Text>      <Text color="white">{FIRST_RUN_NOTICE_VERIFY}</Text>    </Box>  );} 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<OpenWikiRunMode>(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<OpenWikiProvider>(startupProvider);  const [sessionModelId, setSessionModelId] = useState<string | null>(    startupModelId,  );  const activeRunId = useRef(0);  const sessionThreadId = useRef(createOpenWikiThreadId(runtimeCwd));  const sessionThreadMode = useRef<OpenWikiRunMode>(runMode);  const mountedRef = useRef(false);  const nextLogId = useRef(1);  const nextCompletedRunId = useRef(1);  const activeRunCredentialDiagnostics = useRef<    CredentialDiagnostic[] | undefined  >(undefined);  const activeRunLog = useRef<RunLogItem[]>([]);  const [runState, setRunState] = useState<RunState>({ status: "idle" });  const [completedRuns, setCompletedRuns] = useState<CompletedRun[]>([]);  const [activeUserMessage, setActiveUserMessage] = useState<string | null>(    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<OpenWikiCommand | null>(      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<OpenWikiCommand, "init" | "update">,    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") {

Lines 1–500 of 4222.