@aion0/forge 0.5.20 → 0.5.21

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.
package/.forge/mcp.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "forge": {
4
4
  "type": "sse",
5
- "url": "http://localhost:8406/sse?workspaceId=656c9e65-9d73-4cb6-a065-60d966e1fc78"
5
+ "url": "http://localhost:8406/sse?workspaceId=656c9e65-9d73-4cb6-a065-60d966e1fc78&agentId=engineer-1774920478256"
6
6
  }
7
7
  }
8
8
  }
package/RELEASE_NOTES.md CHANGED
@@ -1,12 +1,16 @@
1
- # Forge v0.5.20
1
+ # Forge v0.5.21
2
2
 
3
3
  Released: 2026-04-01
4
4
 
5
- ## Changes since v0.5.19
5
+ ## Changes since v0.5.20
6
6
 
7
7
  ### Bug Fixes
8
- - fix: hook done always sets done, no state check
9
- - fix: hook done accepts idle→done, not just running→done
8
+ - fix: code search excludes node_modules + handles grep exit code 1
9
+ - Revert "fix: hook only broadcasts/suppresses when transitioning from running"
10
+ - fix: hook only broadcasts/suppresses when transitioning from running
10
11
 
12
+ ### Other
13
+ - Revert "fix: hook only broadcasts/suppresses when transitioning from running"
11
14
 
