create-kai 0.1.4 → 0.2.1
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/README.md
CHANGED
|
@@ -15,8 +15,17 @@ history + the kit's local mock: a project that streams a reply on the first
|
|
|
15
15
|
|
|
16
16
|
**First slice of v1.** A `ready` framework has been scaffolded, installed from
|
|
17
17
|
the registry, built by its own build script, and driven in a browser — a message
|
|
18
|
-
sent, and a reply streaming into `<kai-thread>`. Remaining frameworks, layouts
|
|
19
|
-
and gateways are declared in the tables but not offered.
|
|
18
|
+
sent, and a reply streaming into `<kai-thread>`. Remaining frameworks, layouts,
|
|
19
|
+
features and gateways are declared in the tables but not offered.
|
|
20
|
+
|
|
21
|
+
**The prompt offers only what scaffolds, and states the rest.** One surface runs
|
|
22
|
+
today — the hand-composed workspace, with conversation history — so the CLI says
|
|
23
|
+
so rather than showing a menu of six and refusing five of them after the last
|
|
24
|
+
question. Where an axis has one possible answer it is stated, not asked, and
|
|
25
|
+
`--features <id>` for anything unavailable fails immediately with what IS
|
|
26
|
+
available, before a file is written. `test/menu-honesty.test.ts` drives every
|
|
27
|
+
option the prompt offers through `generate()`, so a menu item that cannot be
|
|
28
|
+
scaffolded fails a test rather than a user.
|
|
20
29
|
|
|
21
30
|
**`create-kai --list` is the roster.** This paragraph used to name which
|
|
22
31
|
frameworks were ready, and it was wrong within a day of each one landing — so
|
package/dist/index.js
CHANGED
|
@@ -789,6 +789,29 @@ ${import_picocolors2.default.gray(d2)} ${t}
|
|
|
789
789
|
|
|
790
790
|
`);
|
|
791
791
|
};
|
|
792
|
+
var M2 = { message: (t = "", { symbol: n = import_picocolors2.default.gray(o) } = {}) => {
|
|
793
|
+
const r2 = [`${import_picocolors2.default.gray(o)}`];
|
|
794
|
+
if (t) {
|
|
795
|
+
const [i, ...s] = t.split(`
|
|
796
|
+
`);
|
|
797
|
+
r2.push(`${n} ${i}`, ...s.map((c) => `${import_picocolors2.default.gray(o)} ${c}`));
|
|
798
|
+
}
|
|
799
|
+
process.stdout.write(`${r2.join(`
|
|
800
|
+
`)}
|
|
801
|
+
`);
|
|
802
|
+
}, info: (t) => {
|
|
803
|
+
M2.message(t, { symbol: import_picocolors2.default.blue(q) });
|
|
804
|
+
}, success: (t) => {
|
|
805
|
+
M2.message(t, { symbol: import_picocolors2.default.green(D) });
|
|
806
|
+
}, step: (t) => {
|
|
807
|
+
M2.message(t, { symbol: import_picocolors2.default.green(C) });
|
|
808
|
+
}, warn: (t) => {
|
|
809
|
+
M2.message(t, { symbol: import_picocolors2.default.yellow(U) });
|
|
810
|
+
}, warning: (t) => {
|
|
811
|
+
M2.warn(t);
|
|
812
|
+
}, error: (t) => {
|
|
813
|
+
M2.message(t, { symbol: import_picocolors2.default.red(K2) });
|
|
814
|
+
} };
|
|
792
815
|
var J2 = `${import_picocolors2.default.gray(o)} `;
|
|
793
816
|
var Y2 = ({ indicator: t = "dots" } = {}) => {
|
|
794
817
|
const n = V2 ? ["\u25D2", "\u25D0", "\u25D3", "\u25D1"] : ["\u2022", "o", "O", "0"], r2 = V2 ? 80 : 120, i = process.env.CI === "true";
|
|
@@ -856,7 +879,13 @@ var openai = {
|
|
|
856
879
|
// Both model and tools come from the browser. \`tools\` is undefined unless the
|
|
857
880
|
// front end declared any, and JSON.stringify drops it, so the same handler
|
|
858
881
|
// serves a tool archetype and a plain chat.
|
|
859
|
-
|
|
882
|
+
let chatBody: ChatRequestBody;
|
|
883
|
+
try {
|
|
884
|
+
chatBody = await readChatRequest(request);
|
|
885
|
+
} catch (error) {
|
|
886
|
+
return toChatErrorResponse(error);
|
|
887
|
+
}
|
|
888
|
+
const { model, messages, tools } = chatBody;
|
|
860
889
|
|
|
861
890
|
const upstream = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
862
891
|
method: 'POST',
|
|
@@ -1223,7 +1252,13 @@ function reframeToOpenAISse(body: ReadableStream<Uint8Array>): ReadableStream<Ui
|
|
|
1223
1252
|
async function chatHandler(request: Request): Promise<Response> {
|
|
1224
1253
|
// Both model and tools come from the browser. \`tools\` arrives in OpenAI
|
|
1225
1254
|
// function form and is converted to this API's shape below.
|
|
1226
|
-
|
|
1255
|
+
let chatBody: ChatRequestBody;
|
|
1256
|
+
try {
|
|
1257
|
+
chatBody = await readChatRequest(request);
|
|
1258
|
+
} catch (error) {
|
|
1259
|
+
return toChatErrorResponse(error);
|
|
1260
|
+
}
|
|
1261
|
+
const { model, messages, tools } = chatBody;
|
|
1227
1262
|
const { system, messages: anthropicMessages } = toAnthropicBody(messages);
|
|
1228
1263
|
const anthropicTools = toAnthropicTools(tools);
|
|
1229
1264
|
|
|
@@ -1353,7 +1388,13 @@ var openrouter = {
|
|
|
1353
1388
|
webRoute: `async function chatHandler(request: Request): Promise<Response> {
|
|
1354
1389
|
// tools is undefined unless the front end sent one; JSON.stringify drops it,
|
|
1355
1390
|
// so the same handler serves a tool archetype and a plain chat.
|
|
1356
|
-
|
|
1391
|
+
let chatBody: ChatRequestBody;
|
|
1392
|
+
try {
|
|
1393
|
+
chatBody = await readChatRequest(request);
|
|
1394
|
+
} catch (error) {
|
|
1395
|
+
return toChatErrorResponse(error);
|
|
1396
|
+
}
|
|
1397
|
+
const { model, messages, tools } = chatBody;
|
|
1357
1398
|
|
|
1358
1399
|
const upstream = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
1359
1400
|
method: 'POST',
|
|
@@ -1582,7 +1623,13 @@ const FINISH_REASONS: Record<string, string> = {
|
|
|
1582
1623
|
};
|
|
1583
1624
|
|
|
1584
1625
|
async function chatHandler(request: Request): Promise<Response> {
|
|
1585
|
-
|
|
1626
|
+
let chatBody: ChatRequestBody;
|
|
1627
|
+
try {
|
|
1628
|
+
chatBody = await readChatRequest(request);
|
|
1629
|
+
} catch (error) {
|
|
1630
|
+
return toChatErrorResponse(error);
|
|
1631
|
+
}
|
|
1632
|
+
const { messages, tools } = chatBody;
|
|
1586
1633
|
const toolSet = toToolSet(tools);
|
|
1587
1634
|
const prompt = toModelMessages(messages);
|
|
1588
1635
|
|
|
@@ -1833,7 +1880,13 @@ const agent = createReactAgent({
|
|
|
1833
1880
|
|
|
1834
1881
|
// Stream a compiled LangGraph agent to the browser as OpenAI-format SSE.
|
|
1835
1882
|
async function chatHandler(request: Request): Promise<Response> {
|
|
1836
|
-
|
|
1883
|
+
let chatBody: ChatRequestBody;
|
|
1884
|
+
try {
|
|
1885
|
+
chatBody = await readChatRequest(request);
|
|
1886
|
+
} catch (error) {
|
|
1887
|
+
return toChatErrorResponse(error);
|
|
1888
|
+
}
|
|
1889
|
+
const { messages } = chatBody;
|
|
1837
1890
|
|
|
1838
1891
|
// agent.stream() coerces plain {role, content} objects into BaseMessage
|
|
1839
1892
|
// instances itself, including OpenAI-shaped tool_calls, so the wire messages
|
|
@@ -1952,9 +2005,44 @@ import type { OpenAIWireMessage } from '@kitn.ai/ui/wire';
|
|
|
1952
2005
|
*/
|
|
1953
2006
|
type ChatRequestBody = { messages: OpenAIWireMessage[] };
|
|
1954
2007
|
|
|
2008
|
+
class ChatRequestError extends Error {
|
|
2009
|
+
constructor(readonly status: number, message: string) { super(message); }
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
/** Narrow the JSON body once, at the edge. A bare GET, a malformed body, or a
|
|
2013
|
+
* missing messages array is a ChatRequestError with a status \u2014 NEVER an
|
|
2014
|
+
* unhandled SyntaxError (findings F-10). */
|
|
2015
|
+
async function readChatRequest(request: Request): Promise<ChatRequestBody> {
|
|
2016
|
+
if (request.method !== 'POST') {
|
|
2017
|
+
throw new ChatRequestError(405, \`Method \${request.method} not allowed \u2014 POST /api/chat.\`);
|
|
2018
|
+
}
|
|
2019
|
+
let parsed: unknown;
|
|
2020
|
+
try { parsed = await request.json(); } catch {
|
|
2021
|
+
throw new ChatRequestError(400, 'Request body is not valid JSON.');
|
|
2022
|
+
}
|
|
2023
|
+
const body = parsed as ChatRequestBody;
|
|
2024
|
+
if (!Array.isArray(body?.messages)) {
|
|
2025
|
+
throw new ChatRequestError(400, 'Request body must carry a messages array.');
|
|
2026
|
+
}
|
|
2027
|
+
return body;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
/** Map a guard rejection to the Response its status demands; rethrow anything
|
|
2031
|
+
* else \u2014 an unexpected error should be loud, not laundered into a 400. */
|
|
2032
|
+
function toChatErrorResponse(error: unknown): Response {
|
|
2033
|
+
if (error instanceof ChatRequestError) return Response.json({ error: error.message }, { status: error.status });
|
|
2034
|
+
throw error;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
1955
2037
|
export default {
|
|
1956
2038
|
async fetch(req: Request, env: Env): Promise<Response> {
|
|
1957
|
-
|
|
2039
|
+
let chatBody: ChatRequestBody;
|
|
2040
|
+
try {
|
|
2041
|
+
chatBody = await readChatRequest(req);
|
|
2042
|
+
} catch (error) {
|
|
2043
|
+
return toChatErrorResponse(error);
|
|
2044
|
+
}
|
|
2045
|
+
const { messages } = chatBody;
|
|
1958
2046
|
|
|
1959
2047
|
let nativeStream: ReadableStream<Uint8Array>;
|
|
1960
2048
|
try {
|
|
@@ -2029,7 +2117,13 @@ export default {
|
|
|
2029
2117
|
},
|
|
2030
2118
|
webRoute: `async function chatHandler(request: Request): Promise<Response> {
|
|
2031
2119
|
// Proxy Workers AI over its OpenAI-compatible HTTP endpoint, token server-side.
|
|
2032
|
-
|
|
2120
|
+
let chatBody: ChatRequestBody;
|
|
2121
|
+
try {
|
|
2122
|
+
chatBody = await readChatRequest(request);
|
|
2123
|
+
} catch (error) {
|
|
2124
|
+
return toChatErrorResponse(error);
|
|
2125
|
+
}
|
|
2126
|
+
const { messages } = chatBody;
|
|
2033
2127
|
|
|
2034
2128
|
const upstream = await fetch(
|
|
2035
2129
|
\`https://api.cloudflare.com/client/v4/accounts/\${process.env.CF_ACCOUNT_ID}/ai/v1/chat/completions\`,
|
|
@@ -2123,7 +2217,13 @@ var ollama = {
|
|
|
2123
2217
|
// The model is pinned here, not sent by the browser. tools IS forwarded:
|
|
2124
2218
|
// it is undefined unless the front end declared any, and JSON.stringify
|
|
2125
2219
|
// drops it, so the same handler serves a tool archetype and a plain chat.
|
|
2126
|
-
|
|
2220
|
+
let chatBody: ChatRequestBody;
|
|
2221
|
+
try {
|
|
2222
|
+
chatBody = await readChatRequest(request);
|
|
2223
|
+
} catch (error) {
|
|
2224
|
+
return toChatErrorResponse(error);
|
|
2225
|
+
}
|
|
2226
|
+
const { messages, tools } = chatBody;
|
|
2127
2227
|
|
|
2128
2228
|
const upstream = await fetch('http://localhost:11434/v1/chat/completions', {
|
|
2129
2229
|
method: 'POST',
|
|
@@ -2220,7 +2320,13 @@ const mastra = new MastraClient({ baseUrl: MASTRA_URL });
|
|
|
2220
2320
|
|
|
2221
2321
|
// Proxy a Mastra agent to the browser as OpenAI-format SSE.
|
|
2222
2322
|
async function chatHandler(request: Request): Promise<Response> {
|
|
2223
|
-
|
|
2323
|
+
let chatBody: ChatRequestBody;
|
|
2324
|
+
try {
|
|
2325
|
+
chatBody = await readChatRequest(request);
|
|
2326
|
+
} catch (error) {
|
|
2327
|
+
return toChatErrorResponse(error);
|
|
2328
|
+
}
|
|
2329
|
+
const { messages } = chatBody;
|
|
2224
2330
|
|
|
2225
2331
|
// MastraClient's Agent.stream() takes AI-SDK CoreMessage[], not the OpenAI
|
|
2226
2332
|
// wire format: each message has ONE literal role (not a union) and content
|
|
@@ -2387,9 +2493,64 @@ import type { OpenAIWireMessage } from '@kitn.ai/ui/wire';
|
|
|
2387
2493
|
const app = express();
|
|
2388
2494
|
app.use(express.json());
|
|
2389
2495
|
|
|
2496
|
+
class ChatRequestError extends Error {
|
|
2497
|
+
constructor(readonly status: number, message: string) { super(message); }
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
/** Narrow the JSON body once, at the edge. A bare GET or a missing messages
|
|
2501
|
+
* array is a ChatRequestError with a status \u2014 NEVER an unhandled throw
|
|
2502
|
+
* (findings F-10). Malformed JSON is rejected upstream by express.json(),
|
|
2503
|
+
* which the error handler below turns into a JSON 400. */
|
|
2504
|
+
function readChatRequest(req: express.Request): { messages: OpenAIWireMessage[] } {
|
|
2505
|
+
if (req.method !== 'POST') {
|
|
2506
|
+
throw new ChatRequestError(405, \`Method \${req.method} not allowed \u2014 POST /api/chat.\`);
|
|
2507
|
+
}
|
|
2508
|
+
const body = (req.body ?? {}) as { messages?: OpenAIWireMessage[] };
|
|
2509
|
+
if (!Array.isArray(body.messages)) {
|
|
2510
|
+
throw new ChatRequestError(400, 'Request body must carry a messages array.');
|
|
2511
|
+
}
|
|
2512
|
+
return body as { messages: OpenAIWireMessage[] };
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
/** Map a guard rejection to the response its status demands; rethrow anything
|
|
2516
|
+
* else \u2014 an unexpected error should be loud, not laundered into a 400. */
|
|
2517
|
+
function toChatErrorResponse(error: unknown, res: express.Response): void {
|
|
2518
|
+
if (error instanceof ChatRequestError) {
|
|
2519
|
+
res.status(error.status).json({ error: { message: error.message } });
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
throw error;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
// express.json() rejects a malformed body at the MIDDLEWARE level, before any
|
|
2526
|
+
// handler runs \u2014 without this it surfaces as Express's default HTML error page.
|
|
2527
|
+
app.use(
|
|
2528
|
+
(
|
|
2529
|
+
err: { status?: number; message?: string },
|
|
2530
|
+
_req: express.Request,
|
|
2531
|
+
res: express.Response,
|
|
2532
|
+
_next: express.NextFunction,
|
|
2533
|
+
) => {
|
|
2534
|
+
res.status(err?.status ?? 500).json({ error: { message: err?.message ?? 'Request failed.' } });
|
|
2535
|
+
},
|
|
2536
|
+
);
|
|
2537
|
+
|
|
2538
|
+
// A bare GET is a 405 with the method named, not Express's default 404 \u2014
|
|
2539
|
+
// the resource exists, the verb is wrong.
|
|
2540
|
+
app.get('/api/chat', (_req, res) => {
|
|
2541
|
+
res.status(405).json({ error: { message: 'Method not allowed \u2014 POST /api/chat.' } });
|
|
2542
|
+
});
|
|
2543
|
+
|
|
2390
2544
|
// POST /api/chat: bridge a Pi RPC session to the browser as SSE.
|
|
2391
2545
|
app.post('/api/chat', (req, res) => {
|
|
2392
|
-
|
|
2546
|
+
let chatBody: { messages: OpenAIWireMessage[] };
|
|
2547
|
+
try {
|
|
2548
|
+
chatBody = readChatRequest(req);
|
|
2549
|
+
} catch (error) {
|
|
2550
|
+
toChatErrorResponse(error, res);
|
|
2551
|
+
return;
|
|
2552
|
+
}
|
|
2553
|
+
const { messages } = chatBody;
|
|
2393
2554
|
const last = messages.at(-1)?.content;
|
|
2394
2555
|
// \`content\` is a plain string until the turn carries an attachment, at which
|
|
2395
2556
|
// point it is an ARRAY of content parts. Pi's RPC mode takes a TEXT prompt and
|
|
@@ -2600,24 +2761,86 @@ var mock = {
|
|
|
2600
2761
|
streamFormat: "native",
|
|
2601
2762
|
envVars: [],
|
|
2602
2763
|
routeTemplates: {},
|
|
2603
|
-
|
|
2604
|
-
|
|
2764
|
+
webRoute: `import { createMockResponder } from '@kitn.ai/ui/state';
|
|
2765
|
+
|
|
2766
|
+
// The kit's own mock responder \u2014 no provider, no key, no upstream. MODULE
|
|
2767
|
+
// scope, not per-request: the responder owns the cursor into its canned
|
|
2768
|
+
// replies, so rebuilding it per turn would answer with the first one forever.
|
|
2769
|
+
const mockResponse = createMockResponder();
|
|
2770
|
+
|
|
2771
|
+
async function chatHandler(request: Request): Promise<Response> {
|
|
2772
|
+
// The responder cycles its replies whatever you send, so the prompt is a
|
|
2773
|
+
// courtesy \u2014 but the body is still read exactly the way a real route reads
|
|
2774
|
+
// it, so swapping this handler for a provider's changes nothing upstream of
|
|
2775
|
+
// this file.
|
|
2776
|
+
let chatBody: ChatRequestBody;
|
|
2777
|
+
try {
|
|
2778
|
+
chatBody = await readChatRequest(request);
|
|
2779
|
+
} catch (error) {
|
|
2780
|
+
return toChatErrorResponse(error);
|
|
2781
|
+
}
|
|
2782
|
+
const { messages } = chatBody;
|
|
2783
|
+
const last = [...messages].reverse().find((m) => m.role === 'user');
|
|
2784
|
+
const prompt = last && typeof last.content === 'string' ? last.content : '';
|
|
2785
|
+
|
|
2786
|
+
// createMockResponder() yields COMPLETE OpenAI chat-completions SSE frames \u2014
|
|
2787
|
+
// the same strings the front end's local preview streams \u2014 so this route only
|
|
2788
|
+
// writes them out verbatim. No framing is built here; the browser parses them
|
|
2789
|
+
// with readOpenAIStream from @kitn.ai/ui/wire, exactly as it would a real
|
|
2790
|
+
// route's response. Every frame is marked as a mock (a ': kai-mock' banner, a
|
|
2791
|
+
// _kai_mock field, model 'kai-mock', zero usage).
|
|
2792
|
+
const encoder = new TextEncoder();
|
|
2793
|
+
let open = true;
|
|
2794
|
+
const body = new ReadableStream<Uint8Array>({
|
|
2795
|
+
async start(controller) {
|
|
2796
|
+
for await (const frame of mockResponse(prompt)) {
|
|
2797
|
+
// The browser hangs up when the user asks a new question mid-answer;
|
|
2798
|
+
// stop producing frames rather than writing into a cancelled stream.
|
|
2799
|
+
if (!open) return;
|
|
2800
|
+
controller.enqueue(encoder.encode(frame));
|
|
2801
|
+
}
|
|
2802
|
+
controller.close();
|
|
2803
|
+
},
|
|
2804
|
+
cancel() {
|
|
2805
|
+
open = false;
|
|
2806
|
+
},
|
|
2807
|
+
});
|
|
2808
|
+
|
|
2809
|
+
return new Response(body, {
|
|
2810
|
+
status: 200,
|
|
2811
|
+
headers: {
|
|
2812
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
2813
|
+
// no-transform stops a proxy buffering the stream into one blob.
|
|
2814
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
2815
|
+
Connection: 'keep-alive',
|
|
2816
|
+
// A proxy in front of a preview build would otherwise buffer the whole
|
|
2817
|
+
// stream and the reply would land all at once instead of streaming.
|
|
2818
|
+
'X-Accel-Buffering': 'no',
|
|
2819
|
+
},
|
|
2820
|
+
});
|
|
2821
|
+
}`,
|
|
2822
|
+
streamMapping: "No backend and no provider, but a real wire. createMockResponder() from @kitn.ai/ui/state yields canned SSE frames and the scaffold reads them with readOpenAIStream from @kitn.ai/ui/wire, on the same code path a real route's response takes: same reader, same part folding, same abort handling. The frames carry the OpenAI chat-completions shape because the mock stands in for your /api/chat ROUTE, not for a provider (every other integration here re-frames its provider to that shape server-side), so going live replaces ONE expression: mockResponse(value) becomes the awaited response from your own chat route, and nothing else in the handler changes. Nothing here can be mistaken for a real turn: the stream opens with a ': kai-mock' SSE comment, every frame carries a _kai_mock field naming createMockResponder, model reports as 'kai-mock' (no provider serves it), and usage is all zeros. Block (2) serves the same frames over HTTP as an OPTIONAL route, so the /api/chat seam can be stood up before any provider exists.",
|
|
2823
|
+
runNote: "No backend or API key needed: the reply is generated in the browser and parsed by the same reader a real route feeds. Run the front-end as-is; swap `integration` for a real provider (openai, anthropic, openrouter, ollama) when ready, and the emitted handler differs by one expression. Block (2) is the OPTIONAL server half: the same mock frames served over HTTP, for standing up the /api/chat seam before any provider exists.",
|
|
2605
2824
|
docsSlug: "integrations/mock",
|
|
2606
|
-
// Nothing:
|
|
2607
|
-
//
|
|
2608
|
-
//
|
|
2825
|
+
// Nothing: the front end makes no HTTP request at all, and the optional route
|
|
2826
|
+
// reads only `messages` — and only as a courtesy prompt the responder is free
|
|
2827
|
+
// to ignore. There is no model to pick and no tools array anything would run.
|
|
2609
2828
|
forwardsFromClient: [],
|
|
2610
|
-
// Nothing to install:
|
|
2829
|
+
// Nothing to install: the route's one import is the kit itself, which every
|
|
2830
|
+
// scaffold already depends on (registry.test.ts pins deps.npm to the route's
|
|
2831
|
+
// imports minus @kitn.ai/ui).
|
|
2611
2832
|
deps: { npm: [], pip: [] },
|
|
2612
|
-
// The other 'frontend-safe' entry
|
|
2613
|
-
//
|
|
2614
|
-
//
|
|
2615
|
-
//
|
|
2616
|
-
// precisely why it still has to SAY so rather than be left blank.
|
|
2833
|
+
// The other 'frontend-safe' entry. The route now exists (G-04) but changes
|
|
2834
|
+
// nothing here: it holds no credential, sends no auth header and reaches no
|
|
2835
|
+
// upstream, so there is still nothing a public bundle could give away. The
|
|
2836
|
+
// schema's own refinement re-checks that claim against the route source.
|
|
2617
2837
|
keyExposure: "frontend-safe",
|
|
2618
|
-
// Nothing
|
|
2619
|
-
//
|
|
2620
|
-
// group of create-kai's gateway prompt
|
|
2838
|
+
// Nothing to supply out of band: no upstream, no process, no runtime. The
|
|
2839
|
+
// route is optional — the emitted app streams locally without it — which is
|
|
2840
|
+
// what keeps mock in the "No backend" group of create-kai's gateway prompt
|
|
2841
|
+
// (listGatewayGroups derives that from frontend-safe + outOfBand 'none', not
|
|
2842
|
+
// from the absence of a route, precisely because this route is one nobody
|
|
2843
|
+
// NEEDS).
|
|
2621
2844
|
outOfBand: "none"
|
|
2622
2845
|
};
|
|
2623
2846
|
var mock_default = mock;
|
|
@@ -2652,9 +2875,41 @@ var archetypes = [
|
|
|
2652
2875
|
defaultPlacement: "side",
|
|
2653
2876
|
docsSlug: "examples/agentic-assistant"
|
|
2654
2877
|
},
|
|
2878
|
+
/**
|
|
2879
|
+
* THE FIRST OFFICIAL BLOCK (recast spec 2026-08-20 § 3b; F-16).
|
|
2880
|
+
*
|
|
2881
|
+
* This preset used to carry `kai-artifact` + `kai-resizable` — an UNWIRED
|
|
2882
|
+
* artifact split — while omitting the conversation rail, the one thing its
|
|
2883
|
+
* name promised (rung-3 finding F-16). It now names the workspace BLOCK: the
|
|
2884
|
+
* chat-agnostic `kai-workspace` layout shell, a WIRED `kai-conversations`
|
|
2885
|
+
* rail, `kai-chat` in the main region, and the `@kitn.ai/ui/state` thread
|
|
2886
|
+
* helpers (bindThreadMessages / createThreadSessions / createSaveScheduler /
|
|
2887
|
+
* parseStoredThread) with the persistence POLICY left in consumer-owned
|
|
2888
|
+
* lines. `examples/apps/workspace/` is the block's reference implementation.
|
|
2889
|
+
*
|
|
2890
|
+
* The artifact split did not vanish — it moved to `artifact-split` below, so
|
|
2891
|
+
* its renderer branch keeps its cells in `verify:scaffold`.
|
|
2892
|
+
*/
|
|
2655
2893
|
{
|
|
2656
2894
|
id: "workspace",
|
|
2657
2895
|
title: "Agentic workspace",
|
|
2896
|
+
components: ["kai-chat", "kai-workspace", "kai-conversations"],
|
|
2897
|
+
defaultPlacement: "full-page",
|
|
2898
|
+
docsSlug: "examples/workspace"
|
|
2899
|
+
},
|
|
2900
|
+
/**
|
|
2901
|
+
* THE PRESET THAT KEEPS A CAPABILITY ON THE AXIS — the same reason the
|
|
2902
|
+
* `attachments` preset exists (see its comment below): `listCapabilityGroups`
|
|
2903
|
+
* derives the gate's surface axis from this table, so when the recast moved
|
|
2904
|
+
* the artifact pair off `workspace`, this entry is what kept
|
|
2905
|
+
* `kai-artifact`+`kai-resizable` compiling in `verify:scaffold` instead of
|
|
2906
|
+
* silently losing every cell. The pair is ONE capability because `isArtifactSplit`
|
|
2907
|
+
* in scaffold.ts requires both: a split with no preview pane is an empty
|
|
2908
|
+
* panel, and a preview with no split has nowhere to sit.
|
|
2909
|
+
*/
|
|
2910
|
+
{
|
|
2911
|
+
id: "artifact-split",
|
|
2912
|
+
title: "Artifact preview split",
|
|
2658
2913
|
components: ["kai-chat", "kai-artifact", "kai-resizable"],
|
|
2659
2914
|
defaultPlacement: "side",
|
|
2660
2915
|
docsSlug: "examples/workspace"
|
|
@@ -2765,9 +3020,33 @@ var CHAT_REQUEST_BODY_DECL = [
|
|
|
2765
3020
|
` tools?: unknown[];`,
|
|
2766
3021
|
`};`,
|
|
2767
3022
|
``,
|
|
2768
|
-
|
|
3023
|
+
`class ChatRequestError extends Error {`,
|
|
3024
|
+
` constructor(readonly status: number, message: string) { super(message); }`,
|
|
3025
|
+
`}`,
|
|
3026
|
+
``,
|
|
3027
|
+
`/** Narrow the JSON body once, at the edge. A bare GET, a malformed body, or a`,
|
|
3028
|
+
` * missing messages array is a ChatRequestError with a status \u2014 NEVER an`,
|
|
3029
|
+
` * unhandled SyntaxError: one killed a Vite dev server (findings F-10). */`,
|
|
2769
3030
|
`async function readChatRequest(request: Request): Promise<ChatRequestBody> {`,
|
|
2770
|
-
`
|
|
3031
|
+
` if (request.method !== 'POST') {`,
|
|
3032
|
+
` throw new ChatRequestError(405, \`Method \${request.method} not allowed \u2014 POST /api/chat.\`);`,
|
|
3033
|
+
` }`,
|
|
3034
|
+
` let parsed: unknown;`,
|
|
3035
|
+
` try { parsed = await request.json(); } catch {`,
|
|
3036
|
+
` throw new ChatRequestError(400, 'Request body is not valid JSON.');`,
|
|
3037
|
+
` }`,
|
|
3038
|
+
` const body = parsed as ChatRequestBody;`,
|
|
3039
|
+
` if (!Array.isArray(body?.messages)) {`,
|
|
3040
|
+
` throw new ChatRequestError(400, 'Request body must carry a messages array.');`,
|
|
3041
|
+
` }`,
|
|
3042
|
+
` return body;`,
|
|
3043
|
+
`}`,
|
|
3044
|
+
``,
|
|
3045
|
+
`/** Map a guard rejection to the Response its status demands; rethrow anything`,
|
|
3046
|
+
` * else \u2014 an unexpected error should be loud, not laundered into a 400. */`,
|
|
3047
|
+
`function toChatErrorResponse(error: unknown): Response {`,
|
|
3048
|
+
` if (error instanceof ChatRequestError) return Response.json({ error: error.message }, { status: error.status });`,
|
|
3049
|
+
` throw error;`,
|
|
2771
3050
|
`}`
|
|
2772
3051
|
];
|
|
2773
3052
|
var CONTENT_PARTS_DECL = [
|
|
@@ -2865,6 +3144,11 @@ function rendererComponents() {
|
|
|
2865
3144
|
}
|
|
2866
3145
|
return known;
|
|
2867
3146
|
}
|
|
3147
|
+
function wirableGateways(framework) {
|
|
3148
|
+
return listGateways().filter(
|
|
3149
|
+
(g2) => g2.wired && wirableGateway(g2.integration.id, framework) === null
|
|
3150
|
+
);
|
|
3151
|
+
}
|
|
2868
3152
|
function listGateways() {
|
|
2869
3153
|
const all = listIntegrations();
|
|
2870
3154
|
const mock2 = all.filter((i) => i.id === "mock");
|
|
@@ -2926,13 +3210,16 @@ function getFeature(id) {
|
|
|
2926
3210
|
var DEFAULT_FEATURES = FEATURES.filter((f) => f.default).map(
|
|
2927
3211
|
(f) => f.id
|
|
2928
3212
|
);
|
|
3213
|
+
var GENERATED_SURFACES_WIRED = false;
|
|
3214
|
+
var GENERATED_SURFACE_GAP = "generated feature surfaces are not wired in this release \u2014 no such project has been run end to end, so emitting one would hand back an app nobody has seen work. The composed workspace is the surface that runs today";
|
|
2929
3215
|
var COMPOSED_ONLY = /* @__PURE__ */ new Set(["conversations"]);
|
|
2930
3216
|
function featureEmit(feature, framework) {
|
|
2931
3217
|
if (COMPOSED_ONLY.has(feature.id)) {
|
|
2932
3218
|
return framework.composedWorkspace ? "composed" : "unavailable";
|
|
2933
3219
|
}
|
|
2934
3220
|
const known = rendererComponents();
|
|
2935
|
-
|
|
3221
|
+
if (!feature.components.every((c) => known.has(c))) return "unavailable";
|
|
3222
|
+
return GENERATED_SURFACES_WIRED ? "renderer" : "unavailable";
|
|
2936
3223
|
}
|
|
2937
3224
|
function featureUnavailableReason(feature, framework) {
|
|
2938
3225
|
if (featureEmit(feature, framework) !== "unavailable") return null;
|
|
@@ -2941,11 +3228,17 @@ function featureUnavailableReason(feature, framework) {
|
|
|
2941
3228
|
}
|
|
2942
3229
|
const known = rendererComponents();
|
|
2943
3230
|
const missing = feature.components.filter((c) => !known.has(c));
|
|
2944
|
-
|
|
3231
|
+
if (missing.length > 0) {
|
|
3232
|
+
return `feature '${feature.id}' cannot be emitted for ${framework.label}: no renderer branches on ${missing.join(" / ")}, so the emitted project would compile and run without the feature in it. Compose those components in a kit archetype and this resolves itself.`;
|
|
3233
|
+
}
|
|
3234
|
+
return `feature '${feature.id}' cannot be emitted: ${GENERATED_SURFACE_GAP}.`;
|
|
2945
3235
|
}
|
|
2946
3236
|
function availableFeatures(framework) {
|
|
2947
3237
|
return FEATURES.filter((f) => featureEmit(f, framework) !== "unavailable");
|
|
2948
3238
|
}
|
|
3239
|
+
function composedWorkspaceFeatures(framework) {
|
|
3240
|
+
return FEATURES.filter((f) => featureEmit(f, framework) === "composed");
|
|
3241
|
+
}
|
|
2949
3242
|
function resolveSurface(featureIds, framework) {
|
|
2950
3243
|
const chosen = [];
|
|
2951
3244
|
for (const id of featureIds) {
|
|
@@ -2956,7 +3249,7 @@ function resolveSurface(featureIds, framework) {
|
|
|
2956
3249
|
chosen.push(feature);
|
|
2957
3250
|
}
|
|
2958
3251
|
const composed = chosen.filter((f) => featureEmit(f, framework) === "composed");
|
|
2959
|
-
if (composed.length > 0) {
|
|
3252
|
+
if (composed.length > 0 || chosen.length === 0 && !GENERATED_SURFACES_WIRED) {
|
|
2960
3253
|
if (chosen.length > composed.length) {
|
|
2961
3254
|
const extra = chosen.filter((f) => !composed.includes(f)).map((f) => f.id);
|
|
2962
3255
|
return {
|
|
@@ -2964,10 +3257,30 @@ function resolveSurface(featureIds, framework) {
|
|
|
2964
3257
|
reason: `'${composed.map((f) => f.id).join("', '")}' comes from the hand-composed workspace template, which this release cannot combine with generated features ('${extra.join("', '")}'). Pick one or the other.`
|
|
2965
3258
|
};
|
|
2966
3259
|
}
|
|
2967
|
-
|
|
3260
|
+
const emitted = composedWorkspaceFeatures(framework);
|
|
3261
|
+
if (emitted.length === 0) {
|
|
3262
|
+
return {
|
|
3263
|
+
ok: false,
|
|
3264
|
+
reason: `'${framework.label}' has no composed workspace starter, and ${GENERATED_SURFACE_GAP}. There is no surface to emit for it.`
|
|
3265
|
+
};
|
|
3266
|
+
}
|
|
3267
|
+
const asked = new Set(featureIds);
|
|
3268
|
+
return {
|
|
3269
|
+
ok: true,
|
|
3270
|
+
surface: {
|
|
3271
|
+
kind: "composed",
|
|
3272
|
+
features: emitted.map((f) => f.id),
|
|
3273
|
+
unasked: emitted.filter((f) => !asked.has(f.id)).map((f) => f.id)
|
|
3274
|
+
}
|
|
3275
|
+
};
|
|
2968
3276
|
}
|
|
2969
|
-
|
|
2970
|
-
|
|
3277
|
+
return {
|
|
3278
|
+
ok: true,
|
|
3279
|
+
surface: { kind: "generated", features: featureIds, components: surfaceComponents(chosen) }
|
|
3280
|
+
};
|
|
3281
|
+
}
|
|
3282
|
+
function surfaceComponents(chosen) {
|
|
3283
|
+
return ["kai-chat", ...new Set(chosen.flatMap((f) => f.components))];
|
|
2971
3284
|
}
|
|
2972
3285
|
|
|
2973
3286
|
// src/routes.ts
|
|
@@ -3456,6 +3769,75 @@ function validateProjectName(name) {
|
|
|
3456
3769
|
return null;
|
|
3457
3770
|
}
|
|
3458
3771
|
|
|
3772
|
+
// src/layouts.ts
|
|
3773
|
+
var LAYOUTS = [
|
|
3774
|
+
{
|
|
3775
|
+
id: "full-screen",
|
|
3776
|
+
label: "Full-screen app",
|
|
3777
|
+
hint: "the chat is the page",
|
|
3778
|
+
placement: "full-page",
|
|
3779
|
+
status: "ready"
|
|
3780
|
+
},
|
|
3781
|
+
{
|
|
3782
|
+
id: "widget",
|
|
3783
|
+
label: "Embedded widget",
|
|
3784
|
+
hint: "the chat sits on top of an existing page",
|
|
3785
|
+
placement: "docked-widget",
|
|
3786
|
+
// A widget has no composed-workspace starter — it is a generated surface at
|
|
3787
|
+
// `docked-widget` placement — so it lands on the same unwired path the
|
|
3788
|
+
// generated features do. Offered once that path has been run.
|
|
3789
|
+
status: "planned",
|
|
3790
|
+
note: "needs the generated-surface path, not wired in this release"
|
|
3791
|
+
}
|
|
3792
|
+
];
|
|
3793
|
+
function getLayout(id) {
|
|
3794
|
+
return LAYOUTS.find((l2) => l2.id === id);
|
|
3795
|
+
}
|
|
3796
|
+
function readyLayouts() {
|
|
3797
|
+
return LAYOUTS.filter((l2) => l2.status === "ready");
|
|
3798
|
+
}
|
|
3799
|
+
|
|
3800
|
+
// src/axes.ts
|
|
3801
|
+
function decideAxis(axis) {
|
|
3802
|
+
if (axis.options.length === 0) return { ask: false, only: null, statement: null };
|
|
3803
|
+
if (axis.options.length > 1) return { ask: true, only: null, statement: null };
|
|
3804
|
+
const only = axis.options[0];
|
|
3805
|
+
const statement = axis.because.length > 0 ? `${only.label} \u2014 ${only.hint}; ${axis.because}` : null;
|
|
3806
|
+
return { ask: false, only, statement };
|
|
3807
|
+
}
|
|
3808
|
+
function layoutAxis() {
|
|
3809
|
+
const options = readyLayouts().map((l2) => ({ id: l2.id, label: l2.label, hint: l2.hint }));
|
|
3810
|
+
return {
|
|
3811
|
+
id: "layout",
|
|
3812
|
+
label: "Layout",
|
|
3813
|
+
question: "Where does the chat live?",
|
|
3814
|
+
options,
|
|
3815
|
+
because: "the only layout this release can scaffold, and `--list` shows the rest"
|
|
3816
|
+
};
|
|
3817
|
+
}
|
|
3818
|
+
function gatewayAxis(framework) {
|
|
3819
|
+
const options = wirableGateways(framework).map((g2) => ({
|
|
3820
|
+
id: g2.integration.id,
|
|
3821
|
+
label: g2.integration.id === "mock" ? "None" : g2.integration.title,
|
|
3822
|
+
hint: g2.integration.id === "mock" ? "local mock, no key, no backend" : `${g2.integration.envVars.join(", ")} \u2014 a server route is scaffolded for you`
|
|
3823
|
+
}));
|
|
3824
|
+
return {
|
|
3825
|
+
id: "gateway",
|
|
3826
|
+
label: "Backend",
|
|
3827
|
+
question: "Wire a model gateway?",
|
|
3828
|
+
options,
|
|
3829
|
+
because: `the only gateway wirable for ${framework.label} in this release. A keyed gateway needs a server route and this framework declares no destination for one, so \`--list --json\` is where to see which do`
|
|
3830
|
+
};
|
|
3831
|
+
}
|
|
3832
|
+
async function answerAxis(axis, opts, io) {
|
|
3833
|
+
if (opts.override !== void 0) return opts.override;
|
|
3834
|
+
const decision = decideAxis(axis);
|
|
3835
|
+
if (opts.nonInteractive) return decision.only?.id ?? opts.fallback;
|
|
3836
|
+
if (decision.ask) return io.ask(axis, opts.initialValue ?? opts.fallback);
|
|
3837
|
+
if (decision.statement) io.state(axis.label, decision.statement);
|
|
3838
|
+
return decision.only?.id ?? opts.fallback;
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3459
3841
|
// src/generate.ts
|
|
3460
3842
|
import { cp, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3461
3843
|
import { existsSync } from "node:fs";
|
|
@@ -3957,10 +4339,9 @@ async function generate(plan, options = {}) {
|
|
|
3957
4339
|
const surface = resolveSurface(plan.featureIds, framework);
|
|
3958
4340
|
if (!surface.ok) throw new Error(`create-kai: ${surface.reason}`);
|
|
3959
4341
|
if (surface.surface.kind === "generated") {
|
|
3960
|
-
throw new Error(
|
|
3961
|
-
"create-kai: generated feature surfaces are not wired in this release \u2014 the composed workspace (conversation history) is the path that runs today"
|
|
3962
|
-
);
|
|
4342
|
+
throw new Error(`create-kai: ${GENERATED_SURFACE_GAP}`);
|
|
3963
4343
|
}
|
|
4344
|
+
const emittedFeatures = surface.surface.features;
|
|
3964
4345
|
const templateRoot = options.templateRoot ?? defaultTemplateRoot();
|
|
3965
4346
|
const templateDir = path.join(templateRoot, framework.templateDir);
|
|
3966
4347
|
if (!existsSync(templateDir)) {
|
|
@@ -3995,7 +4376,7 @@ async function generate(plan, options = {}) {
|
|
|
3995
4376
|
const routeFiles = await writeGateway(plan, framework, integration, out, thread);
|
|
3996
4377
|
await writeFile(
|
|
3997
4378
|
path.join(out, "kai.json"),
|
|
3998
|
-
stringifyKaiJson(buildKaiJson(plan, framework)),
|
|
4379
|
+
stringifyKaiJson(buildKaiJson({ ...plan, featureIds: emittedFeatures }, framework)),
|
|
3999
4380
|
"utf8"
|
|
4000
4381
|
);
|
|
4001
4382
|
await writeFile(
|
|
@@ -4180,34 +4561,6 @@ async function listFiles(dir, prefix = "") {
|
|
|
4180
4561
|
return out.sort();
|
|
4181
4562
|
}
|
|
4182
4563
|
|
|
4183
|
-
// src/layouts.ts
|
|
4184
|
-
var LAYOUTS = [
|
|
4185
|
-
{
|
|
4186
|
-
id: "full-screen",
|
|
4187
|
-
label: "Full-screen app",
|
|
4188
|
-
hint: "the chat is the page",
|
|
4189
|
-
placement: "full-page",
|
|
4190
|
-
status: "ready"
|
|
4191
|
-
},
|
|
4192
|
-
{
|
|
4193
|
-
id: "widget",
|
|
4194
|
-
label: "Embedded widget",
|
|
4195
|
-
hint: "the chat sits on top of an existing page",
|
|
4196
|
-
placement: "docked-widget",
|
|
4197
|
-
// A widget has no composed-workspace starter — it is a generated surface at
|
|
4198
|
-
// `docked-widget` placement — so it lands on the same unwired path the
|
|
4199
|
-
// generated features do. Offered once that path has been run.
|
|
4200
|
-
status: "planned",
|
|
4201
|
-
note: "needs the generated-surface path, not wired in this release"
|
|
4202
|
-
}
|
|
4203
|
-
];
|
|
4204
|
-
function getLayout(id) {
|
|
4205
|
-
return LAYOUTS.find((l2) => l2.id === id);
|
|
4206
|
-
}
|
|
4207
|
-
function readyLayouts() {
|
|
4208
|
-
return LAYOUTS.filter((l2) => l2.status === "ready");
|
|
4209
|
-
}
|
|
4210
|
-
|
|
4211
4564
|
// src/pm.ts
|
|
4212
4565
|
var KNOWN = {
|
|
4213
4566
|
npm: { name: "npm", install: ["npm", "install"], run: "npm run dev" },
|
|
@@ -4222,7 +4575,10 @@ function detectPackageManager(userAgent = process.env.npm_config_user_agent) {
|
|
|
4222
4575
|
}
|
|
4223
4576
|
|
|
4224
4577
|
// src/index.ts
|
|
4225
|
-
var
|
|
4578
|
+
var OFFERABLE_FEATURE_IDS = [
|
|
4579
|
+
...new Set(readyFrameworks().flatMap((f) => availableFeatures(f).map((feature) => feature.id)))
|
|
4580
|
+
];
|
|
4581
|
+
var DEFAULT_KIT_RANGE = "^0.27.0";
|
|
4226
4582
|
var HELP = `
|
|
4227
4583
|
${import_picocolors3.default.bold("create-kai")} \u2014 scaffold a runnable @kitn.ai/ui chat app
|
|
4228
4584
|
|
|
@@ -4232,7 +4588,7 @@ ${import_picocolors3.default.bold("create-kai")} \u2014 scaffold a runnable @kit
|
|
|
4232
4588
|
Options
|
|
4233
4589
|
--framework <id> ${readyFrameworks().map((f) => f.id).join(", ")}
|
|
4234
4590
|
--layout <id> ${readyLayouts().map((l2) => l2.id).join(", ")}
|
|
4235
|
-
--features <a,b> ${
|
|
4591
|
+
--features <a,b> ${OFFERABLE_FEATURE_IDS.join(", ")} (or 'none')
|
|
4236
4592
|
--gateway <id> none${[...WIRED_GATEWAYS].filter((g2) => g2 !== "mock").map((g2) => `, ${g2}`).join("")}
|
|
4237
4593
|
--kit <spec> @kitn.ai/ui spec to pin (default ${DEFAULT_KIT_RANGE})
|
|
4238
4594
|
-y, --yes accept every default (zero-config: React + full-screen + mock)
|
|
@@ -4253,7 +4609,7 @@ async function main() {
|
|
|
4253
4609
|
return 0;
|
|
4254
4610
|
}
|
|
4255
4611
|
if (args.version) {
|
|
4256
|
-
console.log("0.1
|
|
4612
|
+
console.log("0.2.1");
|
|
4257
4613
|
return 0;
|
|
4258
4614
|
}
|
|
4259
4615
|
if (args.list) {
|
|
@@ -4291,45 +4647,70 @@ async function main() {
|
|
|
4291
4647
|
`'${framework.id}' is not scaffoldable yet (${framework.note ?? "no template"}). Available: ${readyFrameworks().map((f) => f.id).join(", ")}`
|
|
4292
4648
|
);
|
|
4293
4649
|
}
|
|
4294
|
-
const
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4650
|
+
const layouts = layoutAxis();
|
|
4651
|
+
if (layouts.options.length === 0) {
|
|
4652
|
+
return fail("no layout in this release can be scaffolded \u2014 this build is broken");
|
|
4653
|
+
}
|
|
4654
|
+
const layoutId = await answerAxis(
|
|
4655
|
+
layouts,
|
|
4656
|
+
{ override: args.layout, nonInteractive, fallback: ZERO_CONFIG.layout },
|
|
4657
|
+
clackAxisIo
|
|
4658
|
+
);
|
|
4301
4659
|
const layout = getLayout(layoutId);
|
|
4302
4660
|
if (!layout) return fail(`unknown layout '${layoutId}'`);
|
|
4303
4661
|
if (layout.status !== "ready") {
|
|
4304
|
-
return fail(
|
|
4662
|
+
return fail(
|
|
4663
|
+
`layout '${layout.id}' is not scaffoldable yet (${layout.note ?? "no template"}). Available: ${layouts.options.map((l2) => l2.id).join(", ")}`
|
|
4664
|
+
);
|
|
4305
4665
|
}
|
|
4306
4666
|
const offered = availableFeatures(framework);
|
|
4307
|
-
const
|
|
4667
|
+
const included = composedWorkspaceFeatures(framework);
|
|
4668
|
+
const optional = offered.filter((f) => !included.includes(f));
|
|
4669
|
+
const withheld = FEATURES.filter((f) => !offered.includes(f));
|
|
4670
|
+
const featureIds = args.features ?? (nonInteractive || optional.length === 0 ? DEFAULT_FEATURES.filter((id) => offered.some((f) => f.id === id)) : await ask(
|
|
4308
4671
|
fe({
|
|
4309
4672
|
message: "Which features?",
|
|
4310
4673
|
required: false,
|
|
4311
|
-
initialValues: DEFAULT_FEATURES.filter((id) =>
|
|
4312
|
-
options:
|
|
4674
|
+
initialValues: DEFAULT_FEATURES.filter((id) => optional.some((f) => f.id === id)),
|
|
4675
|
+
options: optional.map((f) => ({ value: f.id, label: f.label, hint: f.hint }))
|
|
4313
4676
|
})
|
|
4314
4677
|
));
|
|
4315
4678
|
for (const id of featureIds) {
|
|
4316
|
-
|
|
4679
|
+
const feature = getFeature(id);
|
|
4680
|
+
if (!feature) {
|
|
4681
|
+
return fail(`unknown feature '${id}'. Available: ${featureList(offered)}`);
|
|
4682
|
+
}
|
|
4683
|
+
const unavailable = featureUnavailableReason(feature, framework);
|
|
4684
|
+
if (unavailable) {
|
|
4685
|
+
return fail(`${unavailable}
|
|
4686
|
+
Available for ${framework.label}: ${featureList(offered)}`);
|
|
4687
|
+
}
|
|
4688
|
+
}
|
|
4689
|
+
const surface = resolveSurface(featureIds, framework);
|
|
4690
|
+
if (!surface.ok) return fail(surface.reason);
|
|
4691
|
+
if (surface.surface.kind === "composed" && surface.surface.unasked.length > 0) {
|
|
4692
|
+
stated(
|
|
4693
|
+
"Also included",
|
|
4694
|
+
`${surface.surface.unasked.join(", ")} \u2014 the ${framework.label} starter is one reviewed tree, so it comes as a whole`
|
|
4695
|
+
);
|
|
4696
|
+
}
|
|
4697
|
+
if (!nonInteractive) {
|
|
4698
|
+
if (optional.length === 0 && included.length > 0) {
|
|
4699
|
+
stated("Features", included.map((f) => `${f.label} \u2014 ${f.hint}`).join("; "));
|
|
4700
|
+
}
|
|
4701
|
+
if (withheld.length > 0) {
|
|
4702
|
+
stated(
|
|
4703
|
+
"Not offered yet",
|
|
4704
|
+
`${withheld.map((f) => f.id).join(", ")} \u2014 ${import_picocolors3.default.dim("see `--list` for the whole table")}`
|
|
4705
|
+
);
|
|
4706
|
+
}
|
|
4317
4707
|
}
|
|
4318
4708
|
const gateways = listGateways();
|
|
4319
|
-
const
|
|
4320
|
-
(
|
|
4709
|
+
const gatewayId = normalizeGateway(args.gateway) ?? await answerAxis(
|
|
4710
|
+
gatewayAxis(framework),
|
|
4711
|
+
{ nonInteractive, fallback: ZERO_CONFIG.gateway, initialValue: ZERO_CONFIG.gateway },
|
|
4712
|
+
clackAxisIo
|
|
4321
4713
|
);
|
|
4322
|
-
const gatewayId = normalizeGateway(args.gateway) ?? (nonInteractive || wired.length === 1 ? ZERO_CONFIG.gateway : await ask(
|
|
4323
|
-
ve({
|
|
4324
|
-
message: "Wire a model gateway?",
|
|
4325
|
-
initialValue: ZERO_CONFIG.gateway,
|
|
4326
|
-
options: wired.map((g2) => ({
|
|
4327
|
-
value: g2.integration.id,
|
|
4328
|
-
label: g2.integration.id === "mock" ? "None" : g2.integration.title,
|
|
4329
|
-
hint: g2.integration.id === "mock" ? "local mock, no key, no backend" : `${g2.integration.envVars.join(", ")} \u2014 a server route is scaffolded for you`
|
|
4330
|
-
}))
|
|
4331
|
-
})
|
|
4332
|
-
));
|
|
4333
4714
|
if (!gateways.some((g2) => g2.integration.id === gatewayId)) {
|
|
4334
4715
|
return fail(`unknown gateway '${gatewayId}'`);
|
|
4335
4716
|
}
|
|
@@ -4348,7 +4729,7 @@ async function main() {
|
|
|
4348
4729
|
// which kit the CLI was built against, which stays true when `--kit` sends
|
|
4349
4730
|
// the dependency somewhere else — the emitted files are this version's shape
|
|
4350
4731
|
// whatever the project ends up installing.
|
|
4351
|
-
kitBuiltAgainst: "0.
|
|
4732
|
+
kitBuiltAgainst: "0.27.0"
|
|
4352
4733
|
};
|
|
4353
4734
|
const spinner = Y2();
|
|
4354
4735
|
spinner.start("Scaffolding");
|
|
@@ -4405,6 +4786,22 @@ function fail(message) {
|
|
|
4405
4786
|
xe(import_picocolors3.default.red(message));
|
|
4406
4787
|
return 1;
|
|
4407
4788
|
}
|
|
4789
|
+
function stated(label, value) {
|
|
4790
|
+
M2.info(`${import_picocolors3.default.dim(`${label}:`)} ${value}`);
|
|
4791
|
+
}
|
|
4792
|
+
var clackAxisIo = {
|
|
4793
|
+
ask: (axis, initialValue) => ask(
|
|
4794
|
+
ve({
|
|
4795
|
+
message: axis.question,
|
|
4796
|
+
initialValue,
|
|
4797
|
+
options: axis.options.map((o2) => ({ value: o2.id, label: o2.label, hint: o2.hint }))
|
|
4798
|
+
})
|
|
4799
|
+
),
|
|
4800
|
+
state: stated
|
|
4801
|
+
};
|
|
4802
|
+
function featureList(features) {
|
|
4803
|
+
return features.length > 0 ? features.map((f) => f.id).join(", ") : "none";
|
|
4804
|
+
}
|
|
4408
4805
|
function run(command, cwd) {
|
|
4409
4806
|
return new Promise((resolve) => {
|
|
4410
4807
|
const child = spawn(command[0], command.slice(1), { cwd, stdio: "ignore", shell: false });
|
|
@@ -4414,12 +4811,12 @@ function run(command, cwd) {
|
|
|
4414
4811
|
}
|
|
4415
4812
|
function printMatrix(asJson) {
|
|
4416
4813
|
const matrix = {
|
|
4417
|
-
cli: "0.1
|
|
4814
|
+
cli: "0.2.1",
|
|
4418
4815
|
kit: DEFAULT_KIT_RANGE,
|
|
4419
4816
|
// The same pair `kai.json` carries. An agent reading this to decide what to
|
|
4420
4817
|
// install gets the range; one reasoning about which kit the templates match
|
|
4421
4818
|
// gets the exact version, without having to parse the range for a floor.
|
|
4422
|
-
kitBuiltAgainst: "0.
|
|
4819
|
+
kitBuiltAgainst: "0.27.0",
|
|
4423
4820
|
frameworks: FRAMEWORKS.map((f) => ({
|
|
4424
4821
|
id: f.id,
|
|
4425
4822
|
label: f.label,
|
|
@@ -4433,7 +4830,16 @@ function printMatrix(asJson) {
|
|
|
4433
4830
|
...f.note ? { note: f.note } : {}
|
|
4434
4831
|
})),
|
|
4435
4832
|
layouts: LAYOUTS.map((l2) => ({ id: l2.id, status: l2.status, ...l2.note ? { note: l2.note } : {} })),
|
|
4436
|
-
features: FEATURES.map((f) => ({
|
|
4833
|
+
features: FEATURES.map((f) => ({
|
|
4834
|
+
id: f.id,
|
|
4835
|
+
components: f.components,
|
|
4836
|
+
default: f.default,
|
|
4837
|
+
// Derived, never restated — and the field the six-item menu needed: the
|
|
4838
|
+
// ready frameworks whose prompt actually offers this feature. An empty
|
|
4839
|
+
// array means the row is catalogued and cannot be scaffolded by anything,
|
|
4840
|
+
// which is what an agent reading this has to be able to see.
|
|
4841
|
+
frameworks: readyFrameworks().filter((framework) => availableFeatures(framework).includes(f)).map((framework) => framework.id)
|
|
4842
|
+
})),
|
|
4437
4843
|
gateways: listGateways().map((g2) => ({
|
|
4438
4844
|
id: g2.integration.id,
|
|
4439
4845
|
title: g2.integration.title,
|
|
@@ -4461,6 +4867,12 @@ function printMatrix(asJson) {
|
|
|
4461
4867
|
for (const l2 of matrix.layouts) {
|
|
4462
4868
|
console.log(` ${mark(l2.status === "ready")} ${l2.id.padEnd(16)}${l2.note ?? ""}`);
|
|
4463
4869
|
}
|
|
4870
|
+
console.log(import_picocolors3.default.bold("\nFeatures"));
|
|
4871
|
+
for (const f of matrix.features) {
|
|
4872
|
+
console.log(
|
|
4873
|
+
` ${mark(f.frameworks.length > 0)} ${f.id.padEnd(16)}${f.frameworks.length > 0 ? "" : "not scaffoldable in this release"}`
|
|
4874
|
+
);
|
|
4875
|
+
}
|
|
4464
4876
|
console.log(import_picocolors3.default.bold("\nGateways"));
|
|
4465
4877
|
for (const g2 of matrix.gateways) {
|
|
4466
4878
|
console.log(` ${mark(g2.wired)} ${g2.id.padEnd(16)}${g2.envVars.join(", ")}`);
|
|
@@ -19,6 +19,13 @@ interface ThreadViewProps {
|
|
|
19
19
|
* is that same channel repeated: a NEW array reference per chunk, which is why
|
|
20
20
|
* mutating the existing array in place would render nothing.
|
|
21
21
|
*
|
|
22
|
+
* The full rule is BOTH halves, and streaming only satisfies them by accident:
|
|
23
|
+
* the fresh array is what notifies, and a new object for the message that changed
|
|
24
|
+
* is what makes the change visible (the row list is a reference-keyed `<For>`).
|
|
25
|
+
* `createAssistantStream` rebuilds the streaming message as a new object every
|
|
26
|
+
* delta, so streaming gets the second half for free — editing a message or a
|
|
27
|
+
* conversation title by hand does not. See the framework guide.
|
|
28
|
+
*
|
|
22
29
|
* This component just bakes the per-message actions onto the assistant turns and
|
|
23
30
|
* wires the custom `speak` action to the browser's speech synthesis. `copy` (and
|
|
24
31
|
* the feedback votes) are handled inside the element.
|
|
@@ -19,6 +19,13 @@ interface ThreadViewProps {
|
|
|
19
19
|
* is that same channel repeated: a NEW array reference per chunk, which is why
|
|
20
20
|
* mutating the existing array in place would render nothing.
|
|
21
21
|
*
|
|
22
|
+
* The full rule is BOTH halves, and streaming only satisfies them by accident:
|
|
23
|
+
* the fresh array is what notifies, and a new object for the message that changed
|
|
24
|
+
* is what makes the change visible (the row list is a reference-keyed `<For>`).
|
|
25
|
+
* `createAssistantStream` rebuilds the streaming message as a new object every
|
|
26
|
+
* delta, so streaming gets the second half for free — editing a message or a
|
|
27
|
+
* conversation title by hand does not. See the framework guide.
|
|
28
|
+
*
|
|
22
29
|
* This component just bakes the per-message actions onto the assistant turns and
|
|
23
30
|
* wires the custom `speak` action to the browser's speech synthesis. `copy` (and
|
|
24
31
|
* the feedback votes) are handled inside the element.
|