@aion0/forge 0.10.89 → 0.11.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.
package/RELEASE_NOTES.md CHANGED
@@ -1,14 +1,20 @@
1
- # Forge v0.10.89
1
+ # Forge v0.11.0
2
2
 
3
- Released: 2026-06-17
3
+ Released: 2026-06-18
4
4
 
5
- ## Changes since v0.10.88
5
+ ## Changes since v0.10.90
6
6
 
7
7
  ### Other
8
- - fix(build): clean install on fresh machines webpack build + valid route exports
9
- - fix(projects): strict resolve requires git-backed match + fresh-clone visibility
10
- - refactor(projects): auto-clone lands in project root, retire cloned-projects
11
- - feat(projects): auto-clone lands in a real project root, not the cache
8
+ - fix(kanban): denser compact card tighter spacing, thin list rows, hide footer, slim callout, +20px height
9
+ - fix(kanban): smaller tiles, single-line list rows, one-row tiles in modal, terse-text guidance
10
+ - fix(kanban): smaller compact metric tiles + wider default modal (760px)
11
+ - feat(kanban): polished widget renderer Style A cards + Style B modal
12
+ - fix(server): don't pass --use-system-ca when NODE_EXTRA_CA_CERTS is set
13
+ - fix(task): auto-inject corporate CA into spawned CLI env
14
+ - fix(kanban): task settle by widget-file (survive exit-1) + reconcile + lean prompt
15
+ - feat(kanban): task-backed execution mode (B) + artifacts + help doc
16
+ - feat(kanban): compact cards + expand modal + in-place prompt editing
17
+ - feat(kanban): connector-driven home dashboard (P1–P4)
12
18
 
13
19
 
