ideacode 1.0.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.
@@ -0,0 +1,27 @@
1
+ import { spawn } from "node:child_process";
2
+ export function runBash(args) {
3
+ return new Promise((resolve) => {
4
+ const cmd = args.cmd ?? "";
5
+ const proc = spawn("/bin/sh", ["-c", cmd], { stdio: ["ignore", "pipe", "pipe"] });
6
+ const outputChunks = [];
7
+ let resolved = false;
8
+ const done = (out) => {
9
+ if (resolved)
10
+ return;
11
+ resolved = true;
12
+ resolve(out);
13
+ };
14
+ const onData = (s) => outputChunks.push(s);
15
+ proc.stdout?.on("data", (chunk) => onData(chunk.toString()));
16
+ proc.stderr?.on("data", (chunk) => onData(chunk.toString()));
17
+ const t = setTimeout(() => {
18
+ proc.kill("SIGKILL");
19
+ outputChunks.push("\n(timed out after 30s)");
20
+ done(outputChunks.join("").trim() || "(empty)");
21
+ }, 30_000);
22
+ proc.on("close", () => {
23
+ clearTimeout(t);
24
+ done(outputChunks.join("").trim() || "(empty)");
25
+ });
26
+ });
27
+ }
@@ -0,0 +1,43 @@
1
+ import * as fs from "node:fs";
2
+ const DEFAULT_READ_LIMIT = 500;
3
+ export function readFile(args) {
4
+ const content = fs.readFileSync(args.path, "utf-8");
5
+ const parts = content.split("\n");
6
+ const lines = content.endsWith("\n") ? parts.slice(0, -1) : parts;
7
+ if (lines.length === 0 && content === "")
8
+ return "";
9
+ const offset = args.offset ?? 0;
10
+ const requestedLimit = args.limit ?? lines.length;
11
+ const limit = Math.min(requestedLimit, DEFAULT_READ_LIMIT);
12
+ const selected = lines.slice(offset, offset + limit);
13
+ const total = lines.length;
14
+ const truncated = total > offset + selected.length;
15
+ const suffix = truncated
16
+ ? `\n\n... (${total - offset - selected.length} more line(s). Use offset/limit to read in chunks.)`
17
+ : "";
18
+ return (selected.map((line, idx) => `${String(offset + idx + 1).padStart(4)}| ${line}`).join("\n") +
19
+ suffix);
20
+ }
21
+ export function writeFile(args) {
22
+ fs.writeFileSync(args.path, args.content, "utf-8");
23
+ return "ok";
24
+ }
25
+ export function editFile(args) {
26
+ const text = fs.readFileSync(args.path, "utf-8");
27
+ const oldStr = args.old;
28
+ const newStr = args.new;
29
+ if (!text.includes(oldStr))
30
+ return "error: old_string not found";
31
+ let count = 0;
32
+ let pos = 0;
33
+ while ((pos = text.indexOf(oldStr, pos)) !== -1) {
34
+ count++;
35
+ pos += oldStr.length;
36
+ }
37
+ if (!args.all && count > 1) {
38
+ return `error: old_string appears ${count} times, must be unique (use all=true)`;
39
+ }
40
+ const replacement = args.all ? text.split(oldStr).join(newStr) : text.replace(oldStr, newStr);
41
+ fs.writeFileSync(args.path, replacement, "utf-8");
42
+ return "ok";
43
+ }
@@ -0,0 +1,80 @@
1
+ import { getBraveSearchApiKey } from "../config.js";
2
+ import { readFile, writeFile, editFile } from "./file.js";
3
+ import { globFiles, grepFiles } from "./search.js";
4
+ import { runBash } from "./bash.js";
5
+ import { webFetch, webSearch } from "./web.js";
6
+ export const TOOLS = {
7
+ read: [
8
+ "Read file with line numbers (file path, not directory). Use limit to read a portion; avoid reading huge files in one go.",
9
+ { path: "string", offset: "number?", limit: "number?" },
10
+ readFile,
11
+ ],
12
+ write: ["Write content to file", { path: "string", content: "string" }, writeFile],
13
+ edit: [
14
+ "Replace old with new in file (old must be unique unless all=true)",
15
+ { path: "string", old: "string", new: "string", all: "boolean?" },
16
+ editFile,
17
+ ],
18
+ glob: [
19
+ "Find files by pattern, sorted by mtime. With path '.' (default), .gitignore entries (e.g. node_modules, dist) are excluded; use path node_modules/<pkg> to search inside a single package.",
20
+ { pat: "string", path: "string?" },
21
+ globFiles,
22
+ ],
23
+ grep: [
24
+ "Search files for regex. With path '.' (default), .gitignore entries are excluded; use path node_modules/<pkg> to search one package. Prefer specific patterns and narrow path. Returns at most limit matches (default 50, max 100).",
25
+ { pat: "string", path: "string?", limit: "number?" },
26
+ grepFiles,
27
+ ],
28
+ bash: ["Run shell command", { cmd: "string" }, runBash],
29
+ web_fetch: [
30
+ "Fetch a URL and return the main text content (handles JS-rendered pages). Use for docs, raw GitHub, any web page.",
31
+ { url: "string" },
32
+ webFetch,
33
+ ],
34
+ web_search: [
35
+ "Search the web and return ranked results (title, url, snippet). Use for current info, docs, GitHub repos.",
36
+ { query: "string" },
37
+ webSearch,
38
+ ],
39
+ };
40
+ function getTools() {
41
+ const braveKey = getBraveSearchApiKey();
42
+ if (braveKey)
43
+ return TOOLS;
44
+ const { web_search: _, ...rest } = TOOLS;
45
+ return rest;
46
+ }
47
+ export async function runTool(name, args) {
48
+ if (name === "web_search" && !getBraveSearchApiKey()) {
49
+ return "error: Brave Search API key not set. Use /brave or set BRAVE_API_KEY to enable web search.";
50
+ }
51
+ const tools = getTools();
52
+ const def = tools[name];
53
+ if (!def)
54
+ return `error: Unknown tool: ${name}`;
55
+ try {
56
+ const result = await def[2](args);
57
+ return result;
58
+ }
59
+ catch (err) {
60
+ return `error: ${err instanceof Error ? err.message : err}`;
61
+ }
62
+ }
63
+ export function makeSchema() {
64
+ return Object.entries(getTools()).map(([name, [description, params]]) => {
65
+ const properties = {};
66
+ const required = [];
67
+ for (const [paramName, paramType] of Object.entries(params)) {
68
+ const isOptional = paramType.endsWith("?");
69
+ const baseType = paramType.replace(/\?$/, "");
70
+ properties[paramName] = { type: baseType === "number" ? "integer" : baseType };
71
+ if (!isOptional)
72
+ required.push(paramName);
73
+ }
74
+ return {
75
+ name,
76
+ description,
77
+ input_schema: { type: "object", properties, required },
78
+ };
79
+ });
80
+ }
@@ -0,0 +1,86 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { globSync } from "glob";
4
+ function toGlobIgnore(line) {
5
+ const s = line.replace(/^\//, "").replace(/\/$/, "");
6
+ if (!s)
7
+ return "";
8
+ if (s.includes("*") || s.startsWith("!"))
9
+ return s;
10
+ if (s.includes("/") || s.endsWith(".env") || /\.\w+$/.test(s))
11
+ return `**/${s}`;
12
+ return `**/${s}/**`;
13
+ }
14
+ function getIgnorePatterns() {
15
+ const gitignorePath = path.join(process.cwd(), ".gitignore");
16
+ try {
17
+ const content = fs.readFileSync(gitignorePath, "utf-8");
18
+ const patterns = content
19
+ .split(/\n/)
20
+ .map((line) => line.trim())
21
+ .filter((line) => line !== "" && !line.startsWith("#"))
22
+ .map(toGlobIgnore)
23
+ .filter(Boolean);
24
+ return patterns.length > 0 ? patterns : ["**/node_modules/**", "**/dist/**"];
25
+ }
26
+ catch {
27
+ return ["**/node_modules/**", "**/dist/**"];
28
+ }
29
+ }
30
+ export function globFiles(args) {
31
+ const base = args.path ?? ".";
32
+ const pat = args.pat;
33
+ const pattern = path.join(base, pat).replace(/\/\/+/g, "/");
34
+ const isProjectRoot = base === "." || base === "";
35
+ const opts = { nodir: true };
36
+ if (isProjectRoot)
37
+ opts.ignore = getIgnorePatterns();
38
+ const files = globSync(pattern, opts);
39
+ const withMtime = files.map((f) => ({ f, m: fs.statSync(f).mtimeMs })).sort((a, b) => b.m - a.m);
40
+ return withMtime.map(({ f }) => f).join("\n") || "none";
41
+ }
42
+ const DEFAULT_GREP_LIMIT = 50;
43
+ const MAX_GREP_LIMIT = 100;
44
+ const MAX_GREP_CHARS = 16_000;
45
+ export function grepFiles(args) {
46
+ const base = args.path ?? ".";
47
+ const pat = new RegExp(args.pat);
48
+ const limit = Math.min(MAX_GREP_LIMIT, Math.max(1, args.limit ?? DEFAULT_GREP_LIMIT));
49
+ const isProjectRoot = base === "." || base === "";
50
+ const grepOpts = { nodir: true };
51
+ if (isProjectRoot)
52
+ grepOpts.ignore = getIgnorePatterns();
53
+ const allFiles = globSync(path.join(base, "**/*").replace(/\/\/+/g, "/"), grepOpts);
54
+ const hits = [];
55
+ for (const filepath of allFiles) {
56
+ try {
57
+ const content = fs.readFileSync(filepath, "utf-8");
58
+ const lines = content.split(/\n/);
59
+ for (let i = 0; i < lines.length; i++) {
60
+ if (pat.test(lines[i]))
61
+ hits.push(`${filepath}:${i + 1}:${lines[i].replace(/\r$/, "")}`);
62
+ }
63
+ }
64
+ catch {
65
+ /* skip */
66
+ }
67
+ if (hits.length >= limit)
68
+ break;
69
+ }
70
+ const sliced = hits.slice(0, limit);
71
+ let out = sliced.join("\n") || "none";
72
+ if (out.length > MAX_GREP_CHARS) {
73
+ let acc = 0;
74
+ let i = 0;
75
+ for (; i < sliced.length; i++) {
76
+ const line = sliced[i];
77
+ if (acc + line.length + 1 > MAX_GREP_CHARS)
78
+ break;
79
+ acc += line.length + 1;
80
+ }
81
+ out = sliced.slice(0, i).join("\n");
82
+ const dropped = sliced.length - i;
83
+ out += `\n\n... (truncated: ${dropped} more matches, total ${hits.length} hit(s). Use a more specific pattern or path to reduce output.)`;
84
+ }
85
+ return out;
86
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Test module for web_search (Brave Search API only).
3
+ * Requires BRAVE_API_KEY or BRAVE_SEARCH_API_KEY in env or .env (free tier: https://brave.com/search/api).
4
+ * Run: npm run test:web-search
5
+ */
6
+ import "dotenv/config";
7
+ import { getBraveSearchApiKey } from "../config.js";
8
+ import { webSearch } from "./web.js";
9
+ const TESTS = [
10
+ {
11
+ name: "single search returns results",
12
+ query: "nodejs typescript",
13
+ expectError: false,
14
+ expectNonEmpty: true,
15
+ },
16
+ {
17
+ name: "empty query returns error",
18
+ query: "",
19
+ expectError: true,
20
+ expectNonEmpty: false,
21
+ },
22
+ {
23
+ name: "whitespace-only query returns error",
24
+ query: " ",
25
+ expectError: true,
26
+ expectNonEmpty: false,
27
+ },
28
+ {
29
+ name: "specific query returns results",
30
+ query: "ideacode cli openrouter",
31
+ expectError: false,
32
+ expectNonEmpty: true,
33
+ },
34
+ ];
35
+ async function runOne(name, query, expectError, expectNonEmpty) {
36
+ const out = await webSearch({ query });
37
+ const isError = out.startsWith("error:");
38
+ const isEmpty = !out.trim() || out === "No results found.";
39
+ if (expectError && !isError) {
40
+ return { ok: false, message: `expected error, got: ${out.slice(0, 80)}...` };
41
+ }
42
+ if (!expectError && isError) {
43
+ return { ok: false, message: `expected success, got: ${out.slice(0, 120)}` };
44
+ }
45
+ if (!expectError && expectNonEmpty && isEmpty) {
46
+ return { ok: false, message: `expected non-empty results, got: ${out.slice(0, 80)}` };
47
+ }
48
+ return { ok: true, message: isError ? out.slice(0, 60) : `got ${out.split("\n").length} lines` };
49
+ }
50
+ async function runRapidTwo() {
51
+ const q1 = await webSearch({ query: "ink react cli" });
52
+ const q2 = await webSearch({ query: "typescript strict" });
53
+ const e1 = q1.startsWith("error:");
54
+ const e2 = q2.startsWith("error:");
55
+ if (e1 && e2) {
56
+ return { ok: false, message: `both rapid searches failed: ${q1.slice(0, 50)}... / ${q2.slice(0, 50)}...` };
57
+ }
58
+ if (e1)
59
+ return { ok: false, message: `first search failed: ${q1.slice(0, 80)}` };
60
+ if (e2)
61
+ return { ok: false, message: `second search failed: ${q2.slice(0, 80)}` };
62
+ return { ok: true, message: "both rapid searches succeeded" };
63
+ }
64
+ async function main() {
65
+ if (!getBraveSearchApiKey()) {
66
+ console.log("web_search tests require BRAVE_API_KEY or BRAVE_SEARCH_API_KEY (free tier: https://brave.com/search/api).");
67
+ process.exit(1);
68
+ }
69
+ console.log("web_search tests (Brave Search API)\n");
70
+ let failed = 0;
71
+ for (const t of TESTS) {
72
+ process.stdout.write(` ${t.name} ... `);
73
+ try {
74
+ const result = await runOne(t.name, t.query, t.expectError, t.expectNonEmpty);
75
+ if (result.ok) {
76
+ console.log("ok");
77
+ }
78
+ else {
79
+ console.log("FAIL");
80
+ console.log(` ${result.message}`);
81
+ failed++;
82
+ }
83
+ }
84
+ catch (err) {
85
+ console.log("FAIL");
86
+ console.log(` ${err instanceof Error ? err.message : String(err)}`);
87
+ failed++;
88
+ }
89
+ }
90
+ process.stdout.write(" two rapid searches ... ");
91
+ try {
92
+ const result = await runRapidTwo();
93
+ if (result.ok) {
94
+ console.log("ok");
95
+ }
96
+ else {
97
+ console.log("FAIL");
98
+ console.log(` ${result.message}`);
99
+ failed++;
100
+ }
101
+ }
102
+ catch (err) {
103
+ console.log("FAIL");
104
+ console.log(` ${err instanceof Error ? err.message : String(err)}`);
105
+ failed++;
106
+ }
107
+ console.log("");
108
+ if (failed > 0) {
109
+ console.log(`${failed} test(s) failed.`);
110
+ process.exit(1);
111
+ }
112
+ console.log("All web_search tests passed.");
113
+ }
114
+ main();
@@ -0,0 +1,147 @@
1
+ import { getBraveSearchApiKey } from "../config.js";
2
+ const MAX_FETCH_CHARS = 40_000;
3
+ const FETCH_TIMEOUT_MS = 20_000;
4
+ const MAX_SEARCH_RESULTS = 8;
5
+ function formatSearchResults(hits) {
6
+ const slice = hits.slice(0, MAX_SEARCH_RESULTS);
7
+ return slice
8
+ .map((r, i) => `${i + 1}. ${(r.title ?? "Untitled").replace(/<[^>]+>/g, "")}\n ${r.url ?? ""}${r.snippet ? "\n " + String(r.snippet).replace(/<[^>]+>/g, "").slice(0, 300) : ""}`)
9
+ .join("\n\n");
10
+ }
11
+ const PLAYWRIGHT_INSTALL_MSG = "Playwright Chromium not installed. Run: npx playwright install chromium (in the project directory). web_fetch uses it only when plain fetch fails (e.g. JS-rendered pages).";
12
+ function stripHtmlToText(html) {
13
+ let s = html
14
+ .replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "")
15
+ .replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "")
16
+ .replace(/<[^>]+>/g, " ");
17
+ return s.replace(/\s+/g, " ").trim();
18
+ }
19
+ async function fetchWithTimeout(url) {
20
+ const ac = new AbortController();
21
+ const t = setTimeout(() => ac.abort(), FETCH_TIMEOUT_MS);
22
+ try {
23
+ const res = await fetch(url, {
24
+ signal: ac.signal,
25
+ headers: { "User-Agent": "ideacode-web-fetch/1" },
26
+ });
27
+ return res;
28
+ }
29
+ finally {
30
+ clearTimeout(t);
31
+ }
32
+ }
33
+ export async function webFetch(args) {
34
+ const url = args.url?.trim();
35
+ if (!url)
36
+ return "error: url is required";
37
+ if (!url.startsWith("http://") && !url.startsWith("https://"))
38
+ return "error: url must be http or https";
39
+ try {
40
+ const res = await fetchWithTimeout(url);
41
+ if (res.ok) {
42
+ const ct = (res.headers.get("content-type") ?? "").toLowerCase();
43
+ if (ct.includes("text/plain") ||
44
+ ct.includes("text/html") ||
45
+ ct.includes("application/json") ||
46
+ ct.includes("text/")) {
47
+ const raw = await res.text();
48
+ const text = ct.includes("text/html") && raw.includes("<")
49
+ ? stripHtmlToText(raw)
50
+ : raw.replace(/\s+/g, " ").trim();
51
+ if (text.length > MAX_FETCH_CHARS) {
52
+ return (text.slice(0, MAX_FETCH_CHARS) +
53
+ "\n\n... (truncated, total " +
54
+ text.length +
55
+ " chars)");
56
+ }
57
+ return text || "(no text content)";
58
+ }
59
+ }
60
+ }
61
+ catch {
62
+ // Fall through to Playwright for JS-rendered or when fetch fails
63
+ }
64
+ try {
65
+ const { chromium } = await import("playwright");
66
+ const browser = await chromium.launch({ headless: true });
67
+ try {
68
+ const page = await browser.newPage();
69
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: FETCH_TIMEOUT_MS });
70
+ const text = await page.evaluate(() => {
71
+ const body = document.body;
72
+ if (!body)
73
+ return "";
74
+ const clone = body.cloneNode(true);
75
+ for (const el of clone.querySelectorAll("script, style, nav, header, footer, [role='navigation']")) {
76
+ el.remove();
77
+ }
78
+ return (clone.innerText ?? clone.textContent ?? "").replace(/\s+/g, " ").trim();
79
+ });
80
+ if (text.length > MAX_FETCH_CHARS) {
81
+ return (text.slice(0, MAX_FETCH_CHARS) +
82
+ "\n\n... (truncated, total " +
83
+ text.length +
84
+ " chars)");
85
+ }
86
+ return text || "(no text content)";
87
+ }
88
+ finally {
89
+ await browser.close().catch(() => { });
90
+ }
91
+ }
92
+ catch (err) {
93
+ const msg = err instanceof Error ? err.message : String(err);
94
+ const causeMsg = err instanceof Error && err.cause instanceof Error ? String(err.cause.message) : "";
95
+ const combined = (msg + " " + causeMsg).toLowerCase();
96
+ const needsPlaywright = combined.includes("executable doesn't exist") ||
97
+ combined.includes("browser was not found") ||
98
+ combined.includes("chromium revision is not downloaded") ||
99
+ (combined.includes("playwright") && (combined.includes("install") || combined.includes("not found")));
100
+ if (needsPlaywright) {
101
+ return `error: ${PLAYWRIGHT_INSTALL_MSG}`;
102
+ }
103
+ return `error: ${msg}`;
104
+ }
105
+ }
106
+ async function braveSearchApi(query, apiKey) {
107
+ const url = `https://api.search.brave.com/res/v1/web/search?${new URLSearchParams({
108
+ q: query,
109
+ count: String(MAX_SEARCH_RESULTS),
110
+ country: "us",
111
+ search_lang: "en",
112
+ })}`;
113
+ const res = await fetch(url, {
114
+ headers: { "X-Subscription-Token": apiKey, Accept: "application/json" },
115
+ });
116
+ if (!res.ok) {
117
+ const text = await res.text();
118
+ return `error: Brave Search API ${res.status}: ${text.slice(0, 200)}`;
119
+ }
120
+ const json = (await res.json());
121
+ if (json.message)
122
+ return `error: ${json.message}`;
123
+ const results = json.web?.results ?? [];
124
+ if (results.length === 0)
125
+ return "No results found.";
126
+ const hits = results.map((r) => ({
127
+ title: r.title ?? "Untitled",
128
+ url: r.url ?? "",
129
+ snippet: r.description,
130
+ }));
131
+ return formatSearchResults(hits);
132
+ }
133
+ export async function webSearch(args) {
134
+ const query = args.query?.trim();
135
+ if (!query)
136
+ return "error: query is required";
137
+ const apiKey = getBraveSearchApiKey();
138
+ if (!apiKey)
139
+ return "error: Brave Search API key not set. Use /brave or set BRAVE_API_KEY (https://brave.com/search/api).";
140
+ try {
141
+ return await braveSearchApi(query, apiKey);
142
+ }
143
+ catch (err) {
144
+ const msg = err instanceof Error ? err.message : String(err);
145
+ return `error: Brave Search API failed. ${msg}`;
146
+ }
147
+ }
@@ -0,0 +1,65 @@
1
+ import { marked } from "marked";
2
+ import { markedTerminal } from "marked-terminal";
3
+ import chalk from "chalk";
4
+ import boxen from "boxen";
5
+ import { colors, icons, theme, inkColors } from "./theme.js";
6
+ const hrLine = () => {
7
+ const cols = process.stdout.columns ?? 80;
8
+ return colors.muted("─".repeat(Math.min(cols, 100)));
9
+ };
10
+ marked.use(markedTerminal({
11
+ code: chalk.hex(theme.syntax.code),
12
+ codespan: chalk.hex(theme.syntax.code),
13
+ blockquote: chalk.hex(theme.colors.muted.main).italic,
14
+ strong: chalk.bold,
15
+ em: chalk.italic,
16
+ heading: chalk.hex(theme.syntax.heading).bold,
17
+ firstHeading: chalk.hex(theme.colors.primary.dim).underline.bold,
18
+ link: chalk.hex(theme.syntax.link),
19
+ href: chalk.hex(theme.syntax.href).underline,
20
+ hr: () => hrLine(),
21
+ }, { theme: { keyword: chalk.hex(theme.syntax.keyword), string: chalk.hex(theme.syntax.string) } }));
22
+ const codeStyle = (s) => chalk.hex(theme.syntax.code)(s);
23
+ export function renderMarkdown(text) {
24
+ let out = marked.parse(text, { async: false });
25
+ out = out.replace(/\`([^`]+)\`/g, (_, code) => codeStyle(code));
26
+ out = out.replace(/\*\*([^*]+)\*\*/g, (_, boldText) => chalk.bold(boldText));
27
+ out = out.replace(/(^|\n)\* /g, "$1• ");
28
+ out = out.replace(/(^|\n)([ \t]*[-*_]{3,}[ \t]*)(\n|$)/gm, (_, before, _rule, after) => before + hrLine() + after);
29
+ return out;
30
+ }
31
+ export function separator() {
32
+ const cols = process.stdout.columns ?? 80;
33
+ return colors.muted("─".repeat(Math.min(cols, 100)));
34
+ }
35
+ export function header(title, subtitle) {
36
+ return boxen(`${chalk.bold(title)}\n${colors.muted(subtitle)}`, {
37
+ padding: { top: 0, bottom: 0, left: 1, right: 1 },
38
+ margin: { bottom: 1 },
39
+ borderColor: inkColors.primary,
40
+ borderStyle: "round",
41
+ });
42
+ }
43
+ const TOOL_INDENT = " ";
44
+ const toolSubdued = chalk.hex("#3d3d3d");
45
+ export function toolCallBox(toolName, argPreview, success = true) {
46
+ const diamondColor = success ? colors.toolSuccess : colors.toolFail;
47
+ const nameColor = success ? colors.toolSuccess : colors.toolFail;
48
+ const argColor = success ? toolSubdued : colors.toolFail;
49
+ const parenColor = chalk.white;
50
+ const name = " " + toolName.charAt(0).toUpperCase() + toolName.slice(1);
51
+ return `${TOOL_INDENT}${diamondColor(icons.tool)}${nameColor(name)}${parenColor("(")}${argColor(argPreview)}${parenColor(")")}`;
52
+ }
53
+ export function toolResultLine(preview, success = true) {
54
+ const pipeColor = success ? toolSubdued : colors.toolFail;
55
+ const textColor = success ? toolSubdued : colors.toolFail;
56
+ return `${TOOL_INDENT}${TOOL_INDENT}${pipeColor(icons.pipe + " ")}${textColor(preview)}`;
57
+ }
58
+ export function agentMessage(text) {
59
+ const rendered = renderMarkdown(text.trim());
60
+ return `${colors.accentPale(icons.agent)} ${rendered}`;
61
+ }
62
+ export function bashOutputLine(line) {
63
+ return ` ${colors.tool(icons.pipe + " ")}${colors.toolDim(line)}`;
64
+ }
65
+ export { colors, icons, theme, inkColors };
@@ -0,0 +1 @@
1
+ export { colors, icons, theme, inkColors, separator, agentMessage, toolCallBox, toolResultLine, bashOutputLine, renderMarkdown, header, } from "./format.js";