@bahulam/code 0.1.6 → 0.1.8
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/LICENSE +1 -1
- package/NOTICE +6 -6
- package/README.md +11 -18
- package/package.json +11 -11
- package/src/agents/loader.mjs +1 -1
- package/src/agents/workflow_loader.mjs +2 -2
- package/src/auth/{tarang-auth.mjs → bahulam-auth.mjs} +4 -3
- package/src/commands/agent.mjs +3 -3
- package/src/commands/device.mjs +2 -2
- package/src/commands/pair.mjs +2 -2
- package/src/commands/remote.mjs +3 -3
- package/src/commands/workflow.mjs +5 -5
- package/src/config/env.mjs +9 -2
- package/src/config/memory-loader.mjs +1 -1
- package/src/config/model-catalog-default.json +4 -3
- package/src/config/model-catalog.mjs +2 -0
- package/src/config/settings.mjs +2 -2
- package/src/context/prose-chunker.mjs +162 -5
- package/src/core/agent-loop.mjs +17 -0
- package/src/core/attachments.mjs +66 -4
- package/src/core/backend-url.mjs +11 -12
- package/src/core/bundled-runtime.mjs +1 -1
- package/src/core/callback-client.mjs +1 -1
- package/src/core/headless.mjs +18 -4
- package/src/core/jsonl-writer.mjs +18 -18
- package/src/core/lint-resolver.mjs +472 -0
- package/src/core/local-agent.mjs +40 -8
- package/src/core/local-store.mjs +5 -1
- package/src/core/mode-selector.mjs +1 -1
- package/src/core/output-filter.mjs +22 -26
- package/src/core/pricing.mjs +10 -3
- package/src/core/project-artifacts.mjs +3 -3
- package/src/core/project-context-loader.mjs +9 -9
- package/src/core/settings-sync.mjs +1 -1
- package/src/core/stagnation.mjs +61 -1
- package/src/core/stream-client.mjs +13 -8
- package/src/core/system-prompt.mjs +2 -2
- package/src/core/tool-executor.mjs +178 -84
- package/src/daemon/http-dashboard.mjs +400 -0
- package/src/daemon/session-core.mjs +1 -0
- package/src/local-service/agent-relay.mjs +868 -0
- package/src/local-service/approval-bridge.mjs +222 -0
- package/src/local-service/browser.mjs +24 -0
- package/src/local-service/command.mjs +155 -0
- package/src/local-service/file-access.mjs +393 -0
- package/src/local-service/machine.mjs +86 -0
- package/src/local-service/paths.mjs +29 -0
- package/src/local-service/preview-converters.mjs +260 -0
- package/src/local-service/server.mjs +2644 -0
- package/src/local-service/session-store.mjs +221 -0
- package/src/mcp/client.mjs +4 -4
- package/src/onboarding/preflight.mjs +2 -2
- package/src/terminal/agents.mjs +2 -2
- package/src/terminal/analytics.mjs +2 -6
- package/src/terminal/ansi.mjs +11 -3
- package/src/terminal/main.mjs +43 -17
- package/src/terminal/model-catalog-display.mjs +111 -0
- package/src/terminal/repl-format.mjs +4 -1
- package/src/terminal/repl-model-form.mjs +81 -14
- package/src/terminal/repl-render.mjs +37 -7
- package/src/terminal/repl-resume.mjs +2 -2
- package/src/terminal/repl-state.mjs +12 -0
- package/src/terminal/repl.mjs +248 -70
- package/src/terminal/tool-display.mjs +32 -0
- package/src/terminal/watch-state.mjs +118 -0
- package/src/tools/agent.mjs +2 -2
- package/src/tools/analyze-image.mjs +158 -0
- package/src/tools/bash.mjs +11 -9
- package/src/tools/generate-image.mjs +411 -0
- package/src/tools/lint.mjs +24 -43
- package/src/tools/project-overview.mjs +12 -12
- package/src/tools/registry.mjs +4 -0
- package/src/ui/formatter.mjs +2 -2
- package/src/ui/icons.mjs +2 -0
- package/src/ui/input-dock.mjs +7 -7
- package/src/ui/palette.mjs +1 -1
- package/src/ui/slash-commands.mjs +4 -6
- package/src/ui/spinner.mjs +1 -1
- package/src/ui/term.mjs +2 -6
- package/src/ui/tool-card.mjs +81 -6
- package/pulse/app/activity/page.tsx +0 -190
- package/pulse/app/api/activity/route.ts +0 -138
- package/pulse/app/api/benchmark/route.ts +0 -113
- package/pulse/app/api/benchmarks/route.ts +0 -195
- package/pulse/app/api/costs/route.ts +0 -88
- package/pulse/app/api/export/route.ts +0 -77
- package/pulse/app/api/history/route.ts +0 -11
- package/pulse/app/api/import/route.ts +0 -31
- package/pulse/app/api/memory/route.ts +0 -50
- package/pulse/app/api/plans/route.ts +0 -9
- package/pulse/app/api/projects/[slug]/route.ts +0 -96
- package/pulse/app/api/projects/route.ts +0 -121
- package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
- package/pulse/app/api/sessions/[id]/route.ts +0 -31
- package/pulse/app/api/sessions/route.ts +0 -112
- package/pulse/app/api/settings/route.ts +0 -14
- package/pulse/app/api/stats/route.ts +0 -143
- package/pulse/app/api/todos/route.ts +0 -9
- package/pulse/app/api/tools/route.ts +0 -160
- package/pulse/app/benchmarks/page.tsx +0 -224
- package/pulse/app/costs/page.tsx +0 -179
- package/pulse/app/export/page.tsx +0 -465
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +0 -263
- package/pulse/app/help/page.tsx +0 -143
- package/pulse/app/history/page.tsx +0 -157
- package/pulse/app/layout.tsx +0 -46
- package/pulse/app/memory/page.tsx +0 -365
- package/pulse/app/overview-client.tsx +0 -393
- package/pulse/app/page.tsx +0 -14
- package/pulse/app/plans/page.tsx +0 -308
- package/pulse/app/projects/[slug]/page.tsx +0 -390
- package/pulse/app/projects/page.tsx +0 -110
- package/pulse/app/sessions/[id]/page.tsx +0 -243
- package/pulse/app/sessions/page.tsx +0 -39
- package/pulse/app/settings/page.tsx +0 -188
- package/pulse/app/todos/page.tsx +0 -211
- package/pulse/app/tools/page.tsx +0 -249
- package/pulse/cli.js +0 -164
- package/pulse/components/activity/day-of-week-chart.tsx +0 -35
- package/pulse/components/activity/streak-card.tsx +0 -36
- package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
- package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
- package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
- package/pulse/components/costs/model-token-table.tsx +0 -60
- package/pulse/components/global-search.tsx +0 -193
- package/pulse/components/keyboard-nav-provider.tsx +0 -23
- package/pulse/components/layout/bottom-nav.tsx +0 -53
- package/pulse/components/layout/client-layout.tsx +0 -31
- package/pulse/components/layout/sidebar-context.tsx +0 -50
- package/pulse/components/layout/sidebar.tsx +0 -183
- package/pulse/components/layout/top-bar.tsx +0 -121
- package/pulse/components/overview/activity-heatmap.tsx +0 -107
- package/pulse/components/overview/conversation-table.tsx +0 -148
- package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
- package/pulse/components/overview/peak-hours-chart.tsx +0 -87
- package/pulse/components/overview/project-activity-donut.tsx +0 -96
- package/pulse/components/overview/stat-card.tsx +0 -102
- package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
- package/pulse/components/projects/project-card.tsx +0 -175
- package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
- package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
- package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
- package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
- package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
- package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
- package/pulse/components/sessions/session-badges.tsx +0 -49
- package/pulse/components/sessions/session-table.tsx +0 -299
- package/pulse/components/theme-provider.tsx +0 -44
- package/pulse/components/tools/feature-adoption-table.tsx +0 -58
- package/pulse/components/tools/mcp-server-panel.tsx +0 -45
- package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
- package/pulse/components/tools/version-history-table.tsx +0 -32
- package/pulse/components/ui/alert.tsx +0 -66
- package/pulse/components/ui/badge.tsx +0 -48
- package/pulse/components/ui/breadcrumb.tsx +0 -109
- package/pulse/components/ui/button.tsx +0 -64
- package/pulse/components/ui/calendar.tsx +0 -220
- package/pulse/components/ui/card.tsx +0 -92
- package/pulse/components/ui/command.tsx +0 -158
- package/pulse/components/ui/dialog.tsx +0 -158
- package/pulse/components/ui/input.tsx +0 -21
- package/pulse/components/ui/popover.tsx +0 -89
- package/pulse/components/ui/progress.tsx +0 -31
- package/pulse/components/ui/select.tsx +0 -190
- package/pulse/components/ui/separator.tsx +0 -28
- package/pulse/components/ui/sheet.tsx +0 -143
- package/pulse/components/ui/skeleton.tsx +0 -13
- package/pulse/components/ui/table.tsx +0 -116
- package/pulse/components/ui/tabs.tsx +0 -91
- package/pulse/components/ui/tooltip.tsx +0 -57
- package/pulse/components/use-global-keyboard-nav.ts +0 -79
- package/pulse/components.json +0 -23
- package/pulse/eslint.config.mjs +0 -18
- package/pulse/lib/bahulam-paths.ts +0 -23
- package/pulse/lib/claude-reader.ts +0 -592
- package/pulse/lib/decode.ts +0 -129
- package/pulse/lib/pricing.ts +0 -102
- package/pulse/lib/replay-parser.ts +0 -165
- package/pulse/lib/tool-categories.ts +0 -140
- package/pulse/lib/utils.ts +0 -6
- package/pulse/next-env.d.ts +0 -6
- package/pulse/next.config.ts +0 -16
- package/pulse/package.json +0 -45
- package/pulse/postcss.config.mjs +0 -7
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +0 -1
- package/pulse/public/globe.svg +0 -1
- package/pulse/public/next.svg +0 -1
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +0 -1
- package/pulse/public/window.svg +0 -1
- package/pulse/tsconfig.json +0 -34
- package/pulse/types/claude.ts +0 -294
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* generate_image — text-to-image generation for Bahulam Code.
|
|
3
|
+
*
|
|
4
|
+
* Uses the OpenRouter-compatible image API shape (`POST /v1/images`) and saves
|
|
5
|
+
* generated bytes to local files so the agent can use the assets in code.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs/promises';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
import { BahulamAuth } from '../auth/bahulam-auth.mjs';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_IMAGE_GENERATION_MODEL = 'google/gemini-3-pro-image';
|
|
13
|
+
const MAX_GENERATED_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
14
|
+
|
|
15
|
+
export const GenerateImageTool = {
|
|
16
|
+
name: 'generate_image',
|
|
17
|
+
description:
|
|
18
|
+
'Generate new image files from a text prompt. Use for requested visual assets, hero images, icons, textures, mockups, and illustrations. Do not use to inspect uploaded images; use analyze_image for that.',
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
prompt: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
description: 'Detailed image prompt: subject, composition, lighting, style, colors, background, and constraints.',
|
|
25
|
+
},
|
|
26
|
+
aspect_ratio: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
enum: ['1:1', '4:3', '3:4', '16:9', '9:16', '21:9'],
|
|
29
|
+
description: 'Optional aspect ratio.',
|
|
30
|
+
},
|
|
31
|
+
resolution: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
enum: ['512', '1K', '2K', '4K'],
|
|
34
|
+
description: 'Optional normalized resolution tier.',
|
|
35
|
+
},
|
|
36
|
+
quality: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
enum: ['auto', 'low', 'medium', 'high'],
|
|
39
|
+
description: 'Optional quality hint.',
|
|
40
|
+
},
|
|
41
|
+
output_format: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
enum: ['png', 'jpeg', 'webp', 'svg'],
|
|
44
|
+
description: 'Optional output format.',
|
|
45
|
+
},
|
|
46
|
+
n: {
|
|
47
|
+
type: 'integer',
|
|
48
|
+
minimum: 1,
|
|
49
|
+
maximum: 4,
|
|
50
|
+
description: 'Number of images to generate. Default 1.',
|
|
51
|
+
},
|
|
52
|
+
output_path: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
description: 'Optional path under the current project. Defaults to generated-images/<prompt-slug>.<ext>.',
|
|
55
|
+
},
|
|
56
|
+
filename: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
description: 'Optional filename alias. output_path takes precedence when both are provided.',
|
|
59
|
+
},
|
|
60
|
+
include_data_url: {
|
|
61
|
+
type: 'boolean',
|
|
62
|
+
description: 'Also return data:image/... URLs. Use sparingly; this can be large.',
|
|
63
|
+
},
|
|
64
|
+
overwrite: {
|
|
65
|
+
type: 'boolean',
|
|
66
|
+
description: 'Overwrite output_path if it already exists. Default false.',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
required: ['prompt'],
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
validateInput(input) {
|
|
73
|
+
const errors = [];
|
|
74
|
+
if (!String(input?.prompt || '').trim()) errors.push('prompt is required');
|
|
75
|
+
return errors;
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
async call(input) {
|
|
79
|
+
const prompt = String(input.prompt || '').trim();
|
|
80
|
+
const n = boundedInt(input.n, 1, 1, 4);
|
|
81
|
+
const model = resolveImageModel();
|
|
82
|
+
let basePath;
|
|
83
|
+
try {
|
|
84
|
+
basePath = safeOutputBase(input.output_path || input.filename, prompt);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
return { success: false, output: String(err?.message || err), _tool: 'generate_image' };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const request = {
|
|
90
|
+
model,
|
|
91
|
+
prompt,
|
|
92
|
+
n,
|
|
93
|
+
aspect_ratio: optionalString(input.aspect_ratio),
|
|
94
|
+
resolution: optionalString(input.resolution),
|
|
95
|
+
quality: optionalString(input.quality),
|
|
96
|
+
output_format: optionalString(input.output_format),
|
|
97
|
+
output_path: optionalString(input.output_path),
|
|
98
|
+
filename: optionalString(input.filename),
|
|
99
|
+
};
|
|
100
|
+
const creds = new BahulamAuth().loadCredentials();
|
|
101
|
+
const apiKey = gatewayApiKey(creds);
|
|
102
|
+
let result;
|
|
103
|
+
if (creds.backendUrl && creds.token) {
|
|
104
|
+
result = await callBackendImageApi({
|
|
105
|
+
backendUrl: creds.backendUrl,
|
|
106
|
+
token: creds.token,
|
|
107
|
+
product: process.env.BAHULAM_PRODUCT || 'bahulam',
|
|
108
|
+
...request,
|
|
109
|
+
});
|
|
110
|
+
} else if (apiKey) {
|
|
111
|
+
result = await callImageApi({ apiKey, ...request });
|
|
112
|
+
} else {
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
output: 'Image generation unavailable: sign in with `bahulam-code login` or configure OPENROUTER_API_KEY / BAHULAM_GATEWAY_API_KEY for direct local generation.',
|
|
116
|
+
_tool: 'generate_image',
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (result.error) {
|
|
120
|
+
return { success: false, output: result.error, _tool: 'generate_image' };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const resultModel = result.model || model;
|
|
124
|
+
const images = [];
|
|
125
|
+
for (let i = 0; i < result.images.length; i += 1) {
|
|
126
|
+
const item = result.images[i];
|
|
127
|
+
const mediaType = mediaTypeFor(item.media_type, input.output_format);
|
|
128
|
+
const buffer = Buffer.from(item.b64_json, 'base64');
|
|
129
|
+
if (buffer.length > maxGeneratedImageBytes()) {
|
|
130
|
+
return {
|
|
131
|
+
success: false,
|
|
132
|
+
output: `Generated image is ${buffer.length} bytes; cap is ${maxGeneratedImageBytes()} bytes.`,
|
|
133
|
+
_tool: 'generate_image',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const filePath = await outputPathFor(basePath, i + 1, result.images.length, mediaType, Boolean(input.overwrite));
|
|
138
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
139
|
+
await fs.writeFile(filePath, buffer);
|
|
140
|
+
|
|
141
|
+
const image = {
|
|
142
|
+
path: filePath,
|
|
143
|
+
mime: mediaType,
|
|
144
|
+
size_bytes: buffer.length,
|
|
145
|
+
};
|
|
146
|
+
if (input.include_data_url) {
|
|
147
|
+
image.data_url = `data:${mediaType};base64,${item.b64_json}`;
|
|
148
|
+
}
|
|
149
|
+
images.push(image);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const usage = result.usage || {};
|
|
153
|
+
const cost = usage.cost != null ? `, cost=${usage.cost}` : '';
|
|
154
|
+
return {
|
|
155
|
+
success: true,
|
|
156
|
+
output: [
|
|
157
|
+
`Generated ${images.length} image${images.length === 1 ? '' : 's'} with ${resultModel}${cost}:`,
|
|
158
|
+
...images.map((image) => `- ${image.path} (${image.mime}, ${image.size_bytes} bytes)`),
|
|
159
|
+
].join('\n'),
|
|
160
|
+
images,
|
|
161
|
+
model: resultModel,
|
|
162
|
+
provider: result.provider || 'openrouter-compatible',
|
|
163
|
+
usage,
|
|
164
|
+
_tool: 'generate_image',
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
async function callBackendImageApi({
|
|
170
|
+
backendUrl,
|
|
171
|
+
token,
|
|
172
|
+
product,
|
|
173
|
+
prompt,
|
|
174
|
+
n,
|
|
175
|
+
aspect_ratio,
|
|
176
|
+
resolution,
|
|
177
|
+
quality,
|
|
178
|
+
output_format,
|
|
179
|
+
output_path,
|
|
180
|
+
filename,
|
|
181
|
+
}) {
|
|
182
|
+
const body = { prompt, include_data_url: true };
|
|
183
|
+
if (n > 1) body.n = n;
|
|
184
|
+
if (aspect_ratio) body.aspect_ratio = aspect_ratio;
|
|
185
|
+
if (resolution) body.resolution = resolution;
|
|
186
|
+
if (quality) body.quality = quality;
|
|
187
|
+
if (output_format) body.output_format = output_format;
|
|
188
|
+
if (output_path) body.output_path = output_path;
|
|
189
|
+
if (filename) body.filename = filename;
|
|
190
|
+
|
|
191
|
+
let response;
|
|
192
|
+
try {
|
|
193
|
+
response = await fetch(`${backendUrl}/api/images/generate`, {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: {
|
|
196
|
+
Authorization: `Bearer ${token}`,
|
|
197
|
+
'Content-Type': 'application/json',
|
|
198
|
+
'X-Product': product || 'bahulam',
|
|
199
|
+
},
|
|
200
|
+
body: JSON.stringify(body),
|
|
201
|
+
});
|
|
202
|
+
} catch (err) {
|
|
203
|
+
return { error: `Image generation backend unreachable: ${err.message || err}` };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const payload = await readJsonResponse(response);
|
|
207
|
+
if (!response.ok) {
|
|
208
|
+
return { error: `Image generation backend error (${response.status}): ${errorMessage(payload, response.status)}` };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const data = Array.isArray(payload?.images) ? payload.images : [];
|
|
212
|
+
if (!data.length) return { error: 'Image generation backend returned no image data.' };
|
|
213
|
+
const images = [];
|
|
214
|
+
for (const item of data) {
|
|
215
|
+
const parsed = normalizeImageData(item, output_format);
|
|
216
|
+
if (parsed.error) return parsed;
|
|
217
|
+
images.push(parsed);
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
images,
|
|
221
|
+
model: payload?.model || '',
|
|
222
|
+
provider: payload?.provider || 'bahulam',
|
|
223
|
+
usage: payload?.usage || {},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function callImageApi({ apiKey, model, prompt, n, aspect_ratio, resolution, quality, output_format }) {
|
|
228
|
+
const body = { model, prompt };
|
|
229
|
+
if (n > 1) body.n = n;
|
|
230
|
+
if (aspect_ratio) body.aspect_ratio = aspect_ratio;
|
|
231
|
+
if (resolution) body.resolution = resolution;
|
|
232
|
+
if (quality) body.quality = quality;
|
|
233
|
+
if (output_format) body.output_format = output_format;
|
|
234
|
+
|
|
235
|
+
const baseUrl = imageApiBaseUrl();
|
|
236
|
+
let response;
|
|
237
|
+
try {
|
|
238
|
+
response = await fetch(`${baseUrl}/images`, {
|
|
239
|
+
method: 'POST',
|
|
240
|
+
headers: {
|
|
241
|
+
Authorization: `Bearer ${apiKey}`,
|
|
242
|
+
'Content-Type': 'application/json',
|
|
243
|
+
'X-Product': process.env.BAHULAM_PRODUCT || 'kepler',
|
|
244
|
+
'X-Bahulam-Client': 'bahulam-code-image-generation',
|
|
245
|
+
},
|
|
246
|
+
body: JSON.stringify(body),
|
|
247
|
+
});
|
|
248
|
+
} catch (err) {
|
|
249
|
+
return { error: `Image generation model unreachable: ${err.message || err}` };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const payload = await readJsonResponse(response);
|
|
253
|
+
if (!response.ok) {
|
|
254
|
+
return { error: `Image generation model error (${response.status}): ${errorMessage(payload, response.status)}` };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const data = Array.isArray(payload.data) ? payload.data : [];
|
|
258
|
+
if (!data.length) return { error: 'Image generation model returned no image data.' };
|
|
259
|
+
const images = [];
|
|
260
|
+
for (const item of data) {
|
|
261
|
+
const parsed = normalizeImageData(item, output_format);
|
|
262
|
+
if (parsed.error) return parsed;
|
|
263
|
+
images.push(parsed);
|
|
264
|
+
}
|
|
265
|
+
return { images, model, provider: 'openrouter-compatible', usage: payload.usage || {} };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function imageApiBaseUrl() {
|
|
269
|
+
return String(
|
|
270
|
+
process.env.BAHULAM_IMAGE_GENERATION_BASE_URL ||
|
|
271
|
+
process.env.BAHULAM_GATEWAY_URL ||
|
|
272
|
+
process.env.OPENROUTER_BASE_URL ||
|
|
273
|
+
'https://openrouter.ai/api/v1'
|
|
274
|
+
).replace(/\/+$/, '');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function gatewayApiKey(creds = {}) {
|
|
278
|
+
return String(
|
|
279
|
+
process.env.BAHULAM_GATEWAY_SERVICE_TOKEN ||
|
|
280
|
+
process.env.BAHULAM_API_KEY ||
|
|
281
|
+
process.env.BAHULAM_CLI_TOKEN ||
|
|
282
|
+
process.env.BAHULAM_GATEWAY_API_KEY ||
|
|
283
|
+
process.env.OPENROUTER_API_KEY ||
|
|
284
|
+
creds.openRouterKey ||
|
|
285
|
+
''
|
|
286
|
+
).trim();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function readJsonResponse(response) {
|
|
290
|
+
const text = await response.text().catch(() => '');
|
|
291
|
+
try {
|
|
292
|
+
return text ? JSON.parse(text) : {};
|
|
293
|
+
} catch {
|
|
294
|
+
return { error: text };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function errorMessage(payload, status) {
|
|
299
|
+
const detail = payload?.detail || payload?.error || payload?.message || `HTTP ${status}`;
|
|
300
|
+
if (typeof detail === 'string') return detail.slice(0, 500);
|
|
301
|
+
return String(detail.message || detail.error || JSON.stringify(detail)).slice(0, 500);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function normalizeImageData(item, requestedFormat) {
|
|
305
|
+
if (item?.b64_json) {
|
|
306
|
+
return {
|
|
307
|
+
b64_json: String(item.b64_json),
|
|
308
|
+
media_type: mediaTypeFor(item.media_type || item.mime, requestedFormat),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const dataUrl = String(item?.data_url || '').trim();
|
|
312
|
+
const parsed = parseDataUrl(dataUrl);
|
|
313
|
+
if (parsed) return parsed;
|
|
314
|
+
return { error: 'Image generation returned an image without base64 data.' };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function parseDataUrl(dataUrl) {
|
|
318
|
+
const match = dataUrl.match(/^data:([^;,]+);base64,([\s\S]+)$/);
|
|
319
|
+
if (!match) return null;
|
|
320
|
+
return { media_type: match[1].toLowerCase(), b64_json: match[2] };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function resolveImageModel() {
|
|
324
|
+
return String(
|
|
325
|
+
process.env.BAHULAM_IMAGE_GENERATION_MODEL ||
|
|
326
|
+
process.env.BAHULAM_CHAT_IMAGE_GENERATION_MODEL ||
|
|
327
|
+
process.env.BAHULAM_IMAGE_GENERATION_MODEL_PAID ||
|
|
328
|
+
process.env.BAHULAM_IMAGE_GENERATION_MODEL_FREE ||
|
|
329
|
+
DEFAULT_IMAGE_GENERATION_MODEL
|
|
330
|
+
).trim();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function boundedInt(value, defaultValue, min, max) {
|
|
334
|
+
const parsed = Number.parseInt(value, 10);
|
|
335
|
+
if (!Number.isFinite(parsed)) return defaultValue;
|
|
336
|
+
return Math.max(min, Math.min(max, parsed));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function optionalString(value) {
|
|
340
|
+
const text = String(value || '').trim();
|
|
341
|
+
return text || null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function maxGeneratedImageBytes() {
|
|
345
|
+
const value = Number.parseInt(process.env.BAHULAM_IMAGE_GENERATION_MAX_BYTES || '', 10);
|
|
346
|
+
return Number.isFinite(value) && value > 0 ? value : MAX_GENERATED_IMAGE_BYTES;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function safeOutputBase(outputPath, prompt) {
|
|
350
|
+
const raw = String(outputPath || '').trim();
|
|
351
|
+
const fallback = path.join('generated-images', `${slugify(prompt)}.png`);
|
|
352
|
+
const target = raw || fallback;
|
|
353
|
+
const resolved = path.resolve(process.cwd(), target);
|
|
354
|
+
const root = `${path.resolve(process.cwd())}${path.sep}`;
|
|
355
|
+
if (resolved !== path.resolve(process.cwd()) && !resolved.startsWith(root)) {
|
|
356
|
+
throw new Error('output_path must stay inside the current project directory');
|
|
357
|
+
}
|
|
358
|
+
return resolved;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function outputPathFor(basePath, index, total, mediaType, overwrite) {
|
|
362
|
+
const ext = extensionFor(mediaType);
|
|
363
|
+
const parsed = path.parse(basePath);
|
|
364
|
+
const stem = parsed.ext ? path.join(parsed.dir, parsed.name) : basePath;
|
|
365
|
+
const base = `${stem}${total > 1 ? `-${index}` : ''}.${ext}`;
|
|
366
|
+
if (overwrite) return base;
|
|
367
|
+
return uniquePath(base);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function uniquePath(filePath) {
|
|
371
|
+
if (!(await exists(filePath))) return filePath;
|
|
372
|
+
const parsed = path.parse(filePath);
|
|
373
|
+
for (let i = 2; i < 1000; i += 1) {
|
|
374
|
+
const candidate = path.join(parsed.dir, `${parsed.name}-${i}${parsed.ext}`);
|
|
375
|
+
if (!(await exists(candidate))) return candidate;
|
|
376
|
+
}
|
|
377
|
+
throw new Error(`Could not choose a unique output path for ${filePath}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function exists(filePath) {
|
|
381
|
+
try {
|
|
382
|
+
await fs.access(filePath);
|
|
383
|
+
return true;
|
|
384
|
+
} catch {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function mediaTypeFor(raw, requestedFormat) {
|
|
390
|
+
const media = String(raw || '').trim().toLowerCase();
|
|
391
|
+
if (media.startsWith('image/')) return media;
|
|
392
|
+
const requested = String(requestedFormat || '').trim().toLowerCase();
|
|
393
|
+
if (requested === 'jpeg' || requested === 'jpg') return 'image/jpeg';
|
|
394
|
+
if (requested === 'webp') return 'image/webp';
|
|
395
|
+
if (requested === 'svg') return 'image/svg+xml';
|
|
396
|
+
return 'image/png';
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function extensionFor(mediaType) {
|
|
400
|
+
if (mediaType === 'image/jpeg') return 'jpg';
|
|
401
|
+
if (mediaType === 'image/webp') return 'webp';
|
|
402
|
+
if (mediaType === 'image/svg+xml') return 'svg';
|
|
403
|
+
return 'png';
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function slugify(prompt) {
|
|
407
|
+
const words = String(prompt || '')
|
|
408
|
+
.toLowerCase()
|
|
409
|
+
.match(/[a-z0-9]+/g);
|
|
410
|
+
return (words || ['generated', 'image']).slice(0, 6).join('-') || 'generated-image';
|
|
411
|
+
}
|
package/src/tools/lint.mjs
CHANGED
|
@@ -1,37 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Legacy lint tool wrapper.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Execution: direct on host (no sandbox needed for read-only).
|
|
8
|
-
* With --fix: routes through sandbox if available.
|
|
4
|
+
* The active model-facing tool is `lint_check` in core/tool-executor.mjs.
|
|
5
|
+
* Keep this wrapper resolver-backed so older imports cannot drift back to a
|
|
6
|
+
* separate linter command table.
|
|
9
7
|
*/
|
|
10
8
|
|
|
11
9
|
import { BashTool } from './bash.mjs';
|
|
12
|
-
|
|
13
|
-
const LINTERS = {
|
|
14
|
-
ruff: { cmd: (p, fix) => `ruff check ${fix ? '--fix ' : ''}${p}`, lang: 'python' },
|
|
15
|
-
mypy: { cmd: (p) => `mypy ${p} --no-error-summary`, lang: 'python' },
|
|
16
|
-
pyright: { cmd: (p) => `pyright ${p}`, lang: 'python' },
|
|
17
|
-
pylint: { cmd: (p) => `pylint ${p} --output-format=text`, lang: 'python' },
|
|
18
|
-
eslint: { cmd: (p, fix) => `eslint ${fix ? '--fix ' : ''}${p}`, lang: 'javascript' },
|
|
19
|
-
tsc: { cmd: () => 'tsc --noEmit', lang: 'typescript' },
|
|
20
|
-
flake8: { cmd: (p) => `flake8 ${p}`, lang: 'python' },
|
|
21
|
-
biome: { cmd: (p, fix) => `biome check ${fix ? '--fix ' : ''}${p}`, lang: 'javascript' },
|
|
22
|
-
};
|
|
10
|
+
import { resolveLintCommand } from '../core/lint-resolver.mjs';
|
|
23
11
|
|
|
24
12
|
export const LintTool = {
|
|
25
13
|
name: 'Lint',
|
|
26
|
-
description: 'Run
|
|
14
|
+
description: 'Run the project-aware linter or syntax checker for a file or directory.',
|
|
27
15
|
inputSchema: {
|
|
28
16
|
type: 'object',
|
|
29
17
|
properties: {
|
|
30
|
-
tool: {
|
|
31
|
-
type: 'string',
|
|
32
|
-
enum: Object.keys(LINTERS),
|
|
33
|
-
description: 'Which linter to run',
|
|
34
|
-
},
|
|
35
18
|
path: {
|
|
36
19
|
type: 'string',
|
|
37
20
|
description: 'File or directory to lint (default: ".")',
|
|
@@ -39,33 +22,31 @@ export const LintTool = {
|
|
|
39
22
|
},
|
|
40
23
|
fix: {
|
|
41
24
|
type: 'boolean',
|
|
42
|
-
description: '
|
|
25
|
+
description: 'Auto-fix is not supported by the resolver-backed lint wrapper.',
|
|
43
26
|
default: false,
|
|
44
27
|
},
|
|
45
28
|
},
|
|
46
|
-
required: ['tool'],
|
|
47
29
|
},
|
|
48
|
-
validateInput(input) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (input.tool && !LINTERS[input.tool]) errors.push(`Unknown linter: ${input.tool}. Supported: ${Object.keys(LINTERS).join(', ')}`);
|
|
52
|
-
return errors;
|
|
30
|
+
validateInput(input = {}) {
|
|
31
|
+
if (input.fix) return ['fix=true is not supported here; run an explicit shell command for fixes'];
|
|
32
|
+
return [];
|
|
53
33
|
},
|
|
54
|
-
async call(input) {
|
|
55
|
-
const linter = LINTERS[input.tool];
|
|
56
|
-
if (!linter) return `Unknown linter: ${input.tool}`;
|
|
57
|
-
|
|
34
|
+
async call(input = {}) {
|
|
58
35
|
const targetPath = input.path || '.';
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
command,
|
|
63
|
-
timeout: 30000, // linters should be fast
|
|
64
|
-
description: `Lint: ${input.tool} ${targetPath}`,
|
|
36
|
+
const lint = resolveLintCommand(targetPath, {
|
|
37
|
+
projectRoot: process.cwd(),
|
|
38
|
+
allowProjectScript: true,
|
|
65
39
|
});
|
|
40
|
+
if (!lint?.command) {
|
|
41
|
+
return `No project-aware linter for path or file type: ${targetPath}`;
|
|
42
|
+
}
|
|
66
43
|
|
|
67
|
-
return
|
|
44
|
+
return await BashTool.call({
|
|
45
|
+
command: lint.command,
|
|
46
|
+
timeout: 30000,
|
|
47
|
+
cwd: lint.cwd,
|
|
48
|
+
description: `Lint: ${targetPath} (${lint.source})`,
|
|
49
|
+
});
|
|
68
50
|
},
|
|
69
|
-
|
|
70
|
-
_executionPolicy: 'direct', // no sandbox for read-only; 'contained' if fix=true
|
|
51
|
+
_executionPolicy: 'direct',
|
|
71
52
|
};
|
|
@@ -322,8 +322,8 @@ function _readIfExists(dir, filename, maxChars = 8000) {
|
|
|
322
322
|
} catch { return ''; }
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
-
function _scanSkills(
|
|
326
|
-
const skillsDir = path.join(
|
|
325
|
+
function _scanSkills(bahulamDir) {
|
|
326
|
+
const skillsDir = path.join(bahulamDir, 'skills');
|
|
327
327
|
if (!fs.existsSync(skillsDir)) return [];
|
|
328
328
|
try {
|
|
329
329
|
return fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
@@ -402,15 +402,15 @@ export class ProjectRegistry {
|
|
|
402
402
|
// .bahulam/KEPLER.md, goal/plan/style, skills, AGENTS.md, etc. changes.
|
|
403
403
|
_attachLiveContext(resource, root) {
|
|
404
404
|
// Resolver — prefers .bahulam/, falls back to .kepler/ for legacy projects.
|
|
405
|
-
const
|
|
405
|
+
const bahulamDir = projectConfigDir(root);
|
|
406
406
|
resource.environment = detectEnvironment();
|
|
407
|
-
resource.project_context = _readIfExists(
|
|
407
|
+
resource.project_context = _readIfExists(bahulamDir, 'KEPLER.md', 10000) ||
|
|
408
408
|
_readIfExists(root, 'KEPLER.md', 10000) ||
|
|
409
|
-
_readIfExists(
|
|
410
|
-
resource.style = _readIfExists(
|
|
411
|
-
resource.goal = _readIfExists(
|
|
412
|
-
resource.plan = _readIfExists(
|
|
413
|
-
resource.skills_index = _scanSkills(
|
|
409
|
+
_readIfExists(bahulamDir, 'project.md', 8000);
|
|
410
|
+
resource.style = _readIfExists(bahulamDir, 'style.md', 4000);
|
|
411
|
+
resource.goal = _readIfExists(bahulamDir, 'goal.md', 2000);
|
|
412
|
+
resource.plan = _readIfExists(bahulamDir, 'plan.md', 6000);
|
|
413
|
+
resource.skills_index = _scanSkills(bahulamDir);
|
|
414
414
|
|
|
415
415
|
if (!resource.project_context) {
|
|
416
416
|
for (const name of ['.bahulam.md', 'AGENTS.md', 'CLAUDE.md']) {
|
|
@@ -426,7 +426,7 @@ export class ProjectRegistry {
|
|
|
426
426
|
throw new Error('get_project_overview requires a project path');
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
-
// LLM sometimes passes shell-escaped paths ("
|
|
429
|
+
// LLM sometimes passes shell-escaped paths ("Bahulam") or paths
|
|
430
430
|
// beginning with "~". Normalize defensively so the tool does not bounce
|
|
431
431
|
// back a "not found" error on a path that's correct apart from quoting.
|
|
432
432
|
rawPath = normalizePathInput(rawPath);
|
|
@@ -600,7 +600,7 @@ export class ProjectRegistry {
|
|
|
600
600
|
}
|
|
601
601
|
|
|
602
602
|
// LLM frequently passes shell-quoted paths copied from a terminal,
|
|
603
|
-
// e.g. "
|
|
603
|
+
// e.g. "Bahulam/src/app/\(kepler\)/page.tsx". Normalize here so
|
|
604
604
|
// every tool benefits, not just get_project_overview.
|
|
605
605
|
rawPath = normalizePathInput(rawPath);
|
|
606
606
|
|
|
@@ -633,7 +633,7 @@ export class ProjectRegistry {
|
|
|
633
633
|
let containingScratchRoot = containingProject ? null : findScratchRoot(candidate);
|
|
634
634
|
|
|
635
635
|
// Two reasons to try the unescaped variant:
|
|
636
|
-
// (1) candidate is outside every project root (literal "
|
|
636
|
+
// (1) candidate is outside every project root (literal "Bahulam"
|
|
637
637
|
// does not contain a real project), or
|
|
638
638
|
// (2) candidate is inside a root but does not exist on disk because
|
|
639
639
|
// a path segment like "\(kepler\)" only resolves once unescaped.
|
package/src/tools/registry.mjs
CHANGED
|
@@ -37,6 +37,8 @@ import { SearchFilesTool } from './search-files.mjs';
|
|
|
37
37
|
import { AnalyzeCodeTool } from './analyze-code.mjs';
|
|
38
38
|
import { ExploreTool, PlanTool, VerifyTool, DebugTool, RefactorTool } from './meta-tools.mjs';
|
|
39
39
|
import { RememberTool } from './remember.mjs';
|
|
40
|
+
import { GenerateImageTool } from './generate-image.mjs';
|
|
41
|
+
import { AnalyzeImageTool } from './analyze-image.mjs';
|
|
40
42
|
|
|
41
43
|
const BUILTIN_TOOLS = [
|
|
42
44
|
BashTool,
|
|
@@ -71,6 +73,8 @@ const BUILTIN_TOOLS = [
|
|
|
71
73
|
ReadFilesTool,
|
|
72
74
|
SearchFilesTool,
|
|
73
75
|
AnalyzeCodeTool,
|
|
76
|
+
AnalyzeImageTool,
|
|
77
|
+
GenerateImageTool,
|
|
74
78
|
ExploreTool,
|
|
75
79
|
PlanTool,
|
|
76
80
|
VerifyTool,
|
package/src/ui/formatter.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Formatter — Clean terminal output for
|
|
2
|
+
* Formatter — Clean terminal output for Bahulam events.
|
|
3
3
|
*
|
|
4
4
|
* Style reference: Claude Code screenshot
|
|
5
5
|
* ◐ Cyan spinner + cyan text for working/thinking
|
|
@@ -135,7 +135,7 @@ export class EventFormatter {
|
|
|
135
135
|
if (!this._lastBlock) return;
|
|
136
136
|
if (compactSame && this._lastBlock === nextBlock) return;
|
|
137
137
|
|
|
138
|
-
const mode = String(process.env.
|
|
138
|
+
const mode = String(process.env.BAHULAM_BLOCK_SEPARATOR || 'space').toLowerCase();
|
|
139
139
|
if (mode === 'off' || mode === 'none') return;
|
|
140
140
|
if (mode === 'dotted' || mode === 'dots') {
|
|
141
141
|
const cols = Math.max(24, process.stderr.columns || process.stdout.columns || 80);
|
package/src/ui/icons.mjs
CHANGED
|
@@ -74,6 +74,7 @@ const TOOL_ICON = Object.freeze({
|
|
|
74
74
|
grep: 'search',
|
|
75
75
|
list_files: 'search',
|
|
76
76
|
analyze_code: 'search',
|
|
77
|
+
analyze_image: 'search',
|
|
77
78
|
get_file_info: 'search',
|
|
78
79
|
git_diff: 'search',
|
|
79
80
|
git_status: 'search',
|
|
@@ -84,6 +85,7 @@ const TOOL_ICON = Object.freeze({
|
|
|
84
85
|
write_project: 'write',
|
|
85
86
|
edit_file: 'write',
|
|
86
87
|
delete_file: 'write',
|
|
88
|
+
generate_image: 'write',
|
|
87
89
|
|
|
88
90
|
// Shell / validate
|
|
89
91
|
shell: 'shell',
|
package/src/ui/input-dock.mjs
CHANGED
|
@@ -23,15 +23,15 @@
|
|
|
23
23
|
* ... ← up to MAX_INPUT_ROWS
|
|
24
24
|
* ← spacer
|
|
25
25
|
* ══════════════════════════════════════════════════════ ← bottom rule (colored)
|
|
26
|
-
* deepseek-chat-v3 ·
|
|
26
|
+
* deepseek-chat-v3 · Bahulam ⎇ main · turn 4 ← meta row
|
|
27
27
|
* [Enter] send · [/] commands … ← tips row
|
|
28
28
|
* (one blank row at very bottom for cursor safety)
|
|
29
29
|
*
|
|
30
30
|
* Config:
|
|
31
|
-
* BAHULAM_INPUT_ROWS_MAX
|
|
32
|
-
* BAHULAM_TTY_MODE=stable
|
|
33
|
-
* BAHULAM_PLAIN=1
|
|
34
|
-
* BAHULAM_FIXED_INPUT=0
|
|
31
|
+
* BAHULAM_INPUT_ROWS_MAX 1..12 hard cap on input row growth (default 6)
|
|
32
|
+
* BAHULAM_TTY_MODE=stable scrollback-safe transcript, no fixed dock
|
|
33
|
+
* BAHULAM_PLAIN=1 deterministic no-ANSI output for automation
|
|
34
|
+
* BAHULAM_FIXED_INPUT=0 disable dock entirely (fallback readline)
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
import { paint, width as visibleWidth } from './palette.mjs';
|
|
@@ -214,7 +214,7 @@ function inputTextBudget() {
|
|
|
214
214
|
function resolveMaxInputRows(requested) {
|
|
215
215
|
const raw = requested != null
|
|
216
216
|
? requested
|
|
217
|
-
:
|
|
217
|
+
: process.env.BAHULAM_INPUT_ROWS_MAX;
|
|
218
218
|
const n = Number.parseInt(String(raw), 10);
|
|
219
219
|
if (!Number.isFinite(n)) return DEFAULT_MAX_INPUT_ROWS;
|
|
220
220
|
return Math.max(MIN_INPUT_ROWS, Math.min(MAX_INPUT_ROWS_CAP, n));
|
|
@@ -530,7 +530,7 @@ export function mountInputDock({
|
|
|
530
530
|
const t = term();
|
|
531
531
|
if (!t.isTTY || t.plain) return false;
|
|
532
532
|
if (t.ttyMode !== 'rich' || t.fixedInput === false) return false;
|
|
533
|
-
if (process.env.BAHULAM_FIXED_INPUT === '0'
|
|
533
|
+
if (process.env.BAHULAM_FIXED_INPUT === '0') return false;
|
|
534
534
|
if (mounted) return true;
|
|
535
535
|
|
|
536
536
|
inputRowsMax = resolveMaxInputRows(requestedMax);
|
package/src/ui/palette.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Bahulam palette — semantic color tokens for the CLI.
|
|
3
3
|
*
|
|
4
4
|
* Every feature module imports from here, never from raw ANSI. Tokens resolve
|
|
5
5
|
* at call time so changing terminal capabilities (resize, `refresh()`) is
|