@rubixkube/rubix 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ const LOGO_LINES = [
4
+ "______ _ _ __ __ _",
5
+ "| ___ \\ | | (_) / // / | |",
6
+ "| |_/ / _| |__ ___ / // /_ _| |__ ___",
7
+ "| / | | | '_ \\| \\ \\/ // '_ \\ / _` '_ \\ / _ \\",
8
+ "| |\\ \\ |_| | |_) | |> <| | | | (_| |_) | __/",
9
+ "\\_| \\_\\__,_|_.__/|_/_/\\_\\_| |_|\\__,_.__/ \\___|",
10
+ ];
11
+ export function BrandPanel({ cwd, model, user }) {
12
+ return (_jsx(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 0, marginBottom: 1, children: _jsxs(Box, { flexDirection: "row", width: "100%", children: [_jsxs(Box, { flexDirection: "column", width: 58, children: [LOGO_LINES.map((line, index) => (_jsx(Text, { color: index % 2 === 0 ? "cyan" : "magenta", children: line }, line))), _jsx(Text, { bold: true, color: "yellow", children: "RubixKube Agent CLI" }), _jsxs(Text, { dimColor: true, children: [model, " • ", user ? `logged in as ${user}` : "not logged in"] }), _jsx(Text, { dimColor: true, children: cwd })] }), _jsx(Box, { marginX: 1, children: _jsx(Text, { dimColor: true, children: "\u2502" }) }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Text, { color: "yellow", bold: true, children: "Tips for getting started" }), _jsx(Text, { dimColor: true, children: "1. Use /login to authenticate first." }), _jsx(Text, { dimColor: true, children: "2. Press / to open command search." }), _jsx(Text, { dimColor: true, children: "3. Use @ to mention files and ! for shell mode." }), _jsx(Box, { marginTop: 1 }), _jsx(Text, { color: "yellow", bold: true, children: "Recent activity" }), _jsx(Text, { dimColor: true, children: "No recent activity" })] })] }) }));
13
+ }
@@ -0,0 +1,179 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { Box, Text } from "ink";
4
+ import { markdownToAnsi } from "../markdown.js";
5
+ import { RUBIX_THEME } from "../theme.js";
6
+ function extractThoughtTitle(content, max = 56) {
7
+ const plain = (content ?? "")
8
+ .replace(/```[\s\S]*?```/g, "")
9
+ .replace(/`([^`]*)`/g, "$1")
10
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
11
+ .replace(/\*([^*]+)\*/g, "$1")
12
+ .replace(/^#+\s+/gm, "")
13
+ .replace(/\[(.*?)\]\([^)]*\)/g, "$1")
14
+ .trim();
15
+ const firstLine = plain.split("\n")[0]?.trim() ?? "";
16
+ if (!firstLine)
17
+ return "Thinking";
18
+ if (firstLine.length <= max)
19
+ return firstLine;
20
+ return `${firstLine.slice(0, max - 3)}...`;
21
+ }
22
+ function compact(value, max = 90) {
23
+ const normalized = (value ?? "").replace(/\s+/g, " ").trim();
24
+ if (normalized.length <= max)
25
+ return normalized;
26
+ return `${normalized.slice(0, max - 3)}...`;
27
+ }
28
+ /** Parse [CONTEXT] blocks from message content (matches console format). */
29
+ function parseContextParts(content) {
30
+ const normalized = (content ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
31
+ if (!normalized)
32
+ return { mainMessage: "", contextParts: [] };
33
+ const parts = normalized.split(/\n\n(?=\[CONTEXT\])/);
34
+ const contextParts = [];
35
+ let mainMessage = "";
36
+ for (const part of parts) {
37
+ const trimmed = part.trim();
38
+ if (trimmed.startsWith("[CONTEXT]")) {
39
+ contextParts.push(trimmed);
40
+ }
41
+ else if (trimmed) {
42
+ mainMessage = mainMessage ? `${mainMessage}\n\n${trimmed}` : trimmed;
43
+ }
44
+ }
45
+ if (contextParts.length === 0)
46
+ return { mainMessage: normalized, contextParts: [] };
47
+ return { mainMessage, contextParts };
48
+ }
49
+ /** Preview of context block content (first line after header, max chars). */
50
+ function previewContext(contextText, max = 72) {
51
+ const afterHeader = contextText.includes("\n") ? contextText.split("\n").slice(1).join("\n") : "";
52
+ const firstLine = afterHeader.trim().split("\n")[0]?.trim() ?? "";
53
+ const plain = firstLine.replace(/\s+/g, " ");
54
+ if (plain.length <= max)
55
+ return plain;
56
+ return `${plain.slice(0, max - 3)}...`;
57
+ }
58
+ function tryPrettyJson(raw) {
59
+ const s = (raw ?? "").trim();
60
+ if (!s)
61
+ return s;
62
+ try {
63
+ const parsed = JSON.parse(s);
64
+ return JSON.stringify(parsed, null, 2);
65
+ }
66
+ catch {
67
+ return s;
68
+ }
69
+ }
70
+ function buildTimelineRows(workflow, fullContent) {
71
+ const rows = [];
72
+ for (let index = 0; index < workflow.length; index += 1) {
73
+ const event = workflow[index];
74
+ const previous = rows[rows.length - 1];
75
+ const name = typeof event.details?.name === "string" ? event.details.name : "";
76
+ const eventId = typeof event.details?.id === "string" || typeof event.details?.id === "number"
77
+ ? String(event.details.id)
78
+ : "";
79
+ if (event.type === "thought") {
80
+ const content = fullContent ? (event.content ?? "").trim() : extractThoughtTitle(event.content);
81
+ if (!fullContent && previous?.label === "thought" && previous.content === content)
82
+ continue;
83
+ rows.push({
84
+ key: `${event.id}-${index}`,
85
+ label: "thought",
86
+ content: content || "Thinking",
87
+ color: RUBIX_THEME.colors.thought,
88
+ });
89
+ continue;
90
+ }
91
+ if (event.type === "function_call") {
92
+ const toolLabel = name || "tool";
93
+ const callContent = fullContent
94
+ ? event.content
95
+ ? `${toolLabel}\n${tryPrettyJson(event.content) || event.content}`
96
+ : toolLabel
97
+ : name || compact(event.content, 80) || "tool";
98
+ if (!fullContent && previous?.label === "tool call" && previous.content === callContent)
99
+ continue;
100
+ rows.push({
101
+ key: `${event.id}-${index}`,
102
+ label: "tool call",
103
+ content: callContent,
104
+ color: RUBIX_THEME.colors.tool,
105
+ });
106
+ continue;
107
+ }
108
+ if (event.type === "function_response") {
109
+ const raw = event.content || `[${name || eventId || "tool"}]`;
110
+ const responseContent = fullContent ? tryPrettyJson(raw) || raw : compact(raw, 88);
111
+ const isError = /error|failed|exception|denied|invalid/i.test(responseContent);
112
+ if (!fullContent && previous?.label === "tool result" && previous.content === responseContent)
113
+ continue;
114
+ rows.push({
115
+ key: `${event.id}-${index}`,
116
+ label: isError ? "tool error" : "tool result",
117
+ content: responseContent,
118
+ color: isError ? "red" : RUBIX_THEME.colors.assistantText,
119
+ });
120
+ continue;
121
+ }
122
+ }
123
+ return rows;
124
+ }
125
+ function buildWorkflowStats(workflow) {
126
+ const toolCalls = workflow.filter((e) => e.type === "function_call").length;
127
+ const withTs = workflow.filter((e) => e.ts != null && e.ts > 0);
128
+ const durationSec = withTs.length >= 2
129
+ ? Math.round((Math.max(...withTs.map((e) => e.ts)) - Math.min(...withTs.map((e) => e.ts))) / 1000)
130
+ : null;
131
+ return { toolCalls, durationSec };
132
+ }
133
+ export const ChatTranscript = React.memo(function ChatTranscript({ messages, workflowViewMode = "detailed", }) {
134
+ if (messages.length === 0) {
135
+ return _jsx(Box, {});
136
+ }
137
+ return (_jsx(Box, { flexDirection: "column", width: "100%", children: messages.map((message) => {
138
+ if (message.role === "user") {
139
+ const { mainMessage, contextParts } = parseContextParts(message.content || "");
140
+ const hasContext = contextParts.length > 0;
141
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [mainMessage ? (_jsx(Box, { flexDirection: "column", children: mainMessage.split("\n").map((line, i) => (_jsxs(Text, { children: [_jsx(Text, { color: RUBIX_THEME.colors.brand, children: i === 0 ? "● " : " " }), _jsx(Text, { color: RUBIX_THEME.colors.userText, children: line })] }, `${message.id}-main-${i}`))) })) : null, hasContext
142
+ ? contextParts.map((ctx, i) => {
143
+ const labelMatch = ctx.match(/^\[CONTEXT\]\s*\*\*([^*]+)\*\*/);
144
+ const label = labelMatch ? labelMatch[1] : `Context ${i + 1}`;
145
+ const preview = previewContext(ctx);
146
+ return (_jsxs(Text, { dimColor: true, children: [" ", _jsx(Text, { color: RUBIX_THEME.colors.brand, children: "\u25CB" }), " ", label, " \u2014 ", preview] }, `${message.id}-ctx-${i}`));
147
+ })
148
+ : null] }, message.id));
149
+ }
150
+ if (message.role === "assistant") {
151
+ const workflow = message.workflow ?? [];
152
+ const stats = buildWorkflowStats(workflow);
153
+ const isDetailed = workflowViewMode === "detailed";
154
+ const timelineRows = buildTimelineRows(workflow, isDetailed);
155
+ const rawContent = message.content || "";
156
+ const ansiContent = markdownToAnsi(rawContent);
157
+ const contentLines = ansiContent.split("\n");
158
+ const hasNonEmptyContent = rawContent.trim().length > 0;
159
+ const isStreaming = message.isAccumulating === true;
160
+ const hasWorkflow = workflow.length > 0;
161
+ // Build mixed timeline: interleave workflow events with text response
162
+ const renderMixedView = () => {
163
+ // Show timeline during streaming if workflow exists, or after streaming in detailed mode
164
+ const shouldShowTimeline = (isStreaming && hasWorkflow) || (!isStreaming && isDetailed && hasWorkflow);
165
+ if (shouldShowTimeline) {
166
+ return (_jsxs(Box, { flexDirection: "column", children: [timelineRows.map((row) => {
167
+ const lines = row.content.split("\n");
168
+ return (_jsx(Box, { flexDirection: "column", children: lines.map((line, i) => (_jsx(Text, { children: i === 0 ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: row.color, children: ["\u25CF ", row.label, ": "] }), _jsx(Text, { italic: true, dimColor: true, children: line })] })) : row.label === "thought" ? (_jsxs(Text, { dimColor: true, children: [" ", line] })) : (_jsxs(Text, { dimColor: true, children: [" ", line] })) }, `${row.key}-${i}`))) }, row.key));
169
+ }), hasNonEmptyContent ? (_jsx(Box, { flexDirection: "column", children: contentLines.map((line, index) => (_jsxs(Text, { children: [_jsx(Text, { color: RUBIX_THEME.colors.assistantText, children: index === 0 ? " ● " : " " }), line.length > 0 ? line : " "] }, `${message.id}-text-${index}`))) })) : null, !isStreaming && (_jsxs(Text, { dimColor: true, children: [" \u00B7\u00B7\u00B7 ", stats.toolCalls, " tool call", stats.toolCalls !== 1 ? "s" : "", stats.durationSec != null ? ` · ${stats.durationSec}s` : "", " (Ctrl+O to collapse)"] }))] }));
170
+ }
171
+ // Minimal view: text + summary stat
172
+ return (_jsxs(Box, { flexDirection: "column", children: [hasNonEmptyContent ? (_jsx(_Fragment, { children: contentLines.map((line, index) => (_jsxs(Text, { children: [_jsx(Text, { color: RUBIX_THEME.colors.assistantText, children: index === 0 ? "● " : " " }), line.length > 0 ? line : " "] }, `${message.id}-${index}`))) })) : null, hasWorkflow ? (_jsxs(Text, { dimColor: true, children: [" ", stats.toolCalls > 0 && `${stats.toolCalls} tool call${stats.toolCalls !== 1 ? "s" : ""}`, stats.toolCalls > 0 && stats.durationSec != null && " · ", stats.durationSec != null && `${stats.durationSec}s`, hasWorkflow && " · Ctrl+O for timeline"] })) : null] }));
173
+ };
174
+ return (_jsx(Box, { flexDirection: "column", marginBottom: 0, children: renderMixedView() }, message.id));
175
+ }
176
+ const isError = /error|failed|timed out|unable/i.test(message.content);
177
+ return (_jsxs(Text, { color: isError ? "red" : undefined, dimColor: !isError, children: [isError ? "! " : "", message.content || ""] }, message.id));
178
+ }) }));
179
+ });
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useCallback, useState, useEffect } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { Spinner } from "@inkjs/ui";
5
+ import { MultilineInput } from "ink-multiline-input";
6
+ import { RUBIX_THEME } from "../theme.js";
7
+ /** Ctrl+letter shortcuts used by App — don't insert into composer. */
8
+ const GLOBAL_CTRL_KEYS = ["c", "d", "l", "o", "x"];
9
+ export const Composer = React.memo(function Composer({ value, resetToken, disabled, shellMode = false, placeholder = "Ask anything, / for commands, @ for files, ! for shell", rightStatus = "", busy = false, suggestion = "", suggestions = [], captureArrowKeys = false, onChange, onSubmit, }) {
10
+ const effectivePlaceholder = disabled ? "busy..." : placeholder;
11
+ const bashMode = shellMode;
12
+ // Buffer value locally so MultilineInput always receives the latest on fast typing.
13
+ // Parent state updates can lag; without this, MultilineInput's closure uses stale value
14
+ // and drops characters (e.g. "different" -> "diffrnt").
15
+ const [bufferedValue, setBufferedValue] = useState(value);
16
+ const lastResetTokenRef = React.useRef(resetToken);
17
+ const effectiveValue = resetToken !== lastResetTokenRef.current ? value : bufferedValue;
18
+ useEffect(() => {
19
+ if (resetToken !== lastResetTokenRef.current) {
20
+ lastResetTokenRef.current = resetToken;
21
+ setBufferedValue(value);
22
+ }
23
+ }, [value, resetToken]);
24
+ const handleChange = (newValue) => {
25
+ setBufferedValue(newValue);
26
+ onChange(newValue);
27
+ };
28
+ const useFilteredInput = useCallback((handler, isActive) => useInput((input, key) => {
29
+ if (key.ctrl && GLOBAL_CTRL_KEYS.includes((input ?? "").toLowerCase()))
30
+ return;
31
+ if (captureArrowKeys && (key.upArrow || key.downArrow))
32
+ return;
33
+ handler(input, key);
34
+ }, { isActive }), [captureArrowKeys]);
35
+ return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [_jsxs(Box, { borderStyle: "single", borderColor: bashMode ? RUBIX_THEME.colors.bash : RUBIX_THEME.colors.brand, paddingX: 1, flexDirection: "row", alignItems: "flex-start", children: [_jsx(Text, { color: bashMode ? RUBIX_THEME.colors.bashPrompt : RUBIX_THEME.colors.assistantText, children: bashMode ? "! " : "> " }), _jsx(MultilineInput, { value: effectiveValue, onChange: handleChange, onSubmit: onSubmit, placeholder: effectivePlaceholder, focus: !disabled, rows: 1, maxRows: 10, textStyle: { color: bashMode ? RUBIX_THEME.colors.bash : RUBIX_THEME.colors.assistantText }, keyBindings: {
36
+ submit: (key) => !!key.return && !key.shift && !key.meta && !key.alt,
37
+ newline: (key) => !!key.return && (!!key.shift || !!key.meta || !!key.alt),
38
+ }, useCustomInput: useFilteredInput }, `composer-${resetToken}`)] }), _jsxs(Box, { justifyContent: "space-between", paddingX: 1, flexDirection: "column", children: [bashMode ? (_jsx(Text, { color: RUBIX_THEME.colors.bash, children: "Shell mode \u00B7 Esc to switch back" })) : null, _jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { dimColor: true, children: suggestion || "? for shortcuts" }), busy ? _jsx(Spinner, { label: rightStatus }) : _jsx(Text, { dimColor: true, children: rightStatus })] })] })] }));
39
+ });
@@ -0,0 +1,63 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { Box, Text } from "ink";
4
+ import { compactSessionId, RUBIX_THEME } from "../theme.js";
5
+ function displayName(user) {
6
+ const normalized = (user ?? "").trim();
7
+ if (!normalized)
8
+ return "operator";
9
+ if (normalized.includes("@")) {
10
+ return normalized.split("@")[0] ?? "operator";
11
+ }
12
+ return normalized.split(/\s+/)[0] ?? "operator";
13
+ }
14
+ function compactWorkspace(cwd) {
15
+ const trimmed = cwd.trim();
16
+ if (!trimmed)
17
+ return "";
18
+ const home = process.env.HOME ?? "";
19
+ const homeRelative = home && trimmed.startsWith(home) ? `~${trimmed.slice(home.length)}` : trimmed;
20
+ if (homeRelative.length <= 60)
21
+ return homeRelative;
22
+ return `...${homeRelative.slice(homeRelative.length - 57)}`;
23
+ }
24
+ function relativeTime(updatedAt) {
25
+ if (!updatedAt)
26
+ return "";
27
+ const ms = Date.now() - new Date(updatedAt).getTime();
28
+ if (ms < 60_000)
29
+ return "just now";
30
+ if (ms < 3_600_000)
31
+ return `${Math.floor(ms / 60_000)}m ago`;
32
+ if (ms < 86_400_000)
33
+ return `${Math.floor(ms / 3_600_000)}h ago`;
34
+ if (ms < 172_800_000)
35
+ return "yesterday";
36
+ if (ms < 604_800_000)
37
+ return `${Math.floor(ms / 86_400_000)}d ago`;
38
+ return new Date(updatedAt).toLocaleDateString(undefined, { month: "short", day: "numeric" });
39
+ }
40
+ function oneLine(input, max = 40) {
41
+ const value = (input ?? "").replace(/\s+/g, " ").trim();
42
+ if (!value)
43
+ return null;
44
+ return value.length <= max ? value : `${value.slice(0, max - 3)}...`;
45
+ }
46
+ const SUGGESTIONS = [
47
+ "check cluster health",
48
+ "run a security audit",
49
+ "inspect performance metrics",
50
+ "debug active issues",
51
+ ];
52
+ const NAV_TIPS = [
53
+ { cmd: "/sessions", desc: "reopen a thread" },
54
+ { cmd: "/new", desc: "start fresh" },
55
+ { cmd: "/cluster", desc: "switch cluster" },
56
+ { cmd: "?", desc: "show shortcuts" },
57
+ ];
58
+ export const DashboardPanel = React.memo(function DashboardPanel({ user, agentName, cwd, recentSessions, selectedCluster }) {
59
+ const greetingName = displayName(user);
60
+ const workspace = compactWorkspace(cwd);
61
+ const latest = recentSessions.slice(0, 2);
62
+ return (_jsx(Box, { flexDirection: "column", width: "100%", marginTop: 1, paddingX: 2, paddingY: 1, children: _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: RUBIX_THEME.colors.brand, paddingX: 2, paddingY: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: ["Hello, ", greetingName] }), _jsx(Text, { dimColor: true, children: "Welcome to Site Reliability Intelligence" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: RUBIX_THEME.colors.border, dimColor: true, children: "─".repeat(64) }) }), _jsxs(Box, { flexDirection: "row", marginTop: 1, width: "100%", alignItems: "flex-start", children: [_jsxs(Box, { flexDirection: "column", width: 42, children: [_jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(Text, { color: RUBIX_THEME.colors.brand, bold: true, children: "\u25C8" }), _jsx(Text, { color: RUBIX_THEME.colors.assistantText, bold: true, children: agentName })] }), selectedCluster ? (_jsxs(Box, { flexDirection: "row", gap: 1, marginTop: 1, children: [_jsx(Text, { color: RUBIX_THEME.colors.brand, children: selectedCluster.name }), _jsx(Text, { color: RUBIX_THEME.colors.border, children: "\u00B7" }), _jsx(Text, { dimColor: true, children: selectedCluster.status }), _jsx(Text, { color: RUBIX_THEME.colors.border, children: "\u00B7" }), _jsx(Text, { dimColor: true, children: "/cluster to switch" })] })) : (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "no cluster \u00B7 /cluster to select" }) })), workspace ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: workspace }) })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: RUBIX_THEME.colors.brand, bold: true, children: "Try asking" }), SUGGESTIONS.map((item) => (_jsxs(Text, { children: [_jsx(Text, { color: RUBIX_THEME.colors.brand, children: "\u203A" }), _jsxs(Text, { dimColor: true, children: [" ", item] })] }, item)))] })] }), _jsx(Box, { paddingX: 2, flexDirection: "column", children: Array.from({ length: 10 }).map((_, i) => (_jsx(Text, { color: RUBIX_THEME.colors.border, children: "\u2502" }, i))) }), _jsxs(Box, { flexGrow: 1, flexDirection: "column", minWidth: 26, children: [_jsx(Text, { color: RUBIX_THEME.colors.brand, bold: true, children: "Quick nav" }), NAV_TIPS.map(({ cmd, desc }) => (_jsxs(Text, { children: [_jsx(Text, { color: RUBIX_THEME.colors.assistantText, children: cmd.padEnd(11) }), _jsx(Text, { dimColor: true, children: desc })] }, cmd))), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: RUBIX_THEME.colors.brand, bold: true, children: "Recent" }), latest.length === 0 ? (_jsx(Text, { dimColor: true, children: "no sessions yet \u00B7 /new to start" })) : (latest.map((session, i) => (_jsxs(Box, { flexDirection: "column", marginTop: i > 0 ? 1 : 0, children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u25CB " }), _jsx(Text, { children: oneLine(session.title) ?? "Untitled session" })] }), _jsxs(Text, { dimColor: true, children: [" ", relativeTime(session.updatedAt) || compactSessionId(session.id)] })] }, session.id))))] })] })] })] }) }));
63
+ });
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useCallback } from "react";
3
+ import { Box, Text } from "ink";
4
+ import BigText from "ink-big-text";
5
+ import Gradient from "ink-gradient";
6
+ import { Select } from "@inkjs/ui";
7
+ import { RUBIX_THEME } from "../theme.js";
8
+ import { VERSION } from "../../version.js";
9
+ const GRAD_BLUE = ["#4ea8ff", "#7f88ff"];
10
+ const WHATS_NEW = [
11
+ "Multi-cluster support with /cluster",
12
+ "Session history with auto-resume",
13
+ "Streaming workflow event tracing",
14
+ "/console opens the web dashboard",
15
+ ];
16
+ const SPLASH_OPTIONS = [
17
+ { label: "Login to RubixKube", value: "login" },
18
+ { label: "Exit", value: "exit" },
19
+ ];
20
+ export const SplashScreen = React.memo(function SplashScreen({ agentName, onActionSelect, selectDisabled = false, }) {
21
+ const handleChange = useCallback((value) => onActionSelect(value), [onActionSelect]);
22
+ return (_jsx(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: RUBIX_THEME.colors.brand, paddingX: 2, paddingY: 1, children: [_jsx(Box, { flexDirection: "column", children: _jsx(Gradient, { colors: GRAD_BLUE, children: _jsx(BigText, { text: "RUBIXKUBE", font: "block" }) }) }), _jsx(Box, { flexDirection: "row", marginTop: 1, children: _jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(Text, { dimColor: true, children: agentName }), _jsx(Text, { color: RUBIX_THEME.colors.border, children: "\u00B7" }), _jsx(Text, { dimColor: true, children: "Site Reliability Intelligence" })] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: RUBIX_THEME.colors.border, dimColor: true, children: "─".repeat(64) }) }), _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsxs(Box, { flexDirection: "column", width: 38, children: [_jsxs(Text, { color: RUBIX_THEME.colors.brand, bold: true, children: ["What's New \u00B7 v", VERSION] }), WHATS_NEW.map((item) => (_jsxs(Text, { children: [_jsx(Text, { color: RUBIX_THEME.colors.brand, children: "\u203A" }), _jsxs(Text, { dimColor: true, children: [" ", item] })] }, item)))] }), _jsx(Box, { paddingX: 2, flexDirection: "column", children: Array.from({ length: 6 }).map((_, i) => (_jsx(Text, { color: RUBIX_THEME.colors.border, children: "\u2502" }, i))) }), _jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [_jsx(Text, { color: RUBIX_THEME.colors.brand, bold: true, children: "Setup" }), _jsx(Text, { dimColor: true, children: "1. Choose Login below or type /login." }), _jsx(Text, { dimColor: true, children: "2. Open the verification URL." }), _jsx(Text, { dimColor: true, children: "3. Enter the shown device code." }), _jsx(Box, { marginTop: 1 }), _jsx(Text, { color: RUBIX_THEME.colors.brand, bold: true, children: "Prompt" }), _jsx(Text, { dimColor: true, children: "/ commands \u00B7 @ files \u00B7 ! shell" })] })] }), _jsxs(Box, { marginTop: 2, flexDirection: "column", children: [_jsx(Text, { color: RUBIX_THEME.colors.brand, bold: true, children: "What would you like to do?" }), _jsx(Box, { marginTop: 1, children: _jsx(Select, { options: SPLASH_OPTIONS, visibleOptionCount: 2, isDisabled: selectDisabled, onChange: handleChange }) })] })] }) }));
23
+ });
@@ -0,0 +1,7 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { RUBIX_THEME } from "../theme.js";
4
+ export function StatusBar({ cwd: _cwd, agentName, user, status }) {
5
+ const rightLabel = user ?? status;
6
+ return (_jsxs(Box, { justifyContent: "space-between", marginBottom: 1, children: [_jsxs(Text, { color: RUBIX_THEME.colors.brand, children: ["\u229E ", agentName, " \u25BE"] }), _jsx(Text, { dimColor: true, children: rightLabel })] }));
7
+ }
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useCallback } from "react";
3
+ import { Box, Text } from "ink";
4
+ import { Select } from "@inkjs/ui";
5
+ import { RUBIX_THEME } from "../theme.js";
6
+ const TRUST_OPTIONS = [
7
+ { label: "Trust this folder and continue", value: "trust" },
8
+ { label: "Exit", value: "exit" },
9
+ ];
10
+ export const TrustDisclaimer = React.memo(function TrustDisclaimer({ folderPath, onActionSelect, selectDisabled = false, }) {
11
+ const handleChange = useCallback((value) => onActionSelect(value), [onActionSelect]);
12
+ return (_jsx(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: RUBIX_THEME.colors.brand, paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, color: RUBIX_THEME.colors.brand, children: "Accessing workspace:" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, children: folderPath }) }), _jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: [_jsx(Text, { children: "By continuing, you allow Rubix to:" }), _jsxs(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 2, children: [_jsxs(Text, { children: ["· ", "Connect to RubixKube Platform"] }), _jsxs(Text, { children: ["· ", "Receive and execute code suggestions from AI agents"] }), _jsxs(Text, { children: ["· ", "Read/write local files based on agent instructions"] })] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "AI agents can make mistakes. Always review suggested commands and code" }), _jsx(Text, { dimColor: true, children: "before executing. Use your own judgement." })] }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Terms & Privacy: https://rubixkube.ai" }) }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(Select, { options: TRUST_OPTIONS, onChange: handleChange, isDisabled: selectDisabled }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter to confirm \u00B7 Ctrl+C to exit" }) })] }) }));
13
+ });
@@ -0,0 +1,37 @@
1
+ import { defaultTheme, extendTheme } from "@inkjs/ui";
2
+ export const inkTheme = extendTheme(defaultTheme, {
3
+ components: {
4
+ Spinner: {
5
+ styles: {
6
+ frame: () => ({
7
+ color: "#5b8def",
8
+ }),
9
+ },
10
+ },
11
+ Select: {
12
+ styles: {
13
+ selectedIndicator: () => ({
14
+ color: "#5b8def",
15
+ }),
16
+ focusIndicator: () => ({
17
+ color: "#5b8def",
18
+ }),
19
+ label: ({ isFocused, isSelected }) => {
20
+ if (isFocused || isSelected) {
21
+ return { color: "#5b8def" };
22
+ }
23
+ return {};
24
+ },
25
+ },
26
+ },
27
+ StatusMessage: {
28
+ styles: {
29
+ icon: ({ variant }) => {
30
+ if (variant === "info")
31
+ return { color: "#5b8def" };
32
+ return {};
33
+ },
34
+ },
35
+ },
36
+ },
37
+ });
@@ -0,0 +1,53 @@
1
+ import { marked } from "marked";
2
+ import { markedTerminal } from "marked-terminal";
3
+ let configured = false;
4
+ function ensureConfigured() {
5
+ if (configured)
6
+ return;
7
+ marked.use(markedTerminal({ width: 100 }));
8
+ configured = true;
9
+ }
10
+ const BOLD_ON = "\x1b[1m";
11
+ const BOLD_OFF = "\x1b[0m";
12
+ /**
13
+ * Fixes over-indented list items. CommonMark treats 4+ leading spaces as a code block,
14
+ * so lists inside indented blocks (e.g. ` * **CPU**`) are rendered as code.
15
+ * Dedent list-like lines to 2 spaces so they parse as lists.
16
+ */
17
+ function fixOverIndentedLists(md) {
18
+ return md.replace(/^( {4,})([-*+]|\d+\.)\s/gm, (_, _spaces, marker) => " " + marker + " ");
19
+ }
20
+ /**
21
+ * Fallback: marked-terminal's text renderer doesn't parse nested tokens, so **bold**
22
+ * inside list items etc. can appear raw. Convert to ANSI. (Skipping *italic* to avoid
23
+ * false matches with list bullets.)
24
+ */
25
+ function fallbackBoldFormatting(s) {
26
+ return s.replace(/\*\*([^*]+)\*\*/g, BOLD_ON + "$1" + BOLD_OFF);
27
+ }
28
+ /**
29
+ * Collapse excessive blank lines. marked-terminal adds \n\n after every block,
30
+ * which creates too much vertical gap. Cap at a single blank line.
31
+ */
32
+ function collapseExcessiveNewlines(s) {
33
+ return s.replace(/\n{3,}/g, "\n\n");
34
+ }
35
+ /**
36
+ * Converts markdown to ANSI-formatted terminal output.
37
+ * Returns the original string if parsing fails.
38
+ */
39
+ export function markdownToAnsi(md) {
40
+ const trimmed = (md ?? "").trim();
41
+ if (!trimmed)
42
+ return "";
43
+ try {
44
+ ensureConfigured();
45
+ const fixed = fixOverIndentedLists(trimmed);
46
+ const result = marked(fixed, { async: false });
47
+ const str = typeof result === "string" ? result : trimmed;
48
+ return collapseExcessiveNewlines(fallbackBoldFormatting(str));
49
+ }
50
+ catch {
51
+ return trimmed;
52
+ }
53
+ }
@@ -0,0 +1,19 @@
1
+ export const RUBIX_THEME = {
2
+ colors: {
3
+ brand: "#5b8def",
4
+ border: "#4a5568",
5
+ userText: "#a0aec0",
6
+ assistantText: "#e2e8f0",
7
+ thought: "#ff9f6b",
8
+ tool: "#63b3ed",
9
+ bash: "#e879f9",
10
+ bashPrompt: "#f87171",
11
+ },
12
+ spinnerFrames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
13
+ };
14
+ export function compactSessionId(sessionId) {
15
+ const value = sessionId.trim();
16
+ if (value.length <= 14)
17
+ return value;
18
+ return `${value.slice(0, 6)}…${value.slice(-4)}`;
19
+ }
@@ -0,0 +1,7 @@
1
+ import { readFileSync } from "fs";
2
+ import { dirname, join } from "path";
3
+ import { fileURLToPath } from "url";
4
+ const __dirname = dirname(fileURLToPath(import.meta.url));
5
+ const pkgPath = join(__dirname, "..", "package.json");
6
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
7
+ export const VERSION = pkg.version;
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@rubixkube/rubix",
3
+ "version": "0.0.1",
4
+ "description": "Chat with your infrastructure from the terminal. RubixKube CLI for Site Reliability Intelligence—predict, prevent, and fix failures with AI.",
5
+ "type": "module",
6
+ "bin": {
7
+ "rubix": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "patches",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "postinstall": "patch-package",
16
+ "prepublishOnly": "npm run build",
17
+ "dev": "tsx src/cli.ts",
18
+ "build": "tsc -p tsconfig.json",
19
+ "start": "node dist/cli.js",
20
+ "typecheck": "tsc -p tsconfig.json --noEmit"
21
+ },
22
+ "keywords": [
23
+ "rubixkube",
24
+ "sri",
25
+ "site-reliability-intelligence",
26
+ "sre",
27
+ "kubernetes",
28
+ "cli",
29
+ "terminal",
30
+ "ai",
31
+ "observability",
32
+ "remediation",
33
+ "mttr",
34
+ "ai-agent"
35
+ ],
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/rubixkube-io/rubix-cli"
40
+ },
41
+ "engines": {
42
+ "node": ">=18"
43
+ },
44
+ "dependencies": {
45
+ "@inkjs/ui": "^2.0.0",
46
+ "commander": "^14.0.3",
47
+ "dotenv": "^17.3.1",
48
+ "ink": "^6.8.0",
49
+ "ink-big-text": "^2.0.0",
50
+ "ink-gradient": "^4.0.0",
51
+ "ink-multiline-input": "^0.1.0",
52
+ "marked": "^15.0.12",
53
+ "marked-terminal": "^7.3.0",
54
+ "react": "^19.2.4",
55
+ "patch-package": "^8.0.1"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "^25.3.0",
59
+ "@types/react": "^19.2.14",
60
+ "tsx": "^4.21.0",
61
+ "typescript": "^5.9.3"
62
+ },
63
+ "homepage": "https://rubixkube.ai",
64
+ "bugs": {
65
+ "url": "https://github.com/rubixkube-io/rubix-cli/issues"
66
+ },
67
+ "author": "RubixKube <connect@rubixkube.ai>"
68
+ }
@@ -0,0 +1,78 @@
1
+ diff --git a/node_modules/ink-multiline-input/dist/index.js b/node_modules/ink-multiline-input/dist/index.js
2
+ index b1262e7..e6e7fca 100644
3
+ --- a/node_modules/ink-multiline-input/dist/index.js
4
+ +++ b/node_modules/ink-multiline-input/dist/index.js
5
+ @@ -360,6 +360,73 @@ var MultilineInput = ({
6
+ setCursorIndex(Math.min(value.length, cursorIndex + 1));
7
+ setPasteLength(0);
8
+ }
9
+ + } else if (key.ctrl && (input === "a" || input === "\u0001")) {
10
+ + if (showCursor) {
11
+ + const lines = normalizeLineEndings(value).split("\n");
12
+ + let currentPos = 0;
13
+ + for (let i = 0; i < lines.length; i++) {
14
+ + const line = lines[i];
15
+ + if (line === void 0) continue;
16
+ + const lineEnd = currentPos + line.length;
17
+ + if (cursorIndex >= currentPos && cursorIndex <= lineEnd) {
18
+ + setCursorIndex(currentPos);
19
+ + setPasteLength(0);
20
+ + break;
21
+ + }
22
+ + currentPos = lineEnd + 1;
23
+ + }
24
+ + }
25
+ + } else if (key.ctrl && (input === "e" || input === "\u0005")) {
26
+ + if (showCursor) {
27
+ + const lines = normalizeLineEndings(value).split("\n");
28
+ + let currentPos = 0;
29
+ + for (let i = 0; i < lines.length; i++) {
30
+ + const line = lines[i];
31
+ + if (line === void 0) continue;
32
+ + const lineEnd = currentPos + line.length;
33
+ + if (cursorIndex >= currentPos && cursorIndex <= lineEnd) {
34
+ + setCursorIndex(lineEnd);
35
+ + setPasteLength(0);
36
+ + break;
37
+ + }
38
+ + currentPos = lineEnd + 1;
39
+ + }
40
+ + }
41
+ + } else if (key.ctrl && (input === "k" || input === "\u000b")) {
42
+ + if (showCursor && cursorIndex < value.length) {
43
+ + const lines = normalizeLineEndings(value).split("\n");
44
+ + let currentPos = 0;
45
+ + for (let i = 0; i < lines.length; i++) {
46
+ + const line = lines[i];
47
+ + if (line === void 0) continue;
48
+ + const lineEnd = currentPos + line.length;
49
+ + if (cursorIndex >= currentPos && cursorIndex <= lineEnd) {
50
+ + const newValue = value.slice(0, cursorIndex) + value.slice(lineEnd);
51
+ + onChange(newValue);
52
+ + setPasteLength(0);
53
+ + break;
54
+ + }
55
+ + currentPos = lineEnd + 1;
56
+ + }
57
+ + }
58
+ + } else if (key.ctrl && (input === "u" || input === "\u0015")) {
59
+ + if (showCursor && cursorIndex > 0) {
60
+ + const lines = normalizeLineEndings(value).split("\n");
61
+ + let currentPos = 0;
62
+ + for (let i = 0; i < lines.length; i++) {
63
+ + const line = lines[i];
64
+ + if (line === void 0) continue;
65
+ + const lineEnd = currentPos + line.length;
66
+ + if (cursorIndex >= currentPos && cursorIndex <= lineEnd) {
67
+ + const newValue = value.slice(0, currentPos) + value.slice(cursorIndex);
68
+ + onChange(newValue);
69
+ + setCursorIndex(currentPos);
70
+ + setPasteLength(0);
71
+ + break;
72
+ + }
73
+ + currentPos = lineEnd + 1;
74
+ + }
75
+ + }
76
+ } else if (key.return) {
77
+ const newValue = value.slice(0, cursorIndex) + "\n" + value.slice(cursorIndex);
78
+ onChange(newValue);