@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.
Files changed (179) 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 +112 -16
  21. package/src/core/tool-executor.mjs +123 -31
  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 +255 -55
  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/project-overview.mjs +5 -2
  51. package/src/tools/registry.mjs +4 -0
  52. package/src/ui/icons.mjs +2 -0
  53. package/src/ui/palette.mjs +1 -1
  54. package/src/ui/slash-commands.mjs +4 -6
  55. package/src/ui/tool-card.mjs +7 -3
  56. package/pulse/app/activity/page.tsx +0 -190
  57. package/pulse/app/api/activity/route.ts +0 -138
  58. package/pulse/app/api/benchmark/route.ts +0 -113
  59. package/pulse/app/api/benchmarks/route.ts +0 -195
  60. package/pulse/app/api/costs/route.ts +0 -88
  61. package/pulse/app/api/export/route.ts +0 -77
  62. package/pulse/app/api/history/route.ts +0 -11
  63. package/pulse/app/api/import/route.ts +0 -31
  64. package/pulse/app/api/memory/route.ts +0 -50
  65. package/pulse/app/api/plans/route.ts +0 -9
  66. package/pulse/app/api/projects/[slug]/route.ts +0 -96
  67. package/pulse/app/api/projects/route.ts +0 -121
  68. package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
  69. package/pulse/app/api/sessions/[id]/route.ts +0 -31
  70. package/pulse/app/api/sessions/route.ts +0 -112
  71. package/pulse/app/api/settings/route.ts +0 -14
  72. package/pulse/app/api/stats/route.ts +0 -143
  73. package/pulse/app/api/todos/route.ts +0 -9
  74. package/pulse/app/api/tools/route.ts +0 -160
  75. package/pulse/app/benchmarks/page.tsx +0 -224
  76. package/pulse/app/costs/page.tsx +0 -179
  77. package/pulse/app/export/page.tsx +0 -465
  78. package/pulse/app/favicon.ico +0 -0
  79. package/pulse/app/globals.css +0 -263
  80. package/pulse/app/help/page.tsx +0 -143
  81. package/pulse/app/history/page.tsx +0 -157
  82. package/pulse/app/layout.tsx +0 -46
  83. package/pulse/app/memory/page.tsx +0 -365
  84. package/pulse/app/overview-client.tsx +0 -393
  85. package/pulse/app/page.tsx +0 -14
  86. package/pulse/app/plans/page.tsx +0 -308
  87. package/pulse/app/projects/[slug]/page.tsx +0 -390
  88. package/pulse/app/projects/page.tsx +0 -110
  89. package/pulse/app/sessions/[id]/page.tsx +0 -243
  90. package/pulse/app/sessions/page.tsx +0 -39
  91. package/pulse/app/settings/page.tsx +0 -188
  92. package/pulse/app/todos/page.tsx +0 -211
  93. package/pulse/app/tools/page.tsx +0 -249
  94. package/pulse/cli.js +0 -164
  95. package/pulse/components/activity/day-of-week-chart.tsx +0 -35
  96. package/pulse/components/activity/streak-card.tsx +0 -36
  97. package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
  98. package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
  99. package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
  100. package/pulse/components/costs/model-token-table.tsx +0 -60
  101. package/pulse/components/global-search.tsx +0 -193
  102. package/pulse/components/keyboard-nav-provider.tsx +0 -23
  103. package/pulse/components/layout/bottom-nav.tsx +0 -53
  104. package/pulse/components/layout/client-layout.tsx +0 -31
  105. package/pulse/components/layout/sidebar-context.tsx +0 -50
  106. package/pulse/components/layout/sidebar.tsx +0 -183
  107. package/pulse/components/layout/top-bar.tsx +0 -121
  108. package/pulse/components/overview/activity-heatmap.tsx +0 -107
  109. package/pulse/components/overview/conversation-table.tsx +0 -148
  110. package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
  111. package/pulse/components/overview/peak-hours-chart.tsx +0 -87
  112. package/pulse/components/overview/project-activity-donut.tsx +0 -96
  113. package/pulse/components/overview/stat-card.tsx +0 -102
  114. package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
  115. package/pulse/components/projects/project-card.tsx +0 -175
  116. package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
  117. package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
  118. package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
  119. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
  120. package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
  121. package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
  122. package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
  123. package/pulse/components/sessions/session-badges.tsx +0 -49
  124. package/pulse/components/sessions/session-table.tsx +0 -299
  125. package/pulse/components/theme-provider.tsx +0 -44
  126. package/pulse/components/tools/feature-adoption-table.tsx +0 -58
  127. package/pulse/components/tools/mcp-server-panel.tsx +0 -45
  128. package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
  129. package/pulse/components/tools/version-history-table.tsx +0 -32
  130. package/pulse/components/ui/alert.tsx +0 -66
  131. package/pulse/components/ui/badge.tsx +0 -48
  132. package/pulse/components/ui/breadcrumb.tsx +0 -109
  133. package/pulse/components/ui/button.tsx +0 -64
  134. package/pulse/components/ui/calendar.tsx +0 -220
  135. package/pulse/components/ui/card.tsx +0 -92
  136. package/pulse/components/ui/command.tsx +0 -158
  137. package/pulse/components/ui/dialog.tsx +0 -158
  138. package/pulse/components/ui/input.tsx +0 -21
  139. package/pulse/components/ui/popover.tsx +0 -89
  140. package/pulse/components/ui/progress.tsx +0 -31
  141. package/pulse/components/ui/select.tsx +0 -190
  142. package/pulse/components/ui/separator.tsx +0 -28
  143. package/pulse/components/ui/sheet.tsx +0 -143
  144. package/pulse/components/ui/skeleton.tsx +0 -13
  145. package/pulse/components/ui/table.tsx +0 -116
  146. package/pulse/components/ui/tabs.tsx +0 -91
  147. package/pulse/components/ui/tooltip.tsx +0 -57
  148. package/pulse/components/use-global-keyboard-nav.ts +0 -79
  149. package/pulse/components.json +0 -23
  150. package/pulse/eslint.config.mjs +0 -18
  151. package/pulse/lib/bahulam-paths.ts +0 -23
  152. package/pulse/lib/claude-reader.ts +0 -592
  153. package/pulse/lib/decode.ts +0 -129
  154. package/pulse/lib/pricing.ts +0 -102
  155. package/pulse/lib/replay-parser.ts +0 -165
  156. package/pulse/lib/tool-categories.ts +0 -140
  157. package/pulse/lib/utils.ts +0 -6
  158. package/pulse/next-env.d.ts +0 -6
  159. package/pulse/next.config.ts +0 -16
  160. package/pulse/package.json +0 -45
  161. package/pulse/postcss.config.mjs +0 -7
  162. package/pulse/public/activity.png +0 -0
  163. package/pulse/public/cc-lens.png +0 -0
  164. package/pulse/public/command-k.png +0 -0
  165. package/pulse/public/costs.png +0 -0
  166. package/pulse/public/dashboard-dark.png +0 -0
  167. package/pulse/public/dashboard-white.png +0 -0
  168. package/pulse/public/export.png +0 -0
  169. package/pulse/public/file.svg +0 -1
  170. package/pulse/public/globe.svg +0 -1
  171. package/pulse/public/next.svg +0 -1
  172. package/pulse/public/projects.png +0 -0
  173. package/pulse/public/session-chat.png +0 -0
  174. package/pulse/public/todos.png +0 -0
  175. package/pulse/public/tools.png +0 -0
  176. package/pulse/public/vercel.svg +0 -1
  177. package/pulse/public/window.svg +0 -1
  178. package/pulse/tsconfig.json +0 -34
  179. 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
  };
@@ -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: 2000,
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 = {};
@@ -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) {