aura-marketing-cli
Version:
AI-powered marketing strategist CLI tool for developers
1,477 lines (1,474 loc) • 48.2 kB
JavaScript
#!/usr/bin/env node
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
import React, { createContext, useContext, useState, useEffect, useRef } from "react";
import { Box, Text, useInput, Spacer, useApp, Newline, Static, render } from "ink";
import process$1 from "node:process";
import "node:util";
import chalk from "chalk";
import fs from "fs/promises";
import TextInput from "ink-text-input";
import SelectInput from "ink-select-input";
import path, { dirname } from "path";
import { fileURLToPath } from "url";
import { openai } from "@ai-sdk/openai";
import { streamText } from "ai";
import dotenv from "dotenv";
function isUnicodeSupported() {
const { env } = process$1;
const { TERM, TERM_PROGRAM } = env;
if (process$1.platform !== "win32") {
return TERM !== "linux";
}
return Boolean(env.WT_SESSION) || Boolean(env.TERMINUS_SUBLIME) || env.ConEmuTask === "{cmd::Cmder}" || TERM_PROGRAM === "Terminus-Sublime" || TERM_PROGRAM === "vscode" || TERM === "xterm-256color" || TERM === "alacritty" || TERM === "rxvt-unicode" || TERM === "rxvt-unicode-256color" || env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
}
const common = {
circleQuestionMark: "(?)",
questionMarkPrefix: "(?)",
square: "█",
squareDarkShade: "▓",
squareMediumShade: "▒",
squareLightShade: "░",
squareTop: "▀",
squareBottom: "▄",
squareLeft: "▌",
squareRight: "▐",
squareCenter: "■",
bullet: "●",
dot: "․",
ellipsis: "…",
pointerSmall: "›",
triangleUp: "▲",
triangleUpSmall: "▴",
triangleDown: "▼",
triangleDownSmall: "▾",
triangleLeftSmall: "◂",
triangleRightSmall: "▸",
home: "⌂",
heart: "♥",
musicNote: "♪",
musicNoteBeamed: "♫",
arrowUp: "↑",
arrowDown: "↓",
arrowLeft: "←",
arrowRight: "→",
arrowLeftRight: "↔",
arrowUpDown: "↕",
almostEqual: "≈",
notEqual: "≠",
lessOrEqual: "≤",
greaterOrEqual: "≥",
identical: "≡",
infinity: "∞",
subscriptZero: "₀",
subscriptOne: "₁",
subscriptTwo: "₂",
subscriptThree: "₃",
subscriptFour: "₄",
subscriptFive: "₅",
subscriptSix: "₆",
subscriptSeven: "₇",
subscriptEight: "₈",
subscriptNine: "₉",
oneHalf: "½",
oneThird: "⅓",
oneQuarter: "¼",
oneFifth: "⅕",
oneSixth: "⅙",
oneEighth: "⅛",
twoThirds: "⅔",
twoFifths: "⅖",
threeQuarters: "¾",
threeFifths: "⅗",
threeEighths: "⅜",
fourFifths: "⅘",
fiveSixths: "⅚",
fiveEighths: "⅝",
sevenEighths: "⅞",
line: "─",
lineBold: "━",
lineDouble: "═",
lineDashed0: "┄",
lineDashed1: "┅",
lineDashed2: "┈",
lineDashed3: "┉",
lineDashed4: "╌",
lineDashed5: "╍",
lineDashed6: "╴",
lineDashed7: "╶",
lineDashed8: "╸",
lineDashed9: "╺",
lineDashed10: "╼",
lineDashed11: "╾",
lineDashed12: "−",
lineDashed13: "–",
lineDashed14: "‐",
lineDashed15: "⁃",
lineVertical: "│",
lineVerticalBold: "┃",
lineVerticalDouble: "║",
lineVerticalDashed0: "┆",
lineVerticalDashed1: "┇",
lineVerticalDashed2: "┊",
lineVerticalDashed3: "┋",
lineVerticalDashed4: "╎",
lineVerticalDashed5: "╏",
lineVerticalDashed6: "╵",
lineVerticalDashed7: "╷",
lineVerticalDashed8: "╹",
lineVerticalDashed9: "╻",
lineVerticalDashed10: "╽",
lineVerticalDashed11: "╿",
lineDownLeft: "┐",
lineDownLeftArc: "╮",
lineDownBoldLeftBold: "┓",
lineDownBoldLeft: "┒",
lineDownLeftBold: "┑",
lineDownDoubleLeftDouble: "╗",
lineDownDoubleLeft: "╖",
lineDownLeftDouble: "╕",
lineDownRight: "┌",
lineDownRightArc: "╭",
lineDownBoldRightBold: "┏",
lineDownBoldRight: "┎",
lineDownRightBold: "┍",
lineDownDoubleRightDouble: "╔",
lineDownDoubleRight: "╓",
lineDownRightDouble: "╒",
lineUpLeft: "┘",
lineUpLeftArc: "╯",
lineUpBoldLeftBold: "┛",
lineUpBoldLeft: "┚",
lineUpLeftBold: "┙",
lineUpDoubleLeftDouble: "╝",
lineUpDoubleLeft: "╜",
lineUpLeftDouble: "╛",
lineUpRight: "└",
lineUpRightArc: "╰",
lineUpBoldRightBold: "┗",
lineUpBoldRight: "┖",
lineUpRightBold: "┕",
lineUpDoubleRightDouble: "╚",
lineUpDoubleRight: "╙",
lineUpRightDouble: "╘",
lineUpDownLeft: "┤",
lineUpBoldDownBoldLeftBold: "┫",
lineUpBoldDownBoldLeft: "┨",
lineUpDownLeftBold: "┥",
lineUpBoldDownLeftBold: "┩",
lineUpDownBoldLeftBold: "┪",
lineUpDownBoldLeft: "┧",
lineUpBoldDownLeft: "┦",
lineUpDoubleDownDoubleLeftDouble: "╣",
lineUpDoubleDownDoubleLeft: "╢",
lineUpDownLeftDouble: "╡",
lineUpDownRight: "├",
lineUpBoldDownBoldRightBold: "┣",
lineUpBoldDownBoldRight: "┠",
lineUpDownRightBold: "┝",
lineUpBoldDownRightBold: "┡",
lineUpDownBoldRightBold: "┢",
lineUpDownBoldRight: "┟",
lineUpBoldDownRight: "┞",
lineUpDoubleDownDoubleRightDouble: "╠",
lineUpDoubleDownDoubleRight: "╟",
lineUpDownRightDouble: "╞",
lineDownLeftRight: "┬",
lineDownBoldLeftBoldRightBold: "┳",
lineDownLeftBoldRightBold: "┯",
lineDownBoldLeftRight: "┰",
lineDownBoldLeftBoldRight: "┱",
lineDownBoldLeftRightBold: "┲",
lineDownLeftRightBold: "┮",
lineDownLeftBoldRight: "┭",
lineDownDoubleLeftDoubleRightDouble: "╦",
lineDownDoubleLeftRight: "╥",
lineDownLeftDoubleRightDouble: "╤",
lineUpLeftRight: "┴",
lineUpBoldLeftBoldRightBold: "┻",
lineUpLeftBoldRightBold: "┷",
lineUpBoldLeftRight: "┸",
lineUpBoldLeftBoldRight: "┹",
lineUpBoldLeftRightBold: "┺",
lineUpLeftRightBold: "┶",
lineUpLeftBoldRight: "┵",
lineUpDoubleLeftDoubleRightDouble: "╩",
lineUpDoubleLeftRight: "╨",
lineUpLeftDoubleRightDouble: "╧",
lineUpDownLeftRight: "┼",
lineUpBoldDownBoldLeftBoldRightBold: "╋",
lineUpDownBoldLeftBoldRightBold: "╈",
lineUpBoldDownLeftBoldRightBold: "╇",
lineUpBoldDownBoldLeftRightBold: "╊",
lineUpBoldDownBoldLeftBoldRight: "╉",
lineUpBoldDownLeftRight: "╀",
lineUpDownBoldLeftRight: "╁",
lineUpDownLeftBoldRight: "┽",
lineUpDownLeftRightBold: "┾",
lineUpBoldDownBoldLeftRight: "╂",
lineUpDownLeftBoldRightBold: "┿",
lineUpBoldDownLeftBoldRight: "╃",
lineUpBoldDownLeftRightBold: "╄",
lineUpDownBoldLeftBoldRight: "╅",
lineUpDownBoldLeftRightBold: "╆",
lineUpDoubleDownDoubleLeftDoubleRightDouble: "╬",
lineUpDoubleDownDoubleLeftRight: "╫",
lineUpDownLeftDoubleRightDouble: "╪",
lineCross: "╳",
lineBackslash: "╲",
lineSlash: "╱"
};
const specialMainSymbols = {
tick: "✔",
info: "ℹ",
warning: "⚠",
cross: "✘",
squareSmall: "◻",
squareSmallFilled: "◼",
circle: "◯",
circleFilled: "◉",
circleDotted: "◌",
circleDouble: "◎",
circleCircle: "ⓞ",
circleCross: "ⓧ",
circlePipe: "Ⓘ",
radioOn: "◉",
radioOff: "◯",
checkboxOn: "☒",
checkboxOff: "☐",
checkboxCircleOn: "ⓧ",
checkboxCircleOff: "Ⓘ",
pointer: "❯",
triangleUpOutline: "△",
triangleLeft: "◀",
triangleRight: "▶",
lozenge: "◆",
lozengeOutline: "◇",
hamburger: "☰",
smiley: "㋡",
mustache: "෴",
star: "★",
play: "▶",
nodejs: "⬢",
oneSeventh: "⅐",
oneNinth: "⅑",
oneTenth: "⅒"
};
const specialFallbackSymbols = {
tick: "√",
info: "i",
warning: "‼",
cross: "×",
squareSmall: "□",
squareSmallFilled: "■",
circle: "( )",
circleFilled: "(*)",
circleDotted: "( )",
circleDouble: "( )",
circleCircle: "(○)",
circleCross: "(×)",
circlePipe: "(│)",
radioOn: "(*)",
radioOff: "( )",
checkboxOn: "[×]",
checkboxOff: "[ ]",
checkboxCircleOn: "(×)",
checkboxCircleOff: "( )",
pointer: ">",
triangleUpOutline: "∆",
triangleLeft: "◄",
triangleRight: "►",
lozenge: "♦",
lozengeOutline: "◊",
hamburger: "≡",
smiley: "☺",
mustache: "┌─┐",
star: "✶",
play: "►",
nodejs: "♦",
oneSeventh: "1/7",
oneNinth: "1/9",
oneTenth: "1/10"
};
const mainSymbols = { ...common, ...specialMainSymbols };
const fallbackSymbols = { ...common, ...specialFallbackSymbols };
const shouldUseMain = isUnicodeSupported();
const figures = shouldUseMain ? mainSymbols : fallbackSymbols;
const colorByVariant$1 = {
info: "blue",
success: "green",
error: "red",
warning: "yellow"
};
const theme$c = {
styles: {
container: ({ variant }) => ({
flexGrow: 1,
borderStyle: "round",
borderColor: colorByVariant$1[variant],
gap: 1,
paddingX: 1
}),
iconContainer: () => ({
flexShrink: 0
}),
icon: ({ variant }) => ({
color: colorByVariant$1[variant]
}),
content: () => ({
flexShrink: 1,
flexGrow: 1,
minWidth: 0,
flexDirection: "column",
gap: 1
}),
title: () => ({
bold: true
}),
message: () => ({})
},
config({ variant }) {
let icon;
if (variant === "info") {
icon = figures.info;
}
if (variant === "success") {
icon = figures.tick;
}
if (variant === "error") {
icon = figures.cross;
}
if (variant === "warning") {
icon = figures.warning;
}
return { icon };
}
};
const theme$b = {
styles: {
container: ({ color }) => ({
backgroundColor: color
}),
label: () => ({
color: "black"
})
}
};
const theme$a = {
styles: {
input: ({ isFocused }) => ({
dimColor: !isFocused
})
}
};
const theme$9 = {
styles: {
container: () => ({
flexDirection: "column"
}),
option: ({ isFocused }) => ({
gap: 1,
paddingLeft: isFocused ? 0 : 2
}),
selectedIndicator: () => ({
color: "green"
}),
focusIndicator: () => ({
color: "blue"
}),
label({ isFocused, isSelected }) {
let color;
if (isSelected) {
color = "green";
}
if (isFocused) {
color = "blue";
}
return { color };
},
highlightedText: () => ({
bold: true
})
}
};
const theme$8 = {
styles: {
list: () => ({
flexDirection: "column"
}),
listItem: () => ({
gap: 1
}),
marker: () => ({
dimColor: true
}),
content: () => ({
flexDirection: "column"
})
}
};
const theme$7 = {
styles: {
container: () => ({
flexGrow: 1,
minWidth: 0
}),
completed: () => ({
color: "magenta"
}),
remaining: () => ({
dimColor: true
})
},
config: () => ({
// Character for rendering a completed bar
completedCharacter: figures.square,
// Character for rendering a remaining bar
remainingCharacter: figures.squareLightShade
})
};
const theme$6 = {
styles: {
container: () => ({
flexDirection: "column"
}),
option: ({ isFocused }) => ({
gap: 1,
paddingLeft: isFocused ? 0 : 2
}),
selectedIndicator: () => ({
color: "green"
}),
focusIndicator: () => ({
color: "blue"
}),
label({ isFocused, isSelected }) {
let color;
if (isSelected) {
color = "green";
}
if (isFocused) {
color = "blue";
}
return { color };
},
highlightedText: () => ({
bold: true
})
}
};
const theme$5 = {
styles: {
container: () => ({
gap: 1
}),
frame: () => ({
color: "blue"
}),
label: () => ({})
}
};
const colorByVariant = {
success: "green",
error: "red",
warning: "yellow",
info: "blue"
};
const iconByVariant = {
success: figures.tick,
error: figures.cross,
warning: figures.warning,
info: figures.info
};
const theme$4 = {
styles: {
container: () => ({
gap: 1
}),
iconContainer: () => ({
flexShrink: 0
}),
icon: ({ variant }) => ({
color: colorByVariant[variant]
}),
message: () => ({})
},
config: ({ variant }) => ({
icon: iconByVariant[variant]
})
};
const theme$3 = {
styles: {
list: () => ({
flexDirection: "column"
}),
listItem: () => ({
gap: 1
}),
marker: () => ({
dimColor: true
}),
content: () => ({
flexDirection: "column"
})
},
config: () => ({
marker: figures.line
})
};
const theme$2 = {
styles: {
value: () => ({})
}
};
const theme$1 = {
styles: {
value: () => ({})
}
};
const theme = {
styles: {
value: () => ({})
}
};
const defaultTheme = {
components: {
Alert: theme$c,
Badge: theme$b,
ConfirmInput: theme$a,
MultiSelect: theme$9,
OrderedList: theme$8,
ProgressBar: theme$7,
Select: theme$6,
Spinner: theme$5,
StatusMessage: theme$4,
UnorderedList: theme$3,
TextInput: theme$2,
EmailInput: theme$1,
PasswordInput: theme
}
};
const ThemeContext = createContext(defaultTheme);
const useComponentTheme = (component) => {
const theme2 = useContext(ThemeContext);
return theme2.components[component];
};
const defaultMarker = figures.line;
createContext({
marker: defaultMarker
});
createContext({
depth: 0
});
chalk.inverse(" ");
createContext({
marker: figures.line
});
createContext({
marker: ""
});
chalk.inverse(" ");
function Alert({ children, variant, title }) {
const { styles, config } = useComponentTheme("Alert");
return React.createElement(
Box,
{ ...styles.container({ variant }) },
React.createElement(
Box,
{ ...styles.iconContainer() },
React.createElement(Text, { ...styles.icon({ variant }) }, config({ variant }).icon)
),
React.createElement(
Box,
{ ...styles.content() },
title && React.createElement(Text, { ...styles.title() }, title),
React.createElement(Text, { ...styles.message() }, children)
)
);
}
chalk.inverse(" ");
const CommandInput = ({
value,
onChange,
onSubmit,
placeholder = "Type a command or message...",
commands = [],
exitPrompt = false,
onClearInput = null,
onExit = null
}) => {
const [suggestions, setSuggestions] = useState([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
useInput((input, key) => {
if (key.ctrl && input === "c") {
if (value.length > 0 && onClearInput) {
onClearInput();
return;
}
if (onExit) {
onExit();
return;
}
}
if (showSuggestions && suggestions.length > 0) {
if (key.upArrow) {
setSelectedIndex((prev) => Math.max(0, prev - 1));
return;
}
if (key.downArrow) {
setSelectedIndex((prev) => Math.min(suggestions.length - 1, prev + 1));
return;
}
if (key.tab) {
const selectedCmd = suggestions[selectedIndex];
if (selectedCmd) {
onChange(`/${selectedCmd.name}`);
setShowSuggestions(false);
}
return;
}
}
});
useEffect(() => {
if (value.startsWith("/")) {
const query = value.substring(1).toLowerCase();
const filteredCommands = commands.filter(
(cmd) => cmd.name.toLowerCase().includes(query) || cmd.description.toLowerCase().includes(query)
);
setSuggestions(filteredCommands);
setShowSuggestions(filteredCommands.length > 0);
setSelectedIndex(0);
} else {
setSuggestions([]);
setShowSuggestions(false);
setSelectedIndex(0);
}
}, [value, commands]);
const handleSubmit = (inputValue) => {
if (showSuggestions && suggestions.length > 0) {
const selectedCmd = suggestions[selectedIndex];
if (selectedCmd) {
setShowSuggestions(false);
onSubmit(`/${selectedCmd.name}`);
return;
}
}
setShowSuggestions(false);
onSubmit(inputValue);
};
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: "100%", children: [
/* @__PURE__ */ jsxs(Box, { paddingX: 1, paddingY: 0, borderColor: "whiteBright", borderStyle: "round", width: "100%", children: [
/* @__PURE__ */ jsx(Text, { color: "blue", children: "> " }),
/* @__PURE__ */ jsx(
TextInput,
{
value,
onChange,
onSubmit: handleSubmit,
placeholder,
focus: true
}
)
] }),
showSuggestions && suggestions.length > 0 && /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, paddingLeft: 2, children: suggestions.map((cmd, index) => /* @__PURE__ */ jsxs(Box, { flexDirection: "row", flexWrap: "wrap", children: [
/* @__PURE__ */ jsx(Box, { width: 16, children: /* @__PURE__ */ jsx(Text, { color: index === selectedIndex ? "blueBright" : "gray", children: /* @__PURE__ */ jsxs(Text, { color: index === selectedIndex ? "blueBright" : "gray", children: [
"/",
cmd.name
] }) }) }),
/* @__PURE__ */ jsx(Box, { paddingLeft: 3, children: /* @__PURE__ */ jsx(Text, { color: index === selectedIndex ? "blueBright" : "gray", children: cmd.description }) })
] }, index)) }),
/* @__PURE__ */ jsxs(Box, { width: "100%", children: [
/* @__PURE__ */ jsx(Text, { dimColor: true, children: exitPrompt ? /* @__PURE__ */ jsx(Text, { color: "yellow", children: "Press Ctrl+C again to exit" }) : value.startsWith("/") ? /* @__PURE__ */ jsx(Fragment, { children: "Press Enter to continue" }) : /* @__PURE__ */ jsx(Fragment, { children: "Type / for commands" }) }),
/* @__PURE__ */ jsx(Spacer, {}),
/* @__PURE__ */ jsx(Text, { dimColor: true, children: !exitPrompt && /* @__PURE__ */ jsxs(Fragment, { children: [
" Ctrl+C to ",
value ? "clear input" : "exit"
] }) })
] })
] });
};
const CompositeInput = ({
options = [],
onSubmit,
placeholder = "Type your answer...",
initialValue = "",
onBack = null,
isReview = false,
onPrevious = null,
showTypeYourAnswer = true,
directSubmitValues = []
}) => {
const [mode, setMode] = useState(options.length > 0 ? "select" : "text");
const [inputValue, setInputValue] = useState("");
const [exitPrompt, setExitPrompt] = useState(false);
const lastCtrlC = useRef(0);
useInput((input, key) => {
if (key.ctrl && input === "c") {
const now = Date.now();
if (now - lastCtrlC.current < 2e3) {
if (onBack) onBack();
} else {
setExitPrompt(true);
lastCtrlC.current = now;
setTimeout(() => {
setExitPrompt(false);
}, 2e3);
}
}
});
useEffect(() => {
setMode(options.length > 0 ? "select" : "text");
setInputValue(initialValue);
setExitPrompt(false);
}, [options, initialValue]);
const selectItems = options.length > 0 ? [
...options.map((opt) => ({
label: opt.label,
value: opt.value
})),
...showTypeYourAnswer ? [
{
label: "Type your answer...",
value: "__custom_input__"
}
] : []
] : [];
const handleSelect = (item) => {
if (item.value === "__custom_input__") {
setMode("text");
setInputValue("");
} else if (directSubmitValues.includes(item.value)) {
onSubmit(item.value);
} else {
setMode("text");
setInputValue(item.label);
}
};
const handleTextSubmit = (value) => {
if (value.trim()) {
if (value.toLowerCase() === "/back" && !isReview && onPrevious) {
onPrevious();
return;
}
onSubmit(value);
}
};
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
/* @__PURE__ */ jsx(Box, { flexDirection: "column", children: mode === "select" && options.length > 0 ? /* @__PURE__ */ jsx(
Box,
{
paddingX: 1,
paddingY: 0,
borderColor: "whiteBright",
borderStyle: "round",
width: "100%",
children: /* @__PURE__ */ jsx(SelectInput, { items: selectItems, onSelect: handleSelect })
}
) : /* @__PURE__ */ jsxs(
Box,
{
paddingX: 1,
paddingY: 0,
borderColor: "whiteBright",
borderStyle: "round",
width: "100%",
children: [
/* @__PURE__ */ jsx(Text, { color: "blue", children: "> " }),
/* @__PURE__ */ jsx(
TextInput,
{
value: inputValue,
onChange: setInputValue,
onSubmit: handleTextSubmit,
placeholder,
focus: true
}
)
]
}
) }),
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, fontSize: 12, children: mode === "select" ? /* @__PURE__ */ jsx(Fragment, { children: "Use ↑↓ to navigate, Enter to select" }) : isReview ? /* @__PURE__ */ jsx(Fragment, { children: "Use /1, /2, etc. to jump to specific questions" }) : /* @__PURE__ */ jsx(Fragment, { children: "Use /back to go to previous question" }) }) }),
exitPrompt && /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "yellow", children: "Press Ctrl+C again to return to main menu" }) })
] });
};
const EmojiSpinner = ({
label = "",
emojis = ["💬", "💭"],
interval = 500
}) => {
const [currentEmojiIndex, setCurrentEmojiIndex] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCurrentEmojiIndex((prev) => (prev + 1) % emojis.length);
}, interval);
return () => clearInterval(timer);
}, [emojis, interval]);
return /* @__PURE__ */ jsxs(Box, { gap: 1, children: [
/* @__PURE__ */ jsx(Text, { children: emojis[currentEmojiIndex] }),
label && /* @__PURE__ */ jsx(Text, { children: label })
] });
};
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const InitPage = ({ onBack }) => {
const [questions, setQuestions] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [answers, setAnswers] = useState([]);
const [showConfirmSubmit, setShowConfirmSubmit] = useState(false);
const [showModifyInput, setShowModifyInput] = useState(false);
const [modifyInputValue, setModifyInputValue] = useState("");
const [editingQuestionIndex, setEditingQuestionIndex] = useState(-1);
const [isProcessing, setIsProcessing] = useState(false);
useEffect(() => {
const loadQuestions = async () => {
try {
const possiblePaths = [
path.join(__dirname, "..", "src", "init", "generated", "questions.json"),
// From dist to src/init
path.join(path.dirname(__dirname), "src", "init", "generated", "questions.json"),
// Alternative dist path
path.resolve("./src/init/generated/questions.json"),
// Absolute from project root
"/Users/leo/Documents/GitHub/rina/src/init/generated/questions.json"
// Fallback absolute path
];
let data = null;
for (const jsonPath of possiblePaths) {
try {
data = await fs.readFile(jsonPath, "utf-8");
break;
} catch (err) {
}
}
if (!data) {
throw new Error("Questions file not found in any expected location");
}
const parsed = JSON.parse(data);
setQuestions(parsed);
setLoading(false);
} catch (err) {
setError(`Failed to load questions: ${err.message}`);
setLoading(false);
}
};
loadQuestions();
}, []);
const handlePrevious = () => {
if (currentQuestionIndex > 0) {
setCurrentQuestionIndex((prev) => prev - 1);
setEditingQuestionIndex(-1);
}
};
const handleCompositeSubmit = (answer) => {
if (answer.startsWith("/")) {
const questionNum = parseInt(answer.substring(1));
if (questionNum >= 1 && questionNum <= questions.totalQuestions) {
setCurrentQuestionIndex(questionNum - 1);
setEditingQuestionIndex(questionNum - 1);
return;
}
}
setIsProcessing(true);
const wasReAnswering = answers.some(
(ans) => ans.question === questions.questions[currentQuestionIndex].question
);
setTimeout(() => {
setAnswers((prev) => {
const newAnswers = [...prev];
const existingIndex = newAnswers.findIndex(
(ans) => ans.question === questions.questions[currentQuestionIndex].question
);
if (existingIndex >= 0) {
newAnswers[existingIndex] = {
question: questions.questions[currentQuestionIndex].question,
answer
};
} else {
newAnswers.push({
question: questions.questions[currentQuestionIndex].question,
answer
});
}
return newAnswers;
});
setEditingQuestionIndex(-1);
setIsProcessing(false);
if (wasReAnswering) {
const answeredQuestions = new Set(answers.map((ans) => ans.question));
answeredQuestions.add(questions.questions[currentQuestionIndex].question);
for (let i = 0; i < questions.questions.length; i++) {
if (!answeredQuestions.has(questions.questions[i].question)) {
setCurrentQuestionIndex(i);
return;
}
}
setShowConfirmSubmit(true);
} else {
if (currentQuestionIndex < questions.questions.length - 1) {
setCurrentQuestionIndex((prev) => prev + 1);
} else {
setShowConfirmSubmit(true);
}
}
}, 2e3);
};
const handleConfirmSubmit = (confirmed) => {
{
if (onBack) onBack();
}
};
const handleModifyInput = (value) => {
if (value.trim()) {
if (value.startsWith("/")) {
const questionNum2 = parseInt(value.substring(1));
if (questionNum2 >= 1 && questionNum2 <= questions.totalQuestions) {
setCurrentQuestionIndex(questionNum2 - 1);
setEditingQuestionIndex(questionNum2 - 1);
setShowModifyInput(false);
return;
}
}
const questionNum = parseInt(value);
if (questionNum >= 1 && questionNum <= questions.totalQuestions) {
setCurrentQuestionIndex(questionNum - 1);
setEditingQuestionIndex(questionNum - 1);
setShowModifyInput(false);
}
}
};
if (loading) {
return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { color: "yellow", children: "Loading questions..." }) });
}
if (error) {
return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { color: "red", children: error }) });
}
if (showConfirmSubmit) {
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
/* @__PURE__ */ jsx(Box, { marginBottom: 2, children: /* @__PURE__ */ jsx(Text, { color: "green", bold: true, children: "🎉 All questions completed!" }) }),
/* @__PURE__ */ jsxs(
Box,
{
paddingX: 2,
paddingY: 1,
borderColor: "white",
borderStyle: "round",
marginBottom: 2,
flexDirection: "column",
children: [
/* @__PURE__ */ jsx(Text, { color: "yellow", children: "Your answers:" }),
answers.map((ans, idx) => {
const questionIndex = questions.questions.findIndex((q) => q.question === ans.question);
const isEditing = editingQuestionIndex === questionIndex;
return /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
/* @__PURE__ */ jsxs(Text, { color: "white", children: [
"Q",
idx + 1,
": ",
ans.question
] }),
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
"A",
idx + 1,
": ",
isEditing ? "[editing]" : ans.answer
] })
] }, idx);
})
]
}
),
isProcessing && /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(EmojiSpinner, { label: "Aura is thinking..." }) }),
/* @__PURE__ */ jsxs(Box, { columnGap: 1, marginBottom: 2, children: [
/* @__PURE__ */ jsx(Text, { children: "🌀" }),
/* @__PURE__ */ jsx(Text, { bold: true, children: "Ready to submit?" })
] }),
/* @__PURE__ */ jsx(
CompositeInput,
{
options: [
{ label: "Yes,submit for deep analysis", value: "submit" },
{ label: "No, modify answers...", value: "modify" }
],
onSubmit: (value) => {
setIsProcessing(true);
setTimeout(() => {
if (value === "submit") {
handleConfirmSubmit();
} else if (value === "modify") {
setShowConfirmSubmit(false);
setShowModifyInput(true);
setModifyInputValue("");
}
setIsProcessing(false);
}, 2e3);
},
onBack,
isReview: true,
showTypeYourAnswer: false,
directSubmitValues: ["submit", "modify"]
}
)
] });
}
if (showModifyInput) {
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
answers.length > 0 && /* @__PURE__ */ jsxs(Box, { marginBottom: 2, flexDirection: "column", children: [
/* @__PURE__ */ jsx(Text, { color: "yellow", bold: true, children: "Your answers:" }),
answers.map((ans, idx) => {
const questionIndex = questions.questions.findIndex((q) => q.question === ans.question);
const isEditing = editingQuestionIndex === questionIndex;
return /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
/* @__PURE__ */ jsxs(Text, { color: "white", children: [
"Q",
idx + 1,
": ",
ans.question
] }),
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
"A",
idx + 1,
": ",
isEditing ? "[editing]" : ans.answer
] })
] }, idx);
})
] }),
/* @__PURE__ */ jsxs(Box, { columnGap: 1, marginBottom: 2, children: [
/* @__PURE__ */ jsx(Text, { children: "🌀" }),
/* @__PURE__ */ jsx(Text, { bold: true, children: "Which question do you want to modify?" })
] }),
/* @__PURE__ */ jsx(
CompositeInput,
{
options: [],
onSubmit: handleModifyInput,
placeholder: "/1, /2, etc. to jump to specific questions",
initialValue: modifyInputValue,
onBack,
isReview: true,
showTypeYourAnswer: false
}
)
] });
}
if (questions && questions.questions[currentQuestionIndex]) {
const currentQuestion = questions.questions[currentQuestionIndex];
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
/* @__PURE__ */ jsxs(Box, { marginBottom: 1, flexDirection: "row", children: [
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { bold: true, children: [
"📝 Question ",
currentQuestionIndex + 1,
" of ",
questions.totalQuestions
] }) }),
/* @__PURE__ */ jsx(Spacer, {}),
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Box, { marginBottom: 1, marginRight: 2, children: (() => {
{
const completed = currentQuestionIndex + 1;
const remaining = questions.totalQuestions - completed;
return /* @__PURE__ */ jsxs(Text, { children: [
/* @__PURE__ */ jsx(Text, { color: "white", children: "🐤".repeat(completed) }),
/* @__PURE__ */ jsx(Text, { color: "white", children: "🥚".repeat(remaining) })
] });
}
})() }) })
] }),
answers.length > 0 && answers.length === questions.totalQuestions && /* @__PURE__ */ jsx(Box, { marginBottom: 2, flexDirection: "column", width: "100%", children: answers.map((ans, idx) => {
const questionIndex = questions.questions.findIndex((q) => q.question === ans.question);
const isEditing = editingQuestionIndex === questionIndex;
return /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
/* @__PURE__ */ jsxs(Text, { color: "white", children: [
"Q",
idx + 1,
": ",
ans.question
] }),
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
"A",
idx + 1,
": ",
isEditing ? "[editing]" : ans.answer
] })
] }, idx);
}) }),
/* @__PURE__ */ jsxs(Box, { columnGap: 1, marginBottom: 2, children: [
/* @__PURE__ */ jsx(Text, { children: "🌀" }),
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", rowGap: 1, children: [
/* @__PURE__ */ jsx(Text, { bold: true, children: currentQuestion.question }),
currentQuestion.explainer && /* @__PURE__ */ jsx(Text, { marginLeft: 2, dimColor: true, fontSize: 12, children: currentQuestion.explainer })
] })
] }),
isProcessing && /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(EmojiSpinner, { label: "Processing your answer..." }) }),
/* @__PURE__ */ jsx(
CompositeInput,
{
options: currentQuestion.options || [],
onSubmit: handleCompositeSubmit,
placeholder: "Type your answer...",
initialValue: editingQuestionIndex === currentQuestionIndex ? answers.find((ans) => ans.question === currentQuestion.question)?.answer || "" : "",
onBack,
onPrevious: handlePrevious,
isReview: answers.length === questions.totalQuestions
}
)
] });
}
return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { color: "red", children: "No questions available" }) });
};
const SettingsPage = ({ onBack }) => {
const [mode, setMode] = useState("menu");
const [inputValue, setInputValue] = useState("");
const [currentSetting, setCurrentSetting] = useState("");
const [settings, setSettings] = useState({
avatars: { aura: "🌀", user: ">" }
});
useEffect(() => {
loadSettings();
}, []);
const loadSettings = async () => {
try {
const data = await fs.readFile("./.aura.json", "utf-8");
setSettings(JSON.parse(data));
} catch {
}
};
const saveSettings = async (newSettings) => {
try {
await fs.writeFile("./.aura.json", JSON.stringify(newSettings, null, 2));
} catch (error) {
console.error("Failed to save settings:", error);
}
};
const menuItems = [
{
label: `${settings.avatars.aura} Set Aura avatar`,
value: "setAura"
},
{
label: `${settings.avatars.user} Set User avatar`,
value: "setUser"
},
{
label: "Reset to defaults",
value: "reset"
},
{
label: "Exit",
value: "exit"
}
];
const handleSelect = async (item) => {
if (item.value === "exit") {
onBack();
} else if (item.value === "reset") {
const defaultSettings = { avatars: { aura: "🌀", user: ">" } };
await saveSettings(defaultSettings);
setSettings(defaultSettings);
onBack();
} else {
setCurrentSetting(item.value);
setMode("input");
setInputValue("");
}
};
const handleAvatarSubmit = async (value) => {
if (value && value.length > 0) {
const char = value.trim();
const newSettings = { ...settings };
if (currentSetting === "setAura") {
newSettings.avatars.aura = char;
} else if (currentSetting === "setUser") {
newSettings.avatars.user = char;
}
await saveSettings(newSettings);
setSettings(newSettings);
setMode("menu");
setInputValue("");
}
};
if (mode === "input") {
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
/* @__PURE__ */ jsx(Box, { paddingY: 1, paddingX: 2, borderColor: "blue", borderStyle: "round", width: "100%", children: /* @__PURE__ */ jsxs(Text, { children: [
/* @__PURE__ */ jsx(Text, { bold: true, children: "Settings" }),
" - Avatar Configuration"
] }) }),
/* @__PURE__ */ jsxs(Box, { marginTop: 2, flexDirection: "column", children: [
/* @__PURE__ */ jsx(Text, { children: currentSetting === "setAura" ? "Enter new Aura avatar (emoji or text):" : "Enter new User avatar (emoji or text):" }),
/* @__PURE__ */ jsxs(Box, { marginTop: 1, paddingX: 1, borderColor: "gray", borderStyle: "single", children: [
/* @__PURE__ */ jsx(Text, { color: "blue", children: "> " }),
/* @__PURE__ */ jsx(
TextInput,
{
value: inputValue,
onChange: setInputValue,
onSubmit: handleAvatarSubmit,
placeholder: "Enter avatar (e.g., 🐤, >, 👾)...",
focus: true
}
)
] }),
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Press Enter to save, or Ctrl+C to cancel" }) })
] })
] });
}
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
/* @__PURE__ */ jsx(Box, { paddingY: 1, paddingX: 2, borderColor: "blue", borderStyle: "round", width: "100%", children: /* @__PURE__ */ jsxs(Text, { children: [
/* @__PURE__ */ jsx(Text, { bold: true, children: "Settings" }),
" - Avatar Configuration"
] }) }),
/* @__PURE__ */ jsxs(Box, { marginTop: 2, flexDirection: "column", children: [
/* @__PURE__ */ jsx(Text, { color: "yellow", children: "Select an option:" }),
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(SelectInput, { items: menuItems, onSelect: handleSelect }) })
] }),
/* @__PURE__ */ jsx(Box, { marginTop: 2, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
"Current: Aura = ",
settings.avatars.aura,
" User = ",
settings.avatars.user
] }) })
] });
};
dotenv.config({ path: ".env.local" });
dotenv.config();
class ChatService {
constructor() {
this.apiKey = process.env.OPENAI_API_KEY;
this.model = process.env.AI_MODEL || "gpt-4o-mini";
this.temperature = parseFloat(process.env.AI_TEMPERATURE || "0.7");
this.maxTokens = parseInt(process.env.AI_MAX_TOKENS || "200");
}
async checkAuraConfig() {
try {
await fs.access("./aura.md");
return true;
} catch {
return false;
}
}
async getSystemPrompt() {
const hasConfig = await this.checkAuraConfig();
if (hasConfig) {
try {
const auraContent = await fs.readFile("./aura.md", "utf-8");
return `You are Aura, an AI marketing strategist. You have analyzed the user's product and created a personalized marketing strategy.
Product Analysis:
${auraContent}
Provide specific, actionable marketing advice based on this product analysis. Be concise and practical.`;
} catch (error) {
console.error("Error reading aura.md:", error);
}
}
return `You are Aura, an AI marketing strategist. The user hasn't run product analysis yet.
Provide general marketing advice and encourage them to run /init for personalized insights.
Keep responses concise and actionable.`;
}
async sendMessage(messages) {
if (!this.apiKey || this.apiKey === "your_openai_api_key_here") {
throw new Error("OpenAI API key not configured. Please set OPENAI_API_KEY in .env file");
}
try {
const systemPrompt = await this.getSystemPrompt();
const result = await streamText({
model: openai(this.model),
system: systemPrompt,
messages,
temperature: this.temperature,
maxTokens: this.maxTokens
});
return result;
} catch (error) {
console.error("Error calling OpenAI API:", error);
throw error;
}
}
}
const chatService = new ChatService();
const App = () => {
const [inputValue, setInputValue] = useState("");
const [exitPrompt, setExitPrompt] = useState(false);
const [currentRoute, setCurrentRoute] = useState("home");
const [hasAuraConfig, setHasAuraConfig] = useState(false);
const [settings, setSettings] = useState({
avatars: { aura: "🌀", user: ">" }
});
const lastCtrlC = useRef(0);
const { exit } = useApp();
const [messages, setMessages] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const checkAuraConfig = async () => {
try {
await fs.access("./aura.md");
setHasAuraConfig(true);
} catch {
setHasAuraConfig(false);
}
};
const loadSettings = async () => {
try {
const data = await fs.readFile("./.aura.json", "utf-8");
setSettings(JSON.parse(data));
} catch {
}
};
checkAuraConfig();
loadSettings();
}, []);
const availableCommands = [
{
name: "init",
description: "Analyze my product and generate a marketing strategy"
},
{
name: "today",
description: "[DEMO] Check today's marketing report and recommended actions"
},
{ name: "post", description: "[DEMO] Tweet, blog post, email, short video, etc" },
{ name: "settings", description: "Manage your Aura settings" },
{
name: "claude-setup",
description: "Make Aura a sub-agent of Claude"
}
];
const handleClearInput = () => {
setInputValue("");
setExitPrompt(false);
};
const handleExit = () => {
const now = Date.now();
if (now - lastCtrlC.current < 2e3) {
exit();
} else {
setExitPrompt(true);
lastCtrlC.current = now;
setTimeout(() => {
setExitPrompt(false);
}, 2e3);
}
};
const handleSubmit = (value) => {
if (value.trim()) {
const trimmedValue = value.trim();
if (trimmedValue.startsWith("/")) {
const command = trimmedValue.substring(1).toLowerCase();
if (command === "init") {
setCurrentRoute("init");
} else if (command === "settings") {
setCurrentRoute("settings");
}
setInputValue("");
} else {
const userMessage = {
id: Date.now().toString(),
role: "user",
content: trimmedValue
};
setMessages((prev) => [...prev, userMessage]);
setError(null);
setIsLoading(true);
chatService.sendMessage([...messages, userMessage]).then(async (stream) => {
let fullContent = "";
const assistantMessageId = (Date.now() + 1).toString();
setMessages((prev) => [...prev, {
id: assistantMessageId,
role: "assistant",
content: ""
}]);
for await (const chunk of stream.textStream) {
fullContent += chunk;
setMessages((prev) => prev.map(
(msg) => msg.id === assistantMessageId ? { ...msg, content: fullContent } : msg
));
}
setIsLoading(false);
}).catch((err) => {
console.error("Chat error:", err);
setError(err.message);
if (err.message.includes("API key not configured")) {
setMessages((prev) => [...prev, {
id: (Date.now() + 1).toString(),
role: "assistant",
content: "⚠️ OpenAI API key not configured. Please set OPENAI_API_KEY in your .env file to enable AI chat."
}]);
} else {
setMessages((prev) => [...prev, {
id: (Date.now() + 1).toString(),
role: "assistant",
content: hasAuraConfig ? "Based on your product analysis, I recommend focusing on content marketing to build authority in your niche. Start with educational blog posts that solve your target audience's pain points." : "I'm using demo analysis results. Run /init to analyze your product for personalized marketing insights! Meanwhile, I can help with general marketing strategies."
}]);
}
setIsLoading(false);
});
setInputValue("");
}
setExitPrompt(false);
}
};
const handleBack = () => {
setCurrentRoute("home");
};
if (currentRoute === "init") {
return /* @__PURE__ */ jsx(InitPage, { onBack: handleBack });
}
if (currentRoute === "settings") {
return /* @__PURE__ */ jsx(SettingsPage, { onBack: () => {
const loadSettings = async () => {
try {
const data = await fs.readFile("./.aura.json", "utf-8");
setSettings(JSON.parse(data));
} catch {
}
};
loadSettings();
handleBack();
} });
}
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
/* @__PURE__ */ jsx(Box, { paddingY: 1, paddingX: 2, borderColor: "blue", borderStyle: "round", width: "100%", children: /* @__PURE__ */ jsxs(Text, { children: [
"🌀 Welcome to ",
/* @__PURE__ */ jsx(Text, { bold: true, children: "Aura!" }),
/* @__PURE__ */ jsx(Newline, {}),
/* @__PURE__ */ jsx(Newline, {}),
/* @__PURE__ */ jsx(Text, { color: "white", dimColor: true, children: "Your AI marketing companion" })
] }) }),
/* @__PURE__ */ jsxs(Box, { marginTop: 1, paddingLeft: 1, flexDirection: "column", children: [
/* @__PURE__ */ jsx(Text, { color: "gray", children: "Tips for getting started:" }),
/* @__PURE__ */ jsx(Newline, {}),
/* @__PURE__ */ jsx(Text, { color: "gray", children: " 1. Run /init to analyze your product and create a marketing strategy" }),
/* @__PURE__ */ jsx(Text, { color: "gray", children: " 2. Chat directly with Aura for marketing insights and recommendations" }),
/* @__PURE__ */ jsx(Text, { color: "gray", children: " 3. Be as specific as you would with a marketing expert for best results" }),
/* @__PURE__ */ jsx(Text, { color: "gray", children: " 4. ✓ View available commands by typing /" })
] }),
!hasAuraConfig && messages.length > 0 && /* @__PURE__ */ jsx(Static, { items: [{ id: "demo-alert" }], children: () => /* @__PURE__ */ jsx(Box, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx(Alert, { variant: "warning", children: "You're chatting with demo analysis results. Run /init for personalized insights." }) }) }),
error && /* @__PURE__ */ jsx(Static, { items: [{ id: "error-alert" }], children: () => /* @__PURE__ */ jsx(Box, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx(Alert, { variant: "error", children: error }) }) }),
messages.length > 0 && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 2, marginBottom: 1, paddingLeft: 1, children: [
messages.filter((m) => m.role !== "system").map((msg) => /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsxs(Text, { children: [
msg.role === "user" ? `${settings.avatars.user} ` : `${settings.avatars.aura} `,
msg.role === "user" ? /* @__PURE__ */ jsx(Text, { children: msg.content }) : /* @__PURE__ */ jsx(Text, { color: "white", children: msg.content })
] }) }, msg.id)),
isLoading && /* @__PURE__ */ jsx(EmojiSpinner, { label: "Aura is thinking..." })
] }),
/* @__PURE__ */ jsx(Box, { marginTop: messages.length === 0 ? 2 : 1, children: /* @__PURE__ */ jsx(
CommandInput,
{
value: inputValue,
onChange: setInputValue,
onSubmit: handleSubmit,
placeholder: "Type a command or message...",
commands: availableCommands,
exitPrompt,
onClearInput: handleClearInput,
onExit: handleExit
}
) })
] });
};
const { waitUntilExit } = render(/* @__PURE__ */ jsx(App, {}), {
exitOnCtrlC: false
// Disable automatic exit on Ctrl+C
});
waitUntilExit();
//# sourceMappingURL=rina.js.map