@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 +14 -8
- package/app/api/kanban/[id]/artifact/[...name]/route.ts +33 -0
- package/app/api/kanban/[id]/run/route.ts +27 -0
- package/app/api/kanban/route.ts +102 -0
- package/app/api/kanban/seed/route.ts +14 -0
- package/bin/forge-server.mjs +20 -9
- package/components/HomeView.tsx +9 -1
- package/components/KanbanBoard.tsx +354 -0
- package/lib/chat/agent-loop.ts +1 -1
- package/lib/claude-process.ts +12 -0
- package/lib/help-docs/14-kanban.md +59 -0
- package/lib/kanban/artifacts.ts +52 -0
- package/lib/kanban/executor.ts +141 -0
- package/lib/kanban/seed.ts +43 -0
- package/lib/kanban/store.ts +219 -0
- package/lib/kanban/task-executor.ts +71 -0
- package/lib/kanban/task-listener.ts +63 -0
- package/lib/kanban/tick.ts +41 -0
- package/lib/kanban/types.ts +104 -0
- package/lib/schedules/scheduler.ts +6 -0
- package/lib/task-manager.ts +11 -1
- package/package.json +2 -2
- package/publish.sh +1 -1
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
# Forge v0.
|
|
1
|
+
# Forge v0.11.0
|
|
2
2
|
|
|
3
|
-
Released: 2026-06-
|
|
3
|
+
Released: 2026-06-18
|
|
4
4
|
|
|
5
|
-
## Changes since v0.10.
|
|
5
|
+
## Changes since v0.10.90
|
|
6
6
|
|
|
7
7
|
### Other
|
|
8
|
-
- fix(
|
|
9
|
-
- fix(
|
|
10
|
-
-
|
|
11
|
-
- feat(
|
|
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.
|
|
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
|
+
}
|
package/bin/forge-server.mjs
CHANGED
|
@@ -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
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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); });
|
package/components/HomeView.tsx
CHANGED
|
@@ -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
|
}
|