@bahulam/code 0.1.6 → 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.
Files changed (178) hide show
  1. package/LICENSE +1 -1
  2. package/NOTICE +6 -6
  3. package/README.md +10 -17
  4. package/package.json +9 -9
  5. package/src/agents/loader.mjs +1 -1
  6. package/src/agents/workflow_loader.mjs +2 -2
  7. package/src/auth/tarang-auth.mjs +3 -2
  8. package/src/config/model-catalog-default.json +4 -3
  9. package/src/config/model-catalog.mjs +2 -0
  10. package/src/context/prose-chunker.mjs +162 -5
  11. package/src/core/attachments.mjs +65 -3
  12. package/src/core/bundled-runtime.mjs +1 -1
  13. package/src/core/headless.mjs +14 -0
  14. package/src/core/jsonl-writer.mjs +3 -3
  15. package/src/core/lint-resolver.mjs +472 -0
  16. package/src/core/local-agent.mjs +15 -4
  17. package/src/core/local-store.mjs +4 -0
  18. package/src/core/output-filter.mjs +21 -25
  19. package/src/core/pricing.mjs +10 -3
  20. package/src/core/stream-client.mjs +9 -4
  21. package/src/core/tool-executor.mjs +105 -29
  22. package/src/daemon/http-dashboard.mjs +400 -0
  23. package/src/daemon/session-core.mjs +1 -0
  24. package/src/local-service/agent-relay.mjs +868 -0
  25. package/src/local-service/approval-bridge.mjs +222 -0
  26. package/src/local-service/browser.mjs +24 -0
  27. package/src/local-service/command.mjs +155 -0
  28. package/src/local-service/file-access.mjs +393 -0
  29. package/src/local-service/machine.mjs +86 -0
  30. package/src/local-service/paths.mjs +29 -0
  31. package/src/local-service/preview-converters.mjs +260 -0
  32. package/src/local-service/server.mjs +2644 -0
  33. package/src/local-service/session-store.mjs +221 -0
  34. package/src/onboarding/preflight.mjs +1 -1
  35. package/src/terminal/analytics.mjs +2 -6
  36. package/src/terminal/ansi.mjs +11 -3
  37. package/src/terminal/main.mjs +37 -11
  38. package/src/terminal/model-catalog-display.mjs +111 -0
  39. package/src/terminal/repl-format.mjs +4 -1
  40. package/src/terminal/repl-model-form.mjs +81 -14
  41. package/src/terminal/repl-render.mjs +35 -5
  42. package/src/terminal/repl-state.mjs +12 -0
  43. package/src/terminal/repl.mjs +231 -53
  44. package/src/terminal/tool-display.mjs +32 -0
  45. package/src/terminal/watch-state.mjs +118 -0
  46. package/src/tools/analyze-image.mjs +158 -0
  47. package/src/tools/bash.mjs +11 -9
  48. package/src/tools/generate-image.mjs +411 -0
  49. package/src/tools/lint.mjs +24 -43
  50. package/src/tools/registry.mjs +4 -0
  51. package/src/ui/icons.mjs +2 -0
  52. package/src/ui/palette.mjs +1 -1
  53. package/src/ui/slash-commands.mjs +4 -6
  54. package/src/ui/tool-card.mjs +7 -3
  55. package/pulse/app/activity/page.tsx +0 -190
  56. package/pulse/app/api/activity/route.ts +0 -138
  57. package/pulse/app/api/benchmark/route.ts +0 -113
  58. package/pulse/app/api/benchmarks/route.ts +0 -195
  59. package/pulse/app/api/costs/route.ts +0 -88
  60. package/pulse/app/api/export/route.ts +0 -77
  61. package/pulse/app/api/history/route.ts +0 -11
  62. package/pulse/app/api/import/route.ts +0 -31
  63. package/pulse/app/api/memory/route.ts +0 -50
  64. package/pulse/app/api/plans/route.ts +0 -9
  65. package/pulse/app/api/projects/[slug]/route.ts +0 -96
  66. package/pulse/app/api/projects/route.ts +0 -121
  67. package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
  68. package/pulse/app/api/sessions/[id]/route.ts +0 -31
  69. package/pulse/app/api/sessions/route.ts +0 -112
  70. package/pulse/app/api/settings/route.ts +0 -14
  71. package/pulse/app/api/stats/route.ts +0 -143
  72. package/pulse/app/api/todos/route.ts +0 -9
  73. package/pulse/app/api/tools/route.ts +0 -160
  74. package/pulse/app/benchmarks/page.tsx +0 -224
  75. package/pulse/app/costs/page.tsx +0 -179
  76. package/pulse/app/export/page.tsx +0 -465
  77. package/pulse/app/favicon.ico +0 -0
  78. package/pulse/app/globals.css +0 -263
  79. package/pulse/app/help/page.tsx +0 -143
  80. package/pulse/app/history/page.tsx +0 -157
  81. package/pulse/app/layout.tsx +0 -46
  82. package/pulse/app/memory/page.tsx +0 -365
  83. package/pulse/app/overview-client.tsx +0 -393
  84. package/pulse/app/page.tsx +0 -14
  85. package/pulse/app/plans/page.tsx +0 -308
  86. package/pulse/app/projects/[slug]/page.tsx +0 -390
  87. package/pulse/app/projects/page.tsx +0 -110
  88. package/pulse/app/sessions/[id]/page.tsx +0 -243
  89. package/pulse/app/sessions/page.tsx +0 -39
  90. package/pulse/app/settings/page.tsx +0 -188
  91. package/pulse/app/todos/page.tsx +0 -211
  92. package/pulse/app/tools/page.tsx +0 -249
  93. package/pulse/cli.js +0 -164
  94. package/pulse/components/activity/day-of-week-chart.tsx +0 -35
  95. package/pulse/components/activity/streak-card.tsx +0 -36
  96. package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
  97. package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
  98. package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
  99. package/pulse/components/costs/model-token-table.tsx +0 -60
  100. package/pulse/components/global-search.tsx +0 -193
  101. package/pulse/components/keyboard-nav-provider.tsx +0 -23
  102. package/pulse/components/layout/bottom-nav.tsx +0 -53
  103. package/pulse/components/layout/client-layout.tsx +0 -31
  104. package/pulse/components/layout/sidebar-context.tsx +0 -50
  105. package/pulse/components/layout/sidebar.tsx +0 -183
  106. package/pulse/components/layout/top-bar.tsx +0 -121
  107. package/pulse/components/overview/activity-heatmap.tsx +0 -107
  108. package/pulse/components/overview/conversation-table.tsx +0 -148
  109. package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
  110. package/pulse/components/overview/peak-hours-chart.tsx +0 -87
  111. package/pulse/components/overview/project-activity-donut.tsx +0 -96
  112. package/pulse/components/overview/stat-card.tsx +0 -102
  113. package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
  114. package/pulse/components/projects/project-card.tsx +0 -175
  115. package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
  116. package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
  117. package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
  118. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
  119. package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
  120. package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
  121. package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
  122. package/pulse/components/sessions/session-badges.tsx +0 -49
  123. package/pulse/components/sessions/session-table.tsx +0 -299
  124. package/pulse/components/theme-provider.tsx +0 -44
  125. package/pulse/components/tools/feature-adoption-table.tsx +0 -58
  126. package/pulse/components/tools/mcp-server-panel.tsx +0 -45
  127. package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
  128. package/pulse/components/tools/version-history-table.tsx +0 -32
  129. package/pulse/components/ui/alert.tsx +0 -66
  130. package/pulse/components/ui/badge.tsx +0 -48
  131. package/pulse/components/ui/breadcrumb.tsx +0 -109
  132. package/pulse/components/ui/button.tsx +0 -64
  133. package/pulse/components/ui/calendar.tsx +0 -220
  134. package/pulse/components/ui/card.tsx +0 -92
  135. package/pulse/components/ui/command.tsx +0 -158
  136. package/pulse/components/ui/dialog.tsx +0 -158
  137. package/pulse/components/ui/input.tsx +0 -21
  138. package/pulse/components/ui/popover.tsx +0 -89
  139. package/pulse/components/ui/progress.tsx +0 -31
  140. package/pulse/components/ui/select.tsx +0 -190
  141. package/pulse/components/ui/separator.tsx +0 -28
  142. package/pulse/components/ui/sheet.tsx +0 -143
  143. package/pulse/components/ui/skeleton.tsx +0 -13
  144. package/pulse/components/ui/table.tsx +0 -116
  145. package/pulse/components/ui/tabs.tsx +0 -91
  146. package/pulse/components/ui/tooltip.tsx +0 -57
  147. package/pulse/components/use-global-keyboard-nav.ts +0 -79
  148. package/pulse/components.json +0 -23
  149. package/pulse/eslint.config.mjs +0 -18
  150. package/pulse/lib/bahulam-paths.ts +0 -23
  151. package/pulse/lib/claude-reader.ts +0 -592
  152. package/pulse/lib/decode.ts +0 -129
  153. package/pulse/lib/pricing.ts +0 -102
  154. package/pulse/lib/replay-parser.ts +0 -165
  155. package/pulse/lib/tool-categories.ts +0 -140
  156. package/pulse/lib/utils.ts +0 -6
  157. package/pulse/next-env.d.ts +0 -6
  158. package/pulse/next.config.ts +0 -16
  159. package/pulse/package.json +0 -45
  160. package/pulse/postcss.config.mjs +0 -7
  161. package/pulse/public/activity.png +0 -0
  162. package/pulse/public/cc-lens.png +0 -0
  163. package/pulse/public/command-k.png +0 -0
  164. package/pulse/public/costs.png +0 -0
  165. package/pulse/public/dashboard-dark.png +0 -0
  166. package/pulse/public/dashboard-white.png +0 -0
  167. package/pulse/public/export.png +0 -0
  168. package/pulse/public/file.svg +0 -1
  169. package/pulse/public/globe.svg +0 -1
  170. package/pulse/public/next.svg +0 -1
  171. package/pulse/public/projects.png +0 -0
  172. package/pulse/public/session-chat.png +0 -0
  173. package/pulse/public/todos.png +0 -0
  174. package/pulse/public/tools.png +0 -0
  175. package/pulse/public/vercel.svg +0 -1
  176. package/pulse/public/window.svg +0 -1
  177. package/pulse/tsconfig.json +0 -34
  178. 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
+ }
@@ -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
  };
@@ -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',
@@ -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
@@ -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-approve routine tools (dangerous still prompts)',
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
 
@@ -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
- return Number.isFinite(raw) && raw >= 1 ? raw : 2;
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) {