@benzsiangco/jarvis 1.0.2 → 1.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.
Files changed (53) hide show
  1. package/dist/cli.js +478 -347
  2. package/dist/electron/main.js +160 -0
  3. package/dist/electron/preload.js +19 -0
  4. package/package.json +19 -6
  5. package/skills.md +147 -0
  6. package/src/agents/index.ts +248 -0
  7. package/src/brain/loader.ts +136 -0
  8. package/src/cli.ts +411 -0
  9. package/src/config/index.ts +363 -0
  10. package/src/core/executor.ts +222 -0
  11. package/src/core/plugins.ts +148 -0
  12. package/src/core/types.ts +217 -0
  13. package/src/electron/main.ts +192 -0
  14. package/src/electron/preload.ts +25 -0
  15. package/src/electron/types.d.ts +20 -0
  16. package/src/index.ts +12 -0
  17. package/src/providers/antigravity-loader.ts +233 -0
  18. package/src/providers/antigravity.ts +585 -0
  19. package/src/providers/index.ts +523 -0
  20. package/src/sessions/index.ts +194 -0
  21. package/src/tools/index.ts +436 -0
  22. package/src/tui/index.tsx +784 -0
  23. package/src/utils/auth-prompt.ts +394 -0
  24. package/src/utils/index.ts +180 -0
  25. package/src/utils/native-picker.ts +71 -0
  26. package/src/utils/skills.ts +99 -0
  27. package/src/utils/table-integration-examples.ts +617 -0
  28. package/src/utils/table-utils.ts +401 -0
  29. package/src/web/build-ui.ts +27 -0
  30. package/src/web/server.ts +674 -0
  31. package/src/web/ui/dist/.gitkeep +0 -0
  32. package/src/web/ui/dist/main.css +1 -0
  33. package/src/web/ui/dist/main.js +320 -0
  34. package/src/web/ui/dist/main.js.map +20 -0
  35. package/src/web/ui/index.html +46 -0
  36. package/src/web/ui/src/App.tsx +143 -0
  37. package/src/web/ui/src/Modules/Safety/GuardianModal.tsx +83 -0
  38. package/src/web/ui/src/components/Layout/ContextPanel.tsx +243 -0
  39. package/src/web/ui/src/components/Layout/Header.tsx +91 -0
  40. package/src/web/ui/src/components/Layout/ModelSelector.tsx +235 -0
  41. package/src/web/ui/src/components/Layout/SessionStats.tsx +369 -0
  42. package/src/web/ui/src/components/Layout/Sidebar.tsx +895 -0
  43. package/src/web/ui/src/components/Modules/Chat/ChatStage.tsx +620 -0
  44. package/src/web/ui/src/components/Modules/Chat/MessageItem.tsx +446 -0
  45. package/src/web/ui/src/components/Modules/Editor/CommandInspector.tsx +71 -0
  46. package/src/web/ui/src/components/Modules/Editor/DiffViewer.tsx +83 -0
  47. package/src/web/ui/src/components/Modules/Terminal/TabbedTerminal.tsx +202 -0
  48. package/src/web/ui/src/components/Settings/SettingsModal.tsx +935 -0
  49. package/src/web/ui/src/config/models.ts +70 -0
  50. package/src/web/ui/src/main.tsx +13 -0
  51. package/src/web/ui/src/store/agentStore.ts +41 -0
  52. package/src/web/ui/src/store/uiStore.ts +64 -0
  53. package/src/web/ui/src/types/index.ts +54 -0
