@suman_biswas/beam 0.1.0

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.
@@ -0,0 +1,566 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { exec as execCallback } from "child_process";
4
+ import { promisify } from "util";
5
+ const exec = promisify(execCallback);
6
+ const COMMAND_TIMEOUT_MS = 30_000;
7
+ const MAX_COMMAND_OUTPUT_LENGTH = 12_000;
8
+ const MAX_GREP_RESULTS = 200;
9
+ const MAX_WEB_RESULTS = 10;
10
+ const MAX_FETCH_TEXT_LENGTH = 20_000;
11
+ const BLOCKED_DELETE_PATH_PARTS = new Set([".git", "node_modules", ".vscode"]);
12
+ function truncateOutput(value) {
13
+ if (value.length <= MAX_COMMAND_OUTPUT_LENGTH) {
14
+ return value;
15
+ }
16
+ return `${value.slice(0, MAX_COMMAND_OUTPUT_LENGTH)}\n...[truncated]`;
17
+ }
18
+ function toRelativeWorkspacePath(workspaceFolder, absolutePath) {
19
+ return path.relative(workspaceFolder, absolutePath) || ".";
20
+ }
21
+ function globPatternToRegExp(pattern) {
22
+ const normalizedPattern = pattern.replace(/\\/g, "/");
23
+ let regex = "^";
24
+ for (let i = 0; i < normalizedPattern.length; i++) {
25
+ const char = normalizedPattern[i];
26
+ if (char === "*") {
27
+ const next = normalizedPattern[i + 1];
28
+ if (next === "*") {
29
+ i++;
30
+ if (normalizedPattern[i + 1] === "/") {
31
+ i++;
32
+ regex += "(?:.*\\/)?";
33
+ }
34
+ else {
35
+ regex += ".*";
36
+ }
37
+ }
38
+ else {
39
+ regex += "[^/]*";
40
+ }
41
+ continue;
42
+ }
43
+ if (char === "?") {
44
+ regex += "[^/]";
45
+ continue;
46
+ }
47
+ if ("\\^$.|+()[]{}".includes(char)) {
48
+ regex += `\\${char}`;
49
+ continue;
50
+ }
51
+ regex += char;
52
+ }
53
+ regex += "$";
54
+ return new RegExp(regex);
55
+ }
56
+ function isDangerousCommand(command) {
57
+ const normalized = command.trim().toLowerCase();
58
+ const blockedPatterns = [
59
+ /\bsudo\b/,
60
+ /\brm\s+-rf\b/,
61
+ /\bgit\s+reset\s+--hard\b/,
62
+ /\bgit\s+clean\s+-fd\b/,
63
+ /\bshutdown\b/,
64
+ /\breboot\b/,
65
+ /\bmkfs\b/,
66
+ /\bdd\s+if=/,
67
+ ];
68
+ return blockedPatterns.some((pattern) => pattern.test(normalized));
69
+ }
70
+ function matchesGitignorePattern(filePath, pattern) {
71
+ const cleanPattern = pattern.replace(/\/$/, "");
72
+ if (filePath === cleanPattern) {
73
+ return true;
74
+ }
75
+ if (filePath.split("/").includes(cleanPattern)) {
76
+ return true;
77
+ }
78
+ if (cleanPattern.includes("*")) {
79
+ const regex = new RegExp("^" + cleanPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$");
80
+ if (regex.test(filePath)) {
81
+ return true;
82
+ }
83
+ }
84
+ return false;
85
+ }
86
+ function decodeHtmlEntities(value) {
87
+ const namedEntities = {
88
+ amp: "&",
89
+ lt: "<",
90
+ gt: ">",
91
+ quot: '"',
92
+ apos: "'",
93
+ nbsp: " ",
94
+ };
95
+ return value
96
+ .replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)))
97
+ .replace(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCharCode(Number.parseInt(code, 16)))
98
+ .replace(/&([a-z]+);/gi, (_, name) => namedEntities[name.toLowerCase()] ?? `&${name};`);
99
+ }
100
+ function stripHtmlTags(value) {
101
+ return decodeHtmlEntities(value
102
+ .replace(/<script[\s\S]*?<\/script>/gi, " ")
103
+ .replace(/<style[\s\S]*?<\/style>/gi, " ")
104
+ .replace(/<noscript[\s\S]*?<\/noscript>/gi, " ")
105
+ .replace(/<\/(p|div|section|article|header|footer|main|li|tr|td|th|blockquote|pre|h[1-6])>/gi, "\n")
106
+ .replace(/<br\s*\/?>/gi, "\n")
107
+ .replace(/<[^>]+>/g, " "))
108
+ .replace(/\r/g, "")
109
+ .replace(/[ \t]+\n/g, "\n")
110
+ .replace(/\n{3,}/g, "\n\n")
111
+ .trim();
112
+ }
113
+ function extractHtmlTitle(html) {
114
+ const match = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
115
+ if (!match?.[1]) {
116
+ return undefined;
117
+ }
118
+ return stripHtmlTags(match[1]).trim() || undefined;
119
+ }
120
+ function normalizeWebUrl(input) {
121
+ if (/^https?:\/\//i.test(input)) {
122
+ return input;
123
+ }
124
+ return `https://${input}`;
125
+ }
126
+ function extractSearchUrl(href) {
127
+ try {
128
+ const parsed = new URL(href, "https://duckduckgo.com");
129
+ const redirectTarget = parsed.searchParams.get("uddg");
130
+ if (redirectTarget) {
131
+ return decodeURIComponent(redirectTarget);
132
+ }
133
+ return parsed.toString();
134
+ }
135
+ catch {
136
+ return href;
137
+ }
138
+ }
139
+ /**
140
+ * Get list of all project file paths as relative paths.
141
+ * Returns a flat array of file paths (no directories).
142
+ * Respects .gitignore patterns and hardcoded ignore list.
143
+ */
144
+ export function getProjectAwareness(rootDir) {
145
+ try {
146
+ const IGNORED_FOLDERS = new Set([
147
+ "node_modules",
148
+ ".git",
149
+ "dist",
150
+ "out",
151
+ "build",
152
+ ".next",
153
+ "coverage",
154
+ ".turbo",
155
+ "public",
156
+ "media",
157
+ "assets",
158
+ ".gitignore",
159
+ ".vscode",
160
+ ".DS_Store",
161
+ ".npmrc",
162
+ ".prettierignore",
163
+ ".eslintignore",
164
+ "yarn.lock",
165
+ "package-lock.json",
166
+ ]);
167
+ let gitignorePatterns = [];
168
+ try {
169
+ const gitignorePath = path.join(rootDir, ".gitignore");
170
+ const gitignoreContent = fs.readFileSync(gitignorePath, "utf8");
171
+ gitignorePatterns = gitignoreContent
172
+ .split("\n")
173
+ .map((line) => line.trim())
174
+ .filter((line) => line && !line.startsWith("#"));
175
+ }
176
+ catch {
177
+ // ignore missing .gitignore
178
+ }
179
+ const shouldIgnore = (name, relPath) => {
180
+ if (IGNORED_FOLDERS.has(name)) {
181
+ return true;
182
+ }
183
+ for (const pattern of gitignorePatterns) {
184
+ if (matchesGitignorePattern(relPath, pattern)) {
185
+ return true;
186
+ }
187
+ }
188
+ return false;
189
+ };
190
+ const filePaths = [];
191
+ function walk(dir, relativePath = "") {
192
+ let entries;
193
+ try {
194
+ entries = fs.readdirSync(dir, { withFileTypes: true });
195
+ }
196
+ catch {
197
+ return;
198
+ }
199
+ entries.forEach((entry) => {
200
+ const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
201
+ if (shouldIgnore(entry.name, relPath)) {
202
+ return;
203
+ }
204
+ const fullPath = path.join(dir, entry.name);
205
+ if (entry.isDirectory()) {
206
+ walk(fullPath, relPath);
207
+ return;
208
+ }
209
+ if (entry.isFile()) {
210
+ filePaths.push(relPath);
211
+ }
212
+ });
213
+ }
214
+ walk(rootDir);
215
+ return filePaths.sort();
216
+ }
217
+ catch (error) {
218
+ console.error("Error reading project paths:", error);
219
+ return [];
220
+ }
221
+ }
222
+ /**
223
+ * Read a file or list a directory.
224
+ */
225
+ export async function readPath(targetPath, cwd) {
226
+ try {
227
+ const resolvedPath = path.resolve(cwd, targetPath);
228
+ const relativePath = path.relative(cwd, resolvedPath);
229
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
230
+ return { success: false, error: "Path must stay within the workspace folder", targetPath };
231
+ }
232
+ const stat = fs.statSync(resolvedPath);
233
+ if (stat.isDirectory()) {
234
+ try {
235
+ const entries = fs.readdirSync(resolvedPath);
236
+ const directoryEntries = entries
237
+ .map((name) => ({
238
+ name,
239
+ type: fs.statSync(path.join(resolvedPath, name)).isDirectory() ? "directory" : "file",
240
+ }))
241
+ .sort((a, b) => a.name.localeCompare(b.name));
242
+ return {
243
+ success: true,
244
+ path: relativePath,
245
+ type: "directory",
246
+ entries: directoryEntries,
247
+ };
248
+ }
249
+ catch (error) {
250
+ const errorMessage = error instanceof Error ? error.message : String(error);
251
+ return { success: false, error: errorMessage, targetPath };
252
+ }
253
+ }
254
+ const content = fs.readFileSync(resolvedPath, "utf8");
255
+ return {
256
+ success: true,
257
+ path: relativePath,
258
+ type: "file",
259
+ content,
260
+ };
261
+ }
262
+ catch (error) {
263
+ const errorMessage = error instanceof Error ? error.message : String(error);
264
+ return { success: false, error: errorMessage, targetPath };
265
+ }
266
+ }
267
+ /**
268
+ * Write content to a file.
269
+ */
270
+ export async function writePath(filePath, content, cwd) {
271
+ try {
272
+ const resolvedPath = path.resolve(cwd, filePath);
273
+ const relativePath = path.relative(cwd, resolvedPath);
274
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
275
+ return { success: false, error: "Path must stay within the workspace folder", filePath };
276
+ }
277
+ const parentDir = path.dirname(resolvedPath);
278
+ fs.mkdirSync(parentDir, { recursive: true });
279
+ fs.writeFileSync(resolvedPath, content, "utf8");
280
+ return {
281
+ success: true,
282
+ message: "File written successfully",
283
+ filePath: relativePath,
284
+ };
285
+ }
286
+ catch (error) {
287
+ const errorMessage = error instanceof Error ? error.message : String(error);
288
+ return { success: false, error: errorMessage, filePath };
289
+ }
290
+ }
291
+ /**
292
+ * Edit a file by replacing literal text.
293
+ */
294
+ export async function editPath(filePath, searchValue, replaceValue, cwd) {
295
+ try {
296
+ const resolvedPath = path.resolve(cwd, filePath);
297
+ const relativePath = path.relative(cwd, resolvedPath);
298
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
299
+ return { success: false, error: "Path must stay within the workspace folder", filePath };
300
+ }
301
+ const content = fs.readFileSync(resolvedPath, "utf8");
302
+ const updated = content.split(searchValue).join(replaceValue);
303
+ fs.writeFileSync(resolvedPath, updated, "utf8");
304
+ return {
305
+ success: true,
306
+ message: "File edited successfully",
307
+ filePath: relativePath,
308
+ };
309
+ }
310
+ catch (error) {
311
+ const errorMessage = error instanceof Error ? error.message : String(error);
312
+ return { success: false, error: errorMessage, filePath };
313
+ }
314
+ }
315
+ /**
316
+ * Delete a file or directory.
317
+ */
318
+ export async function deletePath(targetPath, cwd) {
319
+ try {
320
+ const resolvedPath = path.resolve(cwd, targetPath);
321
+ const relativePath = path.relative(cwd, resolvedPath);
322
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
323
+ return { success: false, error: "Path must stay within the workspace folder", targetPath };
324
+ }
325
+ const pathParts = relativePath.split(path.sep);
326
+ if (pathParts.some((part) => BLOCKED_DELETE_PATH_PARTS.has(part))) {
327
+ return {
328
+ success: false,
329
+ error: "Deleting files from protected paths is blocked",
330
+ path: relativePath,
331
+ };
332
+ }
333
+ const stat = fs.statSync(resolvedPath);
334
+ fs.rmSync(resolvedPath, { recursive: true, force: true });
335
+ return {
336
+ success: true,
337
+ path: relativePath,
338
+ type: stat.isDirectory() ? "directory" : "file",
339
+ message: "Path deleted",
340
+ };
341
+ }
342
+ catch (error) {
343
+ const errorMessage = error instanceof Error ? error.message : String(error);
344
+ return { success: false, error: errorMessage, targetPath };
345
+ }
346
+ }
347
+ /**
348
+ * Find files matching a glob pattern.
349
+ */
350
+ export function glob(pattern, cwd) {
351
+ try {
352
+ const regex = globPatternToRegExp(pattern);
353
+ const matches = getProjectAwareness(cwd).filter((filePath) => regex.test(filePath));
354
+ return {
355
+ success: true,
356
+ pattern,
357
+ matches,
358
+ };
359
+ }
360
+ catch (error) {
361
+ const errorMessage = error instanceof Error ? error.message : String(error);
362
+ return { success: false, error: errorMessage, pattern };
363
+ }
364
+ }
365
+ /**
366
+ * Search file contents using a regular expression.
367
+ */
368
+ export function grep(pattern, flags = "gm", cwd) {
369
+ try {
370
+ let regex;
371
+ try {
372
+ regex = new RegExp(pattern, flags.includes("g") ? flags : `${flags}g`);
373
+ }
374
+ catch (error) {
375
+ const errorMessage = error instanceof Error ? error.message : String(error);
376
+ return { success: false, error: errorMessage, pattern, flags };
377
+ }
378
+ const files = getProjectAwareness(cwd);
379
+ const matches = [];
380
+ for (const file of files) {
381
+ const fullPath = path.join(cwd, file);
382
+ let content;
383
+ try {
384
+ content = fs.readFileSync(fullPath, "utf8");
385
+ }
386
+ catch {
387
+ continue;
388
+ }
389
+ regex.lastIndex = 0;
390
+ for (const match of content.matchAll(regex)) {
391
+ const matchedText = match[0];
392
+ const matchIndex = match.index ?? 0;
393
+ const lineNumber = content.slice(0, matchIndex).split(/\r?\n/).length;
394
+ const lineText = content.split(/\r?\n/)[lineNumber - 1] ?? "";
395
+ matches.push({
396
+ filePath: file,
397
+ lineNumber,
398
+ lineText,
399
+ match: matchedText,
400
+ });
401
+ if (matches.length >= MAX_GREP_RESULTS) {
402
+ return {
403
+ success: true,
404
+ pattern,
405
+ flags,
406
+ truncated: true,
407
+ matches,
408
+ };
409
+ }
410
+ }
411
+ }
412
+ return {
413
+ success: true,
414
+ pattern,
415
+ flags,
416
+ truncated: false,
417
+ matches,
418
+ };
419
+ }
420
+ catch (error) {
421
+ const errorMessage = error instanceof Error ? error.message : String(error);
422
+ return { success: false, error: errorMessage, pattern };
423
+ }
424
+ }
425
+ /**
426
+ * Search the web using DuckDuckGo.
427
+ */
428
+ export async function webSearch(query, limit = MAX_WEB_RESULTS) {
429
+ try {
430
+ const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
431
+ const response = await fetch(url, {
432
+ headers: {
433
+ "user-agent": "Beam/0.1",
434
+ accept: "text/html,application/xhtml+xml",
435
+ },
436
+ });
437
+ if (!response.ok) {
438
+ return {
439
+ success: false,
440
+ error: `Search request failed with status ${response.status}`,
441
+ query,
442
+ };
443
+ }
444
+ const html = await response.text();
445
+ const results = [];
446
+ const resultRegex = /<a[^>]*class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?(?:<a[^>]*class="result__snippet"[^>]*>([\s\S]*?)<\/a>)?/gi;
447
+ let match;
448
+ while ((match = resultRegex.exec(html)) !== null) {
449
+ const urlMatch = extractSearchUrl(match[1] ?? "");
450
+ const title = stripHtmlTags(match[2] ?? "");
451
+ const snippet = match[3] ? stripHtmlTags(match[3]) : undefined;
452
+ if (!title || !urlMatch) {
453
+ continue;
454
+ }
455
+ results.push({ title, url: urlMatch, snippet });
456
+ if (results.length >= limit) {
457
+ break;
458
+ }
459
+ }
460
+ return {
461
+ success: true,
462
+ query,
463
+ source: "duckduckgo",
464
+ results,
465
+ };
466
+ }
467
+ catch (error) {
468
+ const errorMessage = error instanceof Error ? error.message : String(error);
469
+ return { success: false, error: errorMessage, query };
470
+ }
471
+ }
472
+ /**
473
+ * Fetch and process a URL.
474
+ */
475
+ export async function webFetch(url) {
476
+ try {
477
+ const normalizedUrl = normalizeWebUrl(url);
478
+ const parsedUrl = new URL(normalizedUrl);
479
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
480
+ return { success: false, error: "Only http and https URLs are supported", url };
481
+ }
482
+ const response = await fetch(parsedUrl.toString(), {
483
+ headers: {
484
+ "user-agent": "Beam/0.1",
485
+ accept: "text/html,application/json,text/plain,*/*",
486
+ },
487
+ });
488
+ const contentType = response.headers.get("content-type") ?? "unknown";
489
+ const bodyText = await response.text();
490
+ let extractedText = bodyText;
491
+ let title;
492
+ if (contentType.includes("application/json")) {
493
+ try {
494
+ extractedText = JSON.stringify(JSON.parse(bodyText), null, 2);
495
+ }
496
+ catch {
497
+ extractedText = bodyText;
498
+ }
499
+ }
500
+ else if (contentType.includes("text/html")) {
501
+ title = extractHtmlTitle(bodyText);
502
+ extractedText = stripHtmlTags(bodyText);
503
+ }
504
+ const truncated = extractedText.length > MAX_FETCH_TEXT_LENGTH;
505
+ const text = truncated ? extractedText.slice(0, MAX_FETCH_TEXT_LENGTH) : extractedText;
506
+ return {
507
+ success: true,
508
+ url: response.url || parsedUrl.toString(),
509
+ status: response.status,
510
+ ok: response.ok,
511
+ contentType,
512
+ title,
513
+ truncated,
514
+ text,
515
+ };
516
+ }
517
+ catch (error) {
518
+ const errorMessage = error instanceof Error ? error.message : String(error);
519
+ return { success: false, error: errorMessage, url };
520
+ }
521
+ }
522
+ /**
523
+ * Run a shell command.
524
+ */
525
+ export async function runCommand(command, cwd) {
526
+ if (!command.trim()) {
527
+ return {
528
+ success: false,
529
+ error: "Command cannot be empty",
530
+ command,
531
+ };
532
+ }
533
+ if (isDangerousCommand(command)) {
534
+ return {
535
+ success: false,
536
+ error: "Blocked potentially destructive command",
537
+ command,
538
+ };
539
+ }
540
+ try {
541
+ const { stdout, stderr } = await exec(command, {
542
+ cwd,
543
+ timeout: COMMAND_TIMEOUT_MS,
544
+ maxBuffer: 1024 * 1024,
545
+ });
546
+ return {
547
+ success: true,
548
+ command,
549
+ cwd,
550
+ stdout: truncateOutput(stdout),
551
+ stderr: truncateOutput(stderr),
552
+ exitCode: 0,
553
+ };
554
+ }
555
+ catch (error) {
556
+ return {
557
+ success: false,
558
+ command,
559
+ cwd,
560
+ stdout: truncateOutput(error?.stdout ?? ""),
561
+ stderr: truncateOutput(error?.stderr ?? error?.message ?? "Unknown command error"),
562
+ exitCode: typeof error?.code === "number" ? error.code : null,
563
+ timedOut: Boolean(error?.killed),
564
+ };
565
+ }
566
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@suman_biswas/beam",
3
+ "version": "0.1.0",
4
+ "description": "A powerful CLI tool for interacting with AI models",
5
+ "keywords": ["ai", "cli", "beam"],
6
+ "homepage": "https://github.com/sumanbiswas7/beam#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/sumanbiswas7/beam.git",
10
+ "directory": "apps/cli"
11
+ },
12
+ "author": "Suman Biswas <textsumanb@gmail.com>",
13
+ "license": "SEE LICENSE IN LICENSE",
14
+ "type": "module",
15
+ "main": "./dist/apps/cli/src/index.js",
16
+ "types": "./dist/apps/cli/src/index.d.ts",
17
+ "files": [
18
+ "dist/apps/cli",
19
+ "LICENSE"
20
+ ],
21
+ "bin": {
22
+ "beam": "./dist/apps/cli/src/index.js"
23
+ },
24
+ "scripts": {
25
+ "build": "tsc -p ./tsconfig.json",
26
+ "dev:ui": "tsx watch src/index.tsx",
27
+ "start": "node ./dist/apps/cli/src/index.js"
28
+ },
29
+ "dependencies": {
30
+ "@beam/shared": "0.0.0",
31
+ "ink": "^7.0.0",
32
+ "ink-text-input": "^6.0.0",
33
+ "ollama": "^0.6.3",
34
+ "path": "^0.12.7",
35
+ "react": "^19.2.5"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^25.5.2",
39
+ "@types/react": "^19.2.14",
40
+ "tsx": "^4.21.0",
41
+ "typescript": "^5.9.3"
42
+ }
43
+ }