12
- **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.19...v0.5.20
15
+
16
+ **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.20...v0.5.21
@@ -96,10 +96,16 @@ export async function GET(req: Request) {
96
96
  const { execSync } = require('node:child_process');
97
97
  const safeQuery = searchQuery.replace(/['"\\]/g, '\\$&');
98
98
  // Use grep -rn with limits to prevent huge output
99
- const result = execSync(
100
- `grep -rn --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.py' --include='*.java' --include='*.go' --include='*.rs' --include='*.md' --include='*.json' --include='*.yaml' --include='*.yml' --include='*.css' --include='*.html' --include='*.vue' --include='*.svelte' -m 5 '${safeQuery}' . 2>/dev/null | head -100`,
101
- { cwd: resolvedDir, encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] }
102
- ).trim();
99
+ let result = '';
100
+ try {
101
+ result = execSync(
102
+ `grep -rn --exclude-dir=node_modules --exclude-dir=.next --exclude-dir=.git --exclude-dir=dist --exclude-dir=build --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.py' --include='*.java' --include='*.go' --include='*.rs' --include='*.md' --include='*.json' --include='*.yaml' --include='*.yml' --include='*.css' --include='*.html' --include='*.vue' --include='*.svelte' -m 5 '${safeQuery}' . | head -100`,
103
+ { cwd: resolvedDir, encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] }
104
+ ).trim();
105
+ } catch (e: any) {
106
+ // grep exit code 1 = no match (not an error)
107
+ result = e.stdout?.trim() || '';
108
+ }
103
109
  const matches = result ? result.split('\n').map((line: string) => {
104
110
  const match = line.match(/^\.\/(.+?):(\d+):(.*)$/);
105
111
  if (!match) return null;
@@ -379,6 +379,109 @@ function SessionTargetSelector({ target, agents, projectPath, onChange }: {
379
379
  );
380
380
  }
381
381
 
382
+ // ─── Watch Path Picker (file/directory browser) ─────────
383
+
384
+ function WatchPathPicker({ value, projectPath, onChange }: { value: string; projectPath: string; onChange: (v: string) => void }) {
385
+ const [showBrowser, setShowBrowser] = useState(false);
386
+ const [tree, setTree] = useState<any[]>([]);
387
+ const [search, setSearch] = useState('');
388
+ const [flatFiles, setFlatFiles] = useState<string[]>([]);
389
+
390
+ const loadTree = useCallback(() => {
391
+ if (!projectPath) return;
392
+ fetch(`/api/code?dir=${encodeURIComponent(projectPath)}`)
393
+ .then(r => r.json())
394
+ .then(data => {
395
+ setTree(data.tree || []);
396
+ // Build flat list for search
397
+ const files: string[] = [];
398
+ const walk = (nodes: any[], prefix = '') => {
399
+ for (const n of nodes || []) {
400
+ const path = prefix ? `${prefix}/${n.name}` : n.name;
401
+ files.push(n.type === 'dir' ? path + '/' : path);
402
+ if (n.children) walk(n.children, path);
403
+ }
404
+ };
405
+ walk(data.tree || []);
406
+ setFlatFiles(files);
407
+ })
408
+ .catch(() => {});
409
+ }, [projectPath]);
410
+
411
+ const filtered = search ? flatFiles.filter(f => f.toLowerCase().includes(search.toLowerCase())).slice(0, 30) : [];
412
+
413
+ return (
414
+ <div className="flex-1 flex items-center gap-1 relative">
415
+ <input
416
+ value={value}
417
+ onChange={e => onChange(e.target.value)}
418
+ placeholder="./ (project root)"
419
+ className="text-[10px] bg-[#161b22] border border-[#30363d] rounded px-1 py-0.5 text-white flex-1"
420
+ />
421
+ <button onClick={() => { setShowBrowser(!showBrowser); if (!showBrowser) loadTree(); }}
422
+ className="text-[9px] px-1 py-0.5 rounded bg-[#30363d] text-gray-400 hover:text-white shrink-0">📂</button>
423
+
424
+ {showBrowser && (
425
+ <div className="absolute left-0 right-0 top-full mt-1 z-50 bg-[#0d1117] border border-[#30363d] rounded-lg shadow-xl max-h-60 overflow-hidden flex flex-col" style={{ minWidth: 250 }}>
426
+ <input
427
+ value={search}
428
+ onChange={e => setSearch(e.target.value)}
429
+ placeholder="Search files & dirs..."
430
+ autoFocus
431
+ className="text-[10px] bg-[#161b22] border-b border-[#30363d] px-2 py-1 text-white focus:outline-none"
432
+ />
433
+ <div className="overflow-y-auto flex-1">
434
+ {search ? (
435
+ // Search results
436
+ filtered.length > 0 ? filtered.map(f => (
437
+ <div key={f} onClick={() => { onChange(f); setShowBrowser(false); setSearch(''); }}
438
+ className="px-2 py-0.5 text-[9px] text-gray-300 hover:bg-[#161b22] cursor-pointer truncate font-mono">
439
+ {f.endsWith('/') ? `📁 ${f}` : `📄 ${f}`}
440
+ </div>
441
+ )) : <div className="px-2 py-1 text-[9px] text-gray-500">No matches</div>
442
+ ) : (
443
+ // Tree view (first 2 levels)
444
+ tree.map(n => <PathTreeNode key={n.name} node={n} prefix="" onSelect={p => { onChange(p); setShowBrowser(false); }} />)
445
+ )}
446
+ </div>
447
+ <div className="flex items-center justify-between px-2 py-0.5 border-t border-[#30363d] bg-[#161b22]">
448
+ <span className="text-[8px] text-gray-600">{flatFiles.length} items</span>
449
+ <button onClick={() => setShowBrowser(false)} className="text-[8px] text-gray-500 hover:text-white">Close</button>
450
+ </div>
451
+ </div>
452
+ )}
453
+ </div>
454
+ );
455
+ }
456
+
457
+ function PathTreeNode({ node, prefix, onSelect, depth = 0 }: { node: any; prefix: string; onSelect: (path: string) => void; depth?: number }) {
458
+ const [expanded, setExpanded] = useState(depth < 1);
459
+ const path = prefix ? `${prefix}/${node.name}` : node.name;
460
+ const isDir = node.type === 'dir';
461
+
462
+ if (!isDir && depth > 1) return null; // only show files at top 2 levels
463
+
464
+ return (
465
+ <div>
466
+ <div
467
+ onClick={() => isDir ? setExpanded(!expanded) : onSelect(path)}
468
+ className="flex items-center px-2 py-0.5 text-[9px] hover:bg-[#161b22] cursor-pointer"
469
+ style={{ paddingLeft: 8 + depth * 12 }}
470
+ >
471
+ <span className="text-gray-500 mr-1 w-3">{isDir ? (expanded ? '▼' : '▶') : ''}</span>
472
+ <span className={isDir ? 'text-[var(--accent)]' : 'text-gray-400'}>{isDir ? '📁' : '📄'} {node.name}</span>
473
+ {isDir && (
474
+ <button onClick={e => { e.stopPropagation(); onSelect(path + '/'); }}
475
+ className="ml-auto text-[8px] text-gray-600 hover:text-[var(--accent)]">select</button>
476
+ )}
477
+ </div>
478
+ {isDir && expanded && node.children && depth < 2 && (
479
+ node.children.map((c: any) => <PathTreeNode key={c.name} node={c} prefix={path} onSelect={onSelect} depth={depth + 1} />)
480
+ )}
481
+ </div>
482
+ );
483
+ }
484
+
382
485
  // ─── Fixed Session Picker ────────────────────────────────
383
486
 
384
487
  function FixedSessionPicker({ projectPath, value, onChange }: { projectPath?: string; value: string; onChange: (v: string) => void }) {
@@ -487,14 +590,14 @@ function AgentConfigModal({ initial, mode, existingAgents, projectPath, onConfir
487
590
  fetch(`/api/code?dir=${encodeURIComponent(projectPath)}`)
488
591
  .then(r => r.json())
489
592
  .then(data => {
490
- // Flatten directory tree (type='dir') to list of paths
593
+ // Collect directories with depth limit (max 2 levels for readability)
491
594
  const dirs: string[] = [];
492
- const walk = (nodes: any[], prefix = '') => {
595
+ const walk = (nodes: any[], prefix = '', depth = 0) => {
493
596
  for (const n of nodes || []) {
494
597
  if (n.type === 'dir') {
495
598
  const path = prefix ? `${prefix}/${n.name}` : n.name;
496
599
  dirs.push(path);
497
- if (n.children) walk(n.children, path);
600
+ if (n.children && depth < 2) walk(n.children, path, depth + 1);
498
601
  }
499
602
  }
500
603
  };
@@ -785,14 +888,15 @@ function AgentConfigModal({ initial, mode, existingAgents, projectPath, onConfir
785
888
  <option value="agent_status">Agent Status</option>
786
889
  </select>
787
890
  {t.type === 'directory' && (
788
- <select value={t.path || ''} onChange={e => {
789
- const next = [...watchTargets];
790
- next[i] = { ...t, path: e.target.value };
791
- setWatchTargets(next);
792
- }} className="text-[10px] bg-[#161b22] border border-[#30363d] rounded px-1 py-0.5 text-white flex-1">
793
- <option value="">Project root</option>
794
- {projectDirs.map(d => <option key={d} value={d + '/'}>{d}/</option>)}
795
- </select>
891
+ <WatchPathPicker
892
+ value={t.path || ''}
893
+ projectPath={projectPath || ''}
894
+ onChange={v => {
895
+ const next = [...watchTargets];
896
+ next[i] = { ...t, path: v };
897
+ setWatchTargets(next);
898
+ }}
899
+ />
796
900
  )}
797
901
  {t.type === 'agent_status' && (<>
798
902
  <select value={t.path || ''} onChange={e => {
package/next-env.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /// <reference types="next" />
2
2
  /// <reference types="next/image-types/global" />
3
- import "./.next/types/routes.d.ts";
3
+ import "./.next/dev/types/routes.d.ts";
4
4
 
5
5
  // NOTE: This file should not be edited
6
6
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.5.20",
3
+ "version": "0.5.21",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {