@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.
Files changed (204) hide show
  1. package/LICENSE +1 -1
  2. package/NOTICE +6 -6
  3. package/README.md +11 -18
  4. package/package.json +11 -11
  5. package/src/agents/loader.mjs +1 -1
  6. package/src/agents/workflow_loader.mjs +2 -2
  7. package/src/auth/{tarang-auth.mjs → bahulam-auth.mjs} +4 -3
  8. package/src/commands/agent.mjs +3 -3
  9. package/src/commands/device.mjs +2 -2
  10. package/src/commands/pair.mjs +2 -2
  11. package/src/commands/remote.mjs +3 -3
  12. package/src/commands/workflow.mjs +5 -5
  13. package/src/config/env.mjs +9 -2
  14. package/src/config/memory-loader.mjs +1 -1
  15. package/src/config/model-catalog-default.json +4 -3
  16. package/src/config/model-catalog.mjs +2 -0
  17. package/src/config/settings.mjs +2 -2
  18. package/src/context/prose-chunker.mjs +162 -5
  19. package/src/core/agent-loop.mjs +17 -0
  20. package/src/core/attachments.mjs +66 -4
  21. package/src/core/backend-url.mjs +11 -12
  22. package/src/core/bundled-runtime.mjs +1 -1
  23. package/src/core/callback-client.mjs +1 -1
  24. package/src/core/headless.mjs +18 -4
  25. package/src/core/jsonl-writer.mjs +18 -18
  26. package/src/core/lint-resolver.mjs +472 -0
  27. package/src/core/local-agent.mjs +40 -8
  28. package/src/core/local-store.mjs +5 -1
  29. package/src/core/mode-selector.mjs +1 -1
  30. package/src/core/output-filter.mjs +22 -26
  31. package/src/core/pricing.mjs +10 -3
  32. package/src/core/project-artifacts.mjs +3 -3
  33. package/src/core/project-context-loader.mjs +9 -9
  34. package/src/core/settings-sync.mjs +1 -1
  35. package/src/core/stagnation.mjs +61 -1
  36. package/src/core/stream-client.mjs +13 -8
  37. package/src/core/system-prompt.mjs +2 -2
  38. package/src/core/tool-executor.mjs +178 -84
  39. package/src/daemon/http-dashboard.mjs +400 -0
  40. package/src/daemon/session-core.mjs +1 -0
  41. package/src/local-service/agent-relay.mjs +868 -0
  42. package/src/local-service/approval-bridge.mjs +222 -0
  43. package/src/local-service/browser.mjs +24 -0
  44. package/src/local-service/command.mjs +155 -0
  45. package/src/local-service/file-access.mjs +393 -0
  46. package/src/local-service/machine.mjs +86 -0
  47. package/src/local-service/paths.mjs +29 -0
  48. package/src/local-service/preview-converters.mjs +260 -0
  49. package/src/local-service/server.mjs +2644 -0
  50. package/src/local-service/session-store.mjs +221 -0
  51. package/src/mcp/client.mjs +4 -4
  52. package/src/onboarding/preflight.mjs +2 -2
  53. package/src/terminal/agents.mjs +2 -2
  54. package/src/terminal/analytics.mjs +2 -6
  55. package/src/terminal/ansi.mjs +11 -3
  56. package/src/terminal/main.mjs +43 -17
  57. package/src/terminal/model-catalog-display.mjs +111 -0
  58. package/src/terminal/repl-format.mjs +4 -1
  59. package/src/terminal/repl-model-form.mjs +81 -14
  60. package/src/terminal/repl-render.mjs +37 -7
  61. package/src/terminal/repl-resume.mjs +2 -2
  62. package/src/terminal/repl-state.mjs +12 -0
  63. package/src/terminal/repl.mjs +248 -70
  64. package/src/terminal/tool-display.mjs +32 -0
  65. package/src/terminal/watch-state.mjs +118 -0
  66. package/src/tools/agent.mjs +2 -2
  67. package/src/tools/analyze-image.mjs +158 -0
  68. package/src/tools/bash.mjs +11 -9
  69. package/src/tools/generate-image.mjs +411 -0
  70. package/src/tools/lint.mjs +24 -43
  71. package/src/tools/project-overview.mjs +12 -12
  72. package/src/tools/registry.mjs +4 -0
  73. package/src/ui/formatter.mjs +2 -2
  74. package/src/ui/icons.mjs +2 -0
  75. package/src/ui/input-dock.mjs +7 -7
  76. package/src/ui/palette.mjs +1 -1
  77. package/src/ui/slash-commands.mjs +4 -6
  78. package/src/ui/spinner.mjs +1 -1
  79. package/src/ui/term.mjs +2 -6
  80. package/src/ui/tool-card.mjs +81 -6
  81. package/pulse/app/activity/page.tsx +0 -190
  82. package/pulse/app/api/activity/route.ts +0 -138
  83. package/pulse/app/api/benchmark/route.ts +0 -113
  84. package/pulse/app/api/benchmarks/route.ts +0 -195
  85. package/pulse/app/api/costs/route.ts +0 -88
  86. package/pulse/app/api/export/route.ts +0 -77
  87. package/pulse/app/api/history/route.ts +0 -11
  88. package/pulse/app/api/import/route.ts +0 -31
  89. package/pulse/app/api/memory/route.ts +0 -50
  90. package/pulse/app/api/plans/route.ts +0 -9
  91. package/pulse/app/api/projects/[slug]/route.ts +0 -96
  92. package/pulse/app/api/projects/route.ts +0 -121
  93. package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
  94. package/pulse/app/api/sessions/[id]/route.ts +0 -31
  95. package/pulse/app/api/sessions/route.ts +0 -112
  96. package/pulse/app/api/settings/route.ts +0 -14
  97. package/pulse/app/api/stats/route.ts +0 -143
  98. package/pulse/app/api/todos/route.ts +0 -9
  99. package/pulse/app/api/tools/route.ts +0 -160
  100. package/pulse/app/benchmarks/page.tsx +0 -224
  101. package/pulse/app/costs/page.tsx +0 -179
  102. package/pulse/app/export/page.tsx +0 -465
  103. package/pulse/app/favicon.ico +0 -0
  104. package/pulse/app/globals.css +0 -263
  105. package/pulse/app/help/page.tsx +0 -143
  106. package/pulse/app/history/page.tsx +0 -157
  107. package/pulse/app/layout.tsx +0 -46
  108. package/pulse/app/memory/page.tsx +0 -365
  109. package/pulse/app/overview-client.tsx +0 -393
  110. package/pulse/app/page.tsx +0 -14
  111. package/pulse/app/plans/page.tsx +0 -308
  112. package/pulse/app/projects/[slug]/page.tsx +0 -390
  113. package/pulse/app/projects/page.tsx +0 -110
  114. package/pulse/app/sessions/[id]/page.tsx +0 -243
  115. package/pulse/app/sessions/page.tsx +0 -39
  116. package/pulse/app/settings/page.tsx +0 -188
  117. package/pulse/app/todos/page.tsx +0 -211
  118. package/pulse/app/tools/page.tsx +0 -249
  119. package/pulse/cli.js +0 -164
  120. package/pulse/components/activity/day-of-week-chart.tsx +0 -35
  121. package/pulse/components/activity/streak-card.tsx +0 -36
  122. package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
  123. package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
  124. package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
  125. package/pulse/components/costs/model-token-table.tsx +0 -60
  126. package/pulse/components/global-search.tsx +0 -193
  127. package/pulse/components/keyboard-nav-provider.tsx +0 -23
  128. package/pulse/components/layout/bottom-nav.tsx +0 -53
  129. package/pulse/components/layout/client-layout.tsx +0 -31
  130. package/pulse/components/layout/sidebar-context.tsx +0 -50
  131. package/pulse/components/layout/sidebar.tsx +0 -183
  132. package/pulse/components/layout/top-bar.tsx +0 -121
  133. package/pulse/components/overview/activity-heatmap.tsx +0 -107
  134. package/pulse/components/overview/conversation-table.tsx +0 -148
  135. package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
  136. package/pulse/components/overview/peak-hours-chart.tsx +0 -87
  137. package/pulse/components/overview/project-activity-donut.tsx +0 -96
  138. package/pulse/components/overview/stat-card.tsx +0 -102
  139. package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
  140. package/pulse/components/projects/project-card.tsx +0 -175
  141. package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
  142. package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
  143. package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
  144. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
  145. package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
  146. package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
  147. package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
  148. package/pulse/components/sessions/session-badges.tsx +0 -49
  149. package/pulse/components/sessions/session-table.tsx +0 -299
  150. package/pulse/components/theme-provider.tsx +0 -44
  151. package/pulse/components/tools/feature-adoption-table.tsx +0 -58
  152. package/pulse/components/tools/mcp-server-panel.tsx +0 -45
  153. package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
  154. package/pulse/components/tools/version-history-table.tsx +0 -32
  155. package/pulse/components/ui/alert.tsx +0 -66
  156. package/pulse/components/ui/badge.tsx +0 -48
  157. package/pulse/components/ui/breadcrumb.tsx +0 -109
  158. package/pulse/components/ui/button.tsx +0 -64
  159. package/pulse/components/ui/calendar.tsx +0 -220
  160. package/pulse/components/ui/card.tsx +0 -92
  161. package/pulse/components/ui/command.tsx +0 -158
  162. package/pulse/components/ui/dialog.tsx +0 -158
  163. package/pulse/components/ui/input.tsx +0 -21
  164. package/pulse/components/ui/popover.tsx +0 -89
  165. package/pulse/components/ui/progress.tsx +0 -31
  166. package/pulse/components/ui/select.tsx +0 -190
  167. package/pulse/components/ui/separator.tsx +0 -28
  168. package/pulse/components/ui/sheet.tsx +0 -143
  169. package/pulse/components/ui/skeleton.tsx +0 -13
  170. package/pulse/components/ui/table.tsx +0 -116
  171. package/pulse/components/ui/tabs.tsx +0 -91
  172. package/pulse/components/ui/tooltip.tsx +0 -57
  173. package/pulse/components/use-global-keyboard-nav.ts +0 -79
  174. package/pulse/components.json +0 -23
  175. package/pulse/eslint.config.mjs +0 -18
  176. package/pulse/lib/bahulam-paths.ts +0 -23
  177. package/pulse/lib/claude-reader.ts +0 -592
  178. package/pulse/lib/decode.ts +0 -129
  179. package/pulse/lib/pricing.ts +0 -102
  180. package/pulse/lib/replay-parser.ts +0 -165
  181. package/pulse/lib/tool-categories.ts +0 -140
  182. package/pulse/lib/utils.ts +0 -6
  183. package/pulse/next-env.d.ts +0 -6
  184. package/pulse/next.config.ts +0 -16
  185. package/pulse/package.json +0 -45
  186. package/pulse/postcss.config.mjs +0 -7
  187. package/pulse/public/activity.png +0 -0
  188. package/pulse/public/cc-lens.png +0 -0
  189. package/pulse/public/command-k.png +0 -0
  190. package/pulse/public/costs.png +0 -0
  191. package/pulse/public/dashboard-dark.png +0 -0
  192. package/pulse/public/dashboard-white.png +0 -0
  193. package/pulse/public/export.png +0 -0
  194. package/pulse/public/file.svg +0 -1
  195. package/pulse/public/globe.svg +0 -1
  196. package/pulse/public/next.svg +0 -1
  197. package/pulse/public/projects.png +0 -0
  198. package/pulse/public/session-chat.png +0 -0
  199. package/pulse/public/todos.png +0 -0
  200. package/pulse/public/tools.png +0 -0
  201. package/pulse/public/vercel.svg +0 -1
  202. package/pulse/public/window.svg +0 -1
  203. package/pulse/tsconfig.json +0 -34
  204. 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
