@aion0/forge 0.5.49 → 0.5.50

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 (52) hide show
  1. package/RELEASE_NOTES.md +48 -7
  2. package/app/api/craft-system/build/route.ts +78 -0
  3. package/app/api/craft-system/delete/route.ts +28 -0
  4. package/app/api/craft-system/helpers/file/route.ts +20 -0
  5. package/app/api/craft-system/helpers/openapi/route.ts +27 -0
  6. package/app/api/craft-system/helpers/shell/route.ts +26 -0
  7. package/app/api/craft-system/inject/route.ts +41 -0
  8. package/app/api/craft-system/kill-session/route.ts +19 -0
  9. package/app/api/craft-system/manifest/route.ts +71 -0
  10. package/app/api/craft-system/marketplace/install/route.ts +11 -0
  11. package/app/api/craft-system/marketplace/route.ts +18 -0
  12. package/app/api/craft-system/marketplace/uninstall/route.ts +11 -0
  13. package/app/api/craft-system/marketplace/update/route.ts +10 -0
  14. package/app/api/craft-system/marketplace/updates/route.ts +17 -0
  15. package/app/api/craft-system/publish/auto/route.ts +173 -0
  16. package/app/api/craft-system/publish/route.ts +50 -0
  17. package/app/api/craft-system/registry/route.ts +16 -0
  18. package/app/api/craft-system/runtime/react/route.ts +26 -0
  19. package/app/api/craft-system/runtime/react-jsx/route.ts +11 -0
  20. package/app/api/craft-system/runtime/sdk/route.ts +18 -0
  21. package/app/api/craft-system/scaffold/route.ts +164 -0
  22. package/app/api/craft-system/sessions/route.ts +45 -0
  23. package/app/api/craft-system/storage/route.ts +44 -0
  24. package/app/api/craft-system/tmux-sessions/route.ts +62 -0
  25. package/app/api/craft-system/ui/route.ts +30 -0
  26. package/app/api/crafts/[name]/[...route]/route.ts +48 -0
  27. package/app/api/crafts/route.ts +29 -0
  28. package/components/CraftBuilder.tsx +241 -0
  29. package/components/CraftManifestEditor.tsx +258 -0
  30. package/components/CraftMarketplaceModal.tsx +207 -0
  31. package/components/CraftPublishModal.tsx +285 -0
  32. package/components/CraftTabs.tsx +279 -0
  33. package/components/CraftTerminal.tsx +305 -0
  34. package/components/CraftTerminalPicker.tsx +179 -0
  35. package/components/CraftsDropdown.tsx +186 -0
  36. package/components/CraftsMarketplacePanel.tsx +194 -0
  37. package/components/ProjectDetail.tsx +105 -1
  38. package/components/SkillsPanel.tsx +12 -4
  39. package/components/TaskDetail.tsx +49 -1
  40. package/lib/craft-sdk/client.tsx +260 -0
  41. package/lib/craft-sdk/server.ts +14 -0
  42. package/lib/crafts/loader.ts +117 -0
  43. package/lib/crafts/registry.ts +272 -0
  44. package/lib/crafts/runtime.ts +208 -0
  45. package/lib/crafts/types.ts +92 -0
  46. package/lib/forge-skills/craft-builder.md +231 -0
  47. package/lib/help-docs/15-crafts.md +127 -0
  48. package/lib/help-docs/CLAUDE.md +2 -0
  49. package/lib/terminal-standalone.ts +1 -0
  50. package/next.config.ts +1 -1
  51. package/package.json +2 -1
  52. package/tsconfig.json +6 -0
@@ -0,0 +1,231 @@
1
+ ---
2
+ name: craft-builder
3
+ description: Build a Forge "Craft" — a project-scoped mini-app exposed as a tab in Forge. Use when the user asks Forge to "make a tab/dashboard/tool that does X" inside their project.
4
+ ---
5
+
6
+ # Forge Craft Builder
7
+
8
+ A **Craft** is a project-scoped mini-app that appears as a tab in Forge. It can have:
9
+
10
+ - A React UI (`ui.tsx`) — renders inside Forge's project view
11
+ - An optional API server (`server.ts`) — handlers run on Forge's Node process
12
+
13
+ Crafts live at `<project>/.forge/crafts/<craft-name>/` and travel with the project (commit them to git so the team sees the same tabs).
14
+
15
+ ## Your job
16
+
17
+ When invoked, you produce ALL of these files in `<project>/.forge/crafts/<name>/`:
18
+
19
+ ```
20
+ craft.yaml # manifest
21
+ ui.tsx # React component (default export)
22
+ server.ts # optional — only if user needs server-side work
23
+ prompt.md # the original user request + iteration history (you maintain this)
24
+ README.md # 1-paragraph "what it does"
25
+ data/ # auto-created when craft writes via useStore
26
+ ```
27
+
28
+ After writing files, tell the user the new tab will appear in Forge after refresh (or hot-reload if dev mode).
29
+
30
+ ## Naming
31
+
32
+ Pick a kebab-case `name` based on what the user asked for. Keep it short. Example: "API endpoint dashboard" → `api-dashboard`.
33
+
34
+ The `displayName` is the tab label — include an emoji prefix matching the function (📊 for dashboards, 🔍 for explorers, ⚡ for runners, 📝 for editors, 🧪 for testers).
35
+
36
+ ## SDK — UI side (`ui.tsx`)
37
+
38
+ Import from `@forge/craft`. ONLY these hooks are available:
39
+
40
+ ```tsx
41
+ import { useProject, useForgeFetch, useInject, useTask, useStore } from '@forge/craft';
42
+
43
+ // 1. Project context
44
+ const { projectPath, projectName } = useProject();
45
+
46
+ // 2. Fetch data — auto-appends ?projectPath=...; returns { data, loading, error, refetch }
47
+ const { data, loading, error, refetch } = useForgeFetch<MyType>('/api/crafts/<your-name>/items');
48
+ // or any Forge core API:
49
+ const git = useForgeFetch('/api/git/status');
50
+
51
+ // 3. Inject text into the project's bound tmux terminal (auto-resolves session)
52
+ const inject = useInject();
53
+ await inject('Run the test suite'); // sends text + Enter
54
+
55
+ // 4. Spawn a Forge background task in the project
56
+ const runTask = useTask();
57
+ const t = await runTask('Refactor the auth module per CLAUDE.md');
58
+ const stop = t.watch(entry => console.log(entry), final => console.log('done', final));
59
+
60
+ // 5. Persistent JSON storage in <project>/.forge/crafts/<name>/data/<file>.json
61
+ const [items, setItems, { loading, reload }] = useStore<Item[]>('items.json', []);
62
+ await setItems([...items!, newItem]); // writes to disk
63
+ ```
64
+
65
+ Component must `export default` a React component. Use Tailwind classes and Forge CSS variables (`var(--accent)`, `var(--bg-secondary)`, `var(--text-primary)`, `var(--text-secondary)`, `var(--border)`, `var(--bg-primary)`, `var(--bg-tertiary)`) so the tab matches Forge's theme.
66
+
67
+ The component is rendered inside `<div className="flex-1 flex flex-col min-h-0 overflow-hidden">` — the outermost element should be a fragment or `<div className="flex-1 ...">`.
68
+
69
+ **Do not** import React directly (it's auto-injected). Do not import any other npm package — only `@forge/craft`.
70
+
71
+ ## SDK — Server side (`server.ts`, optional)
72
+
73
+ Skip this file entirely if the craft only needs to call existing Forge APIs.
74
+
75
+ ```ts
76
+ import { defineCraftServer } from '@forge/craft/server';
77
+
78
+ export default defineCraftServer({
79
+ routes: {
80
+ 'GET /items': async ({ projectPath, query, forge }) => {
81
+ // Run shell in project cwd
82
+ const r = forge.exec('git log --oneline -20', { timeout: 10000 });
83
+ return { lines: r.stdout.split('\n').filter(Boolean) };
84
+ },
85
+
86
+ 'POST /create': async ({ body, forge }) => {
87
+ forge.storage.write('records.json', body);
88
+ return { ok: true };
89
+ },
90
+
91
+ 'GET /load-spec': async ({ forge }) => {
92
+ const spec = forge.openapi('docs/openapi.json');
93
+ return { paths: Object.keys(spec?.paths || {}) };
94
+ },
95
+
96
+ 'POST /fix': async ({ body, forge }) => {
97
+ const t = forge.task({ prompt: body.prompt });
98
+ return { taskId: t.id };
99
+ },
100
+
101
+ 'POST /run-cmd': async ({ body, forge }) => {
102
+ forge.inject(body.cmd); // paste into bound terminal
103
+ return { ok: true };
104
+ },
105
+ },
106
+ });
107
+ ```
108
+
109
+ `forge` injected helper API:
110
+ - `forge.project` — `{ path, name }`
111
+ - `forge.storage` — `read(file)`, `write(file, data)`, `listFiles()` (scoped to the craft's data dir)
112
+ - `forge.exec(cmd, opts?)` — synchronous shell exec in project cwd, returns `{ stdout, stderr, code }`
113
+ - `forge.task({ prompt, agent? })` — spawn Forge background task, returns `{ id }`
114
+ - `forge.inject(text, opts?)` — paste into bound tmux session
115
+ - `forge.openapi(specPath)` — load + parse OpenAPI JSON from project
116
+ - `forge.log(...)` — structured logging
117
+
118
+ Routes are auto-mounted at `/api/crafts/<craft-name>/<route>`. The UI calls them via `useForgeFetch`.
119
+
120
+ ## Manifest (`craft.yaml`)
121
+
122
+ ```yaml
123
+ name: api-dashboard # kebab-case, dir name
124
+ displayName: 📊 API Dashboard # tab label (with emoji)
125
+ description: One-line summary of what it does
126
+ version: 0.1.0
127
+ icon: "📊" # optional, mainly cosmetic
128
+ author: aion0 # optional, shown in marketplace
129
+ tags: # optional — for marketplace browsing
130
+ - openapi
131
+ - dashboard
132
+ - migration
133
+ requires: # optional — project-type compatibility gate.
134
+ hasFile: # craft is hidden + can't install if NONE match.
135
+ - docs/openapi.json
136
+ - openapi.yaml
137
+ hasGlob:
138
+ - "**/*.java" # any of the matchers passing → compatible
139
+ ui:
140
+ tab: ui.tsx
141
+ showWhen: hasFile("docs/openapi.json") # optional extra UI condition
142
+ server:
143
+ entry: server.ts # omit this whole block if no server.ts
144
+ ```
145
+
146
+ **Tags + requires guidance:**
147
+
148
+ - `tags` are free-form keywords used by the marketplace search. Common ones:
149
+ language (`java`, `typescript`, `python`), framework (`spring`, `react`),
150
+ use-case (`migration`, `testing`, `linting`, `debugging`).
151
+ - `requires.hasFile` lists files that MUST exist somewhere in the project for
152
+ the craft to make sense (any one match = compatible).
153
+ - `requires.hasGlob` is for broader matches like `**/*.java` (project has Java)
154
+ or `package.json` (Node project).
155
+ - Be specific — a craft tagged `java` + `requires.hasGlob: ["**/*.java"]` won't
156
+ show up in a TypeScript project's marketplace.
157
+
158
+ ## prompt.md
159
+
160
+ Always write/update this file with:
161
+ - The original user request (verbatim)
162
+ - Each refine request and what you changed
163
+ - Used by future Refine runs as context
164
+
165
+ ## Iteration
166
+
167
+ When called to refine an existing craft (the dir already exists), READ existing files first, KEEP what works, change only what the user asked. Append the refine request to `prompt.md`.
168
+
169
+ ## Minimum viable example
170
+
171
+ ```yaml
172
+ # craft.yaml
173
+ name: hello
174
+ displayName: 👋 Hello
175
+ description: Demo craft — counts project files by extension
176
+ version: 0.1.0
177
+ ui:
178
+ tab: ui.tsx
179
+ server:
180
+ entry: server.ts
181
+ ```
182
+
183
+ ```ts
184
+ // server.ts
185
+ import { defineCraftServer } from '@forge/craft/server';
186
+
187
+ export default defineCraftServer({
188
+ routes: {
189
+ 'GET /count': async ({ forge }) => {
190
+ const r = forge.exec(`git ls-files | awk -F. 'NF>1{print $NF}' | sort | uniq -c | sort -rn | head -20`);
191
+ return { lines: r.stdout.split('\n').filter(Boolean) };
192
+ },
193
+ },
194
+ });
195
+ ```
196
+
197
+ ```tsx
198
+ // ui.tsx
199
+ import { useProject, useForgeFetch } from '@forge/craft';
200
+
201
+ export default function Tab() {
202
+ const { projectName } = useProject();
203
+ const { data, loading } = useForgeFetch<{ lines: string[] }>('/api/crafts/hello/count');
204
+ return (
205
+ <div className="flex-1 p-4 text-xs overflow-auto">
206
+ <h2 className="font-semibold mb-2">{projectName} — file counts</h2>
207
+ {loading && <div className="text-[var(--text-secondary)]">Loading…</div>}
208
+ {data?.lines.map((l, i) => <div key={i} className="font-mono">{l}</div>)}
209
+ </div>
210
+ );
211
+ }
212
+ ```
213
+
214
+ ## Style guide
215
+
216
+ - Tailwind classes only. Use Forge's color variables, not hardcoded colors.
217
+ - Text sizes: `text-xs` (default), `text-[11px]` for dense tables, `text-[10px]` for metadata.
218
+ - Buttons: `text-[10px] px-2 py-1 rounded bg-[var(--accent)]/20 text-[var(--accent)] hover:bg-[var(--accent)]/30`.
219
+ - Sections inside the tab: `flex-1 flex flex-col min-h-0 overflow-auto p-4 gap-3`.
220
+ - For tables/lists, prefer simple `<table>` or `<div>` grids — no extra deps.
221
+ - Match Forge's compact density (rows ~24-28px tall).
222
+
223
+ ## Final report
224
+
225
+ After writing files, report:
226
+ 1. What craft you created (name + displayName)
227
+ 2. The route(s) registered (if any server)
228
+ 3. The data files used (if any)
229
+ 4. Any assumptions you made
230
+
231
+ End with `[FORGE_DONE]`.
@@ -0,0 +1,127 @@
1
+ # Crafts — project-scoped mini-apps
2
+
3
+ A **Craft** is a tab inside Forge that lives at `<project>/.forge/crafts/<name>/`. It can be hand-written, AI-generated, or shipped as a Forge builtin (open-source samples). Crafts travel with the project — commit them to git so the team sees the same tabs.
4
+
5
+ ## Quick start
6
+
7
+ In any project, click **+ Craft** next to the project tabs. Type what you want (e.g. "show all our REST endpoints with migration status, allow batch run + AI fix"). Forge spawns a background task that uses the `craft-builder` skill to generate the files. After ~30-60s the new tab appears.
8
+
9
+ For an existing craft, switch to its tab and click the small **⚙** badge to refine it ("add a sort button" / "this column should be wider").
10
+
11
+ ## Anatomy
12
+
13
+ ```
14
+ <project>/.forge/crafts/<name>/
15
+ ├── craft.yaml # manifest (name, displayName, icon, conditions)
16
+ ├── ui.tsx # React component (default export)
17
+ ├── server.ts # optional API routes
18
+ ├── prompt.md # original user request + iteration history
19
+ ├── README.md # what this craft does
20
+ └── data/ # craft's persistent JSON storage
21
+ ```
22
+
23
+ ## SDK
24
+
25
+ Imports from `@forge/craft` (UI side):
26
+
27
+ | Hook | What it does |
28
+ |---|---|
29
+ | `useProject()` | `{ projectPath, projectName }` |
30
+ | `useForgeFetch(path)` | Fetch wrapper, auto-injects `?projectPath=...`, returns `{ data, loading, error, refetch }` |
31
+ | `useInject()` | `(text) => Promise` — paste prompt + Enter into the project's bound tmux session |
32
+ | `useTask()` | `(prompt) => TaskHandle` — spawn Forge background task, watch its log stream |
33
+ | `useStore(file, default)` | `[value, save, { loading, reload }]` — JSON storage in `data/<file>.json` |
34
+ | `useOpenAPI(path)` | Load + parse OpenAPI 3 spec from project |
35
+ | `useFile(path, { watch? })` | Read project file with optional polling |
36
+ | `useShell()` | `(cmd) => Promise<{ stdout, stderr, code }>` — exec in project cwd |
37
+ | `useGit()` | Git status / log info |
38
+ | `useToast()` | `(msg, kind)` — quick top notification |
39
+
40
+ Server side (`server.ts`):
41
+
42
+ ```ts
43
+ import { defineCraftServer } from '@forge/craft/server';
44
+
45
+ export default defineCraftServer({
46
+ routes: {
47
+ 'GET /items': async ({ forge, query, params }) => {
48
+ const r = forge.exec('git log --oneline -20');
49
+ return { lines: r.stdout.split('\n') };
50
+ },
51
+ 'POST /run': async ({ body, forge }) => {
52
+ const t = forge.task({ prompt: body.prompt });
53
+ return { taskId: t.id };
54
+ },
55
+ },
56
+ });
57
+ ```
58
+
59
+ `forge` injected helpers:
60
+ - `forge.project` — `{ path, name }`
61
+ - `forge.storage` — `read(file)`, `write(file, data)`, `listFiles()` (scoped to craft data dir)
62
+ - `forge.exec(cmd, opts?)` — sync shell exec in project cwd
63
+ - `forge.task({ prompt })` — spawn background task
64
+ - `forge.inject(text)` — paste into bound tmux session
65
+ - `forge.openapi(specPath)` — load + parse OpenAPI JSON
66
+ - `forge.log(...)` — structured logging
67
+
68
+ Routes are mounted at `/api/crafts/<craft-name>/<route>`. The UI calls them via `useForgeFetch`.
69
+
70
+ ## Manifest
71
+
72
+ ```yaml
73
+ name: api-dashboard # kebab-case, dir name
74
+ displayName: 📊 API Dashboard # tab label
75
+ description: One-line summary
76
+ version: 0.1.0
77
+ icon: "📊"
78
+ ui:
79
+ tab: ui.tsx # default
80
+ showWhen: hasFile("docs/openapi.json") # optional condition
81
+ server:
82
+ entry: server.ts # default; omit if no server
83
+ ```
84
+
85
+ `showWhen` supports `hasFile("path")` (only show tab when file exists) or `always`.
86
+
87
+ ## Builtins
88
+
89
+ `lib/builtin-crafts/<name>/` is the slot for crafts that ship with Forge by default. Currently empty — every craft is project-local at `<project>/.forge/crafts/<name>/`. Builtins (when present) appear automatically in every project; project-local crafts override builtins by name.
90
+
91
+ ## Marketplace
92
+
93
+ Crafts can be published to a shared registry (default: `aiwatching/forge-crafts` on GitHub). The marketplace browser is reachable from the **Crafts ▾** dropdown in any project tab — pick **🛒 Marketplace** to see installable crafts filtered by your project's compatibility.
94
+
95
+ ### Browse + install
96
+ - **Compatible / All / Installed** filter
97
+ - Shows version, author, tags, and a per-item Install / Update / Uninstall button
98
+ - Install copies the registry's files into `<project>/.forge/crafts/<name>/`; the new tab appears immediately
99
+
100
+ ### Project-type filtering (`requires`)
101
+
102
+ Add a `requires` block to `craft.yaml` so the marketplace only suggests the craft to compatible projects:
103
+
104
+ ```yaml
105
+ requires:
106
+ hasFile: # any of these files must exist
107
+ - docs/openapi.json
108
+ hasGlob:
109
+ - "**/*.java" # any of these globs must match
110
+ ```
111
+
112
+ Either matcher passing is enough (OR logic). With an empty/missing `requires`, the craft is compatible with every project.
113
+
114
+ ### Publish
115
+
116
+ When a project-local craft is the active tab, the **📦** button next to ⚙ opens the publish modal. It shows:
117
+ 1. **How to publish** — step-by-step (open a PR on the registry repo).
118
+ 2. **registry.json entry** — JSON snippet to append under `crafts: [...]`.
119
+ 3. **Files** — copy each file's contents (`craft.yaml`, `ui.tsx`, `server.ts`, `README.md`) to drop into the registry repo's `<name>/` folder.
120
+
121
+ Forge does NOT auto-push to GitHub. Submit the PR; once merged, all Forge users see it in their marketplace.
122
+
123
+ The repo URL is configurable via `craftsRepoUrl` in `~/.forge/data/settings.yaml` so teams can run their own private registry.
124
+
125
+ ## Architectural model
126
+
127
+ Forge is the **orchestrator**: discovers crafts, mounts UI tabs + API routes, provides the SDK. The craft is **your project's content** — stored in `<project>/.forge/`, not in Forge core. Generic features (Migration Cockpit will eventually move here) end up as crafts that live in your repo, not in Forge.
@@ -44,6 +44,7 @@ The token is valid for 24 hours. Store it in a variable and reuse for all API ca
44
44
  | `11-workspace.md` | Workspace (Forge Smiths) — multi-agent orchestration, daemon, message bus, profiles |
45
45
  | `12-usage.md` | Token usage analytics — charts, heatmap, cost estimation, by model/project/source |
46
46
  | `13-ide-plugins.md` | VSCode extension + IntelliJ plugin — install, tabs, multi-connection, agent terminal launching |
47
+ | `15-crafts.md` | Crafts — project-scoped mini-app tabs with SDK; AI-generated via "+ Craft" button |
47
48
 
48
49
  ## Matching questions to docs
49
50
 
@@ -67,3 +68,4 @@ The token is valid for 24 hours. Store it in a variable and reuse for all API ca
67
68
  - Sidebar collapse/project tabs/favorites → `07-projects.md`
68
69
  - VSCode/IntelliJ/IDE plugin/extension/marketplace → `13-ide-plugins.md`
69
70
  - vsce/vsix/JetBrains marketplace publish → `13-ide-plugins.md`
71
+ - Craft/custom tab/mini-app/extend project/AI-generated tab/builder → `15-crafts.md`
@@ -209,6 +209,7 @@ function cleanupOrphanedSessions() {
209
209
  for (const s of sessions) {
210
210
  if (s.attached) continue;
211
211
  if (s.name.startsWith(`${SESSION_PREFIX}forge-`)) continue; // workspace agent session — managed by orchestrator
212
+ if (s.name.startsWith('mw-craft-')) continue; // craft session — managed by craft loader
212
213
  if (knownSessions.has(s.name)) continue; // saved in terminal state — preserve
213
214
  const clients = sessionClients.get(s.name)?.size ?? 0;
214
215
  if (clients === 0) {
package/next.config.ts CHANGED
@@ -10,7 +10,7 @@ const localIPs = Object.values(networkInterfaces())
10
10
  .map(i => i!.address);
11
11
 
12
12
  const nextConfig: NextConfig = {
13
- serverExternalPackages: ['better-sqlite3'],
13
+ serverExternalPackages: ['better-sqlite3', 'esbuild'],
14
14
  allowedDevOrigins: localIPs,
15
15
  async rewrites() {
16
16
  return [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.5.49",
3
+ "version": "0.5.50",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -41,6 +41,7 @@
41
41
  "@xyflow/react": "^12.10.1",
42
42
  "ai": "^6.0.116",
43
43
  "better-sqlite3": "^12.6.2",
44
+ "esbuild": "^0.27.3",
44
45
  "next": "^16.2.1",
45
46
  "next-auth": "5.0.0-beta.30",
46
47
  "node-pty": "1.0.0",
package/tsconfig.json CHANGED
@@ -25,6 +25,12 @@
25
25
  "paths": {
26
26
  "@/*": [
27
27
  "./*"
28
+ ],
29
+ "@forge/craft": [
30
+ "./lib/craft-sdk/client.tsx"
31
+ ],
32
+ "@forge/craft/server": [
33
+ "./lib/craft-sdk/server.ts"
28
34
  ]
29
35
  }
30
36
  },