@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.
- package/README.md +41 -0
- package/dist/cli.js +50 -0
- package/dist/commands/chat.js +17 -0
- package/dist/commands/login.js +17 -0
- package/dist/commands/logout.js +5 -0
- package/dist/commands/sessions.js +26 -0
- package/dist/config/env.js +46 -0
- package/dist/core/auth-store.js +39 -0
- package/dist/core/device-auth.js +155 -0
- package/dist/core/file-context.js +71 -0
- package/dist/core/rubix-api.js +656 -0
- package/dist/core/trust-store.js +51 -0
- package/dist/types.js +1 -0
- package/dist/ui/App.js +1766 -0
- package/dist/ui/components/BrandPanel.js +13 -0
- package/dist/ui/components/ChatTranscript.js +179 -0
- package/dist/ui/components/Composer.js +39 -0
- package/dist/ui/components/DashboardPanel.js +63 -0
- package/dist/ui/components/SplashScreen.js +23 -0
- package/dist/ui/components/StatusBar.js +7 -0
- package/dist/ui/components/TrustDisclaimer.js +13 -0
- package/dist/ui/ink-theme.js +37 -0
- package/dist/ui/markdown.js +53 -0
- package/dist/ui/theme.js +19 -0
- package/dist/version.js +7 -0
- package/package.json +68 -0
- package/patches/ink-multiline-input+0.1.0.patch +78 -0
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Rubix CLI
|
|
2
|
+
|
|
3
|
+
Chat with your infrastructure from the terminal. **RubixKube** CLI for Site Reliability Intelligence—predict, prevent, and fix failures with AI.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @rubixkube/rubix
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or run without installing:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx @rubixkube/rubix
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
1. Run `rubix` to start the chat UI
|
|
22
|
+
2. On first run, choose **Login** and complete the Auth0 device flow
|
|
23
|
+
3. Start asking questions about your infrastructure
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- **Auth** — Auth0 device code flow; credentials stored in `~/.rubix/auth.json`
|
|
28
|
+
- **Chat** — Real-time streaming to RubixKube; session resume; cluster-aware
|
|
29
|
+
- **Slash commands** — `/login`, `/logout`, `/status`, `/sessions`, `/cluster`, `/new`, `/clear`, `/console`, `/help`, `/exit`
|
|
30
|
+
- **Shortcuts** — `?` help, `Tab` autocomplete, `Ctrl+X` then `H/C/Q` for help/clear/quit
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- Node.js 18+
|
|
35
|
+
- [RubixKube account](https://console.rubixkube.ai) (free signup)
|
|
36
|
+
|
|
37
|
+
## Links
|
|
38
|
+
|
|
39
|
+
- [RubixKube](https://rubixkube.ai) — Site Reliability Intelligence platform
|
|
40
|
+
- [Documentation](https://docs.rubixkube.ai)
|
|
41
|
+
- [Console](https://console.rubixkube.ai)
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { runChatCommand } from "./commands/chat.js";
|
|
4
|
+
import { VERSION } from "./version.js";
|
|
5
|
+
import { runLoginCommand } from "./commands/login.js";
|
|
6
|
+
import { runLogoutCommand } from "./commands/logout.js";
|
|
7
|
+
import { runSessionsCommand } from "./commands/sessions.js";
|
|
8
|
+
async function main() {
|
|
9
|
+
if (process.argv.length <= 2) {
|
|
10
|
+
await runChatCommand({});
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const program = new Command();
|
|
14
|
+
program
|
|
15
|
+
.name("rubix")
|
|
16
|
+
.description("Rubix terminal client")
|
|
17
|
+
.version(VERSION);
|
|
18
|
+
program
|
|
19
|
+
.command("chat")
|
|
20
|
+
.description("Start interactive chat UI")
|
|
21
|
+
.option("-s, --session-id <id>", "Use an existing session id")
|
|
22
|
+
.option("-p, --prompt <text>", "Send a prompt immediately after launch")
|
|
23
|
+
.action(async (options) => {
|
|
24
|
+
await runChatCommand(options);
|
|
25
|
+
});
|
|
26
|
+
program
|
|
27
|
+
.command("login")
|
|
28
|
+
.description("Authenticate with device code flow")
|
|
29
|
+
.action(async () => {
|
|
30
|
+
await runLoginCommand();
|
|
31
|
+
});
|
|
32
|
+
program
|
|
33
|
+
.command("logout")
|
|
34
|
+
.description("Clear local authentication")
|
|
35
|
+
.action(async () => {
|
|
36
|
+
await runLogoutCommand();
|
|
37
|
+
});
|
|
38
|
+
program
|
|
39
|
+
.command("sessions")
|
|
40
|
+
.description("List sessions")
|
|
41
|
+
.action(async () => {
|
|
42
|
+
await runSessionsCommand();
|
|
43
|
+
});
|
|
44
|
+
await program.parseAsync(process.argv);
|
|
45
|
+
}
|
|
46
|
+
main().catch((error) => {
|
|
47
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
48
|
+
console.error(`Fatal: ${message}`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render } from "ink";
|
|
3
|
+
import { ThemeProvider } from "@inkjs/ui";
|
|
4
|
+
import { App } from "../ui/App.js";
|
|
5
|
+
import { inkTheme } from "../ui/ink-theme.js";
|
|
6
|
+
export async function runChatCommand(options) {
|
|
7
|
+
const instance = render(React.createElement(ThemeProvider, {
|
|
8
|
+
theme: inkTheme,
|
|
9
|
+
children: React.createElement(App, {
|
|
10
|
+
initialSessionId: options.sessionId,
|
|
11
|
+
seedPrompt: options.prompt,
|
|
12
|
+
}),
|
|
13
|
+
}), {
|
|
14
|
+
exitOnCtrlC: true,
|
|
15
|
+
});
|
|
16
|
+
await instance.waitUntilExit();
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { saveAuthConfig } from "../core/auth-store.js";
|
|
2
|
+
import { authenticateWithDeviceFlow } from "../core/device-auth.js";
|
|
3
|
+
export async function runLoginCommand() {
|
|
4
|
+
console.log("Starting Rubix device authentication...");
|
|
5
|
+
try {
|
|
6
|
+
const authConfig = await authenticateWithDeviceFlow((message) => {
|
|
7
|
+
console.log(message);
|
|
8
|
+
});
|
|
9
|
+
await saveAuthConfig(authConfig);
|
|
10
|
+
console.log(`Authentication successful for ${authConfig.userName ?? authConfig.userEmail ?? "user"}.`);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
14
|
+
console.error(`Login not completed: ${message}`);
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { loadAuthConfig } from "../core/auth-store.js";
|
|
2
|
+
import { listSessions } from "../core/rubix-api.js";
|
|
3
|
+
export async function runSessionsCommand() {
|
|
4
|
+
try {
|
|
5
|
+
const auth = await loadAuthConfig();
|
|
6
|
+
if (!auth?.isAuthenticated) {
|
|
7
|
+
console.error("Not authenticated. Run `rubix login` first.");
|
|
8
|
+
process.exitCode = 1;
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const sessions = await listSessions(auth);
|
|
12
|
+
if (sessions.length === 0) {
|
|
13
|
+
console.log("No sessions found.");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
for (const session of sessions) {
|
|
17
|
+
const title = session.title ? ` ${session.title}` : "";
|
|
18
|
+
console.log(`${session.id} ${session.appName} ${session.updatedAt}${title}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
23
|
+
console.error(`Failed to list sessions: ${message}`);
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
dotenv.config({ quiet: true });
|
|
3
|
+
/** Production defaults — baked in for installable binary. Override via env for dev/staging/self-hosted. */
|
|
4
|
+
const DEFAULTS = {
|
|
5
|
+
RUBIXKUBE_AUTH0_DOMAIN: "rubixkube.us.auth0.com",
|
|
6
|
+
RUBIXKUBE_AUTH0_CLIENT_ID: "l732txhrSBbcieX2WgZuaRC8nTZBteVh",
|
|
7
|
+
RUBIXKUBE_AUTH0_AUDIENCE: "",
|
|
8
|
+
RUBIXKUBE_AUTH_API_BASE: "https://console.rubixkube.ai/api/orchestrator",
|
|
9
|
+
RUBIXKUBE_OPEL_API_BASE: "https://console.rubixkube.ai/api/opel",
|
|
10
|
+
RUBIXKUBE_STREAM_TIMEOUT_MS: "600000",
|
|
11
|
+
};
|
|
12
|
+
export const REQUIRED_ENV = [
|
|
13
|
+
"RUBIXKUBE_AUTH0_DOMAIN",
|
|
14
|
+
"RUBIXKUBE_AUTH0_CLIENT_ID",
|
|
15
|
+
"RUBIXKUBE_AUTH_API_BASE",
|
|
16
|
+
"RUBIXKUBE_OPEL_API_BASE",
|
|
17
|
+
];
|
|
18
|
+
export const AUTH_REQUIRED_ENV = [
|
|
19
|
+
"RUBIXKUBE_AUTH0_DOMAIN",
|
|
20
|
+
"RUBIXKUBE_AUTH0_CLIENT_ID",
|
|
21
|
+
"RUBIXKUBE_AUTH_API_BASE",
|
|
22
|
+
];
|
|
23
|
+
export function getConfig() {
|
|
24
|
+
return {
|
|
25
|
+
auth0Domain: process.env.RUBIXKUBE_AUTH0_DOMAIN ?? DEFAULTS.RUBIXKUBE_AUTH0_DOMAIN,
|
|
26
|
+
auth0ClientId: process.env.RUBIXKUBE_AUTH0_CLIENT_ID ?? DEFAULTS.RUBIXKUBE_AUTH0_CLIENT_ID,
|
|
27
|
+
auth0Audience: (process.env.RUBIXKUBE_AUTH0_AUDIENCE ?? DEFAULTS.RUBIXKUBE_AUTH0_AUDIENCE) || undefined,
|
|
28
|
+
authApiBase: process.env.RUBIXKUBE_AUTH_API_BASE ?? DEFAULTS.RUBIXKUBE_AUTH_API_BASE,
|
|
29
|
+
opelApiBase: process.env.RUBIXKUBE_OPEL_API_BASE ?? DEFAULTS.RUBIXKUBE_OPEL_API_BASE,
|
|
30
|
+
streamTimeoutMs: Number(process.env.RUBIXKUBE_STREAM_TIMEOUT_MS ?? DEFAULTS.RUBIXKUBE_STREAM_TIMEOUT_MS) || 600000,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function getMissingEnv(names) {
|
|
34
|
+
const config = getConfig();
|
|
35
|
+
const keyMap = {
|
|
36
|
+
RUBIXKUBE_AUTH0_DOMAIN: config.auth0Domain,
|
|
37
|
+
RUBIXKUBE_AUTH0_CLIENT_ID: config.auth0ClientId,
|
|
38
|
+
RUBIXKUBE_AUTH0_AUDIENCE: config.auth0Audience ?? "",
|
|
39
|
+
RUBIXKUBE_AUTH_API_BASE: config.authApiBase,
|
|
40
|
+
RUBIXKUBE_OPEL_API_BASE: config.opelApiBase,
|
|
41
|
+
};
|
|
42
|
+
return names.filter((name) => !keyMap[name]?.trim());
|
|
43
|
+
}
|
|
44
|
+
export function getMissingRequiredEnv() {
|
|
45
|
+
return getMissingEnv(REQUIRED_ENV);
|
|
46
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const CONFIG_DIR = ".rubix";
|
|
5
|
+
const AUTH_FILE = "auth.json";
|
|
6
|
+
export function getAuthPath() {
|
|
7
|
+
return path.join(os.homedir(), CONFIG_DIR, AUTH_FILE);
|
|
8
|
+
}
|
|
9
|
+
async function ensureConfigDir() {
|
|
10
|
+
await fs.mkdir(path.join(os.homedir(), CONFIG_DIR), { recursive: true, mode: 0o700 });
|
|
11
|
+
}
|
|
12
|
+
export async function loadAuthConfig() {
|
|
13
|
+
try {
|
|
14
|
+
const data = await fs.readFile(getAuthPath(), "utf8");
|
|
15
|
+
return JSON.parse(data);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
const asNodeError = error;
|
|
19
|
+
if (asNodeError.code === "ENOENT") {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function saveAuthConfig(config) {
|
|
26
|
+
await ensureConfigDir();
|
|
27
|
+
await fs.writeFile(getAuthPath(), JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
28
|
+
}
|
|
29
|
+
export async function clearAuthConfig() {
|
|
30
|
+
try {
|
|
31
|
+
await fs.unlink(getAuthPath());
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const asNodeError = error;
|
|
35
|
+
if (asNodeError.code !== "ENOENT") {
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { AUTH_REQUIRED_ENV, getConfig, getMissingEnv } from "../config/env.js";
|
|
3
|
+
function isLocalDomain(domain) {
|
|
4
|
+
return domain.startsWith("localhost") || domain.startsWith("127.0.0.1");
|
|
5
|
+
}
|
|
6
|
+
function auth0BaseUrl() {
|
|
7
|
+
const domain = getConfig().auth0Domain;
|
|
8
|
+
const protocol = isLocalDomain(domain) ? "http" : "https";
|
|
9
|
+
return `${protocol}://${domain}`;
|
|
10
|
+
}
|
|
11
|
+
function decodeJwtClaims(token) {
|
|
12
|
+
const parts = token.split(".");
|
|
13
|
+
if (parts.length < 2) {
|
|
14
|
+
throw new Error("Invalid JWT token format.");
|
|
15
|
+
}
|
|
16
|
+
const payload = parts[1];
|
|
17
|
+
const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
18
|
+
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
|
|
19
|
+
const decoded = Buffer.from(padded, "base64").toString("utf8");
|
|
20
|
+
return JSON.parse(decoded);
|
|
21
|
+
}
|
|
22
|
+
export async function startDeviceAuth() {
|
|
23
|
+
const missing = getMissingEnv(AUTH_REQUIRED_ENV);
|
|
24
|
+
if (missing.length > 0) {
|
|
25
|
+
throw new Error(`Missing required environment variables: ${missing.join(", ")}`);
|
|
26
|
+
}
|
|
27
|
+
const { auth0ClientId, auth0Audience } = getConfig();
|
|
28
|
+
const body = new URLSearchParams();
|
|
29
|
+
body.set("client_id", auth0ClientId);
|
|
30
|
+
body.set("scope", "openid profile email");
|
|
31
|
+
if (auth0Audience) {
|
|
32
|
+
body.set("audience", auth0Audience);
|
|
33
|
+
}
|
|
34
|
+
const response = await fetch(`${auth0BaseUrl()}/oauth/device/code`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
37
|
+
body,
|
|
38
|
+
});
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
const text = await response.text();
|
|
41
|
+
throw new Error(`Device authorization request failed (${response.status}): ${text}`);
|
|
42
|
+
}
|
|
43
|
+
const payload = (await response.json());
|
|
44
|
+
return {
|
|
45
|
+
deviceCode: payload.device_code,
|
|
46
|
+
userCode: payload.user_code,
|
|
47
|
+
verificationUrl: payload.verification_uri_complete ?? payload.verification_uri ?? "",
|
|
48
|
+
expiresIn: payload.expires_in,
|
|
49
|
+
interval: payload.interval ?? 5,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async function pollForTokens(start, log) {
|
|
53
|
+
const body = new URLSearchParams();
|
|
54
|
+
body.set("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
|
|
55
|
+
body.set("device_code", start.deviceCode);
|
|
56
|
+
body.set("client_id", getConfig().auth0ClientId);
|
|
57
|
+
const expiresAt = Date.now() + start.expiresIn * 1000;
|
|
58
|
+
const intervalMs = Math.max(1, start.interval) * 1000;
|
|
59
|
+
let waitingLogged = false;
|
|
60
|
+
while (Date.now() < expiresAt) {
|
|
61
|
+
const response = await fetch(`${auth0BaseUrl()}/oauth/token`, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
64
|
+
body,
|
|
65
|
+
});
|
|
66
|
+
const raw = (await response.json());
|
|
67
|
+
if (response.ok && raw.id_token && raw.access_token) {
|
|
68
|
+
return raw;
|
|
69
|
+
}
|
|
70
|
+
if (raw.error === "authorization_pending" || raw.error === "slow_down") {
|
|
71
|
+
if (!waitingLogged) {
|
|
72
|
+
waitingLogged = true;
|
|
73
|
+
log?.("Waiting for authorization in browser...");
|
|
74
|
+
}
|
|
75
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`Token polling failed: ${JSON.stringify(raw)}`);
|
|
79
|
+
}
|
|
80
|
+
throw new Error("Device authorization timed out.");
|
|
81
|
+
}
|
|
82
|
+
async function validateAuth0Token(idToken) {
|
|
83
|
+
const authBase = getConfig().authApiBase;
|
|
84
|
+
const claims = decodeJwtClaims(idToken);
|
|
85
|
+
const response = await fetch(`${authBase}/auth/auth0-login`, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
Authorization: `Bearer ${idToken}`,
|
|
89
|
+
"Content-Type": "application/json",
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify({
|
|
92
|
+
auth0_user: {
|
|
93
|
+
sub: claims.sub,
|
|
94
|
+
email: claims.email,
|
|
95
|
+
name: claims.name,
|
|
96
|
+
picture: claims.picture,
|
|
97
|
+
email_verified: claims.email_verified,
|
|
98
|
+
},
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
const text = await response.text();
|
|
103
|
+
throw new Error(`auth0-login failed (${response.status}): ${text}`);
|
|
104
|
+
}
|
|
105
|
+
return (await response.json());
|
|
106
|
+
}
|
|
107
|
+
async function getUserProfile(idToken) {
|
|
108
|
+
const authBase = getConfig().authApiBase;
|
|
109
|
+
const response = await fetch(`${authBase}/users/me`, {
|
|
110
|
+
headers: {
|
|
111
|
+
Authorization: `Bearer ${idToken}`,
|
|
112
|
+
"Content-Type": "application/json",
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return (await response.json());
|
|
119
|
+
}
|
|
120
|
+
function openUrlInBrowser(url) {
|
|
121
|
+
const urlStr = String(url).trim();
|
|
122
|
+
if (!urlStr.startsWith("http://") && !urlStr.startsWith("https://"))
|
|
123
|
+
return;
|
|
124
|
+
if (process.platform === "darwin") {
|
|
125
|
+
spawn("open", [urlStr], { stdio: "ignore" });
|
|
126
|
+
}
|
|
127
|
+
else if (process.platform === "win32") {
|
|
128
|
+
spawn("cmd", ["/c", "start", "", urlStr], { stdio: "ignore" });
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
spawn("xdg-open", [urlStr], { stdio: "ignore" });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export async function authenticateWithDeviceFlow(log) {
|
|
135
|
+
const start = await startDeviceAuth();
|
|
136
|
+
openUrlInBrowser(start.verificationUrl);
|
|
137
|
+
log?.(`Opening ${start.verificationUrl} in browser`);
|
|
138
|
+
log?.(`Enter code: ${start.userCode}`);
|
|
139
|
+
const tokens = await pollForTokens(start, log);
|
|
140
|
+
const validation = await validateAuth0Token(tokens.id_token);
|
|
141
|
+
const profile = await getUserProfile(tokens.id_token);
|
|
142
|
+
return {
|
|
143
|
+
authToken: tokens.access_token,
|
|
144
|
+
idToken: tokens.id_token,
|
|
145
|
+
refreshToken: tokens.refresh_token,
|
|
146
|
+
tenantId: profile?.tenant?.tenant_id ?? validation.tenant_id,
|
|
147
|
+
userEmail: profile?.email ?? validation.user?.email,
|
|
148
|
+
userId: profile?.user_id,
|
|
149
|
+
userName: profile?.full_name ?? validation.user?.name,
|
|
150
|
+
userRole: profile?.roles?.[0],
|
|
151
|
+
tenantPlan: profile?.tenant?.plan_tier,
|
|
152
|
+
isAuthenticated: true,
|
|
153
|
+
timestamp: Date.now(),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const MAX_FILE_SIZE = 256 * 1024; // 256KB
|
|
4
|
+
const MAX_URL_SIZE = 512 * 1024; // 512KB
|
|
5
|
+
export async function readFileContext(filePath, cwd) {
|
|
6
|
+
const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
|
|
7
|
+
const label = path.basename(resolved);
|
|
8
|
+
const stat = await fs.stat(resolved);
|
|
9
|
+
if (!stat.isFile()) {
|
|
10
|
+
throw new Error(`Not a file: ${filePath}`);
|
|
11
|
+
}
|
|
12
|
+
if (stat.size > MAX_FILE_SIZE) {
|
|
13
|
+
throw new Error(`File too large (max ${MAX_FILE_SIZE / 1024}KB): ${filePath}`);
|
|
14
|
+
}
|
|
15
|
+
const content = await fs.readFile(resolved, "utf8");
|
|
16
|
+
let truncated = false;
|
|
17
|
+
let out = content;
|
|
18
|
+
if (content.length > MAX_FILE_SIZE) {
|
|
19
|
+
out = content.slice(0, MAX_FILE_SIZE);
|
|
20
|
+
truncated = true;
|
|
21
|
+
}
|
|
22
|
+
return { content: out, label, truncated };
|
|
23
|
+
}
|
|
24
|
+
export async function fetchUrlContext(url) {
|
|
25
|
+
const trimmed = url.trim();
|
|
26
|
+
if (!trimmed.startsWith("http://") && !trimmed.startsWith("https://")) {
|
|
27
|
+
throw new Error(`Invalid URL (must be http or https): ${url}`);
|
|
28
|
+
}
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
const timeout = setTimeout(() => controller.abort(), 15000);
|
|
31
|
+
const response = await fetch(trimmed, {
|
|
32
|
+
signal: controller.signal,
|
|
33
|
+
headers: { "User-Agent": "Rubix-CLI/1.0" },
|
|
34
|
+
});
|
|
35
|
+
clearTimeout(timeout);
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw new Error(`HTTP ${response.status}: ${trimmed}`);
|
|
38
|
+
}
|
|
39
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
40
|
+
if (!contentType.includes("text/") && !contentType.includes("application/json")) {
|
|
41
|
+
throw new Error(`Unsupported content type: ${contentType}`);
|
|
42
|
+
}
|
|
43
|
+
const text = await response.text();
|
|
44
|
+
let label;
|
|
45
|
+
try {
|
|
46
|
+
const u = new URL(trimmed);
|
|
47
|
+
label = u.hostname + (u.pathname.replace(/\/$/, "") || "");
|
|
48
|
+
if (!label)
|
|
49
|
+
label = trimmed;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
label = trimmed;
|
|
53
|
+
}
|
|
54
|
+
let truncated = false;
|
|
55
|
+
let out = text;
|
|
56
|
+
if (text.length > MAX_URL_SIZE) {
|
|
57
|
+
out = text.slice(0, MAX_URL_SIZE);
|
|
58
|
+
truncated = true;
|
|
59
|
+
}
|
|
60
|
+
return { content: out, label, truncated };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Format context block to match console's context message format.
|
|
64
|
+
* Console: [CONTEXT] **label** (type){: description}:\ncontent
|
|
65
|
+
* Parsed by: ^\[CONTEXT\]\s*\*\*([^*]+)\*\*
|
|
66
|
+
*/
|
|
67
|
+
export function formatContextBlock(result, kind, description) {
|
|
68
|
+
const desc = description ?? (result.truncated ? "truncated" : undefined);
|
|
69
|
+
const header = `[CONTEXT] **${result.label}** (${kind})${desc ? ": " + desc : ""}`;
|
|
70
|
+
return `${header}:\n${result.content}`;
|
|
71
|
+
}
|