@bahulam/code 0.1.5 → 0.1.7
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 +10 -17
- package/package.json +9 -9
- package/src/agents/loader.mjs +1 -1
- package/src/agents/workflow_loader.mjs +2 -2
- package/src/auth/tarang-auth.mjs +3 -2
- package/src/config/model-catalog-default.json +4 -3
- package/src/config/model-catalog.mjs +2 -0
- package/src/context/prose-chunker.mjs +162 -5
- package/src/core/attachments.mjs +65 -3
- package/src/core/bundled-runtime.mjs +1 -1
- package/src/core/headless.mjs +14 -0
- package/src/core/jsonl-writer.mjs +3 -3
- package/src/core/lint-resolver.mjs +472 -0
- package/src/core/local-agent.mjs +15 -4
- package/src/core/local-store.mjs +4 -0
- package/src/core/output-filter.mjs +21 -25
- package/src/core/pricing.mjs +10 -3
- package/src/core/stream-client.mjs +112 -16
- package/src/core/tool-executor.mjs +123 -31
- 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/onboarding/preflight.mjs +1 -1
- package/src/terminal/analytics.mjs +2 -6
- package/src/terminal/ansi.mjs +11 -3
- package/src/terminal/main.mjs +37 -11
- 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 +35 -5
- package/src/terminal/repl-state.mjs +12 -0
- package/src/terminal/repl.mjs +255 -55
- package/src/terminal/tool-display.mjs +32 -0
- package/src/terminal/watch-state.mjs +118 -0
- 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 +5 -2
- package/src/tools/registry.mjs +4 -0
- package/src/ui/icons.mjs +2 -0
- package/src/ui/palette.mjs +1 -1
- package/src/ui/slash-commands.mjs +4 -6
- package/src/ui/tool-card.mjs +7 -3
- 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 Kepler 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 { TarangAuth } from '../auth/tarang-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 TarangAuth().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': 'kepler-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
|
};
|
|
@@ -54,6 +54,10 @@ const IGNORED_DIRS = new Set([
|
|
|
54
54
|
'.git', '.bahulam', '.next', '.venv', '__pycache__',
|
|
55
55
|
'build', 'dist', 'node_modules', 'venv',
|
|
56
56
|
]);
|
|
57
|
+
const ENV_PROBE_TIMEOUT_MS = Math.max(
|
|
58
|
+
50,
|
|
59
|
+
Number(process.env.BAHULAM_ENV_PROBE_TIMEOUT_MS || 500) || 500,
|
|
60
|
+
);
|
|
57
61
|
|
|
58
62
|
// Files or directories whose presence at the root implies this IS a project.
|
|
59
63
|
// One is enough. Kept broad so we accept Node/Python/Rust/Go/Ruby/Java/C++
|
|
@@ -227,7 +231,7 @@ function commandVersion(command, args = ['--version']) {
|
|
|
227
231
|
try {
|
|
228
232
|
const result = spawnSync(command, args, {
|
|
229
233
|
encoding: 'utf-8',
|
|
230
|
-
timeout:
|
|
234
|
+
timeout: ENV_PROBE_TIMEOUT_MS,
|
|
231
235
|
windowsHide: true,
|
|
232
236
|
});
|
|
233
237
|
if (result.error || result.status !== 0) return '';
|
|
@@ -244,7 +248,6 @@ function detectEnvironment() {
|
|
|
244
248
|
['git', 'git'],
|
|
245
249
|
['npm', 'npm'],
|
|
246
250
|
['uv', 'uv'],
|
|
247
|
-
['pytest', 'pytest'],
|
|
248
251
|
['docker', 'docker'],
|
|
249
252
|
];
|
|
250
253
|
const tools = {};
|
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/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/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
|
|
@@ -52,12 +52,12 @@ export const COMMANDS = {
|
|
|
52
52
|
'/review': 'Code review agent',
|
|
53
53
|
'/architect': 'Feature architect agent',
|
|
54
54
|
'/safety': 'Show safety guardrail status',
|
|
55
|
-
'/auto': 'Session autopilot: auto
|
|
56
|
-
'/approvals': 'List or edit session approval grants',
|
|
57
|
-
'/revoke': 'Revoke auto-approvals',
|
|
55
|
+
'/auto': 'Session autopilot: `/auto` routine-only · `/auto full` everything · `/auto off`',
|
|
56
|
+
'/approvals': 'List or edit session approval grants (`/approvals clear` revokes all)',
|
|
58
57
|
'/resume': 'Resume a previous session',
|
|
59
58
|
'/sessions': 'List resumable sessions',
|
|
60
59
|
'/logout': 'Sign out and clear credentials',
|
|
60
|
+
'/watch': 'Toggle live agent activity tracking panel',
|
|
61
61
|
'/exit': 'Exit CLI',
|
|
62
62
|
};
|
|
63
63
|
|
|
@@ -118,7 +118,6 @@ export const HELP_GROUPS = [
|
|
|
118
118
|
['/settings logout', 'Sign out'],
|
|
119
119
|
['/settings whoami', 'Current user'],
|
|
120
120
|
['/settings quiet|verbose|surgical', 'Verbosity'],
|
|
121
|
-
['/settings revoke', 'Revoke auto-approvals'],
|
|
122
121
|
],
|
|
123
122
|
},
|
|
124
123
|
{
|
|
@@ -138,6 +137,7 @@ export const HELP_GROUPS = [
|
|
|
138
137
|
title: 'Agents',
|
|
139
138
|
summary: 'specialist modes',
|
|
140
139
|
commands: [
|
|
140
|
+
['/watch', 'Toggle live agent tracking panel'],
|
|
141
141
|
['/agents', 'List built-in and local agents'],
|
|
142
142
|
['/agents create <name>', 'Create .bahulam/agents/<name>.yaml'],
|
|
143
143
|
['/agents edit <name>', 'Open local agent YAML'],
|
|
@@ -207,7 +207,6 @@ export const LEGACY_COMMAND_HINTS = {
|
|
|
207
207
|
'/quiet': '/settings quiet',
|
|
208
208
|
'/verbose': '/settings verbose',
|
|
209
209
|
'/surgical': '/settings surgical',
|
|
210
|
-
'/revoke': '/settings revoke',
|
|
211
210
|
};
|
|
212
211
|
|
|
213
212
|
// Namespaced subcommands. `/status metrics` -> `/stats`, etc.
|
|
@@ -235,7 +234,6 @@ export const NAMESPACED_COMMANDS = {
|
|
|
235
234
|
quiet: '/quiet',
|
|
236
235
|
verbose: '/verbose',
|
|
237
236
|
surgical: '/surgical',
|
|
238
|
-
revoke: '/revoke',
|
|
239
237
|
},
|
|
240
238
|
};
|
|
241
239
|
|
package/src/ui/tool-card.mjs
CHANGED
|
@@ -159,7 +159,7 @@ export function summarizeResult(tool, data) {
|
|
|
159
159
|
// Multi-row preview: first N rows + a "+ M more" tail so long
|
|
160
160
|
// outputs (e.g. `ls`) surface their scale instead of collapsing
|
|
161
161
|
// to a single first line with no hint that more exists.
|
|
162
|
-
const { preview, remaining } = outputPreviewRows(data, shellPreviewRows());
|
|
162
|
+
const { preview, remaining } = outputPreviewRows(data, shellPreviewRows(data));
|
|
163
163
|
if (!preview) return { text: 'ok', tone: 'success' };
|
|
164
164
|
if (remaining === 0) return { text: preview, tone: 'success' };
|
|
165
165
|
const tail = paint.text.dim(`+ ${remaining} more row${remaining === 1 ? '' : 's'}`);
|
|
@@ -434,9 +434,13 @@ function outputPreviewRows(data, n, perRow = 200) {
|
|
|
434
434
|
// Default rows shown in the shell result preview. Overridable via env for
|
|
435
435
|
// power users; keep it small — the result line rides above every command
|
|
436
436
|
// and eats vertical space in a long session.
|
|
437
|
-
function shellPreviewRows() {
|
|
437
|
+
function shellPreviewRows(data = {}) {
|
|
438
438
|
const raw = parseInt(process.env.BAHULAM_SHELL_PREVIEW_ROWS ?? '', 10);
|
|
439
|
-
|
|
439
|
+
if (Number.isFinite(raw) && raw >= 1) return raw;
|
|
440
|
+
const command = String(data?.args?.command || data?.command || '');
|
|
441
|
+
if (/^\s*git\s+(diff|show)\b/i.test(command)) return 8;
|
|
442
|
+
if (/^\s*git\s+status\b/i.test(command)) return 8;
|
|
443
|
+
return 2;
|
|
440
444
|
}
|
|
441
445
|
|
|
442
446
|
function lineCount(s) {
|