14
- **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.88...v0.10.89
20
+ **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.90...v0.11.0
@@ -0,0 +1,33 @@
1
+ /**
2
+ * GET /api/kanban/:id/artifact/:name — serve a file a task-mode card wrote into
3
+ * <dataDir>/kanban/:id/ (report.md, data, …) so widgets can link to richer
4
+ * generated content. Read-only, path-sanitized (no traversal). Auth via proxy.ts.
5
+ */
6
+
7
+ import { NextResponse } from 'next/server';
8
+ import { readFileSync } from 'node:fs';
9
+ import { extname } from 'node:path';
10
+ import { safeArtifactPath } from '@/lib/kanban/artifacts';
11
+
12
+ const TYPES: Record<string, string> = {
13
+ '.md': 'text/markdown; charset=utf-8',
14
+ '.txt': 'text/plain; charset=utf-8',
15
+ '.json': 'application/json; charset=utf-8',
16
+ '.csv': 'text/csv; charset=utf-8',
17
+ '.html': 'text/html; charset=utf-8',
18
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.svg': 'image/svg+xml',
19
+ };
20
+
21
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string; name: string[] }> }) {
22
+ const { id, name } = await params;
23
+ const rel = (name || []).join('/');
24
+ const full = safeArtifactPath(id, rel);
25
+ if (!full) return NextResponse.json({ error: 'not found' }, { status: 404 });
26
+ try {
27
+ const buf = readFileSync(full);
28
+ const ct = TYPES[extname(full).toLowerCase()] || 'application/octet-stream';
29
+ return new NextResponse(buf, { headers: { 'content-type': ct, 'cache-control': 'no-store' } });
30
+ } catch {
31
+ return NextResponse.json({ error: 'unreadable' }, { status: 404 });
32
+ }
33
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * POST /api/kanban/:id/run — run a card now (manual refresh). Inline cards run
3
+ * synchronously and return the fresh widget; task-mode cards are dispatched and
4
+ * return immediately with status 'running' (the task listener settles them).
5
+ * Auth handled by proxy.ts.
6
+ */
7
+
8
+ import { NextResponse } from 'next/server';
9
+ import { runKanbanCard } from '@/lib/kanban/executor';
10
+ import { runKanbanCardViaTask } from '@/lib/kanban/task-executor';
11
+ import { installKanbanTaskListener } from '@/lib/kanban/task-listener';
12
+ import { getCard } from '@/lib/kanban/store';
13
+
14
+ export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
15
+ const { id } = await params;
16
+ const card = getCard(id);
17
+ if (!card) return NextResponse.json({ error: 'card not found' }, { status: 404 });
18
+
19
+ if (card.execMode === 'task') {
20
+ installKanbanTaskListener(); // ensure settle works even if tick never ran
21
+ const taskId = runKanbanCardViaTask(id);
22
+ return NextResponse.json({ ok: !!taskId, dispatched: true, taskId, card: getCard(id) });
23
+ }
24
+
25
+ const r = await runKanbanCard(id);
26
+ return NextResponse.json({ ok: r.ok, error: r.error, card: getCard(id) });
27
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * GET /api/kanban list cards
3
+ * POST /api/kanban create card { connectorId, title, prompt, icon?, periodSec? }
4
+ * /api/kanban body { reorder: [{id, order}] } → batch reorder
5
+ * PATCH /api/kanban?id= update card
6
+ * DELETE /api/kanban?id= delete card
7
+ *
8
+ * Card execution (run/refresh) lands in P2. Auth is handled by proxy.ts.
9
+ */
10
+
11
+ import { NextResponse } from 'next/server';
12
+ import {
13
+ listCards,
14
+ createCard,
15
+ updateCard,
16
+ deleteCard,
17
+ reorderCards,
18
+ } from '@/lib/kanban/store';
19
+ import { getInstalledConnector } from '@/lib/connectors/registry';
20
+ import { reconcileRunningKanbanTaskCards } from '@/lib/kanban/task-listener';
21
+ import type { KanbanCard, UpdateKanbanCardInput } from '@/lib/kanban/types';
22
+
23
+ /** Attach each card's connector website (base_url) so the UI can link out. */
24
+ function withConnectorUrl(cards: KanbanCard[]): KanbanCard[] {
25
+ const cache = new Map<string, string | null>();
26
+ return cards.map((c) => {
27
+ if (!cache.has(c.connectorId)) {
28
+ const cfg = (getInstalledConnector(c.connectorId)?.config || {}) as Record<string, unknown>;
29
+ const url = (cfg.base_url || cfg.baseUrl || cfg.url || null) as string | null;
30
+ cache.set(c.connectorId, url);
31
+ }
32
+ return { ...c, connectorUrl: cache.get(c.connectorId) ?? null };
33
+ });
34
+ }
35
+
36
+ export async function GET() {
37
+ // Self-heal task-mode cards stuck 'running' whose task already finished
38
+ // (covers missed terminal events) before returning the list.
39
+ try { reconcileRunningKanbanTaskCards(listCards()); } catch { /* best effort */ }
40
+ return NextResponse.json({ cards: withConnectorUrl(listCards()) });
41
+ }
42
+
43
+ export async function POST(req: Request) {
44
+ let body: any = {};
45
+ try { body = await req.json(); } catch {}
46
+
47
+ // Batch reorder shares the POST verb (drag-to-reorder sends the whole order).
48
+ if (Array.isArray(body.reorder)) {
49
+ const rows = body.reorder
50
+ .filter((r: any) => r && typeof r.id === 'string' && typeof r.order === 'number')
51
+ .map((r: any) => ({ id: r.id, order: r.order }));
52
+ reorderCards(rows);
53
+ return NextResponse.json({ ok: true, cards: listCards() });
54
+ }
55
+
56
+ const connectorId = typeof body.connectorId === 'string' ? body.connectorId.trim() : '';
57
+ const title = typeof body.title === 'string' ? body.title.trim() : '';
58
+ const prompt = typeof body.prompt === 'string' ? body.prompt.trim() : '';
59
+ if (!connectorId || !title || !prompt) {
60
+ return NextResponse.json(
61
+ { error: 'connectorId, title and prompt are required' },
62
+ { status: 400 },
63
+ );
64
+ }
65
+ const card = createCard({
66
+ connectorId,
67
+ title,
68
+ prompt,
69
+ icon: typeof body.icon === 'string' ? body.icon : null,
70
+ periodSec: typeof body.periodSec === 'number' && body.periodSec > 0 ? body.periodSec : undefined,
71
+ enabled: body.enabled !== false,
72
+ });
73
+ return NextResponse.json({ ok: true, card });
74
+ }
75
+
76
+ export async function PATCH(req: Request) {
77
+ const id = new URL(req.url).searchParams.get('id');
78
+ if (!id) return NextResponse.json({ error: 'id query param required' }, { status: 400 });
79
+
80
+ let body: any = {};
81
+ try { body = await req.json(); } catch {}
82
+
83
+ const patch: UpdateKanbanCardInput = {};
84
+ if (typeof body.title === 'string') patch.title = body.title;
85
+ if (typeof body.prompt === 'string') patch.prompt = body.prompt;
86
+ if (body.icon === null || typeof body.icon === 'string') patch.icon = body.icon;
87
+ if (typeof body.periodSec === 'number' && body.periodSec > 0) patch.periodSec = body.periodSec;
88
+ if (typeof body.order === 'number') patch.order = body.order;
89
+ if (typeof body.enabled === 'boolean') patch.enabled = body.enabled;
90
+
91
+ const ok = updateCard(id, patch);
92
+ if (!ok) return NextResponse.json({ error: 'card not found or nothing to update' }, { status: 404 });
93
+ return NextResponse.json({ ok: true });
94
+ }
95
+
96
+ export async function DELETE(req: Request) {
97
+ const id = new URL(req.url).searchParams.get('id');
98
+ if (!id) return NextResponse.json({ error: 'id query param required' }, { status: 400 });
99
+ const ok = deleteCard(id);
100
+ if (!ok) return NextResponse.json({ error: 'card not found' }, { status: 404 });
101
+ return NextResponse.json({ ok: true });
102
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * POST /api/kanban/seed — create kanban cards from installed connectors'
3
+ * declared `kanban` defaults (idempotent). Returns the newly-created cards +
4
+ * the full list. Auth handled by proxy.ts.
5
+ */
6
+
7
+ import { NextResponse } from 'next/server';
8
+ import { seedKanbanFromConnectors } from '@/lib/kanban/seed';
9
+ import { listCards } from '@/lib/kanban/store';
10
+
11
+ export async function POST() {
12
+ const { created } = seedKanbanFromConnectors();
13
+ return NextResponse.json({ ok: true, created, cards: listCards() });
14
+ }
@@ -66,12 +66,11 @@ function buildNext() {
66
66
  // installed npm-package copy (.npmrc isn't published).
67
67
  execSync('npm install --include=dev --legacy-peer-deps', { cwd: ROOT, stdio: 'inherit' });
68
68
  }
69
- // Build with webpack, not Turbopack (Next 16's default). Turbopack chokes on
70
- // the Proxy/middleware bundle proxy.ts pulls auth + a route module that
71
- // transitively imports Node-native deps (better-sqlite3 ) and dies with
72
- // "TurbopackInternalError: Expected process result to be a module". Webpack
73
- // (the pre-16 path) handles the Node-runtime middleware fine.
74
- execSync('npx next build --webpack', { cwd: ROOT, stdio: 'inherit', env: { ...process.env } });
69
+ // Default bundler (Turbopack). Builds clean on the pinned next@16.2.1 the
70
+ // "Expected process result to be a module" / "Module parse failed" failures
71
+ // were a next version drifting past 16.2.1 (see package.json pin), not a
72
+ // Turbopack-vs-webpack issue.
73
+ execSync('npx next build', { cwd: ROOT, stdio: 'inherit', env: { ...process.env } });
75
74
  }
76
75
 
77
76
  /**
@@ -730,7 +729,11 @@ function startBackground() {
730
729
  // inspection, Zscaler, etc.) install a custom root via MDM; Node
731
730
  // doesn't see it without this flag, so marketplace sync fails with
732
731
  // 'self-signed certificate in certificate chain' on api.github.com.
733
- NODE_OPTIONS: [process.env.NODE_OPTIONS, '--use-system-ca'].filter(Boolean).join(' '),
732
+ // BUT it's inherited by spawned `claude` CLIs, where on some macOS
733
+ // setups it breaks TLS to api.anthropic.com ("SSL certificate
734
+ // verification failed"). Skip it when an explicit CA bundle is
735
+ // configured (NODE_EXTRA_CA_CERTS already covers the corp root).
736
+ NODE_OPTIONS: [process.env.NODE_OPTIONS, process.env.NODE_EXTRA_CA_CERTS ? '' : '--use-system-ca'].filter(Boolean).join(' '),
734
737
  },
735
738
  detached: true,
736
739
  });
@@ -821,7 +824,11 @@ if (isDev) {
821
824
  // inspection, Zscaler, etc.) install a custom root via MDM; Node
822
825
  // doesn't see it without this flag, so marketplace sync fails with
823
826
  // 'self-signed certificate in certificate chain' on api.github.com.
824
- NODE_OPTIONS: [process.env.NODE_OPTIONS, '--use-system-ca'].filter(Boolean).join(' '),
827
+ // BUT it's inherited by spawned `claude` CLIs, where on some macOS
828
+ // setups it breaks TLS to api.anthropic.com ("SSL certificate
829
+ // verification failed"). Skip it when an explicit CA bundle is
830
+ // configured (NODE_EXTRA_CA_CERTS already covers the corp root).
831
+ NODE_OPTIONS: [process.env.NODE_OPTIONS, process.env.NODE_EXTRA_CA_CERTS ? '' : '--use-system-ca'].filter(Boolean).join(' '),
825
832
  },
826
833
  });
827
834
  child.on('exit', (code) => { stopServices(); process.exit(code || 0); });
@@ -840,7 +847,11 @@ if (isDev) {
840
847
  // inspection, Zscaler, etc.) install a custom root via MDM; Node
841
848
  // doesn't see it without this flag, so marketplace sync fails with
842
849
  // 'self-signed certificate in certificate chain' on api.github.com.
843
- NODE_OPTIONS: [process.env.NODE_OPTIONS, '--use-system-ca'].filter(Boolean).join(' '),
850
+ // BUT it's inherited by spawned `claude` CLIs, where on some macOS
851
+ // setups it breaks TLS to api.anthropic.com ("SSL certificate
852
+ // verification failed"). Skip it when an explicit CA bundle is
853
+ // configured (NODE_EXTRA_CA_CERTS already covers the corp root).
854
+ NODE_OPTIONS: [process.env.NODE_OPTIONS, process.env.NODE_EXTRA_CA_CERTS ? '' : '--use-system-ca'].filter(Boolean).join(' '),
844
855
  },
845
856
  });
846
857
  child.on('exit', (code) => { stopServices(); process.exit(code || 0); });
@@ -4,6 +4,7 @@ import { useState, useEffect, useRef, useCallback, lazy, Suspense } from 'react'
4
4
 
5
5
  const WebChatPanel = lazy(() => import('./WebChatPanel'));
6
6
  const PipelineActivityPanel = lazy(() => import('./PipelineActivityPanel'));
7
+ const KanbanBoard = lazy(() => import('./KanbanBoard'));
7
8
 
8
9
  // Home view: 3 columns with draggable splitters.
9
10
  // col 1 — Session list (inside WebChatPanel; width controlled via prop)
@@ -84,7 +85,13 @@ export default function HomeView() {
84
85
  }, [leftWidth, rightWidth]);
85
86
 
86
87
  return (
87
- <div className="flex-1 flex min-h-0 min-w-0">
88
+ <div className="flex-1 flex flex-col min-h-0 min-w-0">
89
+ {/* Top: connector-driven Kanban board (self-renders from prompt output). */}
90
+ <Suspense fallback={null}>
91
+ <KanbanBoard />
92
+ </Suspense>
93
+
94
+ <div className="flex-1 flex min-h-0 min-w-0">
88
95
  {/* Chat (session list + main chat — WebChatPanel handles both internally).
89
96
  The session-list splitter is rendered INSIDE WebChatPanel, between its
90
97
  aside and main, so it sits at the actual sidebar boundary. */}
@@ -137,6 +144,7 @@ export default function HomeView() {
137
144
  </div>
138
145
  </>
139
146
  )}
147
+ </div>
140
148
  </div>
141
149
  );
142
150
  }