open-grok-build 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,260 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promises as fs } from 'node:fs';
3
+ import { basename, join, relative } from 'node:path';
4
+ import { promisify } from 'node:util';
5
+
6
+ const execFileAsync = promisify(execFile);
7
+
8
+ export type RenderableText = { render: (width: number) => string[] };
9
+
10
+ export const MAX_OUTPUT_CHARS = 50_000;
11
+ export const MAX_OUTPUT_BYTES = MAX_OUTPUT_CHARS * 4 + 1024;
12
+ export const MAX_LINES = 500;
13
+
14
+ export function recordFrom(value: unknown): Record<string, unknown> | undefined {
15
+ if (!value || typeof value !== 'object') return undefined;
16
+ return value as Record<string, unknown>;
17
+ }
18
+
19
+ export function stringFrom(value: unknown): string | undefined {
20
+ if (typeof value !== 'string') return undefined;
21
+ return value;
22
+ }
23
+
24
+ export function truncateLines(lines: string[]): string {
25
+ if (lines.length > MAX_LINES) {
26
+ return (
27
+ lines.slice(0, MAX_LINES).join('\n') +
28
+ `\n\n[Showing first ${MAX_LINES} of ${lines.length} results. Refine your pattern to narrow results.]`
29
+ );
30
+ }
31
+ return lines.join('\n');
32
+ }
33
+
34
+ export function truncateChars(output: string): string {
35
+ if (output.length > MAX_OUTPUT_CHARS) {
36
+ return `${output.slice(0, MAX_OUTPUT_CHARS)}\n\n[Output truncated at 50KB]`;
37
+ }
38
+ return output;
39
+ }
40
+
41
+ export function globToRegExp(pattern: string) {
42
+ let source = '^';
43
+ for (let i = 0; i < pattern.length; i += 1) {
44
+ const char = pattern[i];
45
+ const next = pattern[i + 1];
46
+ if (char === '*' && next === '*' && pattern[i + 2] === '/') {
47
+ source += '(?:.*/)?';
48
+ i += 2;
49
+ } else if (char === '*' && next === '*') {
50
+ source += '.*';
51
+ i += 1;
52
+ } else if (char === '*') {
53
+ source += '[^/]*';
54
+ } else if (char === '?') {
55
+ source += '[^/]';
56
+ } else {
57
+ source += char.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
58
+ }
59
+ }
60
+ return new RegExp(`${source}$`);
61
+ }
62
+
63
+ export function normalizePath(filePath: string) {
64
+ return filePath.replaceAll('\\', '/');
65
+ }
66
+
67
+ export async function listFilesRecursive(
68
+ searchPath: string,
69
+ signal?: AbortSignal,
70
+ ): Promise<string[]> {
71
+ if (signal?.aborted) throw new Error('The operation was aborted');
72
+ const stats = await fs.stat(searchPath);
73
+ if (stats.isFile()) return [searchPath];
74
+ if (!stats.isDirectory()) return [];
75
+
76
+ return (
77
+ await Promise.all(
78
+ (
79
+ await fs.readdir(searchPath, { withFileTypes: true })
80
+ ).map((entry) => {
81
+ const entryPath = join(searchPath, entry.name);
82
+ if (entry.isDirectory()) return listFilesRecursive(entryPath, signal);
83
+ if (entry.isFile()) return [entryPath];
84
+ return [];
85
+ }),
86
+ )
87
+ ).flat();
88
+ }
89
+
90
+ let rgAvailable: boolean | undefined;
91
+ export async function hasRipgrep(): Promise<boolean> {
92
+ if (rgAvailable !== undefined) return rgAvailable;
93
+ try {
94
+ await execFileAsync('rg', ['--version']);
95
+ rgAvailable = true;
96
+ } catch {
97
+ rgAvailable = false;
98
+ }
99
+ return rgAvailable;
100
+ }
101
+
102
+ export type ToolError = { code?: number; message?: string };
103
+ export type ToolResult<T> = {
104
+ content: [{ type: 'text'; text: string }];
105
+ details: T;
106
+ };
107
+
108
+ export function text(content: string): RenderableText {
109
+ return {
110
+ render: () => [content],
111
+ };
112
+ }
113
+
114
+ function firstText(result: { content: { type: string; text?: string }[] }) {
115
+ const first = result.content[0];
116
+ if (first?.type !== 'text') return undefined;
117
+ return first.text;
118
+ }
119
+
120
+ export function renderResultText(
121
+ result: { content: { type: string; text?: string }[] },
122
+ expanded: boolean,
123
+ summary: string,
124
+ ): RenderableText {
125
+ if (expanded) return text(firstText(result) ?? summary);
126
+ return text(summary);
127
+ }
128
+
129
+ export function renderRunning(isPartial: boolean): RenderableText | undefined {
130
+ if (!isPartial) return undefined;
131
+ return text('Running...');
132
+ }
133
+
134
+ export function renderResultSummary(
135
+ result: { content: { type: string; text?: string }[] },
136
+ expanded: boolean,
137
+ isPartial: boolean,
138
+ summary: string,
139
+ ): RenderableText {
140
+ const running = renderRunning(isPartial);
141
+ if (running) return running;
142
+ return renderResultText(result, expanded, summary);
143
+ }
144
+
145
+ export function detailRecord(result: { details: unknown }): Record<string, unknown> {
146
+ if (!result.details || typeof result.details !== 'object') return {};
147
+ return result.details as Record<string, unknown>;
148
+ }
149
+
150
+ export function numberDetail(result: { details: unknown }, key: string): number {
151
+ const value = detailRecord(result)[key];
152
+ if (typeof value !== 'number') return 0;
153
+ return value;
154
+ }
155
+
156
+ export function stringDetail(result: { details: unknown }, key: string): string {
157
+ const value = detailRecord(result)[key];
158
+ if (typeof value !== 'string') return '';
159
+ return value;
160
+ }
161
+
162
+ export function booleanDetail(result: { details: unknown }, key: string): boolean {
163
+ const value = detailRecord(result)[key];
164
+ return value === true;
165
+ }
166
+
167
+ type FileDetails = { path: string; [key: string]: unknown };
168
+
169
+ export function fileNotFound<T extends FileDetails>(
170
+ filePath: string,
171
+ extraDetails: Omit<T, 'path'>,
172
+ ): ToolResult<T> {
173
+ return {
174
+ content: [{ type: 'text', text: `File not found: ${filePath}` }],
175
+ details: { path: filePath, ...extraDetails } as T,
176
+ };
177
+ }
178
+
179
+ export function fileError<T extends FileDetails>(
180
+ error: unknown,
181
+ toolName: string,
182
+ filePath: string,
183
+ extraDetails: Omit<T, 'path'>,
184
+ ): ToolResult<T> {
185
+ const err = error as ToolError;
186
+ const message = err.message ?? 'Unknown error';
187
+ return {
188
+ content: [
189
+ {
190
+ type: 'text',
191
+ text: `${toolName} error: ${message}`,
192
+ },
193
+ ],
194
+ details: { path: filePath, ...extraDetails, failed: true, error: message } as unknown as T,
195
+ };
196
+ }
197
+
198
+ export function toolError<T>(error: unknown, toolName: string, emptyDetails: T): ToolResult<T> {
199
+ const err = error as ToolError;
200
+ if (toolName === 'Grep' && err.code === 1) {
201
+ return {
202
+ content: [{ type: 'text', text: 'No matches found' }],
203
+ details: emptyDetails,
204
+ };
205
+ }
206
+ const message = err.message ?? 'Unknown error';
207
+ return {
208
+ content: [
209
+ {
210
+ type: 'text',
211
+ text: `${toolName} error: ${message}`,
212
+ },
213
+ ],
214
+ details: { ...emptyDetails, failed: true, error: message } as T,
215
+ };
216
+ }
217
+
218
+ export async function execWithRgFallback(
219
+ rgArgs: string[],
220
+ options: {
221
+ cwd: string;
222
+ signal?: AbortSignal;
223
+ pattern: string;
224
+ searchPath: string;
225
+ include?: string;
226
+ },
227
+ ): Promise<string> {
228
+ if (await hasRipgrep()) {
229
+ const result = await execFileAsync('rg', rgArgs, {
230
+ cwd: options.cwd,
231
+ maxBuffer: MAX_OUTPUT_BYTES,
232
+ signal: options.signal,
233
+ });
234
+ return result.stdout;
235
+ }
236
+
237
+ const regex = new RegExp(options.pattern);
238
+ const matcher = options.include ? globToRegExp(normalizePath(options.include)) : undefined;
239
+ return (
240
+ await Promise.all(
241
+ (
242
+ await listFilesRecursive(options.searchPath, options.signal)
243
+ )
244
+ .filter((file) => {
245
+ if (!matcher) return true;
246
+ if (!options.include?.includes('/')) return matcher.test(basename(file));
247
+ return matcher.test(normalizePath(relative(options.cwd, file)));
248
+ })
249
+ .map(async (file) =>
250
+ (
251
+ await fs.readFile(file, 'utf8')
252
+ )
253
+ .split(/\r?\n/)
254
+ .flatMap((line, index) => (regex.test(line) ? `${file}:${index + 1}:${line}` : [])),
255
+ ),
256
+ )
257
+ )
258
+ .flat()
259
+ .join('\n');
260
+ }
@@ -0,0 +1,195 @@
1
+ // @ts-nocheck
2
+ import { execFile } from 'node:child_process';
3
+ import { statSync } from 'node:fs';
4
+ import { basename, relative, resolve } from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import {
7
+ execWithRgFallback,
8
+ globToRegExp,
9
+ hasRipgrep,
10
+ listFilesRecursive,
11
+ MAX_OUTPUT_BYTES,
12
+ normalizePath,
13
+ numberDetail,
14
+ recordFrom,
15
+ renderResultText,
16
+ renderRunning,
17
+ stringFrom,
18
+ text,
19
+ toolError,
20
+ truncateChars,
21
+ truncateLines,
22
+ } from './rendering.js';
23
+ import type { ToolRegistrar } from './types.js';
24
+
25
+ const execFileAsync = promisify(execFile);
26
+
27
+ type GrepArgs = { pattern: string; path?: string; include?: string };
28
+ type GlobArgs = { pattern: string; path?: string };
29
+
30
+ function modifiedTimeMs(file: string) {
31
+ try {
32
+ return statSync(file).mtimeMs;
33
+ } catch {
34
+ return 0;
35
+ }
36
+ }
37
+
38
+ export function sortByModifiedNewest(files: string[]) {
39
+ return files.sort((a, b) => {
40
+ const delta = modifiedTimeMs(b) - modifiedTimeMs(a);
41
+ if (delta !== 0) return delta;
42
+ return a.localeCompare(b);
43
+ });
44
+ }
45
+
46
+ export function registerSearchTools(registrar: ToolRegistrar) {
47
+ registrar.registerTool({
48
+ name: 'Grep',
49
+ label: 'Grep',
50
+ description:
51
+ 'Search for a regex pattern in file contents. Returns matching lines with file path and line number. Use the include parameter to filter by file type.',
52
+
53
+ prepareArguments(args) {
54
+ const input = recordFrom(args);
55
+ if (!input) return args as GrepArgs;
56
+ return {
57
+ ...input,
58
+ include: stringFrom(input.include) ?? stringFrom(input.glob_filter),
59
+ } as GrepArgs;
60
+ },
61
+
62
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
63
+ const searchPath = resolve(ctx.cwd, params.path ?? '.');
64
+
65
+ try {
66
+ const rgArgs = ['-n', '-H', '--no-heading', '--color=never'];
67
+ if (params.include) rgArgs.push('--glob', params.include);
68
+ rgArgs.push('--', params.pattern, searchPath);
69
+
70
+ const stdout = await execWithRgFallback(rgArgs, {
71
+ cwd: ctx.cwd,
72
+ signal,
73
+ pattern: params.pattern,
74
+ searchPath,
75
+ include: params.include,
76
+ });
77
+
78
+ const lines = stdout.trim().split('\n').filter(Boolean);
79
+ if (lines.length === 0) {
80
+ return {
81
+ content: [{ type: 'text', text: 'No matches found' }],
82
+ details: { matchCount: 0 },
83
+ };
84
+ }
85
+
86
+ return {
87
+ content: [{ type: 'text', text: truncateChars(truncateLines(lines)) }],
88
+ details: { matchCount: lines.length },
89
+ };
90
+ } catch (error: unknown) {
91
+ return toolError(error, 'Grep', { matchCount: 0 });
92
+ }
93
+ },
94
+ renderCall(args, theme) {
95
+ const path = args.path ? theme.fg('muted', ` in ${args.path}`) : '';
96
+ const include = args.include ? theme.fg('dim', ` [${args.include}]`) : '';
97
+ return text(
98
+ theme.fg('toolTitle', theme.bold('Grep ')) +
99
+ theme.fg('accent', `"${args.pattern}"`) +
100
+ path +
101
+ include,
102
+ );
103
+ },
104
+ renderResult(result, { expanded, isPartial }, theme) {
105
+ const running = renderRunning(isPartial);
106
+ if (running) return running;
107
+ const matchCount = numberDetail(result, 'matchCount');
108
+ return renderResultText(
109
+ result,
110
+ expanded,
111
+ matchCount === 0
112
+ ? theme.fg('dim', 'No matches')
113
+ : theme.fg('muted', `${matchCount} match(es)`),
114
+ );
115
+ },
116
+ });
117
+
118
+ registrar.registerTool({
119
+ name: 'Glob',
120
+ label: 'Glob',
121
+ description:
122
+ 'Find files matching a glob pattern. Returns a list of matching file paths sorted by modification time (newest first).',
123
+
124
+ prepareArguments(args) {
125
+ const input = recordFrom(args);
126
+ if (!input) return args as GlobArgs;
127
+ return {
128
+ ...input,
129
+ pattern: stringFrom(input.pattern) ?? stringFrom(input.glob_pattern),
130
+ } as GlobArgs;
131
+ },
132
+
133
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
134
+ const searchPath = resolve(ctx.cwd, params.path ?? '.');
135
+
136
+ try {
137
+ let files: string[];
138
+
139
+ if (await hasRipgrep()) {
140
+ const result = await execFileAsync(
141
+ 'rg',
142
+ ['--files', '--color=never', '--glob', params.pattern, searchPath],
143
+ { cwd: ctx.cwd, maxBuffer: MAX_OUTPUT_BYTES, signal },
144
+ );
145
+ files = result.stdout.trim().split('\n').filter(Boolean);
146
+ } else {
147
+ const normalizedPattern = normalizePath(params.pattern);
148
+ const matcher = globToRegExp(normalizedPattern);
149
+ const matchesFile = normalizedPattern.includes('/')
150
+ ? (file: string) => matcher.test(normalizePath(relative(ctx.cwd, file)))
151
+ : (file: string) => matcher.test(basename(file));
152
+ files = (await listFilesRecursive(searchPath, signal)).filter(matchesFile);
153
+ }
154
+ files = sortByModifiedNewest(files);
155
+
156
+ if (files.length === 0) {
157
+ return {
158
+ content: [{ type: 'text', text: 'No files found' }],
159
+ details: { fileCount: 0 },
160
+ };
161
+ }
162
+
163
+ return {
164
+ content: [{ type: 'text', text: truncateChars(truncateLines(files)) }],
165
+ details: { fileCount: files.length },
166
+ };
167
+ } catch (error: unknown) {
168
+ const err = error as { code?: unknown; stderr?: string };
169
+ if (err.code === 1 && !err.stderr) {
170
+ return {
171
+ content: [{ type: 'text', text: 'No files found' }],
172
+ details: { fileCount: 0 },
173
+ };
174
+ }
175
+ return toolError(error, 'Glob', { fileCount: 0 });
176
+ }
177
+ },
178
+ renderCall(args, theme) {
179
+ const path = args.path ? theme.fg('muted', ` in ${args.path}`) : '';
180
+ return text(
181
+ theme.fg('toolTitle', theme.bold('Glob ')) + theme.fg('accent', args.pattern) + path,
182
+ );
183
+ },
184
+ renderResult(result, { expanded, isPartial }, theme) {
185
+ const running = renderRunning(isPartial);
186
+ if (running) return running;
187
+ const fileCount = numberDetail(result, 'fileCount');
188
+ return renderResultText(
189
+ result,
190
+ expanded,
191
+ fileCount === 0 ? theme.fg('dim', 'No files') : theme.fg('muted', `${fileCount} file(s)`),
192
+ );
193
+ },
194
+ });
195
+ }
@@ -0,0 +1,142 @@
1
+ // @ts-nocheck
2
+ import { execFile } from 'node:child_process';
3
+ import { existsSync } from 'node:fs';
4
+ import { resolve } from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import {
7
+ detailRecord,
8
+ MAX_OUTPUT_BYTES,
9
+ MAX_OUTPUT_CHARS,
10
+ renderResultText,
11
+ renderRunning,
12
+ text,
13
+ } from './rendering.js';
14
+ import type { ToolRegistrar } from './types.js';
15
+
16
+ const execFileAsync = promisify(execFile);
17
+
18
+ function shellCommand(command: string): { file: string; args: string[] } | undefined {
19
+ if (process.platform === 'win32') {
20
+ if (existsSync('C:\\Windows\\System32\\cmd.exe')) {
21
+ return { file: 'cmd.exe', args: ['/d', '/s', '/c', command] };
22
+ }
23
+ if (existsSync('C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe')) {
24
+ return {
25
+ file: 'powershell.exe',
26
+ args: ['-NoLogo', '-NoProfile', '-Command', command],
27
+ };
28
+ }
29
+ return undefined;
30
+ }
31
+
32
+ if (
33
+ process.platform !== 'darwin' &&
34
+ process.platform !== 'linux' &&
35
+ process.platform !== 'freebsd'
36
+ ) {
37
+ return undefined;
38
+ }
39
+
40
+ if (existsSync('/bin/bash')) return { file: '/bin/bash', args: ['-c', command] };
41
+ if (existsSync('/usr/bin/bash')) return { file: '/usr/bin/bash', args: ['-c', command] };
42
+ if (existsSync('/bin/sh')) return { file: '/bin/sh', args: ['-c', command] };
43
+ if (existsSync('/usr/bin/sh')) return { file: '/usr/bin/sh', args: ['-c', command] };
44
+ return undefined;
45
+ }
46
+
47
+ export function registerShellTool(registrar: ToolRegistrar) {
48
+ // ── Shell tool ───────────────────────────────────────────────────────
49
+
50
+ registrar.registerTool({
51
+ name: 'Shell',
52
+ label: 'Shell',
53
+ description: 'Execute a shell command and return stdout, stderr, and exit code.',
54
+
55
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
56
+ const cwd = params.working_directory ? resolve(ctx.cwd, params.working_directory) : ctx.cwd;
57
+ const timeout = params.timeout ?? 120_000;
58
+
59
+ try {
60
+ const shell = shellCommand(params.command);
61
+ if (!shell) {
62
+ return {
63
+ content: [
64
+ {
65
+ type: 'text',
66
+ text: 'Shell error: unsupported platform or shell not found',
67
+ },
68
+ ],
69
+ details: { exitCode: 1, command: params.command },
70
+ };
71
+ }
72
+ const { stdout, stderr } = await execFileAsync(shell.file, shell.args, {
73
+ cwd,
74
+ maxBuffer: MAX_OUTPUT_BYTES,
75
+ timeout,
76
+ signal,
77
+ });
78
+
79
+ let output = '';
80
+ if (stdout) output += stdout;
81
+ if (stderr) output += `\n[stderr]\n${stderr}`;
82
+
83
+ if (output.length > MAX_OUTPUT_CHARS) {
84
+ output = `${output.slice(0, MAX_OUTPUT_CHARS)}\n\n[Output truncated at 50KB]`;
85
+ }
86
+
87
+ return {
88
+ content: [{ type: 'text', text: output || '(no output)' }],
89
+ details: { exitCode: 0, command: params.command },
90
+ };
91
+ } catch (error: unknown) {
92
+ const err = error as {
93
+ code?: unknown;
94
+ message?: string;
95
+ signal?: unknown;
96
+ stdout?: string;
97
+ stderr?: string;
98
+ };
99
+ const exitCode = typeof err.code === 'number' ? err.code : 1;
100
+
101
+ let output = '';
102
+ if (err.stdout) output += err.stdout;
103
+ if (err.stderr) output += `\n[stderr]\n${err.stderr}`;
104
+
105
+ if (output.length > MAX_OUTPUT_CHARS) {
106
+ output = `${output.slice(0, MAX_OUTPUT_CHARS)}\n\n[Output truncated at 50KB]`;
107
+ }
108
+
109
+ return {
110
+ content: [
111
+ {
112
+ type: 'text',
113
+ text: `Shell error (exit code ${err.code ?? 'unknown'}): ${err.message ?? 'Unknown error'}${output ? `\n${output}` : ''}`,
114
+ },
115
+ ],
116
+ details: {
117
+ exitCode,
118
+ command: params.command,
119
+ ...(typeof err.signal === 'string' ? { signal: err.signal } : {}),
120
+ },
121
+ };
122
+ }
123
+ },
124
+ renderCall(args, theme) {
125
+ const cwd = args.working_directory ? theme.fg('muted', ` in ${args.working_directory}`) : '';
126
+ return text(
127
+ theme.fg('toolTitle', theme.bold('Shell ')) + theme.fg('accent', args.command) + cwd,
128
+ );
129
+ },
130
+ renderResult(result, { expanded, isPartial }, theme) {
131
+ const running = renderRunning(isPartial);
132
+ if (running) return running;
133
+ const exitCode =
134
+ typeof detailRecord(result).exitCode === 'number' ? detailRecord(result).exitCode : 1;
135
+ return renderResultText(
136
+ result,
137
+ expanded,
138
+ exitCode === 0 ? theme.fg('muted', 'Exit 0') : theme.fg('warning', `Exit ${exitCode}`),
139
+ );
140
+ },
141
+ });
142
+ }
@@ -0,0 +1,29 @@
1
+ /** Minimal registrar used to collect Grok/Cursor shim tools. */
2
+
3
+ export type ToolExecuteContext = { cwd: string };
4
+
5
+ export type ToolExecuteResult = {
6
+ content: { type: string; text: string }[];
7
+ details?: Record<string, unknown>;
8
+ };
9
+
10
+ export type ShimRegisteredTool = {
11
+ name: string;
12
+ description: string;
13
+ label?: string;
14
+ prepareArguments?: (params: Record<string, unknown>) => Record<string, unknown>;
15
+ execute: (
16
+ toolCallId: string,
17
+ params: Record<string, unknown>,
18
+ signal: AbortSignal | undefined,
19
+ onUpdate: unknown,
20
+ ctx: ToolExecuteContext,
21
+ ) => Promise<ToolExecuteResult>;
22
+ renderCall?: (...args: unknown[]) => { render: (width: number) => string[] };
23
+ renderResult?: (...args: unknown[]) => { render: (width: number) => string[] };
24
+ };
25
+
26
+ export type ToolRegistrar = {
27
+ registerTool: (tool: ShimRegisteredTool) => void;
28
+ on?: (event: string, handler: unknown) => void;
29
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "resolveJsonModule": true,
11
+ "jsx": "preserve",
12
+ "jsxImportSource": "@opentui/solid",
13
+ "types": ["node"],
14
+ "declaration": true,
15
+ "declarationMap": true,
16
+ "sourceMap": true,
17
+ "outDir": "./dist"
18
+ },
19
+ "include": ["src/**/*.ts", "src/**/*.tsx"],
20
+ "exclude": ["node_modules", "dist"]
21
+ }