+ }
@@ -1,37 +1,20 @@
1
1
  /**
2
- * Lint Tool run linters and type checkers.
2
+ * Legacy lint tool wrapper.
3
3
  *
4
- * Always safe: read-only, no side effects (unless --fix).
5
- * Agent should use this instead of shell("ruff check .").
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 a linter or type checker. Fast, read-only (unless --fix). Use this instead of Bash for code checking.',
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: 'Apply auto-fixes (default: false)',
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
- const errors = [];
50
- if (!input.tool) errors.push('tool is required');
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 command = linter.cmd(targetPath, input.fix);
60
-
61
- const result = await BashTool.call({
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 result;
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
- // Metadata for execution policy
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(keplerDir) {
326
- const skillsDir = path.join(keplerDir, 'skills');
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 keplerDir = projectConfigDir(root);
405
+ const bahulamDir = projectConfigDir(root);
406
406
  resource.environment = detectEnvironment();
407
- resource.project_context = _readIfExists(keplerDir, 'KEPLER.md', 10000) ||
407
+ resource.project_context = _readIfExists(bahulamDir, 'KEPLER.md', 10000) ||
408
408
  _readIfExists(root, 'KEPLER.md', 10000) ||
409
- _readIfExists(keplerDir, 'project.md', 8000);
410
- resource.style = _readIfExists(keplerDir, 'style.md', 4000);
411
- resource.goal = _readIfExists(keplerDir, 'goal.md', 2000);
412
- resource.plan = _readIfExists(keplerDir, 'plan.md', 6000);
413
- resource.skills_index = _scanSkills(keplerDir);
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 ("Tarang\ Orca") or 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. "Tarang\ Orca/src/app/\(kepler\)/page.tsx". Normalize here so
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 "Tarang\ Orca"
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.
@@ -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,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Formatter — Clean terminal output for Tarang events.
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.KEPLER_BLOCK_SEPARATOR || 'space').toLowerCase();
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',
@@ -23,15 +23,15 @@
23
23
  * ... ← up to MAX_INPUT_ROWS
24
24
  * ← spacer
25
25
  * ══════════════════════════════════════════════════════ ← bottom rule (colored)
26
- * deepseek-chat-v3 · Tarang-Orca ⎇ main · turn 4 ← meta row
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 / KEPLER_INPUT_ROWS_MAX 1..12 hard cap on input row growth (default 6)
32
- * BAHULAM_TTY_MODE=stable / KEPLER_TTY_MODE=stable scrollback-safe transcript, no fixed dock
33
- * BAHULAM_PLAIN=1 / KEPLER_PLAIN=1 deterministic no-ANSI output for automation
34
- * BAHULAM_FIXED_INPUT=0 / KEPLER_FIXED_INPUT=0 disable dock entirely (fallback readline)
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
- : (process.env.BAHULAM_INPUT_ROWS_MAX || process.env.KEPLER_INPUT_ROWS_MAX);
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' || process.env.KEPLER_FIXED_INPUT === '0') return false;
533
+ if (process.env.BAHULAM_FIXED_INPUT === '0') return false;
534
534
  if (mounted) return true;
535
535
 
536
536
  inputRowsMax = resolveMaxInputRows(requestedMax);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Kepler palette — semantic color tokens for the CLI.
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