@@ -0,0 +1,436 @@
1
+ // Tool definitions for Jarvis
2
+ import type { Tool, ToolContext, ToolResult } from '../core/types';
3
+ import { execSync, spawn } from 'child_process';
4
+ import { readFileSync, writeFileSync, existsSync, statSync, readdirSync } from 'fs';
5
+ import { resolve, relative, join } from 'path';
6
+ import { glob as globSync } from 'glob';
7
+ import { z } from 'zod';
8
+
9
+ // Helper to create a tool result
10
+ function success(content: string, metadata?: Record<string, unknown>): ToolResult {
11
+ return { success: true, content, metadata };
12
+ }
13
+
14
+ function error(message: string): ToolResult {
15
+ return { success: false, content: '', error: message };
16
+ }
17
+
18
+ // Bash tool
19
+ export const bashTool: Tool = {
20
+ name: 'bash',
21
+ description: 'Execute a bash/shell command. Use for git, npm, system commands, etc.',
22
+ parameters: z.object({
23
+ command: z.string().describe('The command to execute'),
24
+ workdir: z.string().optional().describe('Working directory for the command'),
25
+ timeout: z.number().optional().describe('Timeout in milliseconds (default: 120000)'),
26
+ description: z.string().describe('Brief description of what this command does'),
27
+ }),
28
+ async execute(args, context) {
29
+ const command = args['command'] as string;
30
+ const workdir = (args['workdir'] as string) || context.workdir;
31
+ const timeout = (args['timeout'] as number) || 120000;
32
+
33
+ try {
34
+ const result = execSync(command, {
35
+ cwd: workdir,
36
+ timeout,
37
+ encoding: 'utf-8',
38
+ maxBuffer: 10 * 1024 * 1024,
39
+ windowsHide: true,
40
+ shell: process.platform === 'win32' ? 'powershell.exe' : '/bin/bash',
41
+ });
42
+ return success(result);
43
+ } catch (err: unknown) {
44
+ const execError = err as { stderr?: string; stdout?: string; message?: string };
45
+ const output = execError.stdout || execError.stderr || execError.message || 'Command failed';
46
+ return error(output);
47
+ }
48
+ },
49
+ };
50
+
51
+ // Read tool
52
+ export const readTool: Tool = {
53
+ name: 'read',
54
+ description: 'Read contents of a file',
55
+ parameters: z.object({
56
+ filePath: z.string().describe('Absolute path to the file to read'),
57
+ offset: z.number().optional().describe('Line number to start reading from (0-based)'),
58
+ limit: z.number().optional().describe('Number of lines to read (default: 2000)'),
59
+ }),
60
+ async execute(args, context) {
61
+ const filePath = args['filePath'] as string;
62
+ const offset = (args['offset'] as number) || 0;
63
+ const limit = (args['limit'] as number) || 2000;
64
+
65
+ try {
66
+ const absolutePath = resolve(context.workdir, filePath);
67
+
68
+ if (!existsSync(absolutePath)) {
69
+ return error(`File not found: ${filePath}`);
70
+ }
71
+
72
+ const content = readFileSync(absolutePath, 'utf-8');
73
+ const lines = content.split('\n');
74
+ const selectedLines = lines.slice(offset, offset + limit);
75
+
76
+ // Add line numbers
77
+ const numberedLines = selectedLines.map((line, i) => {
78
+ const lineNum = String(offset + i + 1).padStart(5, ' ');
79
+ return `${lineNum}| ${line}`;
80
+ });
81
+
82
+ return success(numberedLines.join('\n'), {
83
+ totalLines: lines.length,
84
+ readLines: selectedLines.length,
85
+ });
86
+ } catch (err: unknown) {
87
+ return error((err as Error).message);
88
+ }
89
+ },
90
+ };
91
+
92
+ // Write tool
93
+ export const writeTool: Tool = {
94
+ name: 'write',
95
+ description: 'Write content to a file (creates or overwrites)',
96
+ parameters: z.object({
97
+ filePath: z.string().describe('Absolute path to the file to write'),
98
+ content: z.string().describe('Content to write to the file'),
99
+ }),
100
+ async execute(args, context) {
101
+ const filePath = args['filePath'] as string;
102
+ const content = args['content'] as string;
103
+
104
+ try {
105
+ const absolutePath = resolve(context.workdir, filePath);
106
+ writeFileSync(absolutePath, content, 'utf-8');
107
+ return success(`Successfully wrote to ${filePath}`);
108
+ } catch (err: unknown) {
109
+ return error((err as Error).message);
110
+ }
111
+ },
112
+ };
113
+
114
+ // Edit tool
115
+ export const editTool: Tool = {
116
+ name: 'edit',
117
+ description: 'Replace text in a file. oldString must match exactly.',
118
+ parameters: z.object({
119
+ filePath: z.string().describe('Absolute path to the file to edit'),
120
+ oldString: z.string().describe('The text to replace (must match exactly)'),
121
+ newString: z.string().describe('The replacement text'),
122
+ replaceAll: z.boolean().optional().describe('Replace all occurrences (default: false)'),
123
+ }),
124
+ async execute(args, context) {
125
+ const filePath = args['filePath'] as string;
126
+ const oldString = args['oldString'] as string;
127
+ const newString = args['newString'] as string;
128
+ const replaceAll = args['replaceAll'] as boolean || false;
129
+
130
+ try {
131
+ const absolutePath = resolve(context.workdir, filePath);
132
+
133
+ if (!existsSync(absolutePath)) {
134
+ return error(`File not found: ${filePath}`);
135
+ }
136
+
137
+ let content = readFileSync(absolutePath, 'utf-8');
138
+
139
+ if (!content.includes(oldString)) {
140
+ return error('oldString not found in content');
141
+ }
142
+
143
+ const occurrences = content.split(oldString).length - 1;
144
+
145
+ if (occurrences > 1 && !replaceAll) {
146
+ return error('oldString found multiple times. Use replaceAll: true or provide more context.');
147
+ }
148
+
149
+ if (replaceAll) {
150
+ content = content.split(oldString).join(newString);
151
+ } else {
152
+ content = content.replace(oldString, newString);
153
+ }
154
+
155
+ writeFileSync(absolutePath, content, 'utf-8');
156
+ return success(`Successfully edited ${filePath}`, { replacements: replaceAll ? occurrences : 1 });
157
+ } catch (err: unknown) {
158
+ return error((err as Error).message);
159
+ }
160
+ },
161
+ };
162
+
163
+ // Glob tool
164
+ export const globTool: Tool = {
165
+ name: 'glob',
166
+ description: 'Find files matching a glob pattern',
167
+ parameters: z.object({
168
+ pattern: z.string().describe('Glob pattern (e.g., "**/*.ts", "src/**/*.tsx")'),
169
+ path: z.string().optional().describe('Directory to search in'),
170
+ }),
171
+ async execute(args, context) {
172
+ const pattern = args['pattern'] as string;
173
+ const searchPath = (args['path'] as string) || context.workdir;
174
+
175
+ try {
176
+ const files = await globSync(pattern, {
177
+ cwd: searchPath,
178
+ nodir: true,
179
+ ignore: ['node_modules/**', '.git/**', 'dist/**'],
180
+ });
181
+
182
+ if (files.length === 0) {
183
+ return success('No files found matching pattern.');
184
+ }
185
+
186
+ return success(files.join('\n'), { count: files.length });
187
+ } catch (err: unknown) {
188
+ return error((err as Error).message);
189
+ }
190
+ },
191
+ };
192
+
193
+ // Grep tool
194
+ export const grepTool: Tool = {
195
+ name: 'grep',
196
+ description: 'Search for a regex pattern in files',
197
+ parameters: z.object({
198
+ pattern: z.string().describe('Regex pattern to search for'),
199
+ path: z.string().optional().describe('Directory to search in'),
200
+ include: z.string().optional().describe('File pattern to include (e.g., "*.ts")'),
201
+ }),
202
+ async execute(args, context) {
203
+ const pattern = args['pattern'] as string;
204
+ const searchPath = (args['path'] as string) || context.workdir;
205
+ const include = args['include'] as string || '*';
206
+
207
+ try {
208
+ // Use ripgrep if available, fallback to basic search
209
+ const rgCommand = `rg --line-number --no-heading "${pattern}" --glob "${include}" --glob "!node_modules" --glob "!.git"`;
210
+
211
+ try {
212
+ const result = execSync(rgCommand, {
213
+ cwd: searchPath,
214
+ encoding: 'utf-8',
215
+ maxBuffer: 10 * 1024 * 1024,
216
+ });
217
+ return success(result);
218
+ } catch {
219
+ // Fallback: basic file search
220
+ const files = await globSync(`**/${include}`, {
221
+ cwd: searchPath,
222
+ nodir: true,
223
+ ignore: ['node_modules/**', '.git/**'],
224
+ });
225
+
226
+ const regex = new RegExp(pattern, 'g');
227
+ const results: string[] = [];
228
+
229
+ for (const file of files.slice(0, 100)) { // Limit files
230
+ try {
231
+ const content = readFileSync(join(searchPath, file), 'utf-8');
232
+ const lines = content.split('\n');
233
+
234
+ lines.forEach((line, i) => {
235
+ if (regex.test(line)) {
236
+ results.push(`${file}:${i + 1}:${line.substring(0, 200)}`);
237
+ }
238
+ regex.lastIndex = 0; // Reset regex state
239
+ });
240
+ } catch {
241
+ // Skip unreadable files
242
+ }
243
+ }
244
+
245
+ if (results.length === 0) {
246
+ return success('No matches found.');
247
+ }
248
+
249
+ return success(results.join('\n'), { count: results.length });
250
+ }
251
+ } catch (err: unknown) {
252
+ return error((err as Error).message);
253
+ }
254
+ },
255
+ };
256
+
257
+ // List tool (directory listing)
258
+ export const listTool: Tool = {
259
+ name: 'list',
260
+ description: 'List files and directories in a path',
261
+ parameters: z.object({
262
+ path: z.string().describe('Directory path to list'),
263
+ recursive: z.boolean().optional().describe('List recursively'),
264
+ }),
265
+ async execute(args, context) {
266
+ const dirPath = (args['path'] as string) || context.workdir;
267
+ const recursive = args['recursive'] as boolean || false;
268
+
269
+ try {
270
+ const absolutePath = resolve(context.workdir, dirPath);
271
+
272
+ if (!existsSync(absolutePath)) {
273
+ return error(`Directory not found: ${dirPath}`);
274
+ }
275
+
276
+ if (recursive) {
277
+ const files = await globSync('**/*', {
278
+ cwd: absolutePath,
279
+ ignore: ['node_modules/**', '.git/**'],
280
+ });
281
+ return success(files.join('\n'), { count: files.length });
282
+ } else {
283
+ const entries = readdirSync(absolutePath, { withFileTypes: true });
284
+ const output = entries.map(e => {
285
+ const type = e.isDirectory() ? '[D]' : '[F]';
286
+ return `${type} ${e.name}`;
287
+ });
288
+ return success(output.join('\n'), { count: entries.length });
289
+ }
290
+ } catch (err: unknown) {
291
+ return error((err as Error).message);
292
+ }
293
+ },
294
+ };
295
+
296
+ // WebFetch tool
297
+ export const webfetchTool: Tool = {
298
+ name: 'webfetch',
299
+ description: 'Fetch content from a URL',
300
+ parameters: z.object({
301
+ url: z.string().describe('URL to fetch'),
302
+ format: z.enum(['text', 'markdown', 'html']).optional().describe('Response format (default: markdown)'),
303
+ timeout: z.number().optional().describe('Timeout in seconds (max 120)'),
304
+ }),
305
+ async execute(args, context) {
306
+ const url = args['url'] as string;
307
+ const format = (args['format'] as string) || 'markdown';
308
+ const timeout = ((args['timeout'] as number) || 30) * 1000;
309
+
310
+ try {
311
+ const controller = new AbortController();
312
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
313
+
314
+ const response = await fetch(url, {
315
+ headers: {
316
+ 'User-Agent': 'Jarvis-AI/1.0',
317
+ },
318
+ signal: controller.signal,
319
+ });
320
+
321
+ clearTimeout(timeoutId);
322
+
323
+ if (!response.ok) {
324
+ return error(`HTTP ${response.status}: ${response.statusText}`);
325
+ }
326
+
327
+ const content = await response.text();
328
+
329
+ // For now, return raw content. In future, add markdown conversion
330
+ return success(content.substring(0, 50000), {
331
+ status: response.status,
332
+ contentType: response.headers.get('content-type'),
333
+ });
334
+ } catch (err: unknown) {
335
+ return error((err as Error).message);
336
+ }
337
+ },
338
+ };
339
+
340
+ // Task tool (spawn subagent)
341
+ export const taskTool: Tool = {
342
+ name: 'task',
343
+ description: 'Launch a subagent to handle a complex task',
344
+ parameters: z.object({
345
+ description: z.string().describe('Short description of the task (3-5 words)'),
346
+ prompt: z.string().describe('Detailed instructions for the subagent'),
347
+ subagent_type: z.string().describe('Type of agent: "general" or "explore"'),
348
+ session_id: z.string().optional().describe('Existing session to continue'),
349
+ }),
350
+ async execute(args, context) {
351
+ // This will be implemented by the agent executor
352
+ return success('Task tool placeholder - will be implemented by agent executor');
353
+ },
354
+ };
355
+
356
+ // Question tool
357
+ export const questionTool: Tool = {
358
+ name: 'question',
359
+ description: 'Ask the user a question to gather preferences or clarify instructions',
360
+ parameters: z.object({
361
+ questions: z.array(z.object({
362
+ question: z.string().describe('The question text'),
363
+ header: z.string().describe('Short label (max 12 chars)'),
364
+ options: z.array(z.object({
365
+ label: z.string(),
366
+ description: z.string(),
367
+ })),
368
+ multiple: z.boolean().optional().describe('Allow multiple selections'),
369
+ })),
370
+ }),
371
+ async execute(args, context) {
372
+ // This will be handled by the TUI
373
+ return success('Question tool placeholder - will be handled by TUI');
374
+ },
375
+ };
376
+
377
+ // TodoRead tool
378
+ export const todoReadTool: Tool = {
379
+ name: 'todoread',
380
+ description: 'Read the current todo list',
381
+ parameters: z.object({
382
+ _placeholder: z.boolean().describe('Always pass true'),
383
+ }),
384
+ async execute(args, context) {
385
+ // Todo state is managed by the session
386
+ return success('TodoRead placeholder - managed by session');
387
+ },
388
+ };
389
+
390
+ // TodoWrite tool
391
+ export const todoWriteTool: Tool = {
392
+ name: 'todowrite',
393
+ description: 'Create or update the todo list',
394
+ parameters: z.object({
395
+ todos: z.array(z.object({
396
+ id: z.string(),
397
+ content: z.string(),
398
+ status: z.enum(['pending', 'in_progress', 'completed', 'cancelled']),
399
+ priority: z.enum(['high', 'medium', 'low']),
400
+ })),
401
+ }),
402
+ async execute(args, context) {
403
+ // Todo state is managed by the session
404
+ return success('TodoWrite placeholder - managed by session');
405
+ },
406
+ };
407
+
408
+ // Tool registry
409
+ const tools = new Map<string, Tool>();
410
+
411
+ export function initializeTools(): void {
412
+ tools.clear();
413
+ tools.set('bash', bashTool);
414
+ tools.set('read', readTool);
415
+ tools.set('write', writeTool);
416
+ tools.set('edit', editTool);
417
+ tools.set('glob', globTool);
418
+ tools.set('grep', grepTool);
419
+ tools.set('list', listTool);
420
+ tools.set('webfetch', webfetchTool);
421
+ tools.set('task', taskTool);
422
+ tools.set('question', questionTool);
423
+ tools.set('todoread', todoReadTool);
424
+ tools.set('todowrite', todoWriteTool);
425
+ }
426
+
427
+ export function getTool(name: string): Tool | undefined {
428
+ return tools.get(name);
429
+ }
430
+
431
+ export function getAllTools(): Tool[] {
432
+ return Array.from(tools.values());
433
+ }
434
+
435
+ // Initialize on module load
436
+ initializeTools();