@kurtel/cli 0.1.0

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 ADDED
@@ -0,0 +1,91 @@
1
+ # Kurtel CLI
2
+
3
+ Launch self-improving coding agents in the cloud — from your terminal, your
4
+ editor, or CI. `kurtel` drops you into an interactive session (like a coding
5
+ copilot REPL) and also exposes scriptable subcommands.
6
+
7
+ > **Preview build.** The commands run end-to-end locally with realistic output,
8
+ > but they are **not yet connected to the Kurtel cloud**. They're stubs that
9
+ > define the experience.
10
+
11
+ ## Install
12
+
13
+ From source (during preview):
14
+
15
+ ```bash
16
+ git clone <repo> kurtel-cli && cd kurtel-cli
17
+ npm install # builds automatically via the prepare script
18
+ npm link # makes `kurtel` available everywhere (incl. the VSCode terminal)
19
+ ```
20
+
21
+ Then, in any project:
22
+
23
+ ```bash
24
+ kurtel
25
+ ```
26
+
27
+ To remove the global link later: `npm unlink -g kurtel`.
28
+
29
+ > Once published, install will be a one-liner: `npm install -g kurtel`.
30
+
31
+ ## Usage
32
+
33
+ ```text
34
+ kurtel Open the interactive session
35
+ kurtel run "<task>" Launch a cloud agent on a task
36
+ kurtel agents | ps List active agents
37
+ kurtel logs <id> Stream an agent's logs
38
+ kurtel status <id> Show an agent's status
39
+ kurtel stop <id> Stop an agent & tear down its sandbox
40
+ kurtel login | logout Manage your session
41
+ kurtel config [list|get|set] View or change local config
42
+ kurtel init Initialize Kurtel in the current project
43
+ kurtel doctor Check your environment
44
+ kurtel -v | --version Print version
45
+ kurtel -h | --help Show help
46
+ ```
47
+
48
+ ### Interactive session
49
+
50
+ Run `kurtel` with no arguments. Type a task to launch an agent, or use slash
51
+ commands:
52
+
53
+ ```text
54
+ /help Show commands
55
+ /run <task> Launch a cloud agent
56
+ /agents List active agents
57
+ /login Sign in
58
+ /config Show configuration
59
+ /clear Clear the screen
60
+ /exit Quit
61
+ ```
62
+
63
+ Exit with `/exit` or `Ctrl+D`.
64
+
65
+ ## Development
66
+
67
+ ```bash
68
+ npm run dev # run from source with tsx (no build step)
69
+ npm run build # compile TypeScript to dist/
70
+ npm start # run the compiled CLI
71
+ ```
72
+
73
+ ## Project layout
74
+
75
+ ```
76
+ src/
77
+ index.ts # entry — commander wiring + shebang
78
+ session/repl.ts # interactive session
79
+ commands/ # one file per command group (stubs)
80
+ run.ts auth.ts agents.ts runtime.ts config.ts project.ts
81
+ ui/ # colors, box, banner, spinner (zero-dep)
82
+ lib/ # version, config (~/.kurtel/config.json), sample data
83
+ ```
84
+
85
+ ## Notes
86
+
87
+ - Built as an ESM TypeScript project targeting Node ≥ 18.
88
+ - Only one runtime dependency (`commander`); the terminal UI is hand-rolled with
89
+ ANSI escapes and honors `NO_COLOR`.
90
+ - Local config lives at `~/.kurtel/config.json`; `kurtel init` writes a
91
+ project-level `.kurtel/config.json`.
@@ -0,0 +1,7 @@
1
+ import { c, symbols } from "../ui/colors.js";
2
+ export function previewNotice(what) {
3
+ console.log(`${c.yellow(symbols.warn)} ${c.dim(`${what} isn't wired up to the Kurtel cloud yet — this is a preview build.`)}`);
4
+ }
5
+ export function heading(text) {
6
+ console.log(`\n${c.indigoBold(text)}`);
7
+ }
@@ -0,0 +1,28 @@
1
+ import { c } from "../ui/colors.js";
2
+ import { SAMPLE_AGENTS, statusColor, levelBadge, } from "../lib/agents.js";
3
+ import { previewNotice } from "./_shared.js";
4
+ function pad(s, width) {
5
+ // pad based on visible length (strip ANSI)
6
+ // eslint-disable-next-line no-control-regex
7
+ const len = s.replace(/\x1b\[[0-9;]*m/g, "").length;
8
+ return s + " ".repeat(Math.max(0, width - len));
9
+ }
10
+ export function agentsCommand() {
11
+ console.log("");
12
+ const header = pad(c.gray("ID"), 12) +
13
+ pad(c.gray("LEVEL"), 9) +
14
+ pad(c.gray("STATUS"), 14) +
15
+ pad(c.gray("AGE"), 7) +
16
+ c.gray("TASK");
17
+ console.log(header);
18
+ for (const a of SAMPLE_AGENTS) {
19
+ const row = pad(c.indigo(a.id), 12) +
20
+ pad(levelBadge(a.level), 9) +
21
+ pad(statusColor(a.status), 14) +
22
+ pad(c.dim(a.age), 7) +
23
+ c.white(a.task);
24
+ console.log(row);
25
+ }
26
+ console.log("");
27
+ previewNotice("Listing agents");
28
+ }
@@ -0,0 +1,35 @@
1
+ import { c, symbols } from "../ui/colors.js";
2
+ import { Spinner, sleep } from "../ui/spinner.js";
3
+ import { loadConfig, saveConfig } from "../lib/config.js";
4
+ import { previewNotice } from "./_shared.js";
5
+ export async function loginCommand() {
6
+ const config = loadConfig();
7
+ if (config.loggedIn) {
8
+ console.log(`${symbols.check} Already signed in as ${c.indigo(config.account ?? "you@example.com")}.`);
9
+ return;
10
+ }
11
+ console.log("");
12
+ console.log(`${c.indigo(symbols.arrow)} Sign in to Kurtel — we'll open your browser to authorize.`);
13
+ const fakeUrl = "https://kurtel.ai/cli/auth?code=KRTL-PREVIEW";
14
+ console.log(` ${c.dim(fakeUrl)}`);
15
+ console.log("");
16
+ const spin = new Spinner("Waiting for browser authorization…").start();
17
+ await sleep(1200);
18
+ spin.info("Authorization step is a preview stub.");
19
+ config.loggedIn = true;
20
+ config.account = "you@example.com";
21
+ saveConfig(config);
22
+ console.log(`${symbols.check} Signed in as ${c.indigo(config.account)} ${c.dim("(local preview session)")}`);
23
+ previewNotice("Authentication");
24
+ }
25
+ export async function logoutCommand() {
26
+ const config = loadConfig();
27
+ if (!config.loggedIn) {
28
+ console.log(`${c.dim("You're not signed in.")}`);
29
+ return;
30
+ }
31
+ config.loggedIn = false;
32
+ delete config.account;
33
+ saveConfig(config);
34
+ console.log(`${symbols.check} Signed out.`);
35
+ }
@@ -0,0 +1,35 @@
1
+ import { c, symbols } from "../ui/colors.js";
2
+ import { loadConfig, setConfigValue, configPath, } from "../lib/config.js";
3
+ export function configCommand(action, key, value) {
4
+ if (!action || action === "list") {
5
+ const config = loadConfig();
6
+ console.log(`\n${c.dim("config")} ${c.dim(configPath())}\n`);
7
+ for (const [k, v] of Object.entries(config)) {
8
+ console.log(`${c.indigo(k.padEnd(14))} ${c.white(String(v))}`);
9
+ }
10
+ console.log("");
11
+ return;
12
+ }
13
+ if (action === "get") {
14
+ if (!key) {
15
+ console.log(`${c.red(symbols.cross)} Usage: ${c.indigo("kurtel config get <key>")}`);
16
+ process.exitCode = 1;
17
+ return;
18
+ }
19
+ const config = loadConfig();
20
+ console.log(String(config[key] ?? c.dim("(unset)")));
21
+ return;
22
+ }
23
+ if (action === "set") {
24
+ if (!key || value === undefined) {
25
+ console.log(`${c.red(symbols.cross)} Usage: ${c.indigo("kurtel config set <key> <value>")}`);
26
+ process.exitCode = 1;
27
+ return;
28
+ }
29
+ setConfigValue(key, value);
30
+ console.log(`${symbols.check} Set ${c.indigo(key)} = ${c.white(value)}`);
31
+ return;
32
+ }
33
+ console.log(`${c.red(symbols.cross)} Unknown action ${c.white(action)}. Try ${c.indigo("list")}, ${c.indigo("get")}, or ${c.indigo("set")}.`);
34
+ process.exitCode = 1;
35
+ }
@@ -0,0 +1,47 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { c, symbols } from "../ui/colors.js";
4
+ import { Spinner, sleep } from "../ui/spinner.js";
5
+ import { getVersion } from "../lib/version.js";
6
+ import { loadConfig } from "../lib/config.js";
7
+ export async function initCommand() {
8
+ const dir = join(process.cwd(), ".kurtel");
9
+ const file = join(dir, "config.json");
10
+ if (existsSync(file)) {
11
+ console.log(`${c.yellow(symbols.warn)} ${c.dim(".kurtel/config.json already exists.")}`);
12
+ return;
13
+ }
14
+ const spin = new Spinner("Initializing Kurtel in this project…").start();
15
+ await sleep(500);
16
+ if (!existsSync(dir))
17
+ mkdirSync(dir, { recursive: true });
18
+ const template = {
19
+ version: 1,
20
+ engine: "kurtel-sota",
21
+ sandbox: { isolated: true, ephemeral: true },
22
+ review: { openPullRequests: true },
23
+ learn: { shareTeamPatterns: true },
24
+ };
25
+ writeFileSync(file, JSON.stringify(template, null, 2) + "\n", "utf8");
26
+ spin.succeed("Created .kurtel/config.json");
27
+ console.log(`\n${c.dim("Next:")} ${c.indigo("kurtel login")} ${c.dim("then")} ${c.indigo('kurtel run "your first task"')}`);
28
+ }
29
+ export function doctorCommand() {
30
+ const config = loadConfig();
31
+ const checks = [
32
+ [true, `Kurtel CLI v${getVersion()}`],
33
+ [true, `Node.js ${process.version}`],
34
+ [
35
+ process.stdout.isTTY ?? false,
36
+ `Interactive TTY ${process.stdout.isTTY ? "" : "(not detected — interactive mode limited)"}`,
37
+ ],
38
+ [config.loggedIn, config.loggedIn ? "Signed in" : "Not signed in (run: kurtel login)"],
39
+ [false, "Cloud connectivity (preview — not yet enabled)"],
40
+ ];
41
+ console.log(`\n${c.indigoBold("Kurtel doctor")}\n`);
42
+ for (const [ok, label] of checks) {
43
+ const mark = ok ? symbols.check : c.yellow(symbols.warn);
44
+ console.log(` ${mark} ${ok ? c.white(label) : c.dim(label)}`);
45
+ }
46
+ console.log("");
47
+ }
@@ -0,0 +1,27 @@
1
+ import { c, symbols } from "../ui/colors.js";
2
+ import { Spinner, sleep } from "../ui/spinner.js";
3
+ import { previewNotice } from "./_shared.js";
4
+ export async function runCommand(task, opts) {
5
+ if (!task || task.trim() === "") {
6
+ console.log(`${c.red(symbols.cross)} Provide a task, e.g. ${c.indigo('kurtel run "fix flaky auth test"')}`);
7
+ process.exitCode = 1;
8
+ return;
9
+ }
10
+ const id = "agent-" + Math.random().toString(16).slice(2, 6);
11
+ const repo = opts.repo ?? "(current repo)";
12
+ console.log("");
13
+ console.log(`${c.gray("task")} ${c.white(task)}`);
14
+ console.log(`${c.gray("repo")} ${c.white(repo)}`);
15
+ console.log(`${c.gray("branch")} ${c.white(opts.branch ?? "main")}`);
16
+ console.log("");
17
+ const spin = new Spinner("Provisioning isolated sandbox…").start();
18
+ await sleep(700);
19
+ spin.update("Booting agent (codex + claude-code)…");
20
+ await sleep(700);
21
+ spin.update("Cloning repository into sandbox…");
22
+ await sleep(600);
23
+ spin.succeed(`Launched ${c.indigo(id)} ${c.dim(`(${opts.detach ? "detached" : "attached"})`)}`);
24
+ console.log("");
25
+ previewNotice("Launching agents");
26
+ console.log(`${c.dim("Once connected, follow it with")} ${c.indigo(`kurtel logs ${id}`)}${c.dim(".")}`);
27
+ }
@@ -0,0 +1,50 @@
1
+ import { c } from "../ui/colors.js";
2
+ import { Spinner, sleep } from "../ui/spinner.js";
3
+ import { SAMPLE_AGENTS, statusColor, levelBadge } from "../lib/agents.js";
4
+ import { previewNotice } from "./_shared.js";
5
+ function findAgent(id) {
6
+ return SAMPLE_AGENTS.find((a) => a.id === id) ?? SAMPLE_AGENTS[0];
7
+ }
8
+ const FAKE_LOG_LINES = [
9
+ ["plan", "Reading repository structure and conventions"],
10
+ ["plan", "Drafting change set across 4 files"],
11
+ ["edit", "src/webhooks/handler.ts — add idempotency guard"],
12
+ ["test", "Running test suite in sandbox"],
13
+ ["learn", "Captured pattern: prefer repository-scoped idempotency keys"],
14
+ ["done", "Opened pull request #482"],
15
+ ];
16
+ export async function logsCommand(id) {
17
+ const agent = findAgent(id);
18
+ console.log(`\n${c.dim("Streaming logs for")} ${c.indigo(agent.id)} ${c.dim(`· ${agent.repo}`)}\n`);
19
+ for (const [tag, msg] of FAKE_LOG_LINES) {
20
+ const ts = c.dim(new Date().toLocaleTimeString());
21
+ const label = tag === "learn"
22
+ ? c.cyan(`[${tag}]`)
23
+ : tag === "done"
24
+ ? c.green(`[${tag}]`)
25
+ : c.indigo(`[${tag}]`);
26
+ console.log(`${ts} ${label} ${c.white(msg)}`);
27
+ await sleep(450);
28
+ }
29
+ console.log("");
30
+ previewNotice("Live logs");
31
+ }
32
+ export function statusCommand(id) {
33
+ const a = findAgent(id);
34
+ console.log("");
35
+ console.log(`${c.gray("id")} ${c.indigo(a.id)}`);
36
+ console.log(`${c.gray("task")} ${c.white(a.task)}`);
37
+ console.log(`${c.gray("repo")} ${c.white(a.repo)}`);
38
+ console.log(`${c.gray("level")} ${levelBadge(a.level)}`);
39
+ console.log(`${c.gray("status")} ${statusColor(a.status)}`);
40
+ console.log(`${c.gray("progress")} ${c.white(a.progress + "%")}`);
41
+ console.log("");
42
+ previewNotice("Agent status");
43
+ }
44
+ export async function stopCommand(id) {
45
+ const a = findAgent(id);
46
+ const spin = new Spinner(`Stopping ${c.indigo(a.id)} and tearing down sandbox…`).start();
47
+ await sleep(800);
48
+ spin.succeed(`Stopped ${c.indigo(a.id)} ${c.dim("· sandbox destroyed")}`);
49
+ previewNotice("Stopping agents");
50
+ }
package/dist/index.js ADDED
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { getVersion } from "./lib/version.js";
4
+ import { loadConfig } from "./lib/config.js";
5
+ import { c, symbols } from "./ui/colors.js";
6
+ import { startSession } from "./session/repl.js";
7
+ import { runCommand } from "./commands/run.js";
8
+ import { loginCommand, logoutCommand } from "./commands/auth.js";
9
+ import { agentsCommand } from "./commands/agents.js";
10
+ import { logsCommand, statusCommand, stopCommand } from "./commands/runtime.js";
11
+ import { configCommand } from "./commands/config.js";
12
+ import { initCommand, doctorCommand } from "./commands/project.js";
13
+ const program = new Command();
14
+ program
15
+ .name("kurtel")
16
+ .description("Launch self-improving coding agents in the cloud.")
17
+ .version(getVersion(), "-v, --version", "Print the CLI version")
18
+ .helpOption("-h, --help", "Show help")
19
+ .addHelpText("afterAll", `\n${c.dim("Run")} ${c.indigo("kurtel")} ${c.dim("with no arguments to open the interactive session.")}\n`);
20
+ // Default action: interactive session
21
+ program.action(async () => {
22
+ const config = loadConfig();
23
+ await startSession(String(config.engine));
24
+ });
25
+ program
26
+ .command("run")
27
+ .description("Launch a cloud agent on a task")
28
+ .argument("[task...]", "What you want the agent to do")
29
+ .option("-r, --repo <repo>", "Target repository")
30
+ .option("-b, --branch <branch>", "Base branch", "main")
31
+ .option("-d, --detach", "Launch without attaching to logs", false)
32
+ .action(async (taskParts, opts) => {
33
+ await runCommand((taskParts ?? []).join(" "), opts);
34
+ });
35
+ program
36
+ .command("agents")
37
+ .alias("ps")
38
+ .description("List active agents")
39
+ .action(() => agentsCommand());
40
+ program
41
+ .command("logs")
42
+ .description("Stream an agent's logs")
43
+ .argument("<id>", "Agent id (e.g. agent-7f3a)")
44
+ .action(async (id) => logsCommand(id));
45
+ program
46
+ .command("status")
47
+ .description("Show an agent's status")
48
+ .argument("<id>", "Agent id")
49
+ .action((id) => statusCommand(id));
50
+ program
51
+ .command("stop")
52
+ .description("Stop an agent and tear down its sandbox")
53
+ .argument("<id>", "Agent id")
54
+ .action(async (id) => stopCommand(id));
55
+ program
56
+ .command("login")
57
+ .description("Sign in to Kurtel")
58
+ .action(async () => loginCommand());
59
+ program
60
+ .command("logout")
61
+ .description("Sign out")
62
+ .action(async () => logoutCommand());
63
+ program
64
+ .command("config")
65
+ .description("View or change local configuration")
66
+ .argument("[action]", "list | get | set", "list")
67
+ .argument("[key]", "Config key")
68
+ .argument("[value]", "Value (for set)")
69
+ .action((action, key, value) => configCommand(action, key, value));
70
+ program
71
+ .command("init")
72
+ .description("Initialize Kurtel in the current project")
73
+ .action(async () => initCommand());
74
+ program
75
+ .command("doctor")
76
+ .description("Check your environment")
77
+ .action(() => doctorCommand());
78
+ async function main() {
79
+ try {
80
+ await program.parseAsync(process.argv);
81
+ }
82
+ catch (err) {
83
+ console.error(`${c.red(symbols.cross)} ${err instanceof Error ? err.message : String(err)}`);
84
+ process.exit(1);
85
+ }
86
+ }
87
+ main();
@@ -0,0 +1,54 @@
1
+ // Stubbed agent data. In the real product this comes from the Kurtel cloud API.
2
+ // For now it gives the commands something realistic to render.
3
+ import { c } from "../ui/colors.js";
4
+ export const SAMPLE_AGENTS = [
5
+ {
6
+ id: "agent-7f3a",
7
+ task: "Add idempotency keys to billing webhooks",
8
+ repo: "billing-service",
9
+ level: "senior",
10
+ status: "running",
11
+ progress: 82,
12
+ age: "3m",
13
+ },
14
+ {
15
+ id: "agent-2c9d",
16
+ task: "Migrate dashboard to server components",
17
+ repo: "web-dashboard",
18
+ level: "mid",
19
+ status: "learning",
20
+ progress: 46,
21
+ age: "11m",
22
+ },
23
+ {
24
+ id: "agent-b41e",
25
+ task: "Tighten Terraform IAM scopes",
26
+ repo: "infra-terraform",
27
+ level: "junior",
28
+ status: "queued",
29
+ progress: 0,
30
+ age: "30s",
31
+ },
32
+ ];
33
+ export function statusColor(status) {
34
+ switch (status) {
35
+ case "running":
36
+ return c.indigo("● running");
37
+ case "learning":
38
+ return c.cyan("● learning");
39
+ case "queued":
40
+ return c.gray("○ queued");
41
+ case "done":
42
+ return c.green("✔ done");
43
+ case "failed":
44
+ return c.red("✖ failed");
45
+ }
46
+ }
47
+ export function levelBadge(level) {
48
+ const label = level.toUpperCase();
49
+ if (level === "senior")
50
+ return c.indigo(label);
51
+ if (level === "mid")
52
+ return c.ash(label);
53
+ return c.gray(label);
54
+ }
@@ -0,0 +1,35 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
4
+ const DIR = join(homedir(), ".kurtel");
5
+ const FILE = join(DIR, "config.json");
6
+ const DEFAULTS = {
7
+ engine: "kurtel-sota (codex + claude-code)",
8
+ defaultBranch: "main",
9
+ loggedIn: false,
10
+ };
11
+ export function configPath() {
12
+ return FILE;
13
+ }
14
+ export function loadConfig() {
15
+ try {
16
+ if (!existsSync(FILE))
17
+ return { ...DEFAULTS };
18
+ const data = JSON.parse(readFileSync(FILE, "utf8"));
19
+ return { ...DEFAULTS, ...data };
20
+ }
21
+ catch {
22
+ return { ...DEFAULTS };
23
+ }
24
+ }
25
+ export function saveConfig(config) {
26
+ if (!existsSync(DIR))
27
+ mkdirSync(DIR, { recursive: true });
28
+ writeFileSync(FILE, JSON.stringify(config, null, 2) + "\n", "utf8");
29
+ }
30
+ export function setConfigValue(key, value) {
31
+ const config = loadConfig();
32
+ config[key] = value;
33
+ saveConfig(config);
34
+ return config;
35
+ }
@@ -0,0 +1,19 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ import { dirname, join } from "node:path";
4
+ let cached = null;
5
+ export function getVersion() {
6
+ if (cached)
7
+ return cached;
8
+ try {
9
+ const here = dirname(fileURLToPath(import.meta.url));
10
+ // dist/lib/version.js -> ../../package.json
11
+ const pkgPath = join(here, "..", "..", "package.json");
12
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
13
+ cached = pkg.version ?? "0.0.0";
14
+ }
15
+ catch {
16
+ cached = "0.0.0";
17
+ }
18
+ return cached;
19
+ }
@@ -0,0 +1,127 @@
1
+ import * as readline from "node:readline";
2
+ import { c, symbols } from "../ui/colors.js";
3
+ import { banner, welcomeBox } from "../ui/banner.js";
4
+ import { Spinner, sleep } from "../ui/spinner.js";
5
+ import { loadConfig } from "../lib/config.js";
6
+ import { agentsCommand } from "../commands/agents.js";
7
+ import { loginCommand } from "../commands/auth.js";
8
+ import { configCommand } from "../commands/config.js";
9
+ import { previewNotice } from "../commands/_shared.js";
10
+ const SLASH_COMMANDS = [
11
+ ["/help", "Show this help"],
12
+ ["/run <task>", "Launch a cloud agent on a task"],
13
+ ["/agents", "List active agents"],
14
+ ["/login", "Sign in to Kurtel"],
15
+ ["/config", "Show local configuration"],
16
+ ["/clear", "Clear the screen"],
17
+ ["/exit", "Quit the session"],
18
+ ];
19
+ function prompt() {
20
+ return `${c.indigo("kurtel")} ${c.indigo(symbols.arrow)} `;
21
+ }
22
+ function printHelp() {
23
+ console.log("");
24
+ console.log(c.indigoBold("Commands"));
25
+ for (const [cmd, desc] of SLASH_COMMANDS) {
26
+ console.log(` ${c.indigo(cmd.padEnd(16))} ${c.dim(desc)}`);
27
+ }
28
+ console.log("");
29
+ console.log(c.dim("Anything else you type is treated as a task and launches an agent."));
30
+ console.log("");
31
+ }
32
+ async function launchFromPrompt(task) {
33
+ const id = "agent-" + Math.random().toString(16).slice(2, 6);
34
+ const spin = new Spinner("Provisioning sandbox & booting agent…").start();
35
+ await sleep(700);
36
+ spin.update("Planning approach…");
37
+ await sleep(700);
38
+ spin.succeed(`Queued ${c.indigo(id)} for: ${c.white(task)}`);
39
+ previewNotice("Launching agents");
40
+ }
41
+ export async function startSession(model) {
42
+ console.log(banner());
43
+ console.log(welcomeBox(process.cwd(), model));
44
+ console.log("");
45
+ const rl = readline.createInterface({
46
+ input: process.stdin,
47
+ output: process.stdout,
48
+ prompt: prompt(),
49
+ completer: (line) => {
50
+ const hits = SLASH_COMMANDS.map(([cmd]) => cmd.split(" ")[0]).filter((cmd) => cmd.startsWith(line));
51
+ return [hits.length ? hits : [], line];
52
+ },
53
+ });
54
+ rl.prompt();
55
+ rl.on("line", async (input) => {
56
+ const line = input.trim();
57
+ if (line === "") {
58
+ rl.prompt();
59
+ return;
60
+ }
61
+ if (line.startsWith("/")) {
62
+ const [cmd, ...rest] = line.slice(1).split(" ");
63
+ const arg = rest.join(" ").trim();
64
+ switch (cmd) {
65
+ case "help":
66
+ case "?":
67
+ printHelp();
68
+ break;
69
+ case "exit":
70
+ case "quit":
71
+ case "q":
72
+ rl.close();
73
+ return;
74
+ case "clear":
75
+ console.clear();
76
+ console.log(banner());
77
+ break;
78
+ case "agents":
79
+ case "ps":
80
+ rl.pause();
81
+ agentsCommand();
82
+ rl.resume();
83
+ break;
84
+ case "login":
85
+ rl.pause();
86
+ await loginCommand();
87
+ rl.resume();
88
+ break;
89
+ case "config":
90
+ rl.pause();
91
+ configCommand("list");
92
+ rl.resume();
93
+ break;
94
+ case "run":
95
+ rl.pause();
96
+ if (!arg) {
97
+ console.log(`${c.red(symbols.cross)} Usage: ${c.indigo("/run <task>")}`);
98
+ }
99
+ else {
100
+ await launchFromPrompt(arg);
101
+ }
102
+ rl.resume();
103
+ break;
104
+ default:
105
+ console.log(`${c.red(symbols.cross)} Unknown command ${c.white("/" + cmd)}. Type ${c.indigo("/help")}.`);
106
+ }
107
+ rl.prompt();
108
+ return;
109
+ }
110
+ // Free text -> treat as a task
111
+ rl.pause();
112
+ await launchFromPrompt(line);
113
+ rl.resume();
114
+ rl.prompt();
115
+ });
116
+ rl.on("close", () => {
117
+ console.log(`\n${c.dim("See you soon. ")}${c.indigo("›")}\n`);
118
+ process.exit(0);
119
+ });
120
+ // Ctrl+C: confirm-style behavior — first clears line, prompt again.
121
+ rl.on("SIGINT", () => {
122
+ console.log(`\n${c.dim("(use /exit or Ctrl+D to quit)")}`);
123
+ rl.prompt();
124
+ });
125
+ // Touch config so a fresh user has it.
126
+ loadConfig();
127
+ }
@@ -0,0 +1,28 @@
1
+ import { c, symbols } from "./colors.js";
2
+ import { box } from "./box.js";
3
+ import { getVersion } from "../lib/version.js";
4
+ const wordmark = [
5
+ " _ __ _ _ ",
6
+ " | |/ / _ _ _ | |_ ___| |",
7
+ " | ' < | '_| || | _/ -_) |",
8
+ " |_|\\_\\ |_| \\_,_|\\__\\___|_|",
9
+ ];
10
+ export function banner() {
11
+ const art = wordmark.map((l) => c.indigoBold(l)).join("\n");
12
+ const tag = c.gray(" self-improving coding agents in the cloud");
13
+ const ver = c.dim(` v${getVersion()}`);
14
+ return `\n${art}\n${tag}${ver}\n`;
15
+ }
16
+ export function welcomeBox(cwd, model) {
17
+ const lines = [
18
+ `${c.indigo(symbols.arrow)} ${c.bold("Welcome to Kurtel")} ${c.dim("(preview)")}`,
19
+ "",
20
+ `${c.gray("cwd")} ${c.white(cwd)}`,
21
+ `${c.gray("engine")} ${c.white(model)}`,
22
+ `${c.gray("status")} ${c.yellow("● not connected")} ${c.dim("— commands are stubs for now")}`,
23
+ "",
24
+ `${c.dim("Type a task to launch an agent, or")} ${c.indigo("/help")} ${c.dim("for commands.")}`,
25
+ `${c.dim("Exit with")} ${c.indigo("/exit")} ${c.dim("or Ctrl+D.")}`,
26
+ ];
27
+ return box(lines, { borderColor: c.indigo, padding: 1 });
28
+ }
package/dist/ui/box.js ADDED
@@ -0,0 +1,23 @@
1
+ import { c } from "./colors.js";
2
+ // Strip ANSI escapes to measure visible width.
3
+ function visibleLength(s) {
4
+ // eslint-disable-next-line no-control-regex
5
+ return s.replace(/\x1b\[[0-9;]*m/g, "").length;
6
+ }
7
+ export function box(lines, opts = {}) {
8
+ const padding = opts.padding ?? 1;
9
+ const color = opts.borderColor ?? c.indigo;
10
+ const contentWidth = Math.max(...lines.map(visibleLength), opts.title ? visibleLength(opts.title) + 2 : 0);
11
+ const innerWidth = contentWidth + padding * 2;
12
+ const horizontal = "─".repeat(innerWidth);
13
+ const top = opts.title
14
+ ? color("╭─ ") + c.bold(opts.title) + color(" " + "─".repeat(Math.max(0, innerWidth - visibleLength(opts.title) - 3)) + "╮")
15
+ : color("╭" + horizontal + "╮");
16
+ const bottom = color("╰" + horizontal + "╯");
17
+ const pad = " ".repeat(padding);
18
+ const body = lines.map((line) => {
19
+ const fill = " ".repeat(Math.max(0, contentWidth - visibleLength(line)));
20
+ return color("│") + pad + line + fill + pad + color("│");
21
+ });
22
+ return [top, ...body, bottom].join("\n");
23
+ }
@@ -0,0 +1,41 @@
1
+ // Lightweight ANSI styling — no dependencies.
2
+ // Honors NO_COLOR and non-TTY output.
3
+ const enabled = process.env.NO_COLOR === undefined &&
4
+ process.env.TERM !== "dumb" &&
5
+ (process.stdout.isTTY ?? false);
6
+ function wrap(open, close = "\x1b[0m") {
7
+ return (s) => (enabled ? `${open}${s}${close}` : String(s));
8
+ }
9
+ // Brand indigo (#6798ff) via 24-bit truecolor, with graceful no-op fallback.
10
+ const indigoOpen = "\x1b[38;2;103;152;255m";
11
+ export const c = {
12
+ enabled,
13
+ reset: "\x1b[0m",
14
+ bold: wrap("\x1b[1m"),
15
+ dim: wrap("\x1b[2m"),
16
+ italic: wrap("\x1b[3m"),
17
+ underline: wrap("\x1b[4m"),
18
+ // brand
19
+ indigo: wrap(indigoOpen),
20
+ indigoBold: wrap(`\x1b[1m${indigoOpen}`),
21
+ // neutrals / semantic
22
+ white: wrap("\x1b[97m"),
23
+ gray: wrap("\x1b[90m"),
24
+ ash: wrap("\x1b[37m"),
25
+ green: wrap("\x1b[32m"),
26
+ yellow: wrap("\x1b[33m"),
27
+ red: wrap("\x1b[31m"),
28
+ cyan: wrap("\x1b[36m"),
29
+ // background
30
+ bgIndigo: wrap("\x1b[48;2;103;152;255m\x1b[30m"),
31
+ };
32
+ export const symbols = {
33
+ dot: "·",
34
+ arrow: "›",
35
+ check: c.green("✔"),
36
+ cross: c.red("✖"),
37
+ warn: c.yellow("!"),
38
+ info: c.indigo("●"),
39
+ bullet: c.gray("•"),
40
+ spinnerFrames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
41
+ };
@@ -0,0 +1,54 @@
1
+ import { c, symbols } from "./colors.js";
2
+ export class Spinner {
3
+ timer = null;
4
+ frame = 0;
5
+ text;
6
+ active = false;
7
+ constructor(text) {
8
+ this.text = text;
9
+ }
10
+ start() {
11
+ if (!process.stdout.isTTY) {
12
+ process.stdout.write(`${c.indigo(symbols.info)} ${this.text}\n`);
13
+ return this;
14
+ }
15
+ this.active = true;
16
+ process.stdout.write("\x1b[?25l"); // hide cursor
17
+ this.timer = setInterval(() => {
18
+ const f = symbols.spinnerFrames[this.frame % symbols.spinnerFrames.length];
19
+ process.stdout.write(`\r${c.indigo(f)} ${this.text}`);
20
+ this.frame++;
21
+ }, 80);
22
+ return this;
23
+ }
24
+ update(text) {
25
+ this.text = text;
26
+ }
27
+ clearLine() {
28
+ if (this.active)
29
+ process.stdout.write("\r\x1b[K");
30
+ }
31
+ stop() {
32
+ if (this.timer)
33
+ clearInterval(this.timer);
34
+ this.clearLine();
35
+ if (this.active)
36
+ process.stdout.write("\x1b[?25h"); // show cursor
37
+ this.active = false;
38
+ }
39
+ succeed(text) {
40
+ this.stop();
41
+ process.stdout.write(`${symbols.check} ${text ?? this.text}\n`);
42
+ }
43
+ fail(text) {
44
+ this.stop();
45
+ process.stdout.write(`${symbols.cross} ${text ?? this.text}\n`);
46
+ }
47
+ info(text) {
48
+ this.stop();
49
+ process.stdout.write(`${symbols.info} ${text ?? this.text}\n`);
50
+ }
51
+ }
52
+ export function sleep(ms) {
53
+ return new Promise((r) => setTimeout(r, ms));
54
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@kurtel/cli",
3
+ "version": "0.1.0",
4
+ "description": "Launch self-improving coding agents in the cloud — the Kurtel CLI.",
5
+ "type": "module",
6
+ "bin": {
7
+ "kurtel": "dist/index.js"
8
+ },
9
+ "publishConfig": { "access": "public" },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "dev": "tsx src/index.ts",
19
+ "start": "node dist/index.js",
20
+ "prepare": "npm run build"
21
+ },
22
+ "keywords": [
23
+ "kurtel",
24
+ "ai",
25
+ "coding-agent",
26
+ "cli",
27
+ "cloud",
28
+ "agents"
29
+ ],
30
+ "license": "UNLICENSED",
31
+ "dependencies": {
32
+ "commander": "^12.1.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^22.7.0",
36
+ "tsx": "^4.19.1",
37
+ "typescript": "^5.6.2"
38
+ }
39
+ }