@aion0/forge 0.5.3 → 0.5.5

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/RELEASE_NOTES.md CHANGED
@@ -1,11 +1,18 @@
1
- # Forge v0.5.3
1
+ # Forge v0.5.5
2
2
 
3
3
  Released: 2026-03-28
4
4
 
5
- ## Changes since v0.5.2
5
+ ## Changes since v0.5.4
6
+
7
+ ### Features
8
+ - feat: API token auth for Help AI — password-based, 24h validity
9
+ - feat: enhanced agent role presets with detailed prompts + UI Designer
6
10
 
7
11
  ### Bug Fixes
8
- - fix: last @/lib alias in cli-backend → relative path
12
+ - fix: whitelist all /api/workspace routes in auth middleware
13
+
14
+ ### Other
15
+ - revert: restore auth middleware for /api/workspace security
9
16
 
10
17
 
11
- **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.2...v0.5.3
18
+ **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.4...v0.5.5
@@ -0,0 +1,46 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { randomUUID } from 'node:crypto';
3
+
4
+ // In-memory valid tokens — shared across API routes in the same process
5
+ const tokenKey = Symbol.for('forge-api-tokens');
6
+ const g = globalThis as any;
7
+ if (!g[tokenKey]) g[tokenKey] = new Set<string>();
8
+ export const validTokens: Set<string> = g[tokenKey];
9
+
10
+ /** Verify a token is valid (for use in other API routes) */
11
+ export function isValidToken(req: Request): boolean {
12
+ // Check header
13
+ const headerToken = new Headers(req.headers).get('x-forge-token');
14
+ if (headerToken && validTokens.has(headerToken)) return true;
15
+ // Check cookie
16
+ const cookieHeader = new Headers(req.headers).get('cookie') || '';
17
+ const match = cookieHeader.match(/forge-api-token=([^;]+)/);
18
+ if (match && validTokens.has(match[1])) return true;
19
+ return false;
20
+ }
21
+
22
+ export async function POST(req: Request) {
23
+ const body = await req.json();
24
+ const { password } = body;
25
+
26
+ if (!password) {
27
+ return NextResponse.json({ error: 'password required' }, { status: 400 });
28
+ }
29
+
30
+ const { verifyAdmin } = await import('@/lib/password');
31
+ if (!verifyAdmin(password)) {
32
+ return NextResponse.json({ error: 'invalid password' }, { status: 401 });
33
+ }
34
+
35
+ const token = randomUUID();
36
+ validTokens.add(token);
37
+
38
+ const res = NextResponse.json({ ok: true, token });
39
+ res.cookies.set('forge-api-token', token, {
40
+ httpOnly: true,
41
+ sameSite: 'lax',
42
+ maxAge: 86400,
43
+ path: '/',
44
+ });
45
+ return res;
46
+ }
@@ -43,7 +43,7 @@ function scanDir(dir: string, base: string, depth: number = 0): FileNode[] {
43
43
  const nodes: FileNode[] = [];
44
44
 
45
45
  const sorted = entries
46
- .filter(e => !IGNORE.has(e.name) && !e.name.startsWith('.'))
46
+ .filter(e => !IGNORE.has(e.name))
47
47
  .sort((a, b) => {
48
48
  if (a.isDirectory() && !b.isDirectory()) return -1;
49
49
  if (!a.isDirectory() && b.isDirectory()) return 1;
@@ -56,10 +56,8 @@ function scanDir(dir: string, base: string, depth: number = 0): FileNode[] {
56
56
 
57
57
  if (entry.isDirectory()) {
58
58
  const children = scanDir(fullPath, base, depth + 1);
59
- if (children.length > 0) {
60
- nodes.push({ name: entry.name, path: relPath, type: 'dir', children });
61
- }
62
- } else if (isCodeFile(entry.name)) {
59
+ nodes.push({ name: entry.name, path: relPath, type: 'dir', children });
60
+ } else {
63
61
  nodes.push({ name: entry.name, path: relPath, type: 'file' });
64
62
  }
65
63
  }
@@ -58,16 +58,28 @@ export async function GET(req: Request) {
58
58
  const items = await res.json();
59
59
  const files: { name: string; path: string; type: string }[] = [];
60
60
 
61
- const flatten = (list: any[], prefix = '') => {
61
+ const recurse = async (list: any[], prefix = '') => {
62
62
  for (const item of list) {
63
63
  if (item.type === 'file') {
64
64
  files.push({ name: item.name, path: prefix + item.name, type: 'file' });
65
65
  } else if (item.type === 'dir') {
66
- files.push({ name: item.name, path: prefix + item.name, type: 'dir' });
66
+ files.push({ name: item.name + '/', path: prefix + item.name, type: 'dir' });
67
+ // Fetch subdirectory contents
68
+ try {
69
+ const subRes = await fetch(item.url, {
70
+ headers: { 'Accept': 'application/vnd.github.v3+json' },
71
+ });
72
+ if (subRes.ok) {
73
+ const subItems = await subRes.json();
74
+ if (Array.isArray(subItems)) {
75
+ await recurse(subItems, prefix + item.name + '/');
76
+ }
77
+ }
78
+ } catch {}
67
79
  }
68
80
  }
69
81
  };
70
- flatten(Array.isArray(items) ? items : []);
82
+ await recurse(Array.isArray(items) ? items : []);
71
83
 
72
84
  // Sort: dirs first, then files
73
85
  files.sort((a, b) => {
@@ -28,6 +28,102 @@ interface ProjectInfo {
28
28
  name: string;
29
29
  }
30
30
 
31
+ // ─── Skill File Tree (collapsible directories) ──────────
32
+
33
+ interface TreeNode {
34
+ name: string;
35
+ path: string;
36
+ type: 'file' | 'dir';
37
+ children: TreeNode[];
38
+ }
39
+
40
+ function buildTree(files: { name: string; path: string; type: string }[]): TreeNode[] {
41
+ const root: TreeNode[] = [];
42
+ const dirMap = new Map<string, TreeNode>();
43
+
44
+ for (const f of files) {
45
+ const parts = f.path.split('/');
46
+ if (f.type === 'dir') {
47
+ const node: TreeNode = { name: f.name.replace(/\/$/, ''), path: f.path, type: 'dir', children: [] };
48
+ dirMap.set(f.path, node);
49
+ // Find parent
50
+ const parentPath = parts.slice(0, -1).join('/');
51
+ const parent = parentPath ? dirMap.get(parentPath) : null;
52
+ if (parent) parent.children.push(node);
53
+ else root.push(node);
54
+ } else {
55
+ const node: TreeNode = { name: f.name, path: f.path, type: 'file', children: [] };
56
+ const parentPath = parts.slice(0, -1).join('/');
57
+ const parent = parentPath ? dirMap.get(parentPath) : null;
58
+ if (parent) parent.children.push(node);
59
+ else root.push(node);
60
+ }
61
+ }
62
+ return root;
63
+ }
64
+
65
+ function SkillFileTree({ files, activeFile, onSelect }: {
66
+ files: { name: string; path: string; type: string }[];
67
+ activeFile: string | null;
68
+ onSelect: (path: string) => void;
69
+ }) {
70
+ const tree = buildTree(files);
71
+ return <TreeNodeList nodes={tree} depth={0} activeFile={activeFile} onSelect={onSelect} />;
72
+ }
73
+
74
+ function TreeNodeList({ nodes, depth, activeFile, onSelect }: {
75
+ nodes: TreeNode[]; depth: number; activeFile: string | null; onSelect: (path: string) => void;
76
+ }) {
77
+ const [expanded, setExpanded] = useState<Set<string>>(new Set(
78
+ // Auto-expand first level
79
+ nodes.filter(n => n.type === 'dir').map(n => n.path)
80
+ ));
81
+
82
+ const toggle = (path: string) => {
83
+ setExpanded(prev => {
84
+ const next = new Set(prev);
85
+ next.has(path) ? next.delete(path) : next.add(path);
86
+ return next;
87
+ });
88
+ };
89
+
90
+ return (
91
+ <>
92
+ {nodes.map(node => (
93
+ node.type === 'dir' ? (
94
+ <div key={node.path}>
95
+ <button
96
+ onClick={() => toggle(node.path)}
97
+ className="w-full text-left px-1 py-0.5 text-[9px] text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)] flex items-center gap-0.5"
98
+ style={{ paddingLeft: `${depth * 10 + 4}px` }}
99
+ >
100
+ <span className="text-[8px]">{expanded.has(node.path) ? '▼' : '▶'}</span>
101
+ <span>📁 {node.name}</span>
102
+ </button>
103
+ {expanded.has(node.path) && (
104
+ <TreeNodeList nodes={node.children} depth={depth + 1} activeFile={activeFile} onSelect={onSelect} />
105
+ )}
106
+ </div>
107
+ ) : (
108
+ <button
109
+ key={node.path}
110
+ onClick={() => onSelect(node.path)}
111
+ className={`w-full text-left py-0.5 text-[10px] truncate ${
112
+ activeFile === node.path
113
+ ? 'bg-[var(--accent)]/15 text-[var(--accent)]'
114
+ : 'text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)] hover:text-[var(--text-primary)]'
115
+ }`}
116
+ style={{ paddingLeft: `${depth * 10 + 14}px` }}
117
+ title={node.path}
118
+ >
119
+ {node.name}
120
+ </button>
121
+ )
122
+ ))}
123
+ </>
124
+ );
125
+ }
126
+
31
127
  export default function SkillsPanel({ projectFilter }: { projectFilter?: string }) {
32
128
  const { sidebarWidth, onSidebarDragStart } = useSidebarResize({ defaultWidth: 224, minWidth: 140, maxWidth: 400 });
33
129
  const [skills, setSkills] = useState<Skill[]>([]);
@@ -223,14 +319,17 @@ export default function SkillsPanel({ projectFilter }: { projectFilter?: string
223
319
 
224
320
  // Filter by project, type, and search
225
321
  const q = searchQuery.toLowerCase();
226
- const filtered = typeFilter === 'local' ? [] : skills
322
+ const filtered = (typeFilter === 'local' ? [] : skills
227
323
  .filter(s => projectFilter ? (s.installedGlobal || s.installedProjects.includes(projectFilter)) : true)
228
324
  .filter(s => typeFilter === 'all' ? true : s.type === typeFilter)
229
- .filter(s => !q || s.name.toLowerCase().includes(q) || s.displayName.toLowerCase().includes(q) || s.description.toLowerCase().includes(q));
325
+ .filter(s => !q || s.name.toLowerCase().includes(q) || s.displayName.toLowerCase().includes(q) || s.description.toLowerCase().includes(q)
326
+ || s.author.toLowerCase().includes(q) || s.tags.some(t => t.toLowerCase().includes(q)))
327
+ ).sort((a, b) => a.displayName.localeCompare(b.displayName));
230
328
 
231
329
  const filteredLocal = localItems
232
330
  .filter(item => typeFilter === 'local' || typeFilter === 'all' || item.type === typeFilter)
233
- .filter(item => !q || item.name.toLowerCase().includes(q));
331
+ .filter(item => !q || item.name.toLowerCase().includes(q))
332
+ .sort((a, b) => a.name.localeCompare(b.name));
234
333
 
235
334
  // Group local items by scope
236
335
  const localGroups = new Map<string, typeof localItems>();
@@ -339,7 +438,7 @@ export default function SkillsPanel({ projectFilter }: { projectFilter?: string
339
438
  skill.type === 'skill' ? 'bg-purple-500/20 text-purple-400' : 'bg-blue-500/20 text-blue-400'
340
439
  }`}>{skill.type === 'skill' ? 'SKILL' : 'CMD'}</span>
341
440
  <span className="text-[8px] text-[var(--text-secondary)]">{skill.author}</span>
342
- {skill.tags.slice(0, 2).map(t => (
441
+ {skill.tags.slice(0, 3).map(t => (
343
442
  <span key={t} className="text-[7px] px-1 rounded bg-[var(--bg-tertiary)] text-[var(--text-secondary)]">{t}</span>
344
443
  ))}
345
444
  {skill.deletedRemotely && <span className="text-[8px] text-[var(--red)] ml-auto">deleted remotely</span>}
@@ -608,6 +707,13 @@ export default function SkillsPanel({ projectFilter }: { projectFilter?: string
608
707
  {skill?.author && (
609
708
  <div className="text-[9px] text-[var(--text-secondary)] mt-1">By {skill.author}</div>
610
709
  )}
710
+ {skill?.tags && skill.tags.length > 0 && (
711
+ <div className="flex flex-wrap gap-1 mt-1">
712
+ {skill.tags.map(t => (
713
+ <span key={t} className="text-[8px] px-1.5 py-0.5 rounded bg-[var(--bg-tertiary)] text-[var(--text-secondary)]">{t}</span>
714
+ ))}
715
+ </div>
716
+ )}
611
717
  {skill?.sourceUrl && (
612
718
  <a href={skill.sourceUrl} target="_blank" rel="noopener noreferrer" className="text-[9px] text-[var(--accent)] hover:underline mt-0.5 block truncate">{skill.sourceUrl.replace(/^https?:\/\//, '').slice(0, 60)}</a>
613
719
  )}
@@ -632,31 +738,16 @@ export default function SkillsPanel({ projectFilter }: { projectFilter?: string
632
738
 
633
739
  {/* File browser */}
634
740
  <div className="flex-1 flex min-h-0 overflow-hidden">
635
- {/* File list */}
636
- <div className="w-32 border-r border-[var(--border)] overflow-y-auto shrink-0">
741
+ {/* File tree */}
742
+ <div className="w-36 border-r border-[var(--border)] overflow-y-auto shrink-0">
637
743
  {skillFiles.length === 0 ? (
638
744
  <div className="p-2 text-[9px] text-[var(--text-secondary)]">Loading...</div>
639
745
  ) : (
640
- skillFiles.map(f => (
641
- f.type === 'file' ? (
642
- <button
643
- key={f.path}
644
- onClick={() => loadFile(itemName, f.path, isLocal, localItem?.type, localItem?.projectPath)}
645
- className={`w-full text-left px-2 py-1 text-[10px] truncate ${
646
- activeFile === f.path
647
- ? 'bg-[var(--accent)]/15 text-[var(--accent)]'
648
- : 'text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)] hover:text-[var(--text-primary)]'
649
- }`}
650
- title={f.path}
651
- >
652
- {f.name}
653
- </button>
654
- ) : (
655
- <div key={f.path} className="px-2 py-1 text-[9px] text-[var(--text-secondary)] font-semibold">
656
- {f.name}/
657
- </div>
658
- )
659
- ))
746
+ <SkillFileTree
747
+ files={skillFiles}
748
+ activeFile={activeFile}
749
+ onSelect={(path) => loadFile(itemName, path, isLocal, localItem?.type, localItem?.projectPath)}
750
+ />
660
751
  )}
661
752
  {skill?.sourceUrl && (
662
753
  <div className="border-t border-[var(--border)] p-2">
@@ -62,25 +62,101 @@ const TASK_STATUS: Record<string, { label: string; color: string; glow?: boolean
62
62
  };
63
63
 
64
64
  const PRESET_AGENTS: Omit<AgentConfig, 'id'>[] = [
65
- { label: 'PM', icon: '📋', role: 'Product Manager — analyze requirements, write PRD. Do NOT write code.', backend: 'cli', agentId: 'claude', dependsOn: [], outputs: ['docs/prd.md'], steps: [
66
- { id: 'analyze', label: 'Analyze', prompt: 'Read existing docs and project structure. Identify key requirements.' },
67
- { id: 'write', label: 'Write PRD', prompt: 'Write a detailed PRD to docs/prd.md.' },
68
- { id: 'review', label: 'Self-Review', prompt: 'Review and improve the PRD.' },
69
- ]},
70
- { label: 'Engineer', icon: '🔨', role: 'Senior Engineer — design and implement based on PRD.', backend: 'cli', agentId: 'claude', dependsOn: [], outputs: ['src/', 'docs/architecture.md'], steps: [
71
- { id: 'design', label: 'Design', prompt: 'Read PRD, design architecture, write docs/architecture.md.' },
72
- { id: 'implement', label: 'Implement', prompt: 'Implement features based on the architecture.' },
73
- { id: 'test', label: 'Self-Test', prompt: 'Review implementation and fix issues.' },
74
- ]},
75
- { label: 'QA', icon: '🧪', role: 'QA Engineer write and run tests. Do NOT fix bugs, only report.', backend: 'cli', agentId: 'claude', dependsOn: [], outputs: ['tests/', 'docs/test-plan.md'], steps: [
76
- { id: 'plan', label: 'Test Plan', prompt: 'Write test plan to docs/test-plan.md.' },
77
- { id: 'write', label: 'Write Tests', prompt: 'Implement test cases in tests/ directory.' },
78
- { id: 'run', label: 'Run Tests', prompt: 'Run all tests and document results.' },
79
- ]},
80
- { label: 'Reviewer', icon: '🔍', role: 'Code Reviewer review for quality and security. Do NOT modify code.', backend: 'cli', agentId: 'claude', dependsOn: [], outputs: ['docs/review.md'], steps: [
81
- { id: 'review', label: 'Review', prompt: 'Review all code changes for quality and security.' },
82
- { id: 'report', label: 'Report', prompt: 'Write review report to docs/review.md.' },
83
- ]},
65
+ {
66
+ label: 'PM', icon: '🎯', backend: 'cli', agentId: 'claude', dependsOn: [], outputs: ['docs/prd.md'],
67
+ role: `Product Manager — You own the requirements. Your job is to deeply understand the project context, analyze user needs, and produce a clear, actionable PRD.
68
+
69
+ Rules:
70
+ - NEVER write code or implementation details
71
+ - Focus on WHAT and WHY, not HOW
72
+ - Be specific: include user stories, acceptance criteria, edge cases, and priorities (P0/P1/P2)
73
+ - Reference existing codebase structure when relevant
74
+ - If requirements are unclear, list assumptions explicitly
75
+ - PRD format: Summary Goals User Stories Acceptance Criteria Out of Scope Open Questions`,
76
+ steps: [
77
+ { id: 'research', label: 'Research', prompt: 'Read the project README, existing docs, and codebase structure. Understand the current state, tech stack, and conventions. List what you found.' },
78
+ { id: 'analyze', label: 'Analyze Requirements', prompt: 'Based on the input requirements and your research, identify all user stories. For each story, define acceptance criteria. Classify priority as P0 (must have), P1 (should have), P2 (nice to have). List any assumptions and open questions.' },
79
+ { id: 'write-prd', label: 'Write PRD', prompt: 'Write a comprehensive PRD to docs/prd.md. Include: Executive Summary, Goals & Non-Goals, User Stories with Acceptance Criteria, Technical Constraints, Dependencies, Out of Scope, Open Questions. Be specific enough that an engineer can implement without asking questions.' },
80
+ { id: 'self-review', label: 'Self-Review', prompt: 'Review your PRD critically. Check: Are acceptance criteria testable? Are edge cases covered? Is scope clear? Are priorities justified? Revise if needed.' },
81
+ ],
82
+ },
83
+ {
84
+ label: 'Engineer', icon: '🔨', backend: 'cli', agentId: 'claude', dependsOn: [], outputs: ['src/', 'docs/architecture.md'],
85
+ role: `Senior Software Engineer — You design and implement features based on the PRD. You write production-quality code.
86
+
87
+ Rules:
88
+ - Read the PRD thoroughly before writing any code
89
+ - Design before implement: write architecture doc first
90
+ - Follow existing codebase conventions (naming, structure, patterns)
91
+ - Write clean, maintainable code with proper error handling
92
+ - Add inline comments only where logic isn't self-evident
93
+ - Run tests after implementation to catch obvious issues
94
+ - Commit atomically: one logical change per step
95
+ - If the PRD is unclear, make a reasonable decision and document it`,
96
+ steps: [
97
+ { id: 'design', label: 'Architecture', prompt: 'Read the PRD in docs/prd.md. Analyze the existing codebase structure and patterns. Design the architecture: what files to create/modify, data flow, interfaces, error handling strategy. Write docs/architecture.md with diagrams (ASCII or markdown) where helpful.' },
98
+ { id: 'implement', label: 'Implement', prompt: 'Implement the features based on your architecture doc. Follow existing code conventions. Handle errors properly. Add types/interfaces. Keep functions focused and testable. Create/modify files as planned.' },
99
+ { id: 'self-test', label: 'Self-Test', prompt: 'Review your implementation: check for bugs, missing error handling, edge cases, and convention violations. Run any existing tests. Fix issues you find. Do a final git diff review.' },
100
+ ],
101
+ },
102
+ {
103
+ label: 'QA', icon: '🧪', backend: 'cli', agentId: 'claude', dependsOn: [], outputs: ['tests/', 'docs/test-report.md'],
104
+ role: `QA Engineer — You ensure quality through comprehensive testing. You find bugs, you don't fix them.
105
+
106
+ Rules:
107
+ - NEVER fix bugs yourself — only report them clearly
108
+ - Test against PRD acceptance criteria, not assumptions
109
+ - Write both happy path and edge case tests
110
+ - Include integration tests, not just unit tests
111
+ - Run ALL tests (existing + new) and report results
112
+ - Report format: what failed, expected vs actual, steps to reproduce
113
+ - Check for security issues: injection, auth bypass, data leaks
114
+ - Check for performance: N+1 queries, unbounded loops, memory leaks`,
115
+ steps: [
116
+ { id: 'plan', label: 'Test Plan', prompt: 'Read the PRD (docs/prd.md) and the implementation. Create a test plan in docs/test-plan.md covering: unit tests, integration tests, edge cases, error scenarios, security checks, and performance concerns. Map each test to a PRD acceptance criterion.' },
117
+ { id: 'write-tests', label: 'Write Tests', prompt: 'Implement all test cases from your test plan in the tests/ directory. Follow the project\'s existing test framework and conventions. Include setup/teardown, meaningful assertions, and descriptive test names.' },
118
+ { id: 'run-tests', label: 'Run & Report', prompt: 'Run ALL tests (both existing and new). Document results in docs/test-report.md: total tests, passed, failed, skipped. For each failure: test name, expected vs actual, steps to reproduce. Include a summary verdict: PASS (all green) or FAIL (with blocking issues listed).' },
119
+ ],
120
+ },
121
+ {
122
+ label: 'Reviewer', icon: '👁', backend: 'cli', agentId: 'claude', dependsOn: [], outputs: ['docs/review.md'],
123
+ role: `Senior Code Reviewer — You review code for quality, security, maintainability, and correctness. You are the last gate before merge.
124
+
125
+ Rules:
126
+ - NEVER modify code — only review and report
127
+ - Check against PRD requirements: is everything implemented?
128
+ - Review architecture decisions: are they sound?
129
+ - Check code quality: readability, naming, DRY, error handling
130
+ - Check security: OWASP top 10, input validation, auth, secrets exposure
131
+ - Check performance: complexity, queries, caching, memory usage
132
+ - Check test coverage: are critical paths tested?
133
+ - Rate severity: CRITICAL (must fix) / MAJOR (should fix) / MINOR (nice to fix)
134
+ - Give actionable feedback: not just "this is bad" but "change X to Y because Z"`,
135
+ steps: [
136
+ { id: 'review-arch', label: 'Architecture Review', prompt: 'Read docs/prd.md and docs/architecture.md. Evaluate: Does the architecture satisfy all PRD requirements? Are there design flaws, scalability issues, or over-engineering? Document findings.' },
137
+ { id: 'review-code', label: 'Code Review', prompt: 'Review all changed/new files. For each file check: correctness, error handling, security (injection, auth, secrets), performance (N+1, unbounded), naming conventions, code duplication, edge cases. Use git diff to see exact changes.' },
138
+ { id: 'review-tests', label: 'Test Review', prompt: 'Review docs/test-report.md and test code. Check: Are all PRD acceptance criteria covered by tests? Are tests meaningful (not just asserting true)? Are edge cases tested? Any flaky test risks?' },
139
+ { id: 'report', label: 'Final Report', prompt: 'Write docs/review.md: Summary verdict (APPROVE / REQUEST_CHANGES / REJECT). List all findings grouped by severity (CRITICAL → MAJOR → MINOR). For each: file, line, issue, suggested fix. End with an overall assessment and recommendation.' },
140
+ ],
141
+ },
142
+ {
143
+ label: 'UI Designer', icon: '🎨', backend: 'cli', agentId: 'claude', dependsOn: [], outputs: ['docs/ui-spec.md'],
144
+ role: `UI/UX Designer — You design user interfaces and experiences. You create specs that engineers can implement.
145
+
146
+ Rules:
147
+ - Focus on user experience first, aesthetics second
148
+ - Design for the existing tech stack (check project's UI framework)
149
+ - Be specific: colors (hex), spacing (px/rem), typography, component hierarchy
150
+ - Consider responsive design, accessibility (WCAG), dark/light mode
151
+ - Include interaction states: hover, active, disabled, loading, error, empty
152
+ - Provide component tree structure, not just mockups
153
+ - Reference existing UI patterns in the codebase for consistency`,
154
+ steps: [
155
+ { id: 'audit', label: 'UI Audit', prompt: 'Analyze the existing UI: framework used (React/Vue/etc), component library, design tokens (colors, spacing, fonts), layout patterns. Document the current design system.' },
156
+ { id: 'design', label: 'Design Spec', prompt: 'Based on the PRD, design the UI. Write docs/ui-spec.md with: component hierarchy, layout (flexbox/grid), colors, typography, spacing, responsive breakpoints. Include all states (loading, empty, error, success). Use ASCII wireframes or describe precisely.' },
157
+ { id: 'interactions', label: 'Interactions', prompt: 'Define all user interactions: click flows, form validation, transitions, animations, keyboard shortcuts, mobile gestures. Document accessibility requirements (aria labels, focus management, screen reader support).' },
158
+ ],
159
+ },
84
160
  ];
85
161
 
86
162
  // ─── API helpers ─────────────────────────────────────────
@@ -10,6 +10,22 @@ Your job is to answer user questions about Forge features, configuration, and tr
10
10
  4. Give concise, actionable answers with code examples when helpful
11
11
  5. When generating files (YAML workflows, configs, scripts, etc.), **always save the file directly** to the appropriate directory rather than printing it. For pipeline workflows, save to `~/.forge/data/flows/<name>.yaml`. Tell the user the file path so they can find it. The terminal does not support copy/paste.
12
12
 
13
+ ## API Authentication
14
+
15
+ When you need to call Forge APIs (e.g., workspace, settings) that return "unauthorized", ask the user for their admin password and authenticate:
16
+
17
+ ```bash
18
+ # Step 1: Get API token (ask user for their admin password)
19
+ TOKEN=$(curl -s -X POST http://localhost:8403/api/auth/verify \
20
+ -H "Content-Type: application/json" \
21
+ -d '{"password":"USER_PASSWORD"}' | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))")
22
+
23
+ # Step 2: Use token in subsequent requests
24
+ curl -s -H "X-Forge-Token: $TOKEN" http://localhost:8403/api/workspace/...
25
+ ```
26
+
27
+ The token is valid for 24 hours. Store it in a variable and reuse for all API calls in this session. Only ask for the password once.
28
+
13
29
  ## Available documentation
14
30
 
15
31
  | File | Topic |
@@ -28,7 +28,9 @@ import type {
28
28
  import { AgentWorker } from './agent-worker';
29
29
  import { AgentBus } from './agent-bus';
30
30
  import { WatchManager } from './watch-manager';
31
- import { ApiBackend } from './backends/api-backend';
31
+ // ApiBackend loaded dynamically — its dependency chain uses @/src path aliases
32
+ // that only work in Next.js context, not in standalone tsx process
33
+ // import { ApiBackend } from './backends/api-backend';
32
34
  import { CliBackend } from './backends/cli-backend';
33
35
  import { appendAgentLog, saveWorkspace, saveWorkspaceSync, startAutoSave, stopAutoSave } from './persistence';
34
36
  import { hasForgeSkills, installForgeSkills } from './skill-installer';
@@ -1351,7 +1353,9 @@ export class WorkspaceOrchestrator extends EventEmitter {
1351
1353
  private createBackend(config: WorkspaceAgentConfig, agentId?: string) {
1352
1354
  switch (config.backend) {
1353
1355
  case 'api':
1354
- return new ApiBackend();
1356
+ // TODO: ApiBackend uses @/src path aliases that don't work in standalone tsx.
1357
+ // Need to refactor api-backend imports before enabling.
1358
+ throw new Error('API backend not yet supported in workspace daemon. Use CLI backend instead.');
1355
1359
  case 'cli':
1356
1360
  default: {
1357
1361
  // Resume existing claude session if available
@@ -75,7 +75,7 @@ function loadOrchestrator(id: string): WorkspaceOrchestrator {
75
75
  });
76
76
 
77
77
  orchestrators.set(id, orch);
78
- console.log(`[workspace] Loaded orchestrator: ${state.projectName} (${id})`);
78
+ // Loaded silently debug only if needed
79
79
  return orch;
80
80
  }
81
81
 
@@ -92,7 +92,7 @@ function unloadOrchestrator(id: string): void {
92
92
  }
93
93
  sseClients.delete(id);
94
94
  }
95
- console.log(`[workspace] Unloaded orchestrator: ${id}`);
95
+ // Unloaded silently
96
96
  }
97
97
 
98
98
  function evictIdleWorkspace(): boolean {
package/middleware.ts CHANGED
@@ -16,19 +16,28 @@ export function middleware(req: NextRequest) {
16
16
  return NextResponse.next();
17
17
  }
18
18
 
19
- // Check for NextAuth session cookie (works in Edge Runtime, no Node.js imports)
19
+ // Check for NextAuth session cookie (browser login)
20
20
  const hasSession =
21
21
  req.cookies.has('authjs.session-token') ||
22
22
  req.cookies.has('__Secure-authjs.session-token');
23
23
 
24
- if (!hasSession) {
25
- if (pathname.startsWith('/api/')) {
26
- return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
27
- }
28
- return NextResponse.redirect(new URL('/login', req.url));
24
+ if (hasSession) {
25
+ return NextResponse.next();
29
26
  }
30
27
 
31
- return NextResponse.next();
28
+ // Check for Forge API token (Help AI / CLI tools)
29
+ // Token obtained via POST /api/auth/verify with admin password
30
+ const forgeToken = req.headers.get('x-forge-token') || req.cookies.get('forge-api-token')?.value;
31
+ if (forgeToken) {
32
+ // Token validation happens in API route layer via isValidToken()
33
+ // Middleware passes it through — only localhost can obtain tokens
34
+ return NextResponse.next();
35
+ }
36
+
37
+ if (pathname.startsWith('/api/')) {
38
+ return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
39
+ }
40
+ return NextResponse.redirect(new URL('/login', req.url));
32
41
  }
33
42
 
34
43
  export const config = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {