create-kai 0.1.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/README.md +311 -0
- package/dist/index.js +4433 -0
- package/dist/templates/angular/_gitignore +7 -0
- package/dist/templates/angular/angular.json +53 -0
- package/dist/templates/angular/package.json +30 -0
- package/dist/templates/angular/src/app/app.config.ts +11 -0
- package/dist/templates/angular/src/app/app.css +36 -0
- package/dist/templates/angular/src/app/app.html +51 -0
- package/dist/templates/angular/src/app/app.ts +78 -0
- package/dist/templates/angular/src/app/components/composer/composer.css +31 -0
- package/dist/templates/angular/src/app/components/composer/composer.html +18 -0
- package/dist/templates/angular/src/app/components/composer/composer.ts +81 -0
- package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.css +3 -0
- package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.html +12 -0
- package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.ts +15 -0
- package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.css +3 -0
- package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.html +14 -0
- package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.ts +13 -0
- package/dist/templates/angular/src/app/components/sidebar/sidebar.css +24 -0
- package/dist/templates/angular/src/app/components/sidebar/sidebar.html +12 -0
- package/dist/templates/angular/src/app/components/sidebar/sidebar.ts +43 -0
- package/dist/templates/angular/src/app/components/theme-toggle/theme-toggle.html +13 -0
- package/dist/templates/angular/src/app/components/theme-toggle/theme-toggle.ts +21 -0
- package/dist/templates/angular/src/app/components/thread-view/thread-view.css +6 -0
- package/dist/templates/angular/src/app/components/thread-view/thread-view.html +6 -0
- package/dist/templates/angular/src/app/components/thread-view/thread-view.ts +45 -0
- package/dist/templates/angular/src/app/state/chat.store.ts +47 -0
- package/dist/templates/angular/src/app/state/conversations.store.ts +42 -0
- package/dist/templates/angular/src/app/state/voice-input.ts +57 -0
- package/dist/templates/angular/src/app/types.ts +2 -0
- package/dist/templates/angular/src/chat-data.ts +106 -0
- package/dist/templates/angular/src/index.html +16 -0
- package/dist/templates/angular/src/main.ts +16 -0
- package/dist/templates/angular/src/styles.css +10 -0
- package/dist/templates/angular/tsconfig.app.json +9 -0
- package/dist/templates/angular/tsconfig.json +22 -0
- package/dist/templates/nextjs/_gitignore +11 -0
- package/dist/templates/nextjs/app/chat-data.ts +125 -0
- package/dist/templates/nextjs/app/components/Composer.tsx +88 -0
- package/dist/templates/nextjs/app/components/HydrationBadge.tsx +68 -0
- package/dist/templates/nextjs/app/components/Sidebar.tsx +48 -0
- package/dist/templates/nextjs/app/components/ThemeToggle.tsx +27 -0
- package/dist/templates/nextjs/app/components/ThreadView.tsx +48 -0
- package/dist/templates/nextjs/app/components/icons/MoonIcon.tsx +25 -0
- package/dist/templates/nextjs/app/components/icons/SunIcon.tsx +25 -0
- package/dist/templates/nextjs/app/components/icons/index.ts +2 -0
- package/dist/templates/nextjs/app/globals.css +153 -0
- package/dist/templates/nextjs/app/hooks/index.ts +1 -0
- package/dist/templates/nextjs/app/hooks/useConversations.ts +42 -0
- package/dist/templates/nextjs/app/layout.tsx +50 -0
- package/dist/templates/nextjs/app/page.tsx +17 -0
- package/dist/templates/nextjs/app/theme.ts +10 -0
- package/dist/templates/nextjs/app/workspace.tsx +148 -0
- package/dist/templates/nextjs/next.config.mjs +22 -0
- package/dist/templates/nextjs/package.json +23 -0
- package/dist/templates/nextjs/postcss.config.mjs +16 -0
- package/dist/templates/nextjs/tsconfig.json +23 -0
- package/dist/templates/react/_gitignore +6 -0
- package/dist/templates/react/index.html +16 -0
- package/dist/templates/react/package.json +25 -0
- package/dist/templates/react/src/App.tsx +108 -0
- package/dist/templates/react/src/chat-data.ts +106 -0
- package/dist/templates/react/src/components/Composer.tsx +87 -0
- package/dist/templates/react/src/components/Sidebar.tsx +38 -0
- package/dist/templates/react/src/components/ThemeToggle.tsx +22 -0
- package/dist/templates/react/src/components/ThreadView.tsx +40 -0
- package/dist/templates/react/src/components/icons/MoonIcon.tsx +25 -0
- package/dist/templates/react/src/components/icons/SunIcon.tsx +25 -0
- package/dist/templates/react/src/components/icons/index.ts +2 -0
- package/dist/templates/react/src/hooks/index.ts +1 -0
- package/dist/templates/react/src/hooks/useConversations.ts +37 -0
- package/dist/templates/react/src/index.css +111 -0
- package/dist/templates/react/src/main.tsx +15 -0
- package/dist/templates/react/src/vite-env.d.ts +1 -0
- package/dist/templates/react/tsconfig.app.json +28 -0
- package/dist/templates/react/tsconfig.json +7 -0
- package/dist/templates/react/tsconfig.node.json +25 -0
- package/dist/templates/react/vite.config.ts +11 -0
- package/dist/templates/solid/_gitignore +6 -0
- package/dist/templates/solid/index.html +13 -0
- package/dist/templates/solid/package.json +23 -0
- package/dist/templates/solid/src/App.tsx +151 -0
- package/dist/templates/solid/src/chat-data.ts +234 -0
- package/dist/templates/solid/src/components/Composer.tsx +90 -0
- package/dist/templates/solid/src/components/Sidebar.tsx +36 -0
- package/dist/templates/solid/src/components/ThemeToggle.tsx +25 -0
- package/dist/templates/solid/src/components/ThreadView.tsx +122 -0
- package/dist/templates/solid/src/components/icons/index.tsx +84 -0
- package/dist/templates/solid/src/index.tsx +5 -0
- package/dist/templates/solid/src/lib/chat.ts +64 -0
- package/dist/templates/solid/src/lib/conversations.ts +54 -0
- package/dist/templates/solid/src/lib/types.ts +2 -0
- package/dist/templates/solid/src/styles.css +11 -0
- package/dist/templates/solid/tsconfig.json +18 -0
- package/dist/templates/solid/vite.config.ts +10 -0
- package/dist/templates/svelte/_gitignore +6 -0
- package/dist/templates/svelte/index.html +16 -0
- package/dist/templates/svelte/package.json +23 -0
- package/dist/templates/svelte/src/App.svelte +116 -0
- package/dist/templates/svelte/src/app.d.ts +20 -0
- package/dist/templates/svelte/src/chat-data.ts +106 -0
- package/dist/templates/svelte/src/components/Composer.svelte +102 -0
- package/dist/templates/svelte/src/components/Sidebar.svelte +61 -0
- package/dist/templates/svelte/src/components/ThemeToggle.svelte +45 -0
- package/dist/templates/svelte/src/components/ThreadView.svelte +49 -0
- package/dist/templates/svelte/src/components/icons/MoonIcon.svelte +24 -0
- package/dist/templates/svelte/src/components/icons/SunIcon.svelte +24 -0
- package/dist/templates/svelte/src/index.css +111 -0
- package/dist/templates/svelte/src/lib/chat.svelte.ts +52 -0
- package/dist/templates/svelte/src/lib/conversations.svelte.ts +46 -0
- package/dist/templates/svelte/src/lib/types.ts +2 -0
- package/dist/templates/svelte/src/lib/voiceInput.ts +57 -0
- package/dist/templates/svelte/src/main.ts +18 -0
- package/dist/templates/svelte/svelte.config.js +8 -0
- package/dist/templates/svelte/tsconfig.app.json +28 -0
- package/dist/templates/svelte/tsconfig.json +7 -0
- package/dist/templates/svelte/tsconfig.node.json +25 -0
- package/dist/templates/svelte/vite.config.ts +15 -0
- package/dist/templates/tanstack-start/_gitignore +14 -0
- package/dist/templates/tanstack-start/package.json +28 -0
- package/dist/templates/tanstack-start/serve.mjs +129 -0
- package/dist/templates/tanstack-start/serve.traversal.test.mjs +445 -0
- package/dist/templates/tanstack-start/src/chat-data.ts +124 -0
- package/dist/templates/tanstack-start/src/components/Composer.tsx +87 -0
- package/dist/templates/tanstack-start/src/components/HydrationBadge.tsx +63 -0
- package/dist/templates/tanstack-start/src/components/Sidebar.tsx +43 -0
- package/dist/templates/tanstack-start/src/components/ThemeToggle.tsx +26 -0
- package/dist/templates/tanstack-start/src/components/ThreadView.tsx +48 -0
- package/dist/templates/tanstack-start/src/components/icons/MoonIcon.tsx +25 -0
- package/dist/templates/tanstack-start/src/components/icons/SunIcon.tsx +25 -0
- package/dist/templates/tanstack-start/src/components/icons/index.ts +2 -0
- package/dist/templates/tanstack-start/src/hooks/index.ts +1 -0
- package/dist/templates/tanstack-start/src/hooks/useConversations.ts +42 -0
- package/dist/templates/tanstack-start/src/router.tsx +16 -0
- package/dist/templates/tanstack-start/src/routes/__root.tsx +62 -0
- package/dist/templates/tanstack-start/src/routes/index.tsx +135 -0
- package/dist/templates/tanstack-start/src/styles.css +153 -0
- package/dist/templates/tanstack-start/src/theme.ts +10 -0
- package/dist/templates/tanstack-start/tsconfig.json +16 -0
- package/dist/templates/tanstack-start/vite.config.ts +28 -0
- package/dist/templates/vanilla/_gitignore +6 -0
- package/dist/templates/vanilla/index.html +16 -0
- package/dist/templates/vanilla/package.json +19 -0
- package/dist/templates/vanilla/src/chat-data.ts +106 -0
- package/dist/templates/vanilla/src/index.css +111 -0
- package/dist/templates/vanilla/src/main.ts +53 -0
- package/dist/templates/vanilla/src/state.ts +119 -0
- package/dist/templates/vanilla/src/view.ts +222 -0
- package/dist/templates/vanilla/src/vite-env.d.ts +1 -0
- package/dist/templates/vanilla/src/voice-input.ts +55 -0
- package/dist/templates/vanilla/tsconfig.json +24 -0
- package/dist/templates/vanilla/vite.config.ts +14 -0
- package/dist/templates/vue/_gitignore +6 -0
- package/dist/templates/vue/index.html +16 -0
- package/dist/templates/vue/package.json +23 -0
- package/dist/templates/vue/src/App.vue +104 -0
- package/dist/templates/vue/src/chat-data.ts +106 -0
- package/dist/templates/vue/src/components/Composer.vue +96 -0
- package/dist/templates/vue/src/components/Sidebar.vue +48 -0
- package/dist/templates/vue/src/components/ThemeToggle.vue +27 -0
- package/dist/templates/vue/src/components/ThreadView.vue +50 -0
- package/dist/templates/vue/src/components/icons/MoonIcon.vue +24 -0
- package/dist/templates/vue/src/components/icons/SunIcon.vue +24 -0
- package/dist/templates/vue/src/composables/index.ts +3 -0
- package/dist/templates/vue/src/composables/useChat.ts +47 -0
- package/dist/templates/vue/src/composables/useConversations.ts +42 -0
- package/dist/templates/vue/src/composables/useVoiceInput.ts +57 -0
- package/dist/templates/vue/src/index.css +111 -0
- package/dist/templates/vue/src/main.ts +18 -0
- package/dist/templates/vue/src/types.ts +4 -0
- package/dist/templates/vue/src/vite-env.d.ts +1 -0
- package/dist/templates/vue/tsconfig.app.json +29 -0
- package/dist/templates/vue/tsconfig.json +7 -0
- package/dist/templates/vue/tsconfig.node.json +25 -0
- package/dist/templates/vue/vite.config.ts +24 -0
- package/package.json +63 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4433 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
9
|
+
try {
|
|
10
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
11
|
+
} catch (e2) {
|
|
12
|
+
throw mod = 0, e2;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var __copyProps = (to, from, except, desc) => {
|
|
16
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
17
|
+
for (let key of __getOwnPropNames(from))
|
|
18
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
19
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
+
mod
|
|
30
|
+
));
|
|
31
|
+
|
|
32
|
+
// ../../node_modules/sisteransi/src/index.js
|
|
33
|
+
var require_src = __commonJS({
|
|
34
|
+
"../../node_modules/sisteransi/src/index.js"(exports, module) {
|
|
35
|
+
"use strict";
|
|
36
|
+
var ESC = "\x1B";
|
|
37
|
+
var CSI = `${ESC}[`;
|
|
38
|
+
var beep = "\x07";
|
|
39
|
+
var cursor = {
|
|
40
|
+
to(x2, y3) {
|
|
41
|
+
if (!y3) return `${CSI}${x2 + 1}G`;
|
|
42
|
+
return `${CSI}${y3 + 1};${x2 + 1}H`;
|
|
43
|
+
},
|
|
44
|
+
move(x2, y3) {
|
|
45
|
+
let ret = "";
|
|
46
|
+
if (x2 < 0) ret += `${CSI}${-x2}D`;
|
|
47
|
+
else if (x2 > 0) ret += `${CSI}${x2}C`;
|
|
48
|
+
if (y3 < 0) ret += `${CSI}${-y3}A`;
|
|
49
|
+
else if (y3 > 0) ret += `${CSI}${y3}B`;
|
|
50
|
+
return ret;
|
|
51
|
+
},
|
|
52
|
+
up: (count = 1) => `${CSI}${count}A`,
|
|
53
|
+
down: (count = 1) => `${CSI}${count}B`,
|
|
54
|
+
forward: (count = 1) => `${CSI}${count}C`,
|
|
55
|
+
backward: (count = 1) => `${CSI}${count}D`,
|
|
56
|
+
nextLine: (count = 1) => `${CSI}E`.repeat(count),
|
|
57
|
+
prevLine: (count = 1) => `${CSI}F`.repeat(count),
|
|
58
|
+
left: `${CSI}G`,
|
|
59
|
+
hide: `${CSI}?25l`,
|
|
60
|
+
show: `${CSI}?25h`,
|
|
61
|
+
save: `${ESC}7`,
|
|
62
|
+
restore: `${ESC}8`
|
|
63
|
+
};
|
|
64
|
+
var scroll = {
|
|
65
|
+
up: (count = 1) => `${CSI}S`.repeat(count),
|
|
66
|
+
down: (count = 1) => `${CSI}T`.repeat(count)
|
|
67
|
+
};
|
|
68
|
+
var erase = {
|
|
69
|
+
screen: `${CSI}2J`,
|
|
70
|
+
up: (count = 1) => `${CSI}1J`.repeat(count),
|
|
71
|
+
down: (count = 1) => `${CSI}J`.repeat(count),
|
|
72
|
+
line: `${CSI}2K`,
|
|
73
|
+
lineEnd: `${CSI}K`,
|
|
74
|
+
lineStart: `${CSI}1K`,
|
|
75
|
+
lines(count) {
|
|
76
|
+
let clear = "";
|
|
77
|
+
for (let i = 0; i < count; i++)
|
|
78
|
+
clear += this.line + (i < count - 1 ? cursor.up() : "");
|
|
79
|
+
if (count)
|
|
80
|
+
clear += cursor.left;
|
|
81
|
+
return clear;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
module.exports = { cursor, scroll, erase, beep };
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ../../node_modules/picocolors/picocolors.js
|
|
89
|
+
var require_picocolors = __commonJS({
|
|
90
|
+
"../../node_modules/picocolors/picocolors.js"(exports, module) {
|
|
91
|
+
var p2 = process || {};
|
|
92
|
+
var argv = p2.argv || [];
|
|
93
|
+
var env = p2.env || {};
|
|
94
|
+
var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p2.platform === "win32" || (p2.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
|
|
95
|
+
var formatter = (open, close, replace = open) => (input) => {
|
|
96
|
+
let string = "" + input, index = string.indexOf(close, open.length);
|
|
97
|
+
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
|
|
98
|
+
};
|
|
99
|
+
var replaceClose = (string, close, replace, index) => {
|
|
100
|
+
let result = "", cursor = 0;
|
|
101
|
+
do {
|
|
102
|
+
result += string.substring(cursor, index) + replace;
|
|
103
|
+
cursor = index + close.length;
|
|
104
|
+
index = string.indexOf(close, cursor);
|
|
105
|
+
} while (~index);
|
|
106
|
+
return result + string.substring(cursor);
|
|
107
|
+
};
|
|
108
|
+
var createColors = (enabled = isColorSupported) => {
|
|
109
|
+
let f = enabled ? formatter : () => String;
|
|
110
|
+
return {
|
|
111
|
+
isColorSupported: enabled,
|
|
112
|
+
reset: f("\x1B[0m", "\x1B[0m"),
|
|
113
|
+
bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
|
|
114
|
+
dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
|
|
115
|
+
italic: f("\x1B[3m", "\x1B[23m"),
|
|
116
|
+
underline: f("\x1B[4m", "\x1B[24m"),
|
|
117
|
+
inverse: f("\x1B[7m", "\x1B[27m"),
|
|
118
|
+
hidden: f("\x1B[8m", "\x1B[28m"),
|
|
119
|
+
strikethrough: f("\x1B[9m", "\x1B[29m"),
|
|
120
|
+
black: f("\x1B[30m", "\x1B[39m"),
|
|
121
|
+
red: f("\x1B[31m", "\x1B[39m"),
|
|
122
|
+
green: f("\x1B[32m", "\x1B[39m"),
|
|
123
|
+
yellow: f("\x1B[33m", "\x1B[39m"),
|
|
124
|
+
blue: f("\x1B[34m", "\x1B[39m"),
|
|
125
|
+
magenta: f("\x1B[35m", "\x1B[39m"),
|
|
126
|
+
cyan: f("\x1B[36m", "\x1B[39m"),
|
|
127
|
+
white: f("\x1B[37m", "\x1B[39m"),
|
|
128
|
+
gray: f("\x1B[90m", "\x1B[39m"),
|
|
129
|
+
bgBlack: f("\x1B[40m", "\x1B[49m"),
|
|
130
|
+
bgRed: f("\x1B[41m", "\x1B[49m"),
|
|
131
|
+
bgGreen: f("\x1B[42m", "\x1B[49m"),
|
|
132
|
+
bgYellow: f("\x1B[43m", "\x1B[49m"),
|
|
133
|
+
bgBlue: f("\x1B[44m", "\x1B[49m"),
|
|
134
|
+
bgMagenta: f("\x1B[45m", "\x1B[49m"),
|
|
135
|
+
bgCyan: f("\x1B[46m", "\x1B[49m"),
|
|
136
|
+
bgWhite: f("\x1B[47m", "\x1B[49m"),
|
|
137
|
+
blackBright: f("\x1B[90m", "\x1B[39m"),
|
|
138
|
+
redBright: f("\x1B[91m", "\x1B[39m"),
|
|
139
|
+
greenBright: f("\x1B[92m", "\x1B[39m"),
|
|
140
|
+
yellowBright: f("\x1B[93m", "\x1B[39m"),
|
|
141
|
+
blueBright: f("\x1B[94m", "\x1B[39m"),
|
|
142
|
+
magentaBright: f("\x1B[95m", "\x1B[39m"),
|
|
143
|
+
cyanBright: f("\x1B[96m", "\x1B[39m"),
|
|
144
|
+
whiteBright: f("\x1B[97m", "\x1B[39m"),
|
|
145
|
+
bgBlackBright: f("\x1B[100m", "\x1B[49m"),
|
|
146
|
+
bgRedBright: f("\x1B[101m", "\x1B[49m"),
|
|
147
|
+
bgGreenBright: f("\x1B[102m", "\x1B[49m"),
|
|
148
|
+
bgYellowBright: f("\x1B[103m", "\x1B[49m"),
|
|
149
|
+
bgBlueBright: f("\x1B[104m", "\x1B[49m"),
|
|
150
|
+
bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
|
|
151
|
+
bgCyanBright: f("\x1B[106m", "\x1B[49m"),
|
|
152
|
+
bgWhiteBright: f("\x1B[107m", "\x1B[49m")
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
module.exports = createColors();
|
|
156
|
+
module.exports.createColors = createColors;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// src/index.ts
|
|
161
|
+
import { spawn } from "node:child_process";
|
|
162
|
+
import { readdir as readdir2 } from "node:fs/promises";
|
|
163
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
164
|
+
import path2 from "node:path";
|
|
165
|
+
import process2 from "node:process";
|
|
166
|
+
|
|
167
|
+
// node_modules/@clack/prompts/dist/index.mjs
|
|
168
|
+
import { stripVTControlCharacters as S2 } from "node:util";
|
|
169
|
+
|
|
170
|
+
// node_modules/@clack/core/dist/index.mjs
|
|
171
|
+
var import_sisteransi = __toESM(require_src(), 1);
|
|
172
|
+
var import_picocolors = __toESM(require_picocolors(), 1);
|
|
173
|
+
import { stdin as j, stdout as M } from "node:process";
|
|
174
|
+
import * as g from "node:readline";
|
|
175
|
+
import O from "node:readline";
|
|
176
|
+
import { Writable as X } from "node:stream";
|
|
177
|
+
function DD({ onlyFirst: e2 = false } = {}) {
|
|
178
|
+
const t = ["[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?(?:\\u0007|\\u001B\\u005C|\\u009C))", "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))"].join("|");
|
|
179
|
+
return new RegExp(t, e2 ? void 0 : "g");
|
|
180
|
+
}
|
|
181
|
+
var uD = DD();
|
|
182
|
+
function P(e2) {
|
|
183
|
+
if (typeof e2 != "string") throw new TypeError(`Expected a \`string\`, got \`${typeof e2}\``);
|
|
184
|
+
return e2.replace(uD, "");
|
|
185
|
+
}
|
|
186
|
+
function L(e2) {
|
|
187
|
+
return e2 && e2.__esModule && Object.prototype.hasOwnProperty.call(e2, "default") ? e2.default : e2;
|
|
188
|
+
}
|
|
189
|
+
var W = { exports: {} };
|
|
190
|
+
(function(e2) {
|
|
191
|
+
var u2 = {};
|
|
192
|
+
e2.exports = u2, u2.eastAsianWidth = function(F2) {
|
|
193
|
+
var s = F2.charCodeAt(0), i = F2.length == 2 ? F2.charCodeAt(1) : 0, D2 = s;
|
|
194
|
+
return 55296 <= s && s <= 56319 && 56320 <= i && i <= 57343 && (s &= 1023, i &= 1023, D2 = s << 10 | i, D2 += 65536), D2 == 12288 || 65281 <= D2 && D2 <= 65376 || 65504 <= D2 && D2 <= 65510 ? "F" : D2 == 8361 || 65377 <= D2 && D2 <= 65470 || 65474 <= D2 && D2 <= 65479 || 65482 <= D2 && D2 <= 65487 || 65490 <= D2 && D2 <= 65495 || 65498 <= D2 && D2 <= 65500 || 65512 <= D2 && D2 <= 65518 ? "H" : 4352 <= D2 && D2 <= 4447 || 4515 <= D2 && D2 <= 4519 || 4602 <= D2 && D2 <= 4607 || 9001 <= D2 && D2 <= 9002 || 11904 <= D2 && D2 <= 11929 || 11931 <= D2 && D2 <= 12019 || 12032 <= D2 && D2 <= 12245 || 12272 <= D2 && D2 <= 12283 || 12289 <= D2 && D2 <= 12350 || 12353 <= D2 && D2 <= 12438 || 12441 <= D2 && D2 <= 12543 || 12549 <= D2 && D2 <= 12589 || 12593 <= D2 && D2 <= 12686 || 12688 <= D2 && D2 <= 12730 || 12736 <= D2 && D2 <= 12771 || 12784 <= D2 && D2 <= 12830 || 12832 <= D2 && D2 <= 12871 || 12880 <= D2 && D2 <= 13054 || 13056 <= D2 && D2 <= 19903 || 19968 <= D2 && D2 <= 42124 || 42128 <= D2 && D2 <= 42182 || 43360 <= D2 && D2 <= 43388 || 44032 <= D2 && D2 <= 55203 || 55216 <= D2 && D2 <= 55238 || 55243 <= D2 && D2 <= 55291 || 63744 <= D2 && D2 <= 64255 || 65040 <= D2 && D2 <= 65049 || 65072 <= D2 && D2 <= 65106 || 65108 <= D2 && D2 <= 65126 || 65128 <= D2 && D2 <= 65131 || 110592 <= D2 && D2 <= 110593 || 127488 <= D2 && D2 <= 127490 || 127504 <= D2 && D2 <= 127546 || 127552 <= D2 && D2 <= 127560 || 127568 <= D2 && D2 <= 127569 || 131072 <= D2 && D2 <= 194367 || 177984 <= D2 && D2 <= 196605 || 196608 <= D2 && D2 <= 262141 ? "W" : 32 <= D2 && D2 <= 126 || 162 <= D2 && D2 <= 163 || 165 <= D2 && D2 <= 166 || D2 == 172 || D2 == 175 || 10214 <= D2 && D2 <= 10221 || 10629 <= D2 && D2 <= 10630 ? "Na" : D2 == 161 || D2 == 164 || 167 <= D2 && D2 <= 168 || D2 == 170 || 173 <= D2 && D2 <= 174 || 176 <= D2 && D2 <= 180 || 182 <= D2 && D2 <= 186 || 188 <= D2 && D2 <= 191 || D2 == 198 || D2 == 208 || 215 <= D2 && D2 <= 216 || 222 <= D2 && D2 <= 225 || D2 == 230 || 232 <= D2 && D2 <= 234 || 236 <= D2 && D2 <= 237 || D2 == 240 || 242 <= D2 && D2 <= 243 || 247 <= D2 && D2 <= 250 || D2 == 252 || D2 == 254 || D2 == 257 || D2 == 273 || D2 == 275 || D2 == 283 || 294 <= D2 && D2 <= 295 || D2 == 299 || 305 <= D2 && D2 <= 307 || D2 == 312 || 319 <= D2 && D2 <= 322 || D2 == 324 || 328 <= D2 && D2 <= 331 || D2 == 333 || 338 <= D2 && D2 <= 339 || 358 <= D2 && D2 <= 359 || D2 == 363 || D2 == 462 || D2 == 464 || D2 == 466 || D2 == 468 || D2 == 470 || D2 == 472 || D2 == 474 || D2 == 476 || D2 == 593 || D2 == 609 || D2 == 708 || D2 == 711 || 713 <= D2 && D2 <= 715 || D2 == 717 || D2 == 720 || 728 <= D2 && D2 <= 731 || D2 == 733 || D2 == 735 || 768 <= D2 && D2 <= 879 || 913 <= D2 && D2 <= 929 || 931 <= D2 && D2 <= 937 || 945 <= D2 && D2 <= 961 || 963 <= D2 && D2 <= 969 || D2 == 1025 || 1040 <= D2 && D2 <= 1103 || D2 == 1105 || D2 == 8208 || 8211 <= D2 && D2 <= 8214 || 8216 <= D2 && D2 <= 8217 || 8220 <= D2 && D2 <= 8221 || 8224 <= D2 && D2 <= 8226 || 8228 <= D2 && D2 <= 8231 || D2 == 8240 || 8242 <= D2 && D2 <= 8243 || D2 == 8245 || D2 == 8251 || D2 == 8254 || D2 == 8308 || D2 == 8319 || 8321 <= D2 && D2 <= 8324 || D2 == 8364 || D2 == 8451 || D2 == 8453 || D2 == 8457 || D2 == 8467 || D2 == 8470 || 8481 <= D2 && D2 <= 8482 || D2 == 8486 || D2 == 8491 || 8531 <= D2 && D2 <= 8532 || 8539 <= D2 && D2 <= 8542 || 8544 <= D2 && D2 <= 8555 || 8560 <= D2 && D2 <= 8569 || D2 == 8585 || 8592 <= D2 && D2 <= 8601 || 8632 <= D2 && D2 <= 8633 || D2 == 8658 || D2 == 8660 || D2 == 8679 || D2 == 8704 || 8706 <= D2 && D2 <= 8707 || 8711 <= D2 && D2 <= 8712 || D2 == 8715 || D2 == 8719 || D2 == 8721 || D2 == 8725 || D2 == 8730 || 8733 <= D2 && D2 <= 8736 || D2 == 8739 || D2 == 8741 || 8743 <= D2 && D2 <= 8748 || D2 == 8750 || 8756 <= D2 && D2 <= 8759 || 8764 <= D2 && D2 <= 8765 || D2 == 8776 || D2 == 8780 || D2 == 8786 || 8800 <= D2 && D2 <= 8801 || 8804 <= D2 && D2 <= 8807 || 8810 <= D2 && D2 <= 8811 || 8814 <= D2 && D2 <= 8815 || 8834 <= D2 && D2 <= 8835 || 8838 <= D2 && D2 <= 8839 || D2 == 8853 || D2 == 8857 || D2 == 8869 || D2 == 8895 || D2 == 8978 || 9312 <= D2 && D2 <= 9449 || 9451 <= D2 && D2 <= 9547 || 9552 <= D2 && D2 <= 9587 || 9600 <= D2 && D2 <= 9615 || 9618 <= D2 && D2 <= 9621 || 9632 <= D2 && D2 <= 9633 || 9635 <= D2 && D2 <= 9641 || 9650 <= D2 && D2 <= 9651 || 9654 <= D2 && D2 <= 9655 || 9660 <= D2 && D2 <= 9661 || 9664 <= D2 && D2 <= 9665 || 9670 <= D2 && D2 <= 9672 || D2 == 9675 || 9678 <= D2 && D2 <= 9681 || 9698 <= D2 && D2 <= 9701 || D2 == 9711 || 9733 <= D2 && D2 <= 9734 || D2 == 9737 || 9742 <= D2 && D2 <= 9743 || 9748 <= D2 && D2 <= 9749 || D2 == 9756 || D2 == 9758 || D2 == 9792 || D2 == 9794 || 9824 <= D2 && D2 <= 9825 || 9827 <= D2 && D2 <= 9829 || 9831 <= D2 && D2 <= 9834 || 9836 <= D2 && D2 <= 9837 || D2 == 9839 || 9886 <= D2 && D2 <= 9887 || 9918 <= D2 && D2 <= 9919 || 9924 <= D2 && D2 <= 9933 || 9935 <= D2 && D2 <= 9953 || D2 == 9955 || 9960 <= D2 && D2 <= 9983 || D2 == 10045 || D2 == 10071 || 10102 <= D2 && D2 <= 10111 || 11093 <= D2 && D2 <= 11097 || 12872 <= D2 && D2 <= 12879 || 57344 <= D2 && D2 <= 63743 || 65024 <= D2 && D2 <= 65039 || D2 == 65533 || 127232 <= D2 && D2 <= 127242 || 127248 <= D2 && D2 <= 127277 || 127280 <= D2 && D2 <= 127337 || 127344 <= D2 && D2 <= 127386 || 917760 <= D2 && D2 <= 917999 || 983040 <= D2 && D2 <= 1048573 || 1048576 <= D2 && D2 <= 1114109 ? "A" : "N";
|
|
195
|
+
}, u2.characterLength = function(F2) {
|
|
196
|
+
var s = this.eastAsianWidth(F2);
|
|
197
|
+
return s == "F" || s == "W" || s == "A" ? 2 : 1;
|
|
198
|
+
};
|
|
199
|
+
function t(F2) {
|
|
200
|
+
return F2.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[^\uD800-\uDFFF]/g) || [];
|
|
201
|
+
}
|
|
202
|
+
u2.length = function(F2) {
|
|
203
|
+
for (var s = t(F2), i = 0, D2 = 0; D2 < s.length; D2++) i = i + this.characterLength(s[D2]);
|
|
204
|
+
return i;
|
|
205
|
+
}, u2.slice = function(F2, s, i) {
|
|
206
|
+
textLen = u2.length(F2), s = s || 0, i = i || 1, s < 0 && (s = textLen + s), i < 0 && (i = textLen + i);
|
|
207
|
+
for (var D2 = "", C2 = 0, n = t(F2), E = 0; E < n.length; E++) {
|
|
208
|
+
var a = n[E], o2 = u2.length(a);
|
|
209
|
+
if (C2 >= s - (o2 == 2 ? 1 : 0)) if (C2 + o2 <= i) D2 += a;
|
|
210
|
+
else break;
|
|
211
|
+
C2 += o2;
|
|
212
|
+
}
|
|
213
|
+
return D2;
|
|
214
|
+
};
|
|
215
|
+
})(W);
|
|
216
|
+
var tD = W.exports;
|
|
217
|
+
var eD = L(tD);
|
|
218
|
+
var FD = function() {
|
|
219
|
+
return /\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62(?:\uDB40\uDC77\uDB40\uDC6C\uDB40\uDC73|\uDB40\uDC73\uDB40\uDC63\uDB40\uDC74|\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67)\uDB40\uDC7F|(?:\uD83E\uDDD1\uD83C\uDFFF\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB-\uDFFE])|(?:\uD83E\uDDD1\uD83C\uDFFE\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB-\uDFFD\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFD\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFC\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB\uDFFD-\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFB\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFC-\uDFFF])|\uD83D\uDC68(?:\uD83C\uDFFB(?:\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF]))|\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFC-\uDFFF])|[\u2695\u2696\u2708]\uFE0F|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD]))?|(?:\uD83C[\uDFFC-\uDFFF])\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFF]))|\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83D\uDC68|(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFE])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFE\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFC\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|(?:\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708])\uFE0F|\u200D(?:(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D[\uDC66\uDC67])|\uD83D[\uDC66\uDC67])|\uD83C\uDFFF|\uD83C\uDFFE|\uD83C\uDFFD|\uD83C\uDFFC)?|(?:\uD83D\uDC69(?:\uD83C\uDFFB\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69])|(?:\uD83C[\uDFFC-\uDFFF])\u200D\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69]))|\uD83E\uDDD1(?:\uD83C[\uDFFB-\uDFFF])\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1)(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC69\u200D\uD83D\uDC69\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|\uD83D\uDC69(?:\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69])|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFE\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFC\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFB\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD]))|\uD83E\uDDD1(?:\u200D(?:\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFE\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFC\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFB\u200D(?:\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD]))|\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC69\u200D\uD83D\uDC69\u200D(?:\uD83D[\uDC66\uDC67])|\uD83D\uDC69\u200D\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|(?:\uD83D\uDC41\uFE0F\u200D\uD83D\uDDE8|\uD83E\uDDD1(?:\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708]|\uD83C\uDFFB\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708])|\uD83D\uDC69(?:\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708]|\uD83C\uDFFB\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708])|\uD83D\uDE36\u200D\uD83C\uDF2B|\uD83C\uDFF3\uFE0F\u200D\u26A7|\uD83D\uDC3B\u200D\u2744|(?:(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC6F|\uD83E[\uDD3C\uDDDE\uDDDF])\u200D[\u2640\u2642]|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])\u200D[\u2640\u2642]|\uD83C\uDFF4\u200D\u2620|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD])\u200D[\u2640\u2642]|[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u2328\u23CF\u23ED-\u23EF\u23F1\u23F2\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC\u2600-\u2604\u260E\u2611\u2618\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u2692\u2694-\u2697\u2699\u269B\u269C\u26A0\u26A7\u26B0\u26B1\u26C8\u26CF\u26D1\u26D3\u26E9\u26F0\u26F1\u26F4\u26F7\u26F8\u2702\u2708\u2709\u270F\u2712\u2714\u2716\u271D\u2721\u2733\u2734\u2744\u2747\u2763\u27A1\u2934\u2935\u2B05-\u2B07\u3030\u303D\u3297\u3299]|\uD83C[\uDD70\uDD71\uDD7E\uDD7F\uDE02\uDE37\uDF21\uDF24-\uDF2C\uDF36\uDF7D\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F\uDFCD\uDFCE\uDFD4-\uDFDF\uDFF5\uDFF7]|\uD83D[\uDC3F\uDCFD\uDD49\uDD4A\uDD6F\uDD70\uDD73\uDD76-\uDD79\uDD87\uDD8A-\uDD8D\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA\uDECB\uDECD-\uDECF\uDEE0-\uDEE5\uDEE9\uDEF0\uDEF3])\uFE0F|\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08|\uD83D\uDC69\u200D\uD83D\uDC67|\uD83D\uDC69\u200D\uD83D\uDC66|\uD83D\uDE35\u200D\uD83D\uDCAB|\uD83D\uDE2E\u200D\uD83D\uDCA8|\uD83D\uDC15\u200D\uD83E\uDDBA|\uD83E\uDDD1(?:\uD83C\uDFFF|\uD83C\uDFFE|\uD83C\uDFFD|\uD83C\uDFFC|\uD83C\uDFFB)?|\uD83D\uDC69(?:\uD83C\uDFFF|\uD83C\uDFFE|\uD83C\uDFFD|\uD83C\uDFFC|\uD83C\uDFFB)?|\uD83C\uDDFD\uD83C\uDDF0|\uD83C\uDDF6\uD83C\uDDE6|\uD83C\uDDF4\uD83C\uDDF2|\uD83D\uDC08\u200D\u2B1B|\u2764\uFE0F\u200D(?:\uD83D\uDD25|\uD83E\uDE79)|\uD83D\uDC41\uFE0F|\uD83C\uDFF3\uFE0F|\uD83C\uDDFF(?:\uD83C[\uDDE6\uDDF2\uDDFC])|\uD83C\uDDFE(?:\uD83C[\uDDEA\uDDF9])|\uD83C\uDDFC(?:\uD83C[\uDDEB\uDDF8])|\uD83C\uDDFB(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA])|\uD83C\uDDFA(?:\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF])|\uD83C\uDDF9(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF])|\uD83C\uDDF8(?:\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF])|\uD83C\uDDF7(?:\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC])|\uD83C\uDDF5(?:\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE])|\uD83C\uDDF3(?:\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF])|\uD83C\uDDF2(?:\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF])|\uD83C\uDDF1(?:\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE])|\uD83C\uDDF0(?:\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF])|\uD83C\uDDEF(?:\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5])|\uD83C\uDDEE(?:\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9])|\uD83C\uDDED(?:\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA])|\uD83C\uDDEC(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE])|\uD83C\uDDEB(?:\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7])|\uD83C\uDDEA(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA])|\uD83C\uDDE9(?:\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF])|\uD83C\uDDE8(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF])|\uD83C\uDDE7(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF])|\uD83C\uDDE6(?:\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF])|[#\*0-9]\uFE0F\u20E3|\u2764\uFE0F|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])|\uD83C\uDFF4|(?:[\u270A\u270B]|\uD83C[\uDF85\uDFC2\uDFC7]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC]|\uD83E[\uDD0C\uDD0F\uDD18-\uDD1C\uDD1E\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5])(?:\uD83C[\uDFFB-\uDFFF])|(?:[\u261D\u270C\u270D]|\uD83D[\uDD74\uDD90])(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])|[\u270A\u270B]|\uD83C[\uDF85\uDFC2\uDFC7]|\uD83D[\uDC08\uDC15\uDC3B\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE2E\uDE35\uDE36\uDE4C\uDE4F\uDEC0\uDECC]|\uD83E[\uDD0C\uDD0F\uDD18-\uDD1C\uDD1E\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5]|\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD]|\uD83D\uDC6F|\uD83E[\uDD3C\uDDDE\uDDDF]|[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF84\uDF86-\uDF93\uDFA0-\uDFC1\uDFC5\uDFC6\uDFC8\uDFC9\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC07\uDC09-\uDC14\uDC16-\uDC3A\uDC3C-\uDC3E\uDC40\uDC44\uDC45\uDC51-\uDC65\uDC6A\uDC79-\uDC7B\uDC7D-\uDC80\uDC84\uDC88-\uDC8E\uDC90\uDC92-\uDCA9\uDCAB-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDDA4\uDDFB-\uDE2D\uDE2F-\uDE34\uDE37-\uDE44\uDE48-\uDE4A\uDE80-\uDEA2\uDEA4-\uDEB3\uDEB7-\uDEBF\uDEC1-\uDEC5\uDED0-\uDED2\uDED5-\uDED7\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB]|\uD83E[\uDD0D\uDD0E\uDD10-\uDD17\uDD1D\uDD20-\uDD25\uDD27-\uDD2F\uDD3A\uDD3F-\uDD45\uDD47-\uDD76\uDD78\uDD7A-\uDDB4\uDDB7\uDDBA\uDDBC-\uDDCB\uDDD0\uDDE0-\uDDFF\uDE70-\uDE74\uDE78-\uDE7A\uDE80-\uDE86\uDE90-\uDEA8\uDEB0-\uDEB6\uDEC0-\uDEC2\uDED0-\uDED6]|(?:[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u270A\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF93\uDFA0-\uDFCA\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF4\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E\uDC40\uDC42-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDD7A\uDD95\uDD96\uDDA4\uDDFB-\uDE4F\uDE80-\uDEC5\uDECC\uDED0-\uDED2\uDED5-\uDED7\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB]|\uD83E[\uDD0C-\uDD3A\uDD3C-\uDD45\uDD47-\uDD78\uDD7A-\uDDCB\uDDCD-\uDDFF\uDE70-\uDE74\uDE78-\uDE7A\uDE80-\uDE86\uDE90-\uDEA8\uDEB0-\uDEB6\uDEC0-\uDEC2\uDED0-\uDED6])|(?:[#\*0-9\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26A7\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC04\uDCCF\uDD70\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE02\uDE1A\uDE2F\uDE32-\uDE3A\uDE50\uDE51\uDF00-\uDF21\uDF24-\uDF93\uDF96\uDF97\uDF99-\uDF9B\uDF9E-\uDFF0\uDFF3-\uDFF5\uDFF7-\uDFFF]|\uD83D[\uDC00-\uDCFD\uDCFF-\uDD3D\uDD49-\uDD4E\uDD50-\uDD67\uDD6F\uDD70\uDD73-\uDD7A\uDD87\uDD8A-\uDD8D\uDD90\uDD95\uDD96\uDDA4\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA-\uDE4F\uDE80-\uDEC5\uDECB-\uDED2\uDED5-\uDED7\uDEE0-\uDEE5\uDEE9\uDEEB\uDEEC\uDEF0\uDEF3-\uDEFC\uDFE0-\uDFEB]|\uD83E[\uDD0C-\uDD3A\uDD3C-\uDD45\uDD47-\uDD78\uDD7A-\uDDCB\uDDCD-\uDDFF\uDE70-\uDE74\uDE78-\uDE7A\uDE80-\uDE86\uDE90-\uDEA8\uDEB0-\uDEB6\uDEC0-\uDEC2\uDED0-\uDED6])\uFE0F|(?:[\u261D\u26F9\u270A-\u270D]|\uD83C[\uDF85\uDFC2-\uDFC4\uDFC7\uDFCA-\uDFCC]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66-\uDC78\uDC7C\uDC81-\uDC83\uDC85-\uDC87\uDC8F\uDC91\uDCAA\uDD74\uDD75\uDD7A\uDD90\uDD95\uDD96\uDE45-\uDE47\uDE4B-\uDE4F\uDEA3\uDEB4-\uDEB6\uDEC0\uDECC]|\uD83E[\uDD0C\uDD0F\uDD18-\uDD1F\uDD26\uDD30-\uDD39\uDD3C-\uDD3E\uDD77\uDDB5\uDDB6\uDDB8\uDDB9\uDDBB\uDDCD-\uDDCF\uDDD1-\uDDDD])/g;
|
|
220
|
+
};
|
|
221
|
+
var sD = L(FD);
|
|
222
|
+
function p(e2, u2 = {}) {
|
|
223
|
+
if (typeof e2 != "string" || e2.length === 0 || (u2 = { ambiguousIsNarrow: true, ...u2 }, e2 = P(e2), e2.length === 0)) return 0;
|
|
224
|
+
e2 = e2.replace(sD(), " ");
|
|
225
|
+
const t = u2.ambiguousIsNarrow ? 1 : 2;
|
|
226
|
+
let F2 = 0;
|
|
227
|
+
for (const s of e2) {
|
|
228
|
+
const i = s.codePointAt(0);
|
|
229
|
+
if (i <= 31 || i >= 127 && i <= 159 || i >= 768 && i <= 879) continue;
|
|
230
|
+
switch (eD.eastAsianWidth(s)) {
|
|
231
|
+
case "F":
|
|
232
|
+
case "W":
|
|
233
|
+
F2 += 2;
|
|
234
|
+
break;
|
|
235
|
+
case "A":
|
|
236
|
+
F2 += t;
|
|
237
|
+
break;
|
|
238
|
+
default:
|
|
239
|
+
F2 += 1;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return F2;
|
|
243
|
+
}
|
|
244
|
+
var w = 10;
|
|
245
|
+
var N = (e2 = 0) => (u2) => `\x1B[${u2 + e2}m`;
|
|
246
|
+
var I = (e2 = 0) => (u2) => `\x1B[${38 + e2};5;${u2}m`;
|
|
247
|
+
var R = (e2 = 0) => (u2, t, F2) => `\x1B[${38 + e2};2;${u2};${t};${F2}m`;
|
|
248
|
+
var r = { modifier: { reset: [0, 0], bold: [1, 22], dim: [2, 22], italic: [3, 23], underline: [4, 24], overline: [53, 55], inverse: [7, 27], hidden: [8, 28], strikethrough: [9, 29] }, color: { black: [30, 39], red: [31, 39], green: [32, 39], yellow: [33, 39], blue: [34, 39], magenta: [35, 39], cyan: [36, 39], white: [37, 39], blackBright: [90, 39], gray: [90, 39], grey: [90, 39], redBright: [91, 39], greenBright: [92, 39], yellowBright: [93, 39], blueBright: [94, 39], magentaBright: [95, 39], cyanBright: [96, 39], whiteBright: [97, 39] }, bgColor: { bgBlack: [40, 49], bgRed: [41, 49], bgGreen: [42, 49], bgYellow: [43, 49], bgBlue: [44, 49], bgMagenta: [45, 49], bgCyan: [46, 49], bgWhite: [47, 49], bgBlackBright: [100, 49], bgGray: [100, 49], bgGrey: [100, 49], bgRedBright: [101, 49], bgGreenBright: [102, 49], bgYellowBright: [103, 49], bgBlueBright: [104, 49], bgMagentaBright: [105, 49], bgCyanBright: [106, 49], bgWhiteBright: [107, 49] } };
|
|
249
|
+
Object.keys(r.modifier);
|
|
250
|
+
var iD = Object.keys(r.color);
|
|
251
|
+
var CD = Object.keys(r.bgColor);
|
|
252
|
+
[...iD, ...CD];
|
|
253
|
+
function rD() {
|
|
254
|
+
const e2 = /* @__PURE__ */ new Map();
|
|
255
|
+
for (const [u2, t] of Object.entries(r)) {
|
|
256
|
+
for (const [F2, s] of Object.entries(t)) r[F2] = { open: `\x1B[${s[0]}m`, close: `\x1B[${s[1]}m` }, t[F2] = r[F2], e2.set(s[0], s[1]);
|
|
257
|
+
Object.defineProperty(r, u2, { value: t, enumerable: false });
|
|
258
|
+
}
|
|
259
|
+
return Object.defineProperty(r, "codes", { value: e2, enumerable: false }), r.color.close = "\x1B[39m", r.bgColor.close = "\x1B[49m", r.color.ansi = N(), r.color.ansi256 = I(), r.color.ansi16m = R(), r.bgColor.ansi = N(w), r.bgColor.ansi256 = I(w), r.bgColor.ansi16m = R(w), Object.defineProperties(r, { rgbToAnsi256: { value: (u2, t, F2) => u2 === t && t === F2 ? u2 < 8 ? 16 : u2 > 248 ? 231 : Math.round((u2 - 8) / 247 * 24) + 232 : 16 + 36 * Math.round(u2 / 255 * 5) + 6 * Math.round(t / 255 * 5) + Math.round(F2 / 255 * 5), enumerable: false }, hexToRgb: { value: (u2) => {
|
|
260
|
+
const t = /[a-f\d]{6}|[a-f\d]{3}/i.exec(u2.toString(16));
|
|
261
|
+
if (!t) return [0, 0, 0];
|
|
262
|
+
let [F2] = t;
|
|
263
|
+
F2.length === 3 && (F2 = [...F2].map((i) => i + i).join(""));
|
|
264
|
+
const s = Number.parseInt(F2, 16);
|
|
265
|
+
return [s >> 16 & 255, s >> 8 & 255, s & 255];
|
|
266
|
+
}, enumerable: false }, hexToAnsi256: { value: (u2) => r.rgbToAnsi256(...r.hexToRgb(u2)), enumerable: false }, ansi256ToAnsi: { value: (u2) => {
|
|
267
|
+
if (u2 < 8) return 30 + u2;
|
|
268
|
+
if (u2 < 16) return 90 + (u2 - 8);
|
|
269
|
+
let t, F2, s;
|
|
270
|
+
if (u2 >= 232) t = ((u2 - 232) * 10 + 8) / 255, F2 = t, s = t;
|
|
271
|
+
else {
|
|
272
|
+
u2 -= 16;
|
|
273
|
+
const C2 = u2 % 36;
|
|
274
|
+
t = Math.floor(u2 / 36) / 5, F2 = Math.floor(C2 / 6) / 5, s = C2 % 6 / 5;
|
|
275
|
+
}
|
|
276
|
+
const i = Math.max(t, F2, s) * 2;
|
|
277
|
+
if (i === 0) return 30;
|
|
278
|
+
let D2 = 30 + (Math.round(s) << 2 | Math.round(F2) << 1 | Math.round(t));
|
|
279
|
+
return i === 2 && (D2 += 60), D2;
|
|
280
|
+
}, enumerable: false }, rgbToAnsi: { value: (u2, t, F2) => r.ansi256ToAnsi(r.rgbToAnsi256(u2, t, F2)), enumerable: false }, hexToAnsi: { value: (u2) => r.ansi256ToAnsi(r.hexToAnsi256(u2)), enumerable: false } }), r;
|
|
281
|
+
}
|
|
282
|
+
var ED = rD();
|
|
283
|
+
var d = /* @__PURE__ */ new Set(["\x1B", "\x9B"]);
|
|
284
|
+
var oD = 39;
|
|
285
|
+
var y = "\x07";
|
|
286
|
+
var V = "[";
|
|
287
|
+
var nD = "]";
|
|
288
|
+
var G = "m";
|
|
289
|
+
var _ = `${nD}8;;`;
|
|
290
|
+
var z = (e2) => `${d.values().next().value}${V}${e2}${G}`;
|
|
291
|
+
var K = (e2) => `${d.values().next().value}${_}${e2}${y}`;
|
|
292
|
+
var aD = (e2) => e2.split(" ").map((u2) => p(u2));
|
|
293
|
+
var k = (e2, u2, t) => {
|
|
294
|
+
const F2 = [...u2];
|
|
295
|
+
let s = false, i = false, D2 = p(P(e2[e2.length - 1]));
|
|
296
|
+
for (const [C2, n] of F2.entries()) {
|
|
297
|
+
const E = p(n);
|
|
298
|
+
if (D2 + E <= t ? e2[e2.length - 1] += n : (e2.push(n), D2 = 0), d.has(n) && (s = true, i = F2.slice(C2 + 1).join("").startsWith(_)), s) {
|
|
299
|
+
i ? n === y && (s = false, i = false) : n === G && (s = false);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
D2 += E, D2 === t && C2 < F2.length - 1 && (e2.push(""), D2 = 0);
|
|
303
|
+
}
|
|
304
|
+
!D2 && e2[e2.length - 1].length > 0 && e2.length > 1 && (e2[e2.length - 2] += e2.pop());
|
|
305
|
+
};
|
|
306
|
+
var hD = (e2) => {
|
|
307
|
+
const u2 = e2.split(" ");
|
|
308
|
+
let t = u2.length;
|
|
309
|
+
for (; t > 0 && !(p(u2[t - 1]) > 0); ) t--;
|
|
310
|
+
return t === u2.length ? e2 : u2.slice(0, t).join(" ") + u2.slice(t).join("");
|
|
311
|
+
};
|
|
312
|
+
var lD = (e2, u2, t = {}) => {
|
|
313
|
+
if (t.trim !== false && e2.trim() === "") return "";
|
|
314
|
+
let F2 = "", s, i;
|
|
315
|
+
const D2 = aD(e2);
|
|
316
|
+
let C2 = [""];
|
|
317
|
+
for (const [E, a] of e2.split(" ").entries()) {
|
|
318
|
+
t.trim !== false && (C2[C2.length - 1] = C2[C2.length - 1].trimStart());
|
|
319
|
+
let o2 = p(C2[C2.length - 1]);
|
|
320
|
+
if (E !== 0 && (o2 >= u2 && (t.wordWrap === false || t.trim === false) && (C2.push(""), o2 = 0), (o2 > 0 || t.trim === false) && (C2[C2.length - 1] += " ", o2++)), t.hard && D2[E] > u2) {
|
|
321
|
+
const c = u2 - o2, f = 1 + Math.floor((D2[E] - c - 1) / u2);
|
|
322
|
+
Math.floor((D2[E] - 1) / u2) < f && C2.push(""), k(C2, a, u2);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (o2 + D2[E] > u2 && o2 > 0 && D2[E] > 0) {
|
|
326
|
+
if (t.wordWrap === false && o2 < u2) {
|
|
327
|
+
k(C2, a, u2);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
C2.push("");
|
|
331
|
+
}
|
|
332
|
+
if (o2 + D2[E] > u2 && t.wordWrap === false) {
|
|
333
|
+
k(C2, a, u2);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
C2[C2.length - 1] += a;
|
|
337
|
+
}
|
|
338
|
+
t.trim !== false && (C2 = C2.map((E) => hD(E)));
|
|
339
|
+
const n = [...C2.join(`
|
|
340
|
+
`)];
|
|
341
|
+
for (const [E, a] of n.entries()) {
|
|
342
|
+
if (F2 += a, d.has(a)) {
|
|
343
|
+
const { groups: c } = new RegExp(`(?:\\${V}(?<code>\\d+)m|\\${_}(?<uri>.*)${y})`).exec(n.slice(E).join("")) || { groups: {} };
|
|
344
|
+
if (c.code !== void 0) {
|
|
345
|
+
const f = Number.parseFloat(c.code);
|
|
346
|
+
s = f === oD ? void 0 : f;
|
|
347
|
+
} else c.uri !== void 0 && (i = c.uri.length === 0 ? void 0 : c.uri);
|
|
348
|
+
}
|
|
349
|
+
const o2 = ED.codes.get(Number(s));
|
|
350
|
+
n[E + 1] === `
|
|
351
|
+
` ? (i && (F2 += K("")), s && o2 && (F2 += z(o2))) : a === `
|
|
352
|
+
` && (s && o2 && (F2 += z(s)), i && (F2 += K(i)));
|
|
353
|
+
}
|
|
354
|
+
return F2;
|
|
355
|
+
};
|
|
356
|
+
function Y(e2, u2, t) {
|
|
357
|
+
return String(e2).normalize().replace(/\r\n/g, `
|
|
358
|
+
`).split(`
|
|
359
|
+
`).map((F2) => lD(F2, u2, t)).join(`
|
|
360
|
+
`);
|
|
361
|
+
}
|
|
362
|
+
var xD = ["up", "down", "left", "right", "space", "enter", "cancel"];
|
|
363
|
+
var B = { actions: new Set(xD), aliases: /* @__PURE__ */ new Map([["k", "up"], ["j", "down"], ["h", "left"], ["l", "right"], ["", "cancel"], ["escape", "cancel"]]) };
|
|
364
|
+
function $(e2, u2) {
|
|
365
|
+
if (typeof e2 == "string") return B.aliases.get(e2) === u2;
|
|
366
|
+
for (const t of e2) if (t !== void 0 && $(t, u2)) return true;
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
function BD(e2, u2) {
|
|
370
|
+
if (e2 === u2) return;
|
|
371
|
+
const t = e2.split(`
|
|
372
|
+
`), F2 = u2.split(`
|
|
373
|
+
`), s = [];
|
|
374
|
+
for (let i = 0; i < Math.max(t.length, F2.length); i++) t[i] !== F2[i] && s.push(i);
|
|
375
|
+
return s;
|
|
376
|
+
}
|
|
377
|
+
var AD = globalThis.process.platform.startsWith("win");
|
|
378
|
+
var S = /* @__PURE__ */ Symbol("clack:cancel");
|
|
379
|
+
function pD(e2) {
|
|
380
|
+
return e2 === S;
|
|
381
|
+
}
|
|
382
|
+
function m(e2, u2) {
|
|
383
|
+
const t = e2;
|
|
384
|
+
t.isTTY && t.setRawMode(u2);
|
|
385
|
+
}
|
|
386
|
+
function fD({ input: e2 = j, output: u2 = M, overwrite: t = true, hideCursor: F2 = true } = {}) {
|
|
387
|
+
const s = g.createInterface({ input: e2, output: u2, prompt: "", tabSize: 1 });
|
|
388
|
+
g.emitKeypressEvents(e2, s), e2.isTTY && e2.setRawMode(true);
|
|
389
|
+
const i = (D2, { name: C2, sequence: n }) => {
|
|
390
|
+
const E = String(D2);
|
|
391
|
+
if ($([E, C2, n], "cancel")) {
|
|
392
|
+
F2 && u2.write(import_sisteransi.cursor.show), process.exit(0);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (!t) return;
|
|
396
|
+
const a = C2 === "return" ? 0 : -1, o2 = C2 === "return" ? -1 : 0;
|
|
397
|
+
g.moveCursor(u2, a, o2, () => {
|
|
398
|
+
g.clearLine(u2, 1, () => {
|
|
399
|
+
e2.once("keypress", i);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
};
|
|
403
|
+
return F2 && u2.write(import_sisteransi.cursor.hide), e2.once("keypress", i), () => {
|
|
404
|
+
e2.off("keypress", i), F2 && u2.write(import_sisteransi.cursor.show), e2.isTTY && !AD && e2.setRawMode(false), s.terminal = false, s.close();
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
var gD = Object.defineProperty;
|
|
408
|
+
var vD = (e2, u2, t) => u2 in e2 ? gD(e2, u2, { enumerable: true, configurable: true, writable: true, value: t }) : e2[u2] = t;
|
|
409
|
+
var h = (e2, u2, t) => (vD(e2, typeof u2 != "symbol" ? u2 + "" : u2, t), t);
|
|
410
|
+
var x = class {
|
|
411
|
+
constructor(u2, t = true) {
|
|
412
|
+
h(this, "input"), h(this, "output"), h(this, "_abortSignal"), h(this, "rl"), h(this, "opts"), h(this, "_render"), h(this, "_track", false), h(this, "_prevFrame", ""), h(this, "_subscribers", /* @__PURE__ */ new Map()), h(this, "_cursor", 0), h(this, "state", "initial"), h(this, "error", ""), h(this, "value");
|
|
413
|
+
const { input: F2 = j, output: s = M, render: i, signal: D2, ...C2 } = u2;
|
|
414
|
+
this.opts = C2, this.onKeypress = this.onKeypress.bind(this), this.close = this.close.bind(this), this.render = this.render.bind(this), this._render = i.bind(this), this._track = t, this._abortSignal = D2, this.input = F2, this.output = s;
|
|
415
|
+
}
|
|
416
|
+
unsubscribe() {
|
|
417
|
+
this._subscribers.clear();
|
|
418
|
+
}
|
|
419
|
+
setSubscriber(u2, t) {
|
|
420
|
+
const F2 = this._subscribers.get(u2) ?? [];
|
|
421
|
+
F2.push(t), this._subscribers.set(u2, F2);
|
|
422
|
+
}
|
|
423
|
+
on(u2, t) {
|
|
424
|
+
this.setSubscriber(u2, { cb: t });
|
|
425
|
+
}
|
|
426
|
+
once(u2, t) {
|
|
427
|
+
this.setSubscriber(u2, { cb: t, once: true });
|
|
428
|
+
}
|
|
429
|
+
emit(u2, ...t) {
|
|
430
|
+
const F2 = this._subscribers.get(u2) ?? [], s = [];
|
|
431
|
+
for (const i of F2) i.cb(...t), i.once && s.push(() => F2.splice(F2.indexOf(i), 1));
|
|
432
|
+
for (const i of s) i();
|
|
433
|
+
}
|
|
434
|
+
prompt() {
|
|
435
|
+
return new Promise((u2, t) => {
|
|
436
|
+
if (this._abortSignal) {
|
|
437
|
+
if (this._abortSignal.aborted) return this.state = "cancel", this.close(), u2(S);
|
|
438
|
+
this._abortSignal.addEventListener("abort", () => {
|
|
439
|
+
this.state = "cancel", this.close();
|
|
440
|
+
}, { once: true });
|
|
441
|
+
}
|
|
442
|
+
const F2 = new X();
|
|
443
|
+
F2._write = (s, i, D2) => {
|
|
444
|
+
this._track && (this.value = this.rl?.line.replace(/\t/g, ""), this._cursor = this.rl?.cursor ?? 0, this.emit("value", this.value)), D2();
|
|
445
|
+
}, this.input.pipe(F2), this.rl = O.createInterface({ input: this.input, output: F2, tabSize: 2, prompt: "", escapeCodeTimeout: 50, terminal: true }), O.emitKeypressEvents(this.input, this.rl), this.rl.prompt(), this.opts.initialValue !== void 0 && this._track && this.rl.write(this.opts.initialValue), this.input.on("keypress", this.onKeypress), m(this.input, true), this.output.on("resize", this.render), this.render(), this.once("submit", () => {
|
|
446
|
+
this.output.write(import_sisteransi.cursor.show), this.output.off("resize", this.render), m(this.input, false), u2(this.value);
|
|
447
|
+
}), this.once("cancel", () => {
|
|
448
|
+
this.output.write(import_sisteransi.cursor.show), this.output.off("resize", this.render), m(this.input, false), u2(S);
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
onKeypress(u2, t) {
|
|
453
|
+
if (this.state === "error" && (this.state = "active"), t?.name && (!this._track && B.aliases.has(t.name) && this.emit("cursor", B.aliases.get(t.name)), B.actions.has(t.name) && this.emit("cursor", t.name)), u2 && (u2.toLowerCase() === "y" || u2.toLowerCase() === "n") && this.emit("confirm", u2.toLowerCase() === "y"), u2 === " " && this.opts.placeholder && (this.value || (this.rl?.write(this.opts.placeholder), this.emit("value", this.opts.placeholder))), u2 && this.emit("key", u2.toLowerCase()), t?.name === "return") {
|
|
454
|
+
if (this.opts.validate) {
|
|
455
|
+
const F2 = this.opts.validate(this.value);
|
|
456
|
+
F2 && (this.error = F2 instanceof Error ? F2.message : F2, this.state = "error", this.rl?.write(this.value));
|
|
457
|
+
}
|
|
458
|
+
this.state !== "error" && (this.state = "submit");
|
|
459
|
+
}
|
|
460
|
+
$([u2, t?.name, t?.sequence], "cancel") && (this.state = "cancel"), (this.state === "submit" || this.state === "cancel") && this.emit("finalize"), this.render(), (this.state === "submit" || this.state === "cancel") && this.close();
|
|
461
|
+
}
|
|
462
|
+
close() {
|
|
463
|
+
this.input.unpipe(), this.input.removeListener("keypress", this.onKeypress), this.output.write(`
|
|
464
|
+
`), m(this.input, false), this.rl?.close(), this.rl = void 0, this.emit(`${this.state}`, this.value), this.unsubscribe();
|
|
465
|
+
}
|
|
466
|
+
restoreCursor() {
|
|
467
|
+
const u2 = Y(this._prevFrame, process.stdout.columns, { hard: true }).split(`
|
|
468
|
+
`).length - 1;
|
|
469
|
+
this.output.write(import_sisteransi.cursor.move(-999, u2 * -1));
|
|
470
|
+
}
|
|
471
|
+
render() {
|
|
472
|
+
const u2 = Y(this._render(this) ?? "", process.stdout.columns, { hard: true });
|
|
473
|
+
if (u2 !== this._prevFrame) {
|
|
474
|
+
if (this.state === "initial") this.output.write(import_sisteransi.cursor.hide);
|
|
475
|
+
else {
|
|
476
|
+
const t = BD(this._prevFrame, u2);
|
|
477
|
+
if (this.restoreCursor(), t && t?.length === 1) {
|
|
478
|
+
const F2 = t[0];
|
|
479
|
+
this.output.write(import_sisteransi.cursor.move(0, F2)), this.output.write(import_sisteransi.erase.lines(1));
|
|
480
|
+
const s = u2.split(`
|
|
481
|
+
`);
|
|
482
|
+
this.output.write(s[F2]), this._prevFrame = u2, this.output.write(import_sisteransi.cursor.move(0, s.length - F2 - 1));
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (t && t?.length > 1) {
|
|
486
|
+
const F2 = t[0];
|
|
487
|
+
this.output.write(import_sisteransi.cursor.move(0, F2)), this.output.write(import_sisteransi.erase.down());
|
|
488
|
+
const s = u2.split(`
|
|
489
|
+
`).slice(F2);
|
|
490
|
+
this.output.write(s.join(`
|
|
491
|
+
`)), this._prevFrame = u2;
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
this.output.write(import_sisteransi.erase.down());
|
|
495
|
+
}
|
|
496
|
+
this.output.write(u2), this.state === "initial" && (this.state = "active"), this._prevFrame = u2;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
var dD = class extends x {
|
|
501
|
+
get cursor() {
|
|
502
|
+
return this.value ? 0 : 1;
|
|
503
|
+
}
|
|
504
|
+
get _value() {
|
|
505
|
+
return this.cursor === 0;
|
|
506
|
+
}
|
|
507
|
+
constructor(u2) {
|
|
508
|
+
super(u2, false), this.value = !!u2.initialValue, this.on("value", () => {
|
|
509
|
+
this.value = this._value;
|
|
510
|
+
}), this.on("confirm", (t) => {
|
|
511
|
+
this.output.write(import_sisteransi.cursor.move(0, -1)), this.value = t, this.state = "submit", this.close();
|
|
512
|
+
}), this.on("cursor", () => {
|
|
513
|
+
this.value = !this.value;
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
var A;
|
|
518
|
+
A = /* @__PURE__ */ new WeakMap();
|
|
519
|
+
var kD = Object.defineProperty;
|
|
520
|
+
var $D = (e2, u2, t) => u2 in e2 ? kD(e2, u2, { enumerable: true, configurable: true, writable: true, value: t }) : e2[u2] = t;
|
|
521
|
+
var H = (e2, u2, t) => ($D(e2, typeof u2 != "symbol" ? u2 + "" : u2, t), t);
|
|
522
|
+
var SD = class extends x {
|
|
523
|
+
constructor(u2) {
|
|
524
|
+
super(u2, false), H(this, "options"), H(this, "cursor", 0), this.options = u2.options, this.value = [...u2.initialValues ?? []], this.cursor = Math.max(this.options.findIndex(({ value: t }) => t === u2.cursorAt), 0), this.on("key", (t) => {
|
|
525
|
+
t === "a" && this.toggleAll();
|
|
526
|
+
}), this.on("cursor", (t) => {
|
|
527
|
+
switch (t) {
|
|
528
|
+
case "left":
|
|
529
|
+
case "up":
|
|
530
|
+
this.cursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
|
|
531
|
+
break;
|
|
532
|
+
case "down":
|
|
533
|
+
case "right":
|
|
534
|
+
this.cursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
|
|
535
|
+
break;
|
|
536
|
+
case "space":
|
|
537
|
+
this.toggleValue();
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
get _value() {
|
|
543
|
+
return this.options[this.cursor].value;
|
|
544
|
+
}
|
|
545
|
+
toggleAll() {
|
|
546
|
+
const u2 = this.value.length === this.options.length;
|
|
547
|
+
this.value = u2 ? [] : this.options.map((t) => t.value);
|
|
548
|
+
}
|
|
549
|
+
toggleValue() {
|
|
550
|
+
const u2 = this.value.includes(this._value);
|
|
551
|
+
this.value = u2 ? this.value.filter((t) => t !== this._value) : [...this.value, this._value];
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
var OD = Object.defineProperty;
|
|
555
|
+
var PD = (e2, u2, t) => u2 in e2 ? OD(e2, u2, { enumerable: true, configurable: true, writable: true, value: t }) : e2[u2] = t;
|
|
556
|
+
var J = (e2, u2, t) => (PD(e2, typeof u2 != "symbol" ? u2 + "" : u2, t), t);
|
|
557
|
+
var LD = class extends x {
|
|
558
|
+
constructor(u2) {
|
|
559
|
+
super(u2, false), J(this, "options"), J(this, "cursor", 0), this.options = u2.options, this.cursor = this.options.findIndex(({ value: t }) => t === u2.initialValue), this.cursor === -1 && (this.cursor = 0), this.changeValue(), this.on("cursor", (t) => {
|
|
560
|
+
switch (t) {
|
|
561
|
+
case "left":
|
|
562
|
+
case "up":
|
|
563
|
+
this.cursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
|
|
564
|
+
break;
|
|
565
|
+
case "down":
|
|
566
|
+
case "right":
|
|
567
|
+
this.cursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
this.changeValue();
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
get _value() {
|
|
574
|
+
return this.options[this.cursor];
|
|
575
|
+
}
|
|
576
|
+
changeValue() {
|
|
577
|
+
this.value = this._value.value;
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
var RD = class extends x {
|
|
581
|
+
get valueWithCursor() {
|
|
582
|
+
if (this.state === "submit") return this.value;
|
|
583
|
+
if (this.cursor >= this.value.length) return `${this.value}\u2588`;
|
|
584
|
+
const u2 = this.value.slice(0, this.cursor), [t, ...F2] = this.value.slice(this.cursor);
|
|
585
|
+
return `${u2}${import_picocolors.default.inverse(t)}${F2.join("")}`;
|
|
586
|
+
}
|
|
587
|
+
get cursor() {
|
|
588
|
+
return this._cursor;
|
|
589
|
+
}
|
|
590
|
+
constructor(u2) {
|
|
591
|
+
super(u2), this.on("finalize", () => {
|
|
592
|
+
this.value || (this.value = u2.defaultValue);
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
// node_modules/@clack/prompts/dist/index.mjs
|
|
598
|
+
var import_picocolors2 = __toESM(require_picocolors(), 1);
|
|
599
|
+
var import_sisteransi2 = __toESM(require_src(), 1);
|
|
600
|
+
import y2 from "node:process";
|
|
601
|
+
function ce() {
|
|
602
|
+
return y2.platform !== "win32" ? y2.env.TERM !== "linux" : !!y2.env.CI || !!y2.env.WT_SESSION || !!y2.env.TERMINUS_SUBLIME || y2.env.ConEmuTask === "{cmd::Cmder}" || y2.env.TERM_PROGRAM === "Terminus-Sublime" || y2.env.TERM_PROGRAM === "vscode" || y2.env.TERM === "xterm-256color" || y2.env.TERM === "alacritty" || y2.env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
|
|
603
|
+
}
|
|
604
|
+
var V2 = ce();
|
|
605
|
+
var u = (t, n) => V2 ? t : n;
|
|
606
|
+
var le = u("\u25C6", "*");
|
|
607
|
+
var L2 = u("\u25A0", "x");
|
|
608
|
+
var W2 = u("\u25B2", "x");
|
|
609
|
+
var C = u("\u25C7", "o");
|
|
610
|
+
var ue = u("\u250C", "T");
|
|
611
|
+
var o = u("\u2502", "|");
|
|
612
|
+
var d2 = u("\u2514", "\u2014");
|
|
613
|
+
var k2 = u("\u25CF", ">");
|
|
614
|
+
var P2 = u("\u25CB", " ");
|
|
615
|
+
var A2 = u("\u25FB", "[\u2022]");
|
|
616
|
+
var T = u("\u25FC", "[+]");
|
|
617
|
+
var F = u("\u25FB", "[ ]");
|
|
618
|
+
var $e = u("\u25AA", "\u2022");
|
|
619
|
+
var _2 = u("\u2500", "-");
|
|
620
|
+
var me = u("\u256E", "+");
|
|
621
|
+
var de = u("\u251C", "+");
|
|
622
|
+
var pe = u("\u256F", "+");
|
|
623
|
+
var q = u("\u25CF", "\u2022");
|
|
624
|
+
var D = u("\u25C6", "*");
|
|
625
|
+
var U = u("\u25B2", "!");
|
|
626
|
+
var K2 = u("\u25A0", "x");
|
|
627
|
+
var b2 = (t) => {
|
|
628
|
+
switch (t) {
|
|
629
|
+
case "initial":
|
|
630
|
+
case "active":
|
|
631
|
+
return import_picocolors2.default.cyan(le);
|
|
632
|
+
case "cancel":
|
|
633
|
+
return import_picocolors2.default.red(L2);
|
|
634
|
+
case "error":
|
|
635
|
+
return import_picocolors2.default.yellow(W2);
|
|
636
|
+
case "submit":
|
|
637
|
+
return import_picocolors2.default.green(C);
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
var G2 = (t) => {
|
|
641
|
+
const { cursor: n, options: r2, style: i } = t, s = t.maxItems ?? Number.POSITIVE_INFINITY, c = Math.max(process.stdout.rows - 4, 0), a = Math.min(c, Math.max(s, 5));
|
|
642
|
+
let l2 = 0;
|
|
643
|
+
n >= l2 + a - 3 ? l2 = Math.max(Math.min(n - a + 3, r2.length - a), 0) : n < l2 + 2 && (l2 = Math.max(n - 2, 0));
|
|
644
|
+
const $2 = a < r2.length && l2 > 0, g2 = a < r2.length && l2 + a < r2.length;
|
|
645
|
+
return r2.slice(l2, l2 + a).map((p2, v2, f) => {
|
|
646
|
+
const j2 = v2 === 0 && $2, E = v2 === f.length - 1 && g2;
|
|
647
|
+
return j2 || E ? import_picocolors2.default.dim("...") : i(p2, v2 + l2 === n);
|
|
648
|
+
});
|
|
649
|
+
};
|
|
650
|
+
var he = (t) => new RD({ validate: t.validate, placeholder: t.placeholder, defaultValue: t.defaultValue, initialValue: t.initialValue, render() {
|
|
651
|
+
const n = `${import_picocolors2.default.gray(o)}
|
|
652
|
+
${b2(this.state)} ${t.message}
|
|
653
|
+
`, r2 = t.placeholder ? import_picocolors2.default.inverse(t.placeholder[0]) + import_picocolors2.default.dim(t.placeholder.slice(1)) : import_picocolors2.default.inverse(import_picocolors2.default.hidden("_")), i = this.value ? this.valueWithCursor : r2;
|
|
654
|
+
switch (this.state) {
|
|
655
|
+
case "error":
|
|
656
|
+
return `${n.trim()}
|
|
657
|
+
${import_picocolors2.default.yellow(o)} ${i}
|
|
658
|
+
${import_picocolors2.default.yellow(d2)} ${import_picocolors2.default.yellow(this.error)}
|
|
659
|
+
`;
|
|
660
|
+
case "submit":
|
|
661
|
+
return `${n}${import_picocolors2.default.gray(o)} ${import_picocolors2.default.dim(this.value || t.placeholder)}`;
|
|
662
|
+
case "cancel":
|
|
663
|
+
return `${n}${import_picocolors2.default.gray(o)} ${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(this.value ?? ""))}${this.value?.trim() ? `
|
|
664
|
+
${import_picocolors2.default.gray(o)}` : ""}`;
|
|
665
|
+
default:
|
|
666
|
+
return `${n}${import_picocolors2.default.cyan(o)} ${i}
|
|
667
|
+
${import_picocolors2.default.cyan(d2)}
|
|
668
|
+
`;
|
|
669
|
+
}
|
|
670
|
+
} }).prompt();
|
|
671
|
+
var ye = (t) => {
|
|
672
|
+
const n = t.active ?? "Yes", r2 = t.inactive ?? "No";
|
|
673
|
+
return new dD({ active: n, inactive: r2, initialValue: t.initialValue ?? true, render() {
|
|
674
|
+
const i = `${import_picocolors2.default.gray(o)}
|
|
675
|
+
${b2(this.state)} ${t.message}
|
|
676
|
+
`, s = this.value ? n : r2;
|
|
677
|
+
switch (this.state) {
|
|
678
|
+
case "submit":
|
|
679
|
+
return `${i}${import_picocolors2.default.gray(o)} ${import_picocolors2.default.dim(s)}`;
|
|
680
|
+
case "cancel":
|
|
681
|
+
return `${i}${import_picocolors2.default.gray(o)} ${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(s))}
|
|
682
|
+
${import_picocolors2.default.gray(o)}`;
|
|
683
|
+
default:
|
|
684
|
+
return `${i}${import_picocolors2.default.cyan(o)} ${this.value ? `${import_picocolors2.default.green(k2)} ${n}` : `${import_picocolors2.default.dim(P2)} ${import_picocolors2.default.dim(n)}`} ${import_picocolors2.default.dim("/")} ${this.value ? `${import_picocolors2.default.dim(P2)} ${import_picocolors2.default.dim(r2)}` : `${import_picocolors2.default.green(k2)} ${r2}`}
|
|
685
|
+
${import_picocolors2.default.cyan(d2)}
|
|
686
|
+
`;
|
|
687
|
+
}
|
|
688
|
+
} }).prompt();
|
|
689
|
+
};
|
|
690
|
+
var ve = (t) => {
|
|
691
|
+
const n = (r2, i) => {
|
|
692
|
+
const s = r2.label ?? String(r2.value);
|
|
693
|
+
switch (i) {
|
|
694
|
+
case "selected":
|
|
695
|
+
return `${import_picocolors2.default.dim(s)}`;
|
|
696
|
+
case "active":
|
|
697
|
+
return `${import_picocolors2.default.green(k2)} ${s} ${r2.hint ? import_picocolors2.default.dim(`(${r2.hint})`) : ""}`;
|
|
698
|
+
case "cancelled":
|
|
699
|
+
return `${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(s))}`;
|
|
700
|
+
default:
|
|
701
|
+
return `${import_picocolors2.default.dim(P2)} ${import_picocolors2.default.dim(s)}`;
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
return new LD({ options: t.options, initialValue: t.initialValue, render() {
|
|
705
|
+
const r2 = `${import_picocolors2.default.gray(o)}
|
|
706
|
+
${b2(this.state)} ${t.message}
|
|
707
|
+
`;
|
|
708
|
+
switch (this.state) {
|
|
709
|
+
case "submit":
|
|
710
|
+
return `${r2}${import_picocolors2.default.gray(o)} ${n(this.options[this.cursor], "selected")}`;
|
|
711
|
+
case "cancel":
|
|
712
|
+
return `${r2}${import_picocolors2.default.gray(o)} ${n(this.options[this.cursor], "cancelled")}
|
|
713
|
+
${import_picocolors2.default.gray(o)}`;
|
|
714
|
+
default:
|
|
715
|
+
return `${r2}${import_picocolors2.default.cyan(o)} ${G2({ cursor: this.cursor, options: this.options, maxItems: t.maxItems, style: (i, s) => n(i, s ? "active" : "inactive") }).join(`
|
|
716
|
+
${import_picocolors2.default.cyan(o)} `)}
|
|
717
|
+
${import_picocolors2.default.cyan(d2)}
|
|
718
|
+
`;
|
|
719
|
+
}
|
|
720
|
+
} }).prompt();
|
|
721
|
+
};
|
|
722
|
+
var fe = (t) => {
|
|
723
|
+
const n = (r2, i) => {
|
|
724
|
+
const s = r2.label ?? String(r2.value);
|
|
725
|
+
return i === "active" ? `${import_picocolors2.default.cyan(A2)} ${s} ${r2.hint ? import_picocolors2.default.dim(`(${r2.hint})`) : ""}` : i === "selected" ? `${import_picocolors2.default.green(T)} ${import_picocolors2.default.dim(s)} ${r2.hint ? import_picocolors2.default.dim(`(${r2.hint})`) : ""}` : i === "cancelled" ? `${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(s))}` : i === "active-selected" ? `${import_picocolors2.default.green(T)} ${s} ${r2.hint ? import_picocolors2.default.dim(`(${r2.hint})`) : ""}` : i === "submitted" ? `${import_picocolors2.default.dim(s)}` : `${import_picocolors2.default.dim(F)} ${import_picocolors2.default.dim(s)}`;
|
|
726
|
+
};
|
|
727
|
+
return new SD({ options: t.options, initialValues: t.initialValues, required: t.required ?? true, cursorAt: t.cursorAt, validate(r2) {
|
|
728
|
+
if (this.required && r2.length === 0) return `Please select at least one option.
|
|
729
|
+
${import_picocolors2.default.reset(import_picocolors2.default.dim(`Press ${import_picocolors2.default.gray(import_picocolors2.default.bgWhite(import_picocolors2.default.inverse(" space ")))} to select, ${import_picocolors2.default.gray(import_picocolors2.default.bgWhite(import_picocolors2.default.inverse(" enter ")))} to submit`))}`;
|
|
730
|
+
}, render() {
|
|
731
|
+
const r2 = `${import_picocolors2.default.gray(o)}
|
|
732
|
+
${b2(this.state)} ${t.message}
|
|
733
|
+
`, i = (s, c) => {
|
|
734
|
+
const a = this.value.includes(s.value);
|
|
735
|
+
return c && a ? n(s, "active-selected") : a ? n(s, "selected") : n(s, c ? "active" : "inactive");
|
|
736
|
+
};
|
|
737
|
+
switch (this.state) {
|
|
738
|
+
case "submit":
|
|
739
|
+
return `${r2}${import_picocolors2.default.gray(o)} ${this.options.filter(({ value: s }) => this.value.includes(s)).map((s) => n(s, "submitted")).join(import_picocolors2.default.dim(", ")) || import_picocolors2.default.dim("none")}`;
|
|
740
|
+
case "cancel": {
|
|
741
|
+
const s = this.options.filter(({ value: c }) => this.value.includes(c)).map((c) => n(c, "cancelled")).join(import_picocolors2.default.dim(", "));
|
|
742
|
+
return `${r2}${import_picocolors2.default.gray(o)} ${s.trim() ? `${s}
|
|
743
|
+
${import_picocolors2.default.gray(o)}` : ""}`;
|
|
744
|
+
}
|
|
745
|
+
case "error": {
|
|
746
|
+
const s = this.error.split(`
|
|
747
|
+
`).map((c, a) => a === 0 ? `${import_picocolors2.default.yellow(d2)} ${import_picocolors2.default.yellow(c)}` : ` ${c}`).join(`
|
|
748
|
+
`);
|
|
749
|
+
return `${r2 + import_picocolors2.default.yellow(o)} ${G2({ options: this.options, cursor: this.cursor, maxItems: t.maxItems, style: i }).join(`
|
|
750
|
+
${import_picocolors2.default.yellow(o)} `)}
|
|
751
|
+
${s}
|
|
752
|
+
`;
|
|
753
|
+
}
|
|
754
|
+
default:
|
|
755
|
+
return `${r2}${import_picocolors2.default.cyan(o)} ${G2({ options: this.options, cursor: this.cursor, maxItems: t.maxItems, style: i }).join(`
|
|
756
|
+
${import_picocolors2.default.cyan(o)} `)}
|
|
757
|
+
${import_picocolors2.default.cyan(d2)}
|
|
758
|
+
`;
|
|
759
|
+
}
|
|
760
|
+
} }).prompt();
|
|
761
|
+
};
|
|
762
|
+
var Me = (t = "", n = "") => {
|
|
763
|
+
const r2 = `
|
|
764
|
+
${t}
|
|
765
|
+
`.split(`
|
|
766
|
+
`), i = S2(n).length, s = Math.max(r2.reduce((a, l2) => {
|
|
767
|
+
const $2 = S2(l2);
|
|
768
|
+
return $2.length > a ? $2.length : a;
|
|
769
|
+
}, 0), i) + 2, c = r2.map((a) => `${import_picocolors2.default.gray(o)} ${import_picocolors2.default.dim(a)}${" ".repeat(s - S2(a).length)}${import_picocolors2.default.gray(o)}`).join(`
|
|
770
|
+
`);
|
|
771
|
+
process.stdout.write(`${import_picocolors2.default.gray(o)}
|
|
772
|
+
${import_picocolors2.default.green(C)} ${import_picocolors2.default.reset(n)} ${import_picocolors2.default.gray(_2.repeat(Math.max(s - i - 1, 1)) + me)}
|
|
773
|
+
${c}
|
|
774
|
+
${import_picocolors2.default.gray(de + _2.repeat(s + 2) + pe)}
|
|
775
|
+
`);
|
|
776
|
+
};
|
|
777
|
+
var xe = (t = "") => {
|
|
778
|
+
process.stdout.write(`${import_picocolors2.default.gray(d2)} ${import_picocolors2.default.red(t)}
|
|
779
|
+
|
|
780
|
+
`);
|
|
781
|
+
};
|
|
782
|
+
var Ie = (t = "") => {
|
|
783
|
+
process.stdout.write(`${import_picocolors2.default.gray(ue)} ${t}
|
|
784
|
+
`);
|
|
785
|
+
};
|
|
786
|
+
var Se = (t = "") => {
|
|
787
|
+
process.stdout.write(`${import_picocolors2.default.gray(o)}
|
|
788
|
+
${import_picocolors2.default.gray(d2)} ${t}
|
|
789
|
+
|
|
790
|
+
`);
|
|
791
|
+
};
|
|
792
|
+
var J2 = `${import_picocolors2.default.gray(o)} `;
|
|
793
|
+
var Y2 = ({ indicator: t = "dots" } = {}) => {
|
|
794
|
+
const n = V2 ? ["\u25D2", "\u25D0", "\u25D3", "\u25D1"] : ["\u2022", "o", "O", "0"], r2 = V2 ? 80 : 120, i = process.env.CI === "true";
|
|
795
|
+
let s, c, a = false, l2 = "", $2, g2 = performance.now();
|
|
796
|
+
const p2 = (m2) => {
|
|
797
|
+
const h2 = m2 > 1 ? "Something went wrong" : "Canceled";
|
|
798
|
+
a && N2(h2, m2);
|
|
799
|
+
}, v2 = () => p2(2), f = () => p2(1), j2 = () => {
|
|
800
|
+
process.on("uncaughtExceptionMonitor", v2), process.on("unhandledRejection", v2), process.on("SIGINT", f), process.on("SIGTERM", f), process.on("exit", p2);
|
|
801
|
+
}, E = () => {
|
|
802
|
+
process.removeListener("uncaughtExceptionMonitor", v2), process.removeListener("unhandledRejection", v2), process.removeListener("SIGINT", f), process.removeListener("SIGTERM", f), process.removeListener("exit", p2);
|
|
803
|
+
}, B2 = () => {
|
|
804
|
+
if ($2 === void 0) return;
|
|
805
|
+
i && process.stdout.write(`
|
|
806
|
+
`);
|
|
807
|
+
const m2 = $2.split(`
|
|
808
|
+
`);
|
|
809
|
+
process.stdout.write(import_sisteransi2.cursor.move(-999, m2.length - 1)), process.stdout.write(import_sisteransi2.erase.down(m2.length));
|
|
810
|
+
}, R2 = (m2) => m2.replace(/\.+$/, ""), O2 = (m2) => {
|
|
811
|
+
const h2 = (performance.now() - m2) / 1e3, w2 = Math.floor(h2 / 60), I2 = Math.floor(h2 % 60);
|
|
812
|
+
return w2 > 0 ? `[${w2}m ${I2}s]` : `[${I2}s]`;
|
|
813
|
+
}, H2 = (m2 = "") => {
|
|
814
|
+
a = true, s = fD(), l2 = R2(m2), g2 = performance.now(), process.stdout.write(`${import_picocolors2.default.gray(o)}
|
|
815
|
+
`);
|
|
816
|
+
let h2 = 0, w2 = 0;
|
|
817
|
+
j2(), c = setInterval(() => {
|
|
818
|
+
if (i && l2 === $2) return;
|
|
819
|
+
B2(), $2 = l2;
|
|
820
|
+
const I2 = import_picocolors2.default.magenta(n[h2]);
|
|
821
|
+
if (i) process.stdout.write(`${I2} ${l2}...`);
|
|
822
|
+
else if (t === "timer") process.stdout.write(`${I2} ${l2} ${O2(g2)}`);
|
|
823
|
+
else {
|
|
824
|
+
const z2 = ".".repeat(Math.floor(w2)).slice(0, 3);
|
|
825
|
+
process.stdout.write(`${I2} ${l2}${z2}`);
|
|
826
|
+
}
|
|
827
|
+
h2 = h2 + 1 < n.length ? h2 + 1 : 0, w2 = w2 < n.length ? w2 + 0.125 : 0;
|
|
828
|
+
}, r2);
|
|
829
|
+
}, N2 = (m2 = "", h2 = 0) => {
|
|
830
|
+
a = false, clearInterval(c), B2();
|
|
831
|
+
const w2 = h2 === 0 ? import_picocolors2.default.green(C) : h2 === 1 ? import_picocolors2.default.red(L2) : import_picocolors2.default.red(W2);
|
|
832
|
+
l2 = R2(m2 ?? l2), t === "timer" ? process.stdout.write(`${w2} ${l2} ${O2(g2)}
|
|
833
|
+
`) : process.stdout.write(`${w2} ${l2}
|
|
834
|
+
`), E(), s();
|
|
835
|
+
};
|
|
836
|
+
return { start: H2, stop: N2, message: (m2 = "") => {
|
|
837
|
+
l2 = R2(m2 ?? l2);
|
|
838
|
+
} };
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
// src/index.ts
|
|
842
|
+
var import_picocolors3 = __toESM(require_picocolors(), 1);
|
|
843
|
+
|
|
844
|
+
// ../ui/src/agent-tooling/integrations/openai.ts
|
|
845
|
+
var openai = {
|
|
846
|
+
id: "openai",
|
|
847
|
+
title: "OpenAI",
|
|
848
|
+
category: "provider",
|
|
849
|
+
language: "ts",
|
|
850
|
+
streamFormat: "openai-sse",
|
|
851
|
+
envVars: ["OPENAI_API_KEY"],
|
|
852
|
+
// No per-framework templates: the handler below is web-standard, so the
|
|
853
|
+
// scaffolder wraps it in whatever the target framework routes with.
|
|
854
|
+
routeTemplates: {},
|
|
855
|
+
webRoute: `async function chatHandler(request: Request): Promise<Response> {
|
|
856
|
+
// Both model and tools come from the browser. \`tools\` is undefined unless the
|
|
857
|
+
// front end declared any, and JSON.stringify drops it, so the same handler
|
|
858
|
+
// serves a tool archetype and a plain chat.
|
|
859
|
+
const { model, messages, tools } = await readChatRequest(request);
|
|
860
|
+
|
|
861
|
+
const upstream = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
862
|
+
method: 'POST',
|
|
863
|
+
headers: {
|
|
864
|
+
Authorization: \`Bearer \${process.env.OPENAI_API_KEY}\`,
|
|
865
|
+
'Content-Type': 'application/json',
|
|
866
|
+
},
|
|
867
|
+
// Model ids here carry NO vendor prefix: 'gpt-4o-mini', never
|
|
868
|
+
// 'openai/gpt-4o-mini' \u2014 the prefixed form is an OpenRouter slug and this
|
|
869
|
+
// host answers it with a 404. The front end's \`model\` const is the one to
|
|
870
|
+
// edit; it is seeded from CLIENT_MODEL_IDS.openai.
|
|
871
|
+
body: JSON.stringify({ model, messages, tools, stream: true }),
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
// FORWARD THE STATUS. Returning 200 here is how a missing key turns into
|
|
875
|
+
// silence: the 401 body is JSON, it goes out labelled text/event-stream, the
|
|
876
|
+
// SSE reader finds no frame, and the turn resolves empty with nothing logged
|
|
877
|
+
// and no bubble. With the status intact readOpenAIStream throws a WireError
|
|
878
|
+
// carrying OpenAI's own message, which is the most useful thing you can be
|
|
879
|
+
// handed on a first run.
|
|
880
|
+
if (!upstream.ok) {
|
|
881
|
+
return new Response(await upstream.text(), {
|
|
882
|
+
status: upstream.status,
|
|
883
|
+
headers: {
|
|
884
|
+
'Content-Type': upstream.headers.get('content-type') ?? 'application/json',
|
|
885
|
+
},
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
if (!upstream.body) {
|
|
889
|
+
return new Response(JSON.stringify({ error: { message: 'OpenAI returned no body to stream.' } }), {
|
|
890
|
+
status: 502,
|
|
891
|
+
headers: { 'Content-Type': 'application/json' },
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// OpenAI IS the format readOpenAIStream parses. Pass it straight through.
|
|
896
|
+
return new Response(upstream.body, {
|
|
897
|
+
status: 200,
|
|
898
|
+
headers: {
|
|
899
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
900
|
+
// no-transform stops a proxy buffering the stream into one blob.
|
|
901
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
902
|
+
Connection: 'keep-alive',
|
|
903
|
+
},
|
|
904
|
+
});
|
|
905
|
+
}`,
|
|
906
|
+
streamMapping: "OpenAI's /v1/chat/completions is the reference OpenAI-format SSE stream \u2014 this is the one integration with no re-framing at all. Pipe upstream.body straight to the browser; readOpenAIStream from @kitn.ai/ui/wire parses it, including tool calls (delta.tool_calls) and reasoning (delta.reasoning on the reasoning models). Set stream_options: { include_usage: true } in the body if you want a final usage frame; readOpenAIStream reads prompt_tokens/completion_tokens off it.",
|
|
907
|
+
runNote: "Set OPENAI_API_KEY (platform.openai.com). Model ids have NO vendor prefix: 'gpt-4o-mini', not 'openai/gpt-4o-mini' \u2014 the prefixed form is an OpenRouter slug and api.openai.com answers it with a 404. Change the pinned model in the route.",
|
|
908
|
+
docsSlug: "integrations/connect-any-model",
|
|
909
|
+
// 'model' was withheld until scaffold.ts could seed it correctly: the shared
|
|
910
|
+
// fallback is 'openai/gpt-4o-mini', an OpenRouter slug that api.openai.com
|
|
911
|
+
// 404s, and shipping an editable const with a broken default is the exact
|
|
912
|
+
// dead-const defect forwardsFromClient exists to prevent. CLIENT_MODEL_IDS now
|
|
913
|
+
// carries a per-integration entry ('gpt-4o-mini'), so the const is real and the
|
|
914
|
+
// route reads it. 'tools' is what lets an agentic scaffold fill its kai-tool
|
|
915
|
+
// panel.
|
|
916
|
+
forwardsFromClient: ["model", "tools"],
|
|
917
|
+
// The handler forwards `tools` unconverted to /v1/chat/completions — this IS
|
|
918
|
+
// the OpenAI wire, so the client sends OpenAI's own function envelope.
|
|
919
|
+
clientToolFormat: "openai",
|
|
920
|
+
// Nothing to install. The route is global `fetch` and imports no module at all.
|
|
921
|
+
deps: { npm: [], pip: [] },
|
|
922
|
+
// The route reads OPENAI_API_KEY and puts it in an `Authorization: Bearer`
|
|
923
|
+
// header. A static bundle carrying that is a published key.
|
|
924
|
+
keyExposure: "needs-proxy",
|
|
925
|
+
// A remote HTTPS endpoint and a key. Nothing to install, nothing to start.
|
|
926
|
+
outOfBand: "none"
|
|
927
|
+
};
|
|
928
|
+
var openai_default = openai;
|
|
929
|
+
|
|
930
|
+
// ../ui/src/agent-tooling/integrations/anthropic.ts
|
|
931
|
+
var anthropic = {
|
|
932
|
+
id: "anthropic",
|
|
933
|
+
title: "Anthropic",
|
|
934
|
+
category: "provider",
|
|
935
|
+
language: "ts",
|
|
936
|
+
// 'native': the Messages API streams its OWN SSE dialect (message_start,
|
|
937
|
+
// content_block_delta, message_stop), not OpenAI's. The route re-frames it —
|
|
938
|
+
// see `reframeToOpenAISse` and the note in streamMapping for why.
|
|
939
|
+
streamFormat: "native",
|
|
940
|
+
envVars: ["ANTHROPIC_API_KEY"],
|
|
941
|
+
// No per-framework templates: the handler below is web-standard, so the
|
|
942
|
+
// scaffolder wraps it in whatever the target framework routes with.
|
|
943
|
+
routeTemplates: {},
|
|
944
|
+
webRoute: `/** Where an image or document's bytes come from. Anthropic takes both forms
|
|
945
|
+
* for both block types, which is why this route has no gap where the
|
|
946
|
+
* OpenAI-shaped one does: a chat-completions \`file\` part is base64-only. */
|
|
947
|
+
type AnthropicSource =
|
|
948
|
+
| { type: 'base64'; media_type: string; data: string }
|
|
949
|
+
| { type: 'url'; url: string };
|
|
950
|
+
|
|
951
|
+
/** One Anthropic content block. Open union on the wire; these are the ones the
|
|
952
|
+
* scaffold's own thread can produce. */
|
|
953
|
+
type AnthropicBlock =
|
|
954
|
+
| { type: 'text'; text: string }
|
|
955
|
+
| { type: 'image'; source: AnthropicSource }
|
|
956
|
+
| { type: 'document'; source: AnthropicSource }
|
|
957
|
+
| { type: 'tool_use'; id: string; name: string; input: unknown }
|
|
958
|
+
| { type: 'tool_result'; tool_use_id: string; content: string };
|
|
959
|
+
|
|
960
|
+
/** Anthropic messages carry ONLY 'user' and 'assistant'. There is no 'system'
|
|
961
|
+
* role here and no 'tool' role: a system prompt is a top-level field, and a
|
|
962
|
+
* tool RESULT rides on a user message. Both are handled in toAnthropicBody. */
|
|
963
|
+
type AnthropicMessage = { role: 'user' | 'assistant'; content: AnthropicBlock[] };
|
|
964
|
+
|
|
965
|
+
/** Anthropic calls the JSON Schema \`input_schema\`; OpenAI calls it
|
|
966
|
+
* \`parameters\`, and nests everything under \`function\`. Handing the OpenAI
|
|
967
|
+
* shape over unconverted is a 400. */
|
|
968
|
+
type AnthropicTool = { name: string; description?: string; input_schema: unknown };
|
|
969
|
+
|
|
970
|
+
/** The OpenAI tool shape the front end sends, narrowed from \`unknown[]\`. */
|
|
971
|
+
type OpenAIFunctionTool = {
|
|
972
|
+
function?: { name?: string; description?: string; parameters?: unknown };
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
/** One decoded Messages-API SSE frame. Every field is optional because this is
|
|
976
|
+
* a provider-owned union and an unrecognised frame must be skipped, not throw. */
|
|
977
|
+
type AnthropicEvent = {
|
|
978
|
+
type?: string;
|
|
979
|
+
index?: number;
|
|
980
|
+
content_block?: { type?: string; id?: string; name?: string };
|
|
981
|
+
delta?: {
|
|
982
|
+
type?: string;
|
|
983
|
+
text?: string;
|
|
984
|
+
thinking?: string;
|
|
985
|
+
partial_json?: string;
|
|
986
|
+
stop_reason?: string;
|
|
987
|
+
};
|
|
988
|
+
error?: { message?: string };
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
/** Anthropic stop reasons -> OpenAI finish_reason. 'refusal' has no OpenAI
|
|
992
|
+
* equivalent and is reported in band instead (see the message_delta case). */
|
|
993
|
+
const FINISH_REASONS: Record<string, string> = {
|
|
994
|
+
end_turn: 'stop',
|
|
995
|
+
stop_sequence: 'stop',
|
|
996
|
+
max_tokens: 'length',
|
|
997
|
+
tool_use: 'tool_calls',
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
function toAnthropicTools(tools: unknown[] | undefined): AnthropicTool[] | undefined {
|
|
1001
|
+
if (!tools || tools.length === 0) return undefined;
|
|
1002
|
+
return tools.map((raw) => {
|
|
1003
|
+
const fn = (raw as OpenAIFunctionTool).function ?? {};
|
|
1004
|
+
return {
|
|
1005
|
+
name: fn.name ?? '',
|
|
1006
|
+
description: fn.description,
|
|
1007
|
+
input_schema: fn.parameters ?? { type: 'object', properties: {} },
|
|
1008
|
+
};
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* The OpenAI-shaped thread the front end posts -> an Anthropic Messages body.
|
|
1014
|
+
*
|
|
1015
|
+
* \`system\` comes back SEPARATELY because it is a top-level request field here.
|
|
1016
|
+
* A \`{ role: 'system' }\` entry left in \`messages\` is a 400, and it is the most
|
|
1017
|
+
* common thing to get wrong when porting a route across.
|
|
1018
|
+
*/
|
|
1019
|
+
function toAnthropicBody(messages: ChatRequestBody['messages']): {
|
|
1020
|
+
system?: string;
|
|
1021
|
+
messages: AnthropicMessage[];
|
|
1022
|
+
} {
|
|
1023
|
+
let system: string | undefined;
|
|
1024
|
+
const out: AnthropicMessage[] = [];
|
|
1025
|
+
|
|
1026
|
+
// Append to the trailing user message when there is one. A tool result and the
|
|
1027
|
+
// user's next turn are one turn on this wire, and several Anthropic-compatible
|
|
1028
|
+
// proxies enforce strict alternation.
|
|
1029
|
+
const pushUser = (content: AnthropicBlock[]): void => {
|
|
1030
|
+
const last = out[out.length - 1];
|
|
1031
|
+
if (last?.role === 'user') last.content.push(...content);
|
|
1032
|
+
else out.push({ role: 'user', content });
|
|
1033
|
+
};
|
|
1034
|
+
|
|
1035
|
+
for (const message of messages) {
|
|
1036
|
+
switch (message.role) {
|
|
1037
|
+
case 'system': {
|
|
1038
|
+
// TOP-LEVEL, not a message. Several system turns concatenate.
|
|
1039
|
+
const text = wireText(message.content);
|
|
1040
|
+
system = system === undefined ? text : \`\${system}\\n\\n\${text}\`;
|
|
1041
|
+
break;
|
|
1042
|
+
}
|
|
1043
|
+
case 'user': {
|
|
1044
|
+
// Attachments ride HERE, as image and document blocks beside the text,
|
|
1045
|
+
// in the order the thread authored them.
|
|
1046
|
+
const blocks = wireParts(message.content).map((part): AnthropicBlock => {
|
|
1047
|
+
if (part.kind === 'text') return { type: 'text', text: part.text };
|
|
1048
|
+
const source: AnthropicSource =
|
|
1049
|
+
part.source.type === 'data'
|
|
1050
|
+
? { type: 'base64', media_type: part.mediaType, data: part.source.data }
|
|
1051
|
+
: { type: 'url', url: part.source.url };
|
|
1052
|
+
// \`image/png\` and the bare top-level \`image\` both mean an image; a URL
|
|
1053
|
+
// source reports only the segment because the wire carries no more.
|
|
1054
|
+
return part.mediaType.startsWith('image') ? { type: 'image', source } : { type: 'document', source };
|
|
1055
|
+
});
|
|
1056
|
+
if (blocks.length > 0) pushUser(blocks);
|
|
1057
|
+
break;
|
|
1058
|
+
}
|
|
1059
|
+
case 'tool': {
|
|
1060
|
+
// A tool RESULT is a block on a USER message here, not a role of its own.
|
|
1061
|
+
pushUser([
|
|
1062
|
+
{
|
|
1063
|
+
type: 'tool_result',
|
|
1064
|
+
tool_use_id: message.tool_call_id ?? '',
|
|
1065
|
+
content: wireText(message.content),
|
|
1066
|
+
},
|
|
1067
|
+
]);
|
|
1068
|
+
break;
|
|
1069
|
+
}
|
|
1070
|
+
case 'assistant': {
|
|
1071
|
+
const content: AnthropicBlock[] = [];
|
|
1072
|
+
const text = wireText(message.content);
|
|
1073
|
+
if (text) content.push({ type: 'text', text });
|
|
1074
|
+
for (const call of message.tool_calls ?? []) {
|
|
1075
|
+
content.push({
|
|
1076
|
+
type: 'tool_use',
|
|
1077
|
+
id: call.id,
|
|
1078
|
+
name: call.function.name,
|
|
1079
|
+
// \`input\` is a parsed OBJECT here. OpenAI carries the same value as a
|
|
1080
|
+
// JSON STRING in \`function.arguments\`; forwarding the string is a 400.
|
|
1081
|
+
input: call.function.arguments ? JSON.parse(call.function.arguments) : {},
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
// An assistant turn with nothing in it is not a legal message.
|
|
1085
|
+
if (content.length > 0) out.push({ role: 'assistant', content });
|
|
1086
|
+
break;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
return { system, messages: out };
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Anthropic Messages SSE -> OpenAI-format SSE.
|
|
1096
|
+
*
|
|
1097
|
+
* WHY THIS EXISTS AT ALL: the front end reads with readOpenAIStream. Handing it
|
|
1098
|
+
* Anthropic frames does not throw \u2014 it parses to NOTHING, so the turn ends empty
|
|
1099
|
+
* with no error anywhere, which is the worst failure this kit has. Re-framing
|
|
1100
|
+
* here keeps the browser half of an anthropic scaffold identical to every other
|
|
1101
|
+
* integration's, the same way the cloudflare Worker route re-frames env.AI.
|
|
1102
|
+
*
|
|
1103
|
+
* The alternative, if you would rather keep the native frames: return
|
|
1104
|
+
* \`upstream.body\` untouched and read it with readAnthropicStream from
|
|
1105
|
+
* '@kitn.ai/ui/wire'. That is also the ONLY way to round-trip thinking blocks
|
|
1106
|
+
* verbatim (toAnthropicMessages echoes \`part.raw\` \u2014 a rebuilt block is a hard
|
|
1107
|
+
* 400), so it is the path to take the moment you enable thinking below.
|
|
1108
|
+
*/
|
|
1109
|
+
function reframeToOpenAISse(body: ReadableStream<Uint8Array>): ReadableStream<Uint8Array> {
|
|
1110
|
+
const encoder = new TextEncoder();
|
|
1111
|
+
const decoder = new TextDecoder();
|
|
1112
|
+
// THE INDEX TRAP. Anthropic's \`index\` counts CONTENT BLOCKS; OpenAI's
|
|
1113
|
+
// \`tool_calls[].index\` counts TOOL CALLS. A thinking or text block ahead of a
|
|
1114
|
+
// tool call shifts the block index, but the call is still tool call 0 \u2014 so the
|
|
1115
|
+
// block index is translated through this map rather than passed through.
|
|
1116
|
+
const toolIndexOfBlock = new Map<number, number>();
|
|
1117
|
+
let toolCount = 0;
|
|
1118
|
+
|
|
1119
|
+
return new ReadableStream<Uint8Array>({
|
|
1120
|
+
async start(controller) {
|
|
1121
|
+
const send = (chunk: unknown): void => {
|
|
1122
|
+
controller.enqueue(encoder.encode(\`data: \${JSON.stringify(chunk)}\\n\\n\`));
|
|
1123
|
+
};
|
|
1124
|
+
const reader = body.getReader();
|
|
1125
|
+
let buffer = '';
|
|
1126
|
+
try {
|
|
1127
|
+
for (;;) {
|
|
1128
|
+
const { value, done } = await reader.read();
|
|
1129
|
+
if (done) break;
|
|
1130
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1131
|
+
// Split on \\n rather than readline: readline also breaks on the
|
|
1132
|
+
// Unicode separators U+2028/U+2029, which appear inside model output.
|
|
1133
|
+
const lines = buffer.split('\\n');
|
|
1134
|
+
buffer = lines.pop() ?? ''; // hold the partial line for the next chunk
|
|
1135
|
+
for (const line of lines) {
|
|
1136
|
+
const trimmed = line.trim();
|
|
1137
|
+
// \`event:\` lines are redundant \u2014 every frame's data carries \`type\`.
|
|
1138
|
+
if (!trimmed.startsWith('data:')) continue;
|
|
1139
|
+
let event: AnthropicEvent;
|
|
1140
|
+
try {
|
|
1141
|
+
event = JSON.parse(trimmed.slice(5)) as AnthropicEvent;
|
|
1142
|
+
} catch {
|
|
1143
|
+
continue; // skip a malformed frame rather than kill the turn
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
if (event.type === 'content_block_start') {
|
|
1147
|
+
const block = event.content_block;
|
|
1148
|
+
if (block?.type !== 'tool_use') continue;
|
|
1149
|
+
const index = toolCount;
|
|
1150
|
+
toolCount += 1;
|
|
1151
|
+
toolIndexOfBlock.set(event.index ?? 0, index);
|
|
1152
|
+
send({
|
|
1153
|
+
choices: [{
|
|
1154
|
+
delta: {
|
|
1155
|
+
tool_calls: [{
|
|
1156
|
+
index,
|
|
1157
|
+
id: block.id,
|
|
1158
|
+
type: 'function',
|
|
1159
|
+
function: { name: block.name, arguments: '' },
|
|
1160
|
+
}],
|
|
1161
|
+
},
|
|
1162
|
+
}],
|
|
1163
|
+
});
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
if (event.type === 'content_block_delta') {
|
|
1168
|
+
const delta = event.delta;
|
|
1169
|
+
if (delta?.type === 'text_delta') {
|
|
1170
|
+
send({ choices: [{ delta: { content: delta.text } }] });
|
|
1171
|
+
} else if (delta?.type === 'thinking_delta') {
|
|
1172
|
+
send({ choices: [{ delta: { reasoning: delta.thinking } }] });
|
|
1173
|
+
} else if (delta?.type === 'input_json_delta') {
|
|
1174
|
+
const index = toolIndexOfBlock.get(event.index ?? 0);
|
|
1175
|
+
// No entry means these fragments belong to a block that is not a
|
|
1176
|
+
// tool_use. Dropping them beats inventing a tool call.
|
|
1177
|
+
if (index !== undefined) {
|
|
1178
|
+
send({
|
|
1179
|
+
choices: [{
|
|
1180
|
+
delta: {
|
|
1181
|
+
tool_calls: [{ index, function: { arguments: delta.partial_json } }],
|
|
1182
|
+
},
|
|
1183
|
+
}],
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
if (event.type === 'message_delta') {
|
|
1191
|
+
const reason = event.delta?.stop_reason;
|
|
1192
|
+
if (!reason) continue;
|
|
1193
|
+
if (reason === 'refusal') {
|
|
1194
|
+
// A safety classifier declined. HTTP 200, no content, so without
|
|
1195
|
+
// this the turn just ends blank. Report it IN BAND:
|
|
1196
|
+
// readOpenAIStream lands it on turn.error.
|
|
1197
|
+
send({ error: { message: 'The model declined this request (stop_reason: refusal).' } });
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
send({ choices: [{ delta: {}, finish_reason: FINISH_REASONS[reason] ?? 'stop' }] });
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
if (event.type === 'error') {
|
|
1205
|
+
send({ error: { message: event.error?.message ?? 'Anthropic returned an error frame.' } });
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
} catch (err) {
|
|
1210
|
+
// The headers went out with the first byte, so the status is spent:
|
|
1211
|
+
// report IN BAND. readOpenAIStream lands this on turn.error and keeps
|
|
1212
|
+
// whatever already streamed.
|
|
1213
|
+
const message = err instanceof Error ? err.message : 'Anthropic stream failed';
|
|
1214
|
+
send({ error: { message } });
|
|
1215
|
+
}
|
|
1216
|
+
// Always: an unclosed stream leaves the browser waiting forever.
|
|
1217
|
+
controller.enqueue(encoder.encode('data: [DONE]\\n\\n'));
|
|
1218
|
+
controller.close();
|
|
1219
|
+
},
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
async function chatHandler(request: Request): Promise<Response> {
|
|
1224
|
+
// Both model and tools come from the browser. \`tools\` arrives in OpenAI
|
|
1225
|
+
// function form and is converted to this API's shape below.
|
|
1226
|
+
const { model, messages, tools } = await readChatRequest(request);
|
|
1227
|
+
const { system, messages: anthropicMessages } = toAnthropicBody(messages);
|
|
1228
|
+
const anthropicTools = toAnthropicTools(tools);
|
|
1229
|
+
|
|
1230
|
+
const upstream = await fetch('https://api.anthropic.com/v1/messages', {
|
|
1231
|
+
method: 'POST',
|
|
1232
|
+
headers: {
|
|
1233
|
+
'x-api-key': \`\${process.env.ANTHROPIC_API_KEY}\`,
|
|
1234
|
+
'anthropic-version': '2023-06-01',
|
|
1235
|
+
'Content-Type': 'application/json',
|
|
1236
|
+
},
|
|
1237
|
+
body: JSON.stringify({
|
|
1238
|
+
// Model ids here are Anthropic's own \u2014 'claude-opus-5', 'claude-sonnet-5',
|
|
1239
|
+
// 'claude-haiku-4-5'. An OpenAI id or an OpenRouter 'vendor/model' slug is
|
|
1240
|
+
// a 404. The front end's \`model\` const is the one to edit; it is seeded
|
|
1241
|
+
// from CLIENT_MODEL_IDS.anthropic.
|
|
1242
|
+
model,
|
|
1243
|
+
// REQUIRED. OpenAI defaults it; this API 400s without it. It caps thinking
|
|
1244
|
+
// AND response text together, so raise it if you turn thinking on.
|
|
1245
|
+
max_tokens: 4096,
|
|
1246
|
+
// TOP-LEVEL, never a { role: 'system' } entry in \`messages\`.
|
|
1247
|
+
...(system ? { system } : {}),
|
|
1248
|
+
messages: anthropicMessages,
|
|
1249
|
+
...(anthropicTools ? { tools: anthropicTools } : {}),
|
|
1250
|
+
// tool_choice, if you set one, is an OBJECT: { type: 'auto' } |
|
|
1251
|
+
// { type: 'any' } | { type: 'tool', name: 'search' }. OpenAI's bare string
|
|
1252
|
+
// ('auto') is a 400 here.
|
|
1253
|
+
// tool_choice: { type: 'auto' },
|
|
1254
|
+
//
|
|
1255
|
+
// THINKING. The rules changed with the model generation, so check yours:
|
|
1256
|
+
// \xB7 claude-opus-5 / sonnet-5 / opus-4.8 / opus-4.7 / fable-5 \u2014
|
|
1257
|
+
// \`budget_tokens\` is REMOVED and returns a 400. Use
|
|
1258
|
+
// \`thinking: { type: 'adaptive', display: 'summarized' }\` plus
|
|
1259
|
+
// \`output_config: { effort: 'low' | 'medium' | 'high' | 'xhigh' | 'max' }\`.
|
|
1260
|
+
// On claude-opus-5 thinking is already ON by default; what is NOT on is
|
|
1261
|
+
// \`display\`, which defaults to 'omitted' and streams thinking blocks
|
|
1262
|
+
// with EMPTY text. That reads exactly like "this model has no reasoning
|
|
1263
|
+
// mode" \u2014 a silent 200 with nothing in the panel \u2014 and it is the same
|
|
1264
|
+
// shape of mistake as the budget bug below. Set display explicitly.
|
|
1265
|
+
// \xB7 older models (claude-haiku-4-5, claude-sonnet-4-5) \u2014 still
|
|
1266
|
+
// \`thinking: { type: 'enabled', budget_tokens: N }\`, with N >= 1024 and
|
|
1267
|
+
// max_tokens STRICTLY GREATER than N. Deriving N as a fraction of a
|
|
1268
|
+
// small max_tokens returns HTTP 200 with no thinking and no error.
|
|
1269
|
+
// Either way, once thinking is on a tool loop must echo every thinking
|
|
1270
|
+
// block back VERBATIM, which this route cannot do: it re-frames to OpenAI
|
|
1271
|
+
// SSE and the thread is re-encoded with toOpenAIMessages each round. Switch
|
|
1272
|
+
// to readAnthropicStream + toAnthropicMessages (both from '@kitn.ai/ui/wire')
|
|
1273
|
+
// and pass upstream.body through untouched before you enable it.
|
|
1274
|
+
stream: true,
|
|
1275
|
+
}),
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
// FORWARD THE STATUS. Returning 200 here is how a missing key turns into
|
|
1279
|
+
// silence: the 401 body is JSON, it goes out labelled text/event-stream, the
|
|
1280
|
+
// SSE reader finds no frame, and the turn resolves empty with nothing logged
|
|
1281
|
+
// and no bubble. With the status intact readOpenAIStream throws a WireError
|
|
1282
|
+
// carrying Anthropic's own message \u2014 which for this API is usually the exact
|
|
1283
|
+
// field you got wrong.
|
|
1284
|
+
if (!upstream.ok) {
|
|
1285
|
+
return new Response(await upstream.text(), {
|
|
1286
|
+
status: upstream.status,
|
|
1287
|
+
headers: {
|
|
1288
|
+
'Content-Type': upstream.headers.get('content-type') ?? 'application/json',
|
|
1289
|
+
},
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
if (!upstream.body) {
|
|
1293
|
+
return new Response(JSON.stringify({ error: { message: 'Anthropic returned no body to stream.' } }), {
|
|
1294
|
+
status: 502,
|
|
1295
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
return new Response(reframeToOpenAISse(upstream.body), {
|
|
1300
|
+
status: 200,
|
|
1301
|
+
headers: {
|
|
1302
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
1303
|
+
// no-transform stops a proxy buffering the stream into one blob.
|
|
1304
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
1305
|
+
Connection: 'keep-alive',
|
|
1306
|
+
},
|
|
1307
|
+
});
|
|
1308
|
+
}`,
|
|
1309
|
+
streamMapping: "The Anthropic Messages API streams its OWN SSE dialect: message_start, content_block_start / content_block_delta / content_block_stop, message_delta, message_stop. It is NOT OpenAI-format, and feeding it to readOpenAIStream does not throw \u2014 it parses to nothing and the turn ends silently empty. The route therefore re-frames each frame to OpenAI form before it reaches the browser: text_delta -> delta.content, thinking_delta -> delta.reasoning, a tool_use content_block_start plus its input_json_delta fragments -> delta.tool_calls, message_delta.stop_reason -> finish_reason, an error frame -> an in-band {error:{message}}. readOpenAIStream from @kitn.ai/ui/wire then parses it exactly as it does every other integration. Anthropic indexes by CONTENT BLOCK while OpenAI's tool_calls[].index counts TOOL CALLS, so the route keeps a block-index -> tool-index map: a thinking block ahead of a tool call shifts the block index but not the tool index. Usage is the one thing not re-framed (input_tokens arrives on message_start and output_tokens on message_delta, so joining them needs state) \u2014 map both onto a {usage:{prompt_tokens,completion_tokens}} frame if you want token counts. To keep the native frames instead, return upstream.body untouched and read it with readAnthropicStream; that is also the only way to round-trip thinking blocks verbatim.",
|
|
1310
|
+
runNote: "Set ANTHROPIC_API_KEY (console.anthropic.com). The route pins claude-opus-5; claude-sonnet-5 and claude-haiku-4-5 are the cheaper swaps. Four things this API does NOT share with the OpenAI one, all of them 400s: system is a top-level field and not a message, max_tokens is required, tool_choice is an object ({ type: 'auto' }) and not a string, and tool schemas use input_schema rather than function.parameters. Extended thinking: on claude-opus-5 and the other current models budget_tokens is REMOVED (400) \u2014 use thinking: { type: 'adaptive', display: 'summarized' } with output_config.effort, and note that display defaults to 'omitted', which streams empty thinking blocks that look exactly like no reasoning at all. On older models (claude-haiku-4-5, claude-sonnet-4-5) budget_tokens still applies and must be >= 1024 with max_tokens strictly greater.",
|
|
1311
|
+
docsSlug: "integrations/connect-any-model",
|
|
1312
|
+
// 'model' was withheld until scaffold.ts could seed it correctly: the shared
|
|
1313
|
+
// fallback is 'openai/gpt-4o-mini', which api.anthropic.com rejects outright,
|
|
1314
|
+
// and shipping an editable const with a broken default is the dead-const defect
|
|
1315
|
+
// forwardsFromClient exists to prevent. CLIENT_MODEL_IDS now carries a real
|
|
1316
|
+
// Anthropic id, so the const is live and the route reads it.
|
|
1317
|
+
forwardsFromClient: ["model", "tools"],
|
|
1318
|
+
// 'openai', NOT 'anthropic' — and this is the counter-intuitive one, so read
|
|
1319
|
+
// `toAnthropicTools` above before changing it. This route CONVERTS the array
|
|
1320
|
+
// server-side: it reads `raw.function.name` and `raw.function.parameters` off
|
|
1321
|
+
// each entry, i.e. the OpenAI envelope, and rebuilds it as
|
|
1322
|
+
// `{ name, description, input_schema }`. Sending Anthropic's own shape from the
|
|
1323
|
+
// client would leave `.function` undefined, so every tool would go upstream
|
|
1324
|
+
// with a blank name and an empty schema. The envelope belongs to the ROUTE's
|
|
1325
|
+
// request contract, not to the provider it happens to POST to — which is
|
|
1326
|
+
// exactly why this is declared here rather than derived from `streamFormat`.
|
|
1327
|
+
clientToolFormat: "openai",
|
|
1328
|
+
// Nothing to install. The route hand-rolls the Messages API over global
|
|
1329
|
+
// `fetch` and its own re-framer, so it imports no module — not even
|
|
1330
|
+
// @anthropic-ai/sdk.
|
|
1331
|
+
deps: { npm: [], pip: [] },
|
|
1332
|
+
// The route reads ANTHROPIC_API_KEY into an `x-api-key` header. Note the
|
|
1333
|
+
// header is NOT `Authorization`, which is why the schema's detector matches
|
|
1334
|
+
// both spellings.
|
|
1335
|
+
keyExposure: "needs-proxy",
|
|
1336
|
+
// A remote HTTPS endpoint and a key. Nothing to install, nothing to start.
|
|
1337
|
+
outOfBand: "none"
|
|
1338
|
+
};
|
|
1339
|
+
var anthropic_default = anthropic;
|
|
1340
|
+
|
|
1341
|
+
// ../ui/src/agent-tooling/integrations/openrouter.ts
|
|
1342
|
+
var openrouter = {
|
|
1343
|
+
id: "openrouter",
|
|
1344
|
+
title: "OpenRouter",
|
|
1345
|
+
category: "gateway",
|
|
1346
|
+
language: "ts",
|
|
1347
|
+
streamFormat: "openai-sse",
|
|
1348
|
+
envVars: ["OPENROUTER_API_KEY"],
|
|
1349
|
+
// No per-framework templates: the handler below is web-standard, so the
|
|
1350
|
+
// scaffolder wraps it in whatever the target framework routes with. This used
|
|
1351
|
+
// to be a `next`-only entry that every other framework inherited verbatim.
|
|
1352
|
+
routeTemplates: {},
|
|
1353
|
+
webRoute: `async function chatHandler(request: Request): Promise<Response> {
|
|
1354
|
+
// tools is undefined unless the front end sent one; JSON.stringify drops it,
|
|
1355
|
+
// so the same handler serves a tool archetype and a plain chat.
|
|
1356
|
+
const { model, messages, tools } = await readChatRequest(request);
|
|
1357
|
+
|
|
1358
|
+
const upstream = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
1359
|
+
method: 'POST',
|
|
1360
|
+
headers: {
|
|
1361
|
+
Authorization: \`Bearer \${process.env.OPENROUTER_API_KEY}\`,
|
|
1362
|
+
'Content-Type': 'application/json',
|
|
1363
|
+
},
|
|
1364
|
+
body: JSON.stringify({ model, messages, tools, stream: true }),
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
// FORWARD THE STATUS. Returning 200 here is how a missing key turns into
|
|
1368
|
+
// silence: the 401 body is JSON, it goes out labelled text/event-stream, the
|
|
1369
|
+
// SSE reader finds no frame, and the turn resolves empty with nothing logged
|
|
1370
|
+
// and no bubble. With the status intact readOpenAIStream throws a WireError
|
|
1371
|
+
// carrying the provider's own message.
|
|
1372
|
+
if (!upstream.ok) {
|
|
1373
|
+
return new Response(await upstream.text(), {
|
|
1374
|
+
status: upstream.status,
|
|
1375
|
+
headers: {
|
|
1376
|
+
'Content-Type': upstream.headers.get('content-type') ?? 'application/json',
|
|
1377
|
+
},
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
if (!upstream.body) {
|
|
1381
|
+
return new Response(JSON.stringify({ error: { message: 'The provider returned no body to stream.' } }), {
|
|
1382
|
+
status: 502,
|
|
1383
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
return new Response(upstream.body, {
|
|
1388
|
+
status: 200,
|
|
1389
|
+
headers: {
|
|
1390
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
1391
|
+
// no-transform stops a proxy buffering the stream into one blob.
|
|
1392
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
1393
|
+
Connection: 'keep-alive',
|
|
1394
|
+
},
|
|
1395
|
+
});
|
|
1396
|
+
}`,
|
|
1397
|
+
streamMapping: "OpenRouter returns OpenAI-format SSE. Pipe upstream.body straight to the browser; readOpenAIStream from @kitn.ai/ui/wire parses it, including tool calls and reasoning.",
|
|
1398
|
+
runNote: "Set OPENROUTER_API_KEY. Model ids are vendor/model, e.g. openai/gpt-4o.",
|
|
1399
|
+
docsSlug: "integrations/connect-any-model",
|
|
1400
|
+
// The route forwards both, so a scaffold's editable consts reach the gateway.
|
|
1401
|
+
forwardsFromClient: ["model", "tools"],
|
|
1402
|
+
// The handler puts the client's `tools` straight into an OpenAI-compatible
|
|
1403
|
+
// /chat/completions body, unconverted — so the client must send OpenAI's own
|
|
1404
|
+
// `{ type: 'function', function: { parameters } }` envelope.
|
|
1405
|
+
clientToolFormat: "openai",
|
|
1406
|
+
// Nothing to install. The route is global `fetch` and imports no module.
|
|
1407
|
+
deps: { npm: [], pip: [] },
|
|
1408
|
+
// The route reads OPENROUTER_API_KEY into an `Authorization: Bearer` header.
|
|
1409
|
+
keyExposure: "needs-proxy",
|
|
1410
|
+
// A remote HTTPS endpoint and a key. Nothing to install, nothing to start.
|
|
1411
|
+
outOfBand: "none"
|
|
1412
|
+
};
|
|
1413
|
+
var openrouter_default = openrouter;
|
|
1414
|
+
|
|
1415
|
+
// ../ui/src/agent-tooling/integrations/vercel-ai-sdk.ts
|
|
1416
|
+
var vercelAiSdk = {
|
|
1417
|
+
id: "vercel-ai-sdk",
|
|
1418
|
+
title: "Vercel AI SDK",
|
|
1419
|
+
category: "framework",
|
|
1420
|
+
language: "ts",
|
|
1421
|
+
streamFormat: "ai-sdk",
|
|
1422
|
+
envVars: ["AI_GATEWAY_API_KEY"],
|
|
1423
|
+
// No per-framework templates: the handler below is web-standard, so the
|
|
1424
|
+
// scaffolder wraps it in the target framework's own route declaration.
|
|
1425
|
+
routeTemplates: {},
|
|
1426
|
+
webRoute: `import { dynamicTool, jsonSchema, streamText } from 'ai';
|
|
1427
|
+
import type {
|
|
1428
|
+
AssistantContent,
|
|
1429
|
+
FilePart,
|
|
1430
|
+
JSONSchema7,
|
|
1431
|
+
ModelMessage,
|
|
1432
|
+
SystemModelMessage,
|
|
1433
|
+
ToolResultPart,
|
|
1434
|
+
ToolSet,
|
|
1435
|
+
UserContent,
|
|
1436
|
+
} from 'ai';
|
|
1437
|
+
|
|
1438
|
+
// Next.js only: add \`export const maxDuration = 30\` to the route file to allow
|
|
1439
|
+
// long streaming responses. It is a Next route-segment config, not part of the
|
|
1440
|
+
// handler, so it lives in the file rather than in here.
|
|
1441
|
+
|
|
1442
|
+
/**
|
|
1443
|
+
* The model, PINNED \u2014 and deliberately NOT read off the request body.
|
|
1444
|
+
*
|
|
1445
|
+
* Change THIS LINE to change the model; it is the only place one is named. The
|
|
1446
|
+
* AI Gateway takes a \`creator/model-name\` string, so any id it routes works
|
|
1447
|
+
* here without touching another import.
|
|
1448
|
+
*
|
|
1449
|
+
* Why it is not forwarded from the client, unlike the openai / openrouter /
|
|
1450
|
+
* anthropic routes:
|
|
1451
|
+
*
|
|
1452
|
+
* \xB7 Those three POST to ONE host with ONE id space, so the scaffold can seed a
|
|
1453
|
+
* valid default. The Gateway is a router across every vendor's id space at
|
|
1454
|
+
* once, so there is no default that is right for it \u2014 only one vendor's guess
|
|
1455
|
+
* baked into a provider-agnostic template.
|
|
1456
|
+
* \xB7 A forwarded model id on a \`needs-proxy\` route is a spend lever handed to
|
|
1457
|
+
* anything that can POST here, and the Gateway bills per token per model.
|
|
1458
|
+
*
|
|
1459
|
+
* WHY THIS ID. It is the one this route has actually been driven against live \u2014
|
|
1460
|
+
* text, a single tool call and a multi-round tool loop, through the Gateway \u2014
|
|
1461
|
+
* and, unlike a frontier default, it ANSWERS ON A FREE GATEWAY ACCOUNT. A paid
|
|
1462
|
+
* id fails a first \`npm run dev\` with
|
|
1463
|
+
* \`Free tier users do not have access to this model\`, which reads as a broken
|
|
1464
|
+
* scaffold rather than as a billing setting. It also supports tools and
|
|
1465
|
+
* reasoning, so the two things this route re-frames are reachable by default,
|
|
1466
|
+
* and it is cheaper than gpt-4o by roughly two orders of magnitude.
|
|
1467
|
+
*/
|
|
1468
|
+
const MODEL = 'openai/gpt-oss-120b';
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* One attachment to an AI SDK FilePart.
|
|
1472
|
+
*
|
|
1473
|
+
* \`data\` takes the BARE shorthand \u2014 a base64 string, or a URL object for a
|
|
1474
|
+
* remote file \u2014 rather than the newer tagged \`{ type: 'data' | 'url' }\` form.
|
|
1475
|
+
* Both are accepted by the installed SDK; the shorthand is also what AI SDK v5
|
|
1476
|
+
* and v6 accept, so this route keeps compiling if the app pins an older \`ai\`.
|
|
1477
|
+
*
|
|
1478
|
+
* Images arrive as \`image_url\` and become FileParts too. \`ImagePart\` still
|
|
1479
|
+
* exists but is deprecated in favour of exactly this.
|
|
1480
|
+
*/
|
|
1481
|
+
function toFilePart(part: Extract<WirePart, { kind: 'file' }>): FilePart {
|
|
1482
|
+
return {
|
|
1483
|
+
type: 'file',
|
|
1484
|
+
mediaType: part.mediaType,
|
|
1485
|
+
...(part.filename === undefined ? {} : { filename: part.filename }),
|
|
1486
|
+
data: part.source.type === 'data' ? part.source.data : new URL(part.source.url),
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// The kit's wire format is OpenAI-shaped (tool_calls on the assistant message,
|
|
1491
|
+
// a flat content string on the tool message). The AI SDK's ModelMessage is not:
|
|
1492
|
+
// a tool call is a CONTENT PART on the assistant message, and a tool result is a
|
|
1493
|
+
// tagged output union, not a bare string. Converting rather than casting is the
|
|
1494
|
+
// point \u2014 a cast would compile but hand the SDK the wrong shape at runtime.
|
|
1495
|
+
function toModelMessages(messages: ChatRequestBody['messages']): ModelMessage[] {
|
|
1496
|
+
return messages.map((message): ModelMessage => {
|
|
1497
|
+
switch (message.role) {
|
|
1498
|
+
// A system prompt is text-only on every wire, so the array form collapses.
|
|
1499
|
+
case 'system':
|
|
1500
|
+
return { role: 'system', content: wireText(message.content) };
|
|
1501
|
+
case 'user': {
|
|
1502
|
+
const parts = wireParts(message.content);
|
|
1503
|
+
// Plain string for an ordinary turn: identical to what this route sent
|
|
1504
|
+
// before attachments existed, and the shape the SDK docs lead with.
|
|
1505
|
+
if (parts.every((p) => p.kind === 'text')) {
|
|
1506
|
+
return { role: 'user', content: wireText(message.content) };
|
|
1507
|
+
}
|
|
1508
|
+
const content: UserContent = parts.map((p) =>
|
|
1509
|
+
p.kind === 'text' ? { type: 'text' as const, text: p.text } : toFilePart(p),
|
|
1510
|
+
);
|
|
1511
|
+
return { role: 'user', content };
|
|
1512
|
+
}
|
|
1513
|
+
case 'tool': {
|
|
1514
|
+
const result: ToolResultPart = {
|
|
1515
|
+
type: 'tool-result',
|
|
1516
|
+
toolCallId: message.tool_call_id ?? '',
|
|
1517
|
+
toolName: message.name ?? '',
|
|
1518
|
+
output: { type: 'text', value: wireText(message.content) },
|
|
1519
|
+
};
|
|
1520
|
+
return { role: 'tool', content: [result] };
|
|
1521
|
+
}
|
|
1522
|
+
case 'assistant': {
|
|
1523
|
+
const text = wireText(message.content);
|
|
1524
|
+
if (!message.tool_calls?.length) return { role: 'assistant', content: text };
|
|
1525
|
+
const content: AssistantContent = [];
|
|
1526
|
+
if (text) content.push({ type: 'text', text });
|
|
1527
|
+
for (const call of message.tool_calls) {
|
|
1528
|
+
content.push({
|
|
1529
|
+
type: 'tool-call',
|
|
1530
|
+
toolCallId: call.id,
|
|
1531
|
+
toolName: call.function.name,
|
|
1532
|
+
input: JSON.parse(call.function.arguments),
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
return { role: 'assistant', content };
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
/** The OpenAI function-calling envelope the front end sends, narrowed from
|
|
1542
|
+
* \`unknown[]\`. Declared as this integration's \`clientToolFormat\`. */
|
|
1543
|
+
type OpenAIFunctionTool = {
|
|
1544
|
+
function?: { name?: string; description?: string; parameters?: unknown };
|
|
1545
|
+
};
|
|
1546
|
+
|
|
1547
|
+
/**
|
|
1548
|
+
* OpenAI function schemas -> the AI SDK's own ToolSet.
|
|
1549
|
+
*
|
|
1550
|
+
* \`dynamicTool\` is the helper for a schema known only at RUNTIME. The ordinary
|
|
1551
|
+
* \`tool()\` infers its input type from a Zod schema written in the route, which
|
|
1552
|
+
* a list arriving in the request body cannot have.
|
|
1553
|
+
*
|
|
1554
|
+
* NO \`execute\`, deliberately. A tool the SDK can run makes the ROUTE the loop
|
|
1555
|
+
* owner: streamText would call it, feed the result back and answer in a single
|
|
1556
|
+
* response, so the tool call would never reach the browser and \`<kai-tool>\`
|
|
1557
|
+
* would have nothing to render. Without \`execute\` the SDK emits the call and
|
|
1558
|
+
* stops, which is the contract the kit's front end already implements \u2014 run the
|
|
1559
|
+
* tool, \`applyToolOutput\`, POST the thread again.
|
|
1560
|
+
*/
|
|
1561
|
+
function toToolSet(tools: ChatRequestBody['tools']): ToolSet | undefined {
|
|
1562
|
+
if (!tools?.length) return undefined;
|
|
1563
|
+
const out: ToolSet = {};
|
|
1564
|
+
for (const raw of tools) {
|
|
1565
|
+
const fn = (raw as OpenAIFunctionTool).function;
|
|
1566
|
+
if (!fn?.name) continue;
|
|
1567
|
+
out[fn.name] = dynamicTool({
|
|
1568
|
+
description: fn.description ?? '',
|
|
1569
|
+
inputSchema: jsonSchema((fn.parameters as JSONSchema7 | undefined) ?? { type: 'object' }),
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
/** AI SDK finish reasons -> OpenAI's spelling. They agree on 'stop', 'length'
|
|
1576
|
+
* and 'error' and disagree on the other two, and readOpenAIStream's table reads
|
|
1577
|
+
* OpenAI's \u2014 so an unmapped 'tool-calls' normalises to 'other' and the turn
|
|
1578
|
+
* stops saying why it stopped. */
|
|
1579
|
+
const FINISH_REASONS: Record<string, string> = {
|
|
1580
|
+
'tool-calls': 'tool_calls',
|
|
1581
|
+
'content-filter': 'content_filter',
|
|
1582
|
+
};
|
|
1583
|
+
|
|
1584
|
+
async function chatHandler(request: Request): Promise<Response> {
|
|
1585
|
+
const { messages, tools } = await readChatRequest(request);
|
|
1586
|
+
const toolSet = toToolSet(tools);
|
|
1587
|
+
const prompt = toModelMessages(messages);
|
|
1588
|
+
|
|
1589
|
+
// THE SYSTEM TURN DOES NOT GO IN \`messages\`, and this is the one that costs a
|
|
1590
|
+
// live run to find. \`SystemModelMessage\` is still part of the \`ModelMessage\`
|
|
1591
|
+
// union, so a system entry in this array TYPECHECKS \u2014 and then \`ai\` v7's
|
|
1592
|
+
// \`standardizePrompt\` throws \`InvalidPromptError: System messages are not
|
|
1593
|
+
// allowed in the prompt or messages fields. Use the instructions option
|
|
1594
|
+
// instead.\` The kit's own encoder puts the system prompt at \`messages[0]\`, so
|
|
1595
|
+
// that is every single turn of a scaffolded app, not an edge case.
|
|
1596
|
+
//
|
|
1597
|
+
// Hoisted rather than joined into a string: \`instructions\` takes the message
|
|
1598
|
+
// array, so several system turns keep their order and their count.
|
|
1599
|
+
//
|
|
1600
|
+
// On \`ai\` v5/v6 there is no \`instructions\` option and a system message in
|
|
1601
|
+
// \`messages\` is correct \u2014 drop this split and pass \`prompt\` straight through
|
|
1602
|
+
// if you pin an older SDK.
|
|
1603
|
+
const instructions = prompt.filter((m): m is SystemModelMessage => m.role === 'system');
|
|
1604
|
+
const conversation = prompt.filter((m) => m.role !== 'system');
|
|
1605
|
+
|
|
1606
|
+
// \`streamText\` is not awaited: it returns synchronously and does its work as
|
|
1607
|
+
// the stream is iterated. Prompt validation is part of that work, so an
|
|
1608
|
+
// invalid prompt surfaces from \`for await (\u2026 of result.fullStream)\` below
|
|
1609
|
+
// rather than from this line \u2014 which is why the catch that reports it lives
|
|
1610
|
+
// in the stream and not around this call. Confirmed by observation: a rejected
|
|
1611
|
+
// prompt reached the browser as an in-band error frame, not as a 500.
|
|
1612
|
+
const result = streamText({
|
|
1613
|
+
model: MODEL, // AI Gateway id; needs AI_GATEWAY_API_KEY
|
|
1614
|
+
...(instructions.length > 0 ? { instructions } : {}),
|
|
1615
|
+
messages: conversation,
|
|
1616
|
+
...(toolSet ? { tools: toolSet } : {}),
|
|
1617
|
+
});
|
|
1618
|
+
|
|
1619
|
+
const encoder = new TextEncoder();
|
|
1620
|
+
|
|
1621
|
+
// OpenAI correlates tool-call fragments by their POSITION in the tool_calls
|
|
1622
|
+
// array; the SDK identifies each call by id and never sends a position. So one
|
|
1623
|
+
// is derived from the other, in first-seen order, and every fragment of a call
|
|
1624
|
+
// carries the same number. Getting this wrong does not throw \u2014 the fragments
|
|
1625
|
+
// land on the wrong call and the arguments come out as spliced JSON.
|
|
1626
|
+
const toolIndex = new Map<string, number>();
|
|
1627
|
+
const indexOf = (id: string): number => {
|
|
1628
|
+
const known = toolIndex.get(id);
|
|
1629
|
+
if (known !== undefined) return known;
|
|
1630
|
+
const next = toolIndex.size;
|
|
1631
|
+
toolIndex.set(id, next);
|
|
1632
|
+
return next;
|
|
1633
|
+
};
|
|
1634
|
+
// How many argument characters a call streamed. \`tool-call\` re-sends the whole
|
|
1635
|
+
// input at the end, so emitting it unconditionally would DOUBLE the arguments
|
|
1636
|
+
// of every call that streamed \u2014 and skipping it unconditionally would empty
|
|
1637
|
+
// the arguments of any provider that does not stream them. Neither is safe to
|
|
1638
|
+
// assume, so the decision is made per call from what actually arrived.
|
|
1639
|
+
const streamedArgs = new Map<string, number>();
|
|
1640
|
+
|
|
1641
|
+
const sse = new ReadableStream({
|
|
1642
|
+
async start(controller) {
|
|
1643
|
+
const send = (chunk: unknown): void => {
|
|
1644
|
+
controller.enqueue(encoder.encode(\`data: \${JSON.stringify(chunk)}\\n\\n\`));
|
|
1645
|
+
};
|
|
1646
|
+
try {
|
|
1647
|
+
// fullStream, NOT textStream. textStream is text deltas only: a tool call
|
|
1648
|
+
// or a reasoning block goes past it silently, so a route built on it
|
|
1649
|
+
// emits a plain answer and nothing else however the model replied.
|
|
1650
|
+
for await (const part of result.fullStream) {
|
|
1651
|
+
switch (part.type) {
|
|
1652
|
+
case 'text-delta':
|
|
1653
|
+
send({ choices: [{ delta: { content: part.text } }] });
|
|
1654
|
+
break;
|
|
1655
|
+
|
|
1656
|
+
case 'reasoning-delta':
|
|
1657
|
+
send({ choices: [{ delta: { reasoning: part.text } }] });
|
|
1658
|
+
break;
|
|
1659
|
+
|
|
1660
|
+
// The call is ANNOUNCED here, before its arguments exist, which is
|
|
1661
|
+
// what lets <kai-tool> open a panel with the tool's name in it while
|
|
1662
|
+
// the arguments are still being written.
|
|
1663
|
+
case 'tool-input-start':
|
|
1664
|
+
streamedArgs.set(part.id, 0);
|
|
1665
|
+
send({
|
|
1666
|
+
choices: [{
|
|
1667
|
+
delta: {
|
|
1668
|
+
tool_calls: [{
|
|
1669
|
+
index: indexOf(part.id),
|
|
1670
|
+
id: part.id,
|
|
1671
|
+
type: 'function',
|
|
1672
|
+
function: { name: part.toolName, arguments: '' },
|
|
1673
|
+
}],
|
|
1674
|
+
},
|
|
1675
|
+
}],
|
|
1676
|
+
});
|
|
1677
|
+
break;
|
|
1678
|
+
|
|
1679
|
+
case 'tool-input-delta':
|
|
1680
|
+
streamedArgs.set(part.id, (streamedArgs.get(part.id) ?? 0) + part.delta.length);
|
|
1681
|
+
send({
|
|
1682
|
+
choices: [{
|
|
1683
|
+
delta: { tool_calls: [{ index: indexOf(part.id), function: { arguments: part.delta } }] },
|
|
1684
|
+
}],
|
|
1685
|
+
});
|
|
1686
|
+
break;
|
|
1687
|
+
|
|
1688
|
+
case 'tool-call':
|
|
1689
|
+
// Only when nothing streamed: see \`streamedArgs\`.
|
|
1690
|
+
if ((streamedArgs.get(part.toolCallId) ?? 0) === 0) {
|
|
1691
|
+
send({
|
|
1692
|
+
choices: [{
|
|
1693
|
+
delta: {
|
|
1694
|
+
tool_calls: [{
|
|
1695
|
+
index: indexOf(part.toolCallId),
|
|
1696
|
+
id: part.toolCallId,
|
|
1697
|
+
type: 'function',
|
|
1698
|
+
function: {
|
|
1699
|
+
name: part.toolName,
|
|
1700
|
+
arguments: JSON.stringify(part.input ?? {}),
|
|
1701
|
+
},
|
|
1702
|
+
}],
|
|
1703
|
+
},
|
|
1704
|
+
}],
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
break;
|
|
1708
|
+
|
|
1709
|
+
// One frame carries both, the way chat-completions sends them.
|
|
1710
|
+
// \`reasoning_tokens\` is the number that proves thinking happened even
|
|
1711
|
+
// when the provider streamed no reasoning text.
|
|
1712
|
+
case 'finish':
|
|
1713
|
+
send({
|
|
1714
|
+
choices: [{
|
|
1715
|
+
delta: {},
|
|
1716
|
+
finish_reason: FINISH_REASONS[part.finishReason] ?? part.finishReason,
|
|
1717
|
+
}],
|
|
1718
|
+
usage: {
|
|
1719
|
+
prompt_tokens: part.totalUsage.inputTokens,
|
|
1720
|
+
completion_tokens: part.totalUsage.outputTokens,
|
|
1721
|
+
total_tokens: part.totalUsage.totalTokens,
|
|
1722
|
+
completion_tokens_details: {
|
|
1723
|
+
reasoning_tokens: part.totalUsage.outputTokenDetails.reasoningTokens,
|
|
1724
|
+
},
|
|
1725
|
+
},
|
|
1726
|
+
});
|
|
1727
|
+
break;
|
|
1728
|
+
|
|
1729
|
+
// An error the SDK caught mid-stream. The status is long spent, so it
|
|
1730
|
+
// goes IN BAND like the catch below.
|
|
1731
|
+
case 'error':
|
|
1732
|
+
send({
|
|
1733
|
+
error: {
|
|
1734
|
+
message: part.error instanceof Error ? part.error.message : String(part.error),
|
|
1735
|
+
},
|
|
1736
|
+
});
|
|
1737
|
+
break;
|
|
1738
|
+
|
|
1739
|
+
// Everything else \u2014 text-start/end, tool-input-end, sources, files,
|
|
1740
|
+
// step boundaries, raw provider frames \u2014 has no OpenAI-wire spelling
|
|
1741
|
+
// and is dropped. \`source\` is the one worth knowing about: map it to
|
|
1742
|
+
// \`delta.annotations[].url_citation\` if your model cites its sources.
|
|
1743
|
+
default:
|
|
1744
|
+
break;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
} catch (err) {
|
|
1748
|
+
// The status is spent by the time the SDK fails \u2014 the headers went out
|
|
1749
|
+
// with the first byte \u2014 so report it IN BAND. readOpenAIStream lands
|
|
1750
|
+
// this on turn.error and keeps whatever streamed before it. Without it a
|
|
1751
|
+
// failed key is an empty bubble and nothing in the console.
|
|
1752
|
+
const message = err instanceof Error ? err.message : 'Model stream failed';
|
|
1753
|
+
send({ error: { message } });
|
|
1754
|
+
}
|
|
1755
|
+
controller.enqueue(encoder.encode('data: [DONE]\\n\\n'));
|
|
1756
|
+
controller.close();
|
|
1757
|
+
},
|
|
1758
|
+
});
|
|
1759
|
+
|
|
1760
|
+
return new Response(sse, {
|
|
1761
|
+
status: 200,
|
|
1762
|
+
headers: {
|
|
1763
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
1764
|
+
// no-transform stops a proxy buffering the stream into one blob.
|
|
1765
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
1766
|
+
Connection: 'keep-alive',
|
|
1767
|
+
},
|
|
1768
|
+
});
|
|
1769
|
+
}`,
|
|
1770
|
+
streamMapping: "The Vercel AI SDK's toUIMessageStreamResponse() and toTextStreamResponse() don't emit OpenAI-format SSE, so the route re-frames the stream itself and readOpenAIStream from @kitn.ai/ui/wire parses it exactly as it does every other integration. Iterate result.fullStream, NOT result.textStream: textStream carries text deltas only, so a route built on it emits a plain answer however the model replied and drops every tool call and every reasoning block silently. fullStream yields typed parts: text-delta.text -> delta.content, reasoning-delta.text -> delta.reasoning, tool-input-start plus its tool-input-delta fragments -> delta.tool_calls, finish -> finish_reason plus a usage frame, error -> an in-band {error:{message}}. Two traps. (1) OpenAI correlates tool-call fragments by their POSITION in the tool_calls array and the SDK only ever gives an id, so the route keeps an id -> index map; passing anything else through as the index splices one call's arguments into another. (2) fullStream sends the complete input AGAIN on the tool-call part after streaming it in fragments, so emitting both doubles the arguments \u2014 the route tracks how much each call streamed and emits the tool-call part only for a provider that streamed none. Parts with no OpenAI spelling (text-start/end, tool-input-end, step boundaries, raw) are dropped; source parts have one \u2014 delta.annotations[].url_citation \u2014 and are left unmapped because the SDK's Source union carries document sources a url_citation cannot express. On the REQUEST side the trap that only a live run finds: ai v7 REFUSES a system message inside `messages` (InvalidPromptError, 'Use the instructions option instead') even though SystemModelMessage is still in the ModelMessage union and therefore typechecks \u2014 and the kit's encoder puts the system prompt at messages[0], so that is every turn. Hoist system turns into `instructions` and pass the rest as `messages`. That failure arrives from ITERATING fullStream, not from the streamText() call \u2014 streamText returns synchronously and validates as the stream is read \u2014 so the in-band catch around the loop is what reports it, and it reaches the browser as an error frame rather than as a 500.",
|
|
1771
|
+
runNote: "Set AI_GATEWAY_API_KEY for the AI Gateway (string model id form: creator/model-name). The route pins `const MODEL = 'openai/gpt-oss-120b'` \u2014 one line, at the top, and any id the Gateway routes works in it. That id is pinned because it answers on a FREE Gateway account: most ids (deepseek/*, meta/*, anthropic/*) return `Free tier users do not have access to this model` or a free-tier rate limit until the account has paid credits, which looks like a broken scaffold rather than a billing setting. For direct provider access, import its provider package (e.g. @ai-sdk/openai) and set the corresponding key (e.g. OPENAI_API_KEY). The tools the front end posts become `dynamicTool`s with NO `execute`, which is what keeps the tool loop in the app: the SDK emits the call and stops, the app runs it, renders it in <kai-tool> and posts the thread back. Give a tool an `execute` and the SDK runs the whole loop server-side, so nothing reaches the browser but the final sentence.",
|
|
1772
|
+
docsSlug: "integrations/vercel-ai-sdk",
|
|
1773
|
+
// `tools` only. `model` is deliberately NOT forwarded — the route pins it in
|
|
1774
|
+
// one named const, and see that const's comment for why the Gateway is the one
|
|
1775
|
+
// host where a client-supplied id has no correct default. The catalog check
|
|
1776
|
+
// agrees from the other direction: `every integration that forwards a model
|
|
1777
|
+
// emits one valid for the host it POSTs to` reads the host off the route's own
|
|
1778
|
+
// fetch(), and this route makes no fetch call at all — the SDK owns the
|
|
1779
|
+
// transport — so a forwarded model here could not be validated against
|
|
1780
|
+
// anything.
|
|
1781
|
+
forwardsFromClient: ["tools"],
|
|
1782
|
+
// 'openai': the ROUTE's request contract, not the SDK's own. `toToolSet` reads
|
|
1783
|
+
// `raw.function.name` / `.function.parameters` off each entry — the OpenAI
|
|
1784
|
+
// function-calling envelope — and rebuilds it as a `dynamicTool` with a
|
|
1785
|
+
// `jsonSchema()` input. Sending the SDK's own tool shape from the client would
|
|
1786
|
+
// leave `.function` undefined and every tool would arrive nameless.
|
|
1787
|
+
clientToolFormat: "openai",
|
|
1788
|
+
// `ai` only. A direct provider (e.g. @ai-sdk/openai) is the alternative path
|
|
1789
|
+
// described in runNote, not what this route imports, so it is not listed: the
|
|
1790
|
+
// rule is what the emitted code actually imports.
|
|
1791
|
+
deps: { npm: ["ai"], pip: [] },
|
|
1792
|
+
// Grep the route for a key and you find NOTHING — no header, no process.env.
|
|
1793
|
+
// The AI SDK reads AI_GATEWAY_API_KEY out of the environment itself, inside
|
|
1794
|
+
// streamText(). This is the case that makes inference from route source
|
|
1795
|
+
// unsafe and the declaration necessary; `envVars` is what the schema's safety
|
|
1796
|
+
// net can still see.
|
|
1797
|
+
keyExposure: "needs-proxy",
|
|
1798
|
+
// The AI Gateway is a remote HTTPS endpoint reached through the `ai` package,
|
|
1799
|
+
// which is an ordinary npm dependency (see `deps`). Nothing to start.
|
|
1800
|
+
outOfBand: "none"
|
|
1801
|
+
};
|
|
1802
|
+
var vercel_ai_sdk_default = vercelAiSdk;
|
|
1803
|
+
|
|
1804
|
+
// ../ui/src/agent-tooling/integrations/langgraph.ts
|
|
1805
|
+
var langgraph = {
|
|
1806
|
+
id: "langgraph",
|
|
1807
|
+
title: "LangGraph",
|
|
1808
|
+
category: "framework",
|
|
1809
|
+
language: "ts",
|
|
1810
|
+
streamFormat: "openai-sse",
|
|
1811
|
+
envVars: ["OPENAI_API_KEY"],
|
|
1812
|
+
// No per-framework templates: the handler below is web-standard, so the
|
|
1813
|
+
// scaffolder wraps it in the target framework's own route declaration.
|
|
1814
|
+
routeTemplates: {},
|
|
1815
|
+
webRoute: `import { createReactAgent } from '@langchain/langgraph/prebuilt';
|
|
1816
|
+
import { ChatOpenAI } from '@langchain/openai';
|
|
1817
|
+
import { tool } from '@langchain/core/tools';
|
|
1818
|
+
import { z } from 'zod';
|
|
1819
|
+
|
|
1820
|
+
const getWeather = tool(
|
|
1821
|
+
async ({ city }) => \`It's 18\xB0C and clear in \${city}.\`,
|
|
1822
|
+
{
|
|
1823
|
+
name: 'get_weather',
|
|
1824
|
+
description: 'Get the current weather for a city.',
|
|
1825
|
+
schema: z.object({ city: z.string() }),
|
|
1826
|
+
},
|
|
1827
|
+
);
|
|
1828
|
+
|
|
1829
|
+
const agent = createReactAgent({
|
|
1830
|
+
llm: new ChatOpenAI({ model: 'gpt-4o' }),
|
|
1831
|
+
tools: [getWeather],
|
|
1832
|
+
});
|
|
1833
|
+
|
|
1834
|
+
// Stream a compiled LangGraph agent to the browser as OpenAI-format SSE.
|
|
1835
|
+
async function chatHandler(request: Request): Promise<Response> {
|
|
1836
|
+
const { messages } = await readChatRequest(request);
|
|
1837
|
+
|
|
1838
|
+
// agent.stream() coerces plain {role, content} objects into BaseMessage
|
|
1839
|
+
// instances itself, including OpenAI-shaped tool_calls, so the wire messages
|
|
1840
|
+
// can be passed through almost as-is. The one incompatible bit is content:
|
|
1841
|
+
// OpenAI represents a tool-calls-only assistant turn as content: null, and
|
|
1842
|
+
// LangChain's MessageContent type (and runtime coercion) only accepts string.
|
|
1843
|
+
const agentMessages = messages.map((m) => ({ ...m, content: m.content ?? '' }));
|
|
1844
|
+
|
|
1845
|
+
// Let TS infer the stream's element type from this call instead of hand
|
|
1846
|
+
// writing it: the real type is a [BaseMessage, metadata] tuple, keyed off the
|
|
1847
|
+
// streamMode: 'messages' literal, not the plain object shape it looks like.
|
|
1848
|
+
const startStream = () => agent.stream({ messages: agentMessages }, { streamMode: 'messages' });
|
|
1849
|
+
|
|
1850
|
+
let stream: Awaited<ReturnType<typeof startStream>>;
|
|
1851
|
+
try {
|
|
1852
|
+
stream = await startStream();
|
|
1853
|
+
} catch (err) {
|
|
1854
|
+
// A REAL status, before a byte is streamed: a missing OPENAI_API_KEY fails
|
|
1855
|
+
// here. Returning 200 would send this JSON out labelled text/event-stream,
|
|
1856
|
+
// the SSE reader would find no frame, and the turn would resolve empty with
|
|
1857
|
+
// nothing logged and no bubble.
|
|
1858
|
+
return new Response(
|
|
1859
|
+
JSON.stringify({ error: { message: err instanceof Error ? err.message : 'Agent failed to start' } }),
|
|
1860
|
+
{ status: 502, headers: { 'Content-Type': 'application/json' } },
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
const encoder = new TextEncoder();
|
|
1865
|
+
const body = new ReadableStream({
|
|
1866
|
+
async start(controller) {
|
|
1867
|
+
const send = (obj: unknown) =>
|
|
1868
|
+
controller.enqueue(encoder.encode(\`data: \${JSON.stringify(obj)}\\n\\n\`));
|
|
1869
|
+
|
|
1870
|
+
try {
|
|
1871
|
+
for await (const [chunk] of stream) {
|
|
1872
|
+
if (typeof chunk.content === 'string' && chunk.content) {
|
|
1873
|
+
send({ choices: [{ delta: { content: chunk.content } }] });
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
} catch (err) {
|
|
1877
|
+
// The status is spent once the stream started, so report IN BAND:
|
|
1878
|
+
// readOpenAIStream lands this on turn.error and keeps what streamed.
|
|
1879
|
+
send({ error: { message: err instanceof Error ? err.message : 'Agent stream failed' } });
|
|
1880
|
+
}
|
|
1881
|
+
controller.enqueue(encoder.encode('data: [DONE]\\n\\n'));
|
|
1882
|
+
controller.close();
|
|
1883
|
+
},
|
|
1884
|
+
});
|
|
1885
|
+
|
|
1886
|
+
return new Response(body, {
|
|
1887
|
+
status: 200,
|
|
1888
|
+
headers: {
|
|
1889
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
1890
|
+
// no-transform stops a proxy buffering the stream into one blob.
|
|
1891
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
1892
|
+
Connection: 'keep-alive',
|
|
1893
|
+
},
|
|
1894
|
+
});
|
|
1895
|
+
}`,
|
|
1896
|
+
streamMapping: "Use graph.stream(input, { streamMode: 'messages' }) to get [messageChunk, metadata] tuples. Extract chunk.content (string) and forward as OpenAI-format SSE frames: data: {choices:[{delta:{content}}]}. Close with data: [DONE]. readOpenAIStream from @kitn.ai/ui/wire parses tool calls and reasoning too, but the route template forwards chunk.content only, which is text: the same message chunks carry the tool-call fragments on chunk.tool_call_chunks, so re-frame those onto delta.tool_calls to fill kai-tool.",
|
|
1897
|
+
// No install list here: `deps` below is the one, and the scaffolder emits it.
|
|
1898
|
+
// This sentence used to end "Install @langchain/langgraph, @langchain/openai,
|
|
1899
|
+
// @langchain/core." — three of the four packages, for as long as nobody was
|
|
1900
|
+
// comparing it to anything.
|
|
1901
|
+
runNote: "Set OPENAI_API_KEY (or the key for your chosen model provider).",
|
|
1902
|
+
docsSlug: "integrations/langgraph",
|
|
1903
|
+
// Nothing. The agent owns both: ChatOpenAI({ model: 'gpt-4o' }) and the tools
|
|
1904
|
+
// array passed to createReactAgent are server-side.
|
|
1905
|
+
forwardsFromClient: [],
|
|
1906
|
+
// Four, one per import in the route above. `zod` is the one the deleted prose
|
|
1907
|
+
// forgot: the tool's `schema:` is a z.object(), so an app installed from that
|
|
1908
|
+
// sentence alone fails to build. Deriving this from the imports rather than
|
|
1909
|
+
// from a sentence is the point of the field.
|
|
1910
|
+
deps: { npm: ["@langchain/langgraph", "@langchain/openai", "@langchain/core", "zod"], pip: [] },
|
|
1911
|
+
// No key appears in the route: `new ChatOpenAI(...)` reads OPENAI_API_KEY from
|
|
1912
|
+
// the environment itself. Same invisible-key shape as vercel-ai-sdk.
|
|
1913
|
+
keyExposure: "needs-proxy",
|
|
1914
|
+
// 'none', and this is the entry most likely to be "corrected" to something
|
|
1915
|
+
// else, so the reason is here rather than assumed. The create-kai spec lists
|
|
1916
|
+
// LangGraph under "Bring a server or runtime". That is WRONG for this route:
|
|
1917
|
+
// the graph is built and compiled IN PROCESS above (`createReactAgent` over a
|
|
1918
|
+
// `new ChatOpenAI(...)`), so there is no LangGraph server, no port and no
|
|
1919
|
+
// second process — the four packages in `deps` are the whole install and
|
|
1920
|
+
// `runNote` asks for a key and nothing else.
|
|
1921
|
+
//
|
|
1922
|
+
// LangGraph Platform / `langgraph dev` IS a server, and an integration that
|
|
1923
|
+
// talked to one over HTTP would be 'local-server'. That is a different route
|
|
1924
|
+
// than the one above, and if it is ever added it must be added as its own
|
|
1925
|
+
// catalog entry rather than by changing this line.
|
|
1926
|
+
outOfBand: "none"
|
|
1927
|
+
};
|
|
1928
|
+
var langgraph_default = langgraph;
|
|
1929
|
+
|
|
1930
|
+
// ../ui/src/agent-tooling/integrations/cloudflare.ts
|
|
1931
|
+
var cloudflare = {
|
|
1932
|
+
id: "cloudflare",
|
|
1933
|
+
title: "Cloudflare AI",
|
|
1934
|
+
category: "provider",
|
|
1935
|
+
language: "ts",
|
|
1936
|
+
streamFormat: "openai-sse",
|
|
1937
|
+
envVars: ["CF_ACCOUNT_ID", "CF_API_TOKEN"],
|
|
1938
|
+
routeTemplates: {
|
|
1939
|
+
// Kept as a framework-specific template because it cannot be expressed as a
|
|
1940
|
+
// portable handler: `env.AI` is a Worker binding, and only a Worker has one.
|
|
1941
|
+
// Everything else uses `webRoute` below.
|
|
1942
|
+
worker: `// Worker handler: env.AI is bound in wrangler.toml
|
|
1943
|
+
// env.AI.run emits Cloudflare-native SSE (data: {"response":"<token>"}).
|
|
1944
|
+
// The TransformStream below re-frames each chunk to OpenAI-format SSE so
|
|
1945
|
+
// readOpenAIStream reads it without any client-side changes.
|
|
1946
|
+
import type { OpenAIWireMessage } from '@kitn.ai/ui/wire';
|
|
1947
|
+
|
|
1948
|
+
/**
|
|
1949
|
+
* What the front end POSTs. \`req.json()\` is \`unknown\` (it is whatever the
|
|
1950
|
+
* client sent), so the body is narrowed once here instead of at every use \u2014
|
|
1951
|
+
* without it this Worker does not compile. Widen it as you add fields.
|
|
1952
|
+
*/
|
|
1953
|
+
type ChatRequestBody = { messages: OpenAIWireMessage[] };
|
|
1954
|
+
|
|
1955
|
+
export default {
|
|
1956
|
+
async fetch(req: Request, env: Env): Promise<Response> {
|
|
1957
|
+
const { messages } = (await req.json()) as ChatRequestBody;
|
|
1958
|
+
|
|
1959
|
+
let nativeStream: ReadableStream<Uint8Array>;
|
|
1960
|
+
try {
|
|
1961
|
+
// env.AI.run is typed to return the non-streaming shape; \`stream: true\`
|
|
1962
|
+
// makes it a ReadableStream at runtime, which the types cannot express, so
|
|
1963
|
+
// the hop through \`unknown\` is required rather than cosmetic.
|
|
1964
|
+
nativeStream = (await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
|
|
1965
|
+
messages,
|
|
1966
|
+
stream: true,
|
|
1967
|
+
})) as unknown as ReadableStream<Uint8Array>;
|
|
1968
|
+
} catch (err) {
|
|
1969
|
+
// A REAL status, before a byte is streamed. Returning 200 here would send
|
|
1970
|
+
// a JSON error labelled text/event-stream: the reader finds no frame and
|
|
1971
|
+
// the turn resolves empty, with nothing logged and no bubble.
|
|
1972
|
+
return new Response(
|
|
1973
|
+
JSON.stringify({ error: { message: err instanceof Error ? err.message : 'Workers AI call failed' } }),
|
|
1974
|
+
{ status: 502, headers: { 'Content-Type': 'application/json' } },
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// Re-frame Cloudflare-native SSE \u2192 OpenAI-format SSE
|
|
1979
|
+
const { readable, writable } = new TransformStream();
|
|
1980
|
+
const writer = writable.getWriter();
|
|
1981
|
+
const encoder = new TextEncoder();
|
|
1982
|
+
const decoder = new TextDecoder();
|
|
1983
|
+
|
|
1984
|
+
(async () => {
|
|
1985
|
+
const reader = nativeStream.getReader();
|
|
1986
|
+
let buffer = '';
|
|
1987
|
+
try {
|
|
1988
|
+
while (true) {
|
|
1989
|
+
const { value, done } = await reader.read();
|
|
1990
|
+
if (done) break;
|
|
1991
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1992
|
+
const lines = buffer.split('\\n');
|
|
1993
|
+
buffer = lines.pop() ?? '';
|
|
1994
|
+
for (const line of lines) {
|
|
1995
|
+
const s = line.trim();
|
|
1996
|
+
if (!s.startsWith('data:')) continue;
|
|
1997
|
+
const payload = s.slice(5).trim();
|
|
1998
|
+
if (payload === '[DONE]') continue;
|
|
1999
|
+
try {
|
|
2000
|
+
const { response } = JSON.parse(payload) as { response?: string };
|
|
2001
|
+
if (response == null) continue;
|
|
2002
|
+
const openaiChunk = JSON.stringify({ choices: [{ delta: { content: response } }] });
|
|
2003
|
+
await writer.write(encoder.encode(\`data: \${openaiChunk}\\n\\n\`));
|
|
2004
|
+
} catch { /* skip malformed lines */ }
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
} catch (err) {
|
|
2008
|
+
// The response is already streaming, so the status is spent: report the
|
|
2009
|
+
// failure IN BAND. readOpenAIStream lands this on turn.error and keeps
|
|
2010
|
+
// whatever already streamed.
|
|
2011
|
+
const message = err instanceof Error ? err.message : 'Workers AI stream failed';
|
|
2012
|
+
await writer.write(encoder.encode(\`data: \${JSON.stringify({ error: { message } })}\\n\\n\`));
|
|
2013
|
+
} finally {
|
|
2014
|
+
// Always: an unclosed writer leaves the browser waiting forever.
|
|
2015
|
+
await writer.write(encoder.encode('data: [DONE]\\n\\n'));
|
|
2016
|
+
await writer.close();
|
|
2017
|
+
}
|
|
2018
|
+
})();
|
|
2019
|
+
|
|
2020
|
+
return new Response(readable, {
|
|
2021
|
+
status: 200,
|
|
2022
|
+
headers: {
|
|
2023
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
2024
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
2025
|
+
},
|
|
2026
|
+
});
|
|
2027
|
+
},
|
|
2028
|
+
};`
|
|
2029
|
+
},
|
|
2030
|
+
webRoute: `async function chatHandler(request: Request): Promise<Response> {
|
|
2031
|
+
// Proxy Workers AI over its OpenAI-compatible HTTP endpoint, token server-side.
|
|
2032
|
+
const { messages } = await readChatRequest(request);
|
|
2033
|
+
|
|
2034
|
+
const upstream = await fetch(
|
|
2035
|
+
\`https://api.cloudflare.com/client/v4/accounts/\${process.env.CF_ACCOUNT_ID}/ai/v1/chat/completions\`,
|
|
2036
|
+
{
|
|
2037
|
+
method: 'POST',
|
|
2038
|
+
headers: {
|
|
2039
|
+
Authorization: \`Bearer \${process.env.CF_API_TOKEN}\`,
|
|
2040
|
+
'Content-Type': 'application/json',
|
|
2041
|
+
},
|
|
2042
|
+
body: JSON.stringify({
|
|
2043
|
+
model: '@cf/meta/llama-3.1-8b-instruct',
|
|
2044
|
+
messages,
|
|
2045
|
+
stream: true,
|
|
2046
|
+
}),
|
|
2047
|
+
},
|
|
2048
|
+
);
|
|
2049
|
+
|
|
2050
|
+
// FORWARD THE STATUS. A wrong CF_API_TOKEN is a 401/403: returning 200 sends
|
|
2051
|
+
// its JSON body out labelled text/event-stream, the SSE reader finds no frame,
|
|
2052
|
+
// and the turn resolves empty with nothing logged and no bubble.
|
|
2053
|
+
if (!upstream.ok) {
|
|
2054
|
+
return new Response(await upstream.text(), {
|
|
2055
|
+
status: upstream.status,
|
|
2056
|
+
headers: {
|
|
2057
|
+
'Content-Type': upstream.headers.get('content-type') ?? 'application/json',
|
|
2058
|
+
},
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2061
|
+
if (!upstream.body) {
|
|
2062
|
+
return new Response(JSON.stringify({ error: { message: 'Workers AI returned no body to stream.' } }), {
|
|
2063
|
+
status: 502,
|
|
2064
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
// Workers AI returns OpenAI-format SSE. Pass it straight through.
|
|
2069
|
+
return new Response(upstream.body, {
|
|
2070
|
+
status: 200,
|
|
2071
|
+
headers: {
|
|
2072
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
2073
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
2074
|
+
Connection: 'keep-alive',
|
|
2075
|
+
},
|
|
2076
|
+
});
|
|
2077
|
+
}`,
|
|
2078
|
+
streamMapping: `Workers AI via the OpenAI-compatible HTTP endpoint returns OpenAI-format SSE. Pipe upstream.body straight to the browser; readOpenAIStream from @kitn.ai/ui/wire parses it, including tool calls and reasoning. The native env.AI binding streams Cloudflare's own format (data: {"response":"...token..."}); the worker route template re-frames these chunks to OpenAI-format SSE via a TransformStream before returning.`,
|
|
2079
|
+
runNote: 'Set CF_ACCOUNT_ID and CF_API_TOKEN. Model ids are prefixed with @cf/, e.g. @cf/meta/llama-3.1-8b-instruct. For the AI binding (worker key), add an [ai] block with binding = "AI" in wrangler.toml.',
|
|
2080
|
+
docsSlug: "integrations/cloudflare-ai",
|
|
2081
|
+
// Nothing. Both routes pin '@cf/meta/llama-3.1-8b-instruct' (the generic
|
|
2082
|
+
// 'openai/gpt-4o-mini' a scaffold used to default to is not even a valid
|
|
2083
|
+
// Workers AI id), and the worker route's re-framing reader forwards only the
|
|
2084
|
+
// text `response` field, so a tool call could not reach the browser anyway.
|
|
2085
|
+
forwardsFromClient: [],
|
|
2086
|
+
// Nothing. The webRoute is global `fetch`, and the worker template's only
|
|
2087
|
+
// import is a type from '@kitn.ai/ui/wire' — the kit itself, which every
|
|
2088
|
+
// scaffold already depends on. Its `Env` is an ambient wrangler type, not an
|
|
2089
|
+
// import, and the Worker skeleton owns wrangler/@cloudflare/workers-types.
|
|
2090
|
+
deps: { npm: [], pip: [] },
|
|
2091
|
+
// The webRoute reads CF_API_TOKEN into an `Authorization: Bearer` header.
|
|
2092
|
+
//
|
|
2093
|
+
// The two routes differ and the stricter one governs: the worker template uses
|
|
2094
|
+
// the `env.AI` binding and holds no token in code at all, but a binding is a
|
|
2095
|
+
// Worker capability, not a browser one. Either way a server is required.
|
|
2096
|
+
keyExposure: "needs-proxy",
|
|
2097
|
+
// Workers AI is a remote HTTPS endpoint (the REST route) or a platform binding
|
|
2098
|
+
// (the worker route) — either way there is nothing on the developer's machine
|
|
2099
|
+
// to install or run. `wrangler` is a devDependency of a Worker app, not an
|
|
2100
|
+
// out-of-band prerequisite of this integration.
|
|
2101
|
+
outOfBand: "none"
|
|
2102
|
+
};
|
|
2103
|
+
var cloudflare_default = cloudflare;
|
|
2104
|
+
|
|
2105
|
+
// ../ui/src/agent-tooling/integrations/ollama.ts
|
|
2106
|
+
var ollama = {
|
|
2107
|
+
id: "ollama",
|
|
2108
|
+
title: "Ollama",
|
|
2109
|
+
category: "provider",
|
|
2110
|
+
language: "ts",
|
|
2111
|
+
streamFormat: "openai-sse",
|
|
2112
|
+
envVars: [],
|
|
2113
|
+
// No per-framework templates. The handler below is web-standard, so the
|
|
2114
|
+
// scaffolder wraps it in the target framework's own route declaration.
|
|
2115
|
+
//
|
|
2116
|
+
// There was never an `html` entry and there must not be one: routeTemplates
|
|
2117
|
+
// keyed by 'html' are selected as the BACKEND ROUTE for framework: 'html', and
|
|
2118
|
+
// this integration's used to be a second <kai-chat id="chat"> plus a second
|
|
2119
|
+
// kai-submit listener, so pasting both blocks gave a duplicate id and two
|
|
2120
|
+
// fetches per submit. chooseRoute refuses an 'html' key outright.
|
|
2121
|
+
routeTemplates: {},
|
|
2122
|
+
webRoute: `async function chatHandler(request: Request): Promise<Response> {
|
|
2123
|
+
// The model is pinned here, not sent by the browser. tools IS forwarded:
|
|
2124
|
+
// it is undefined unless the front end declared any, and JSON.stringify
|
|
2125
|
+
// drops it, so the same handler serves a tool archetype and a plain chat.
|
|
2126
|
+
const { messages, tools } = await readChatRequest(request);
|
|
2127
|
+
|
|
2128
|
+
const upstream = await fetch('http://localhost:11434/v1/chat/completions', {
|
|
2129
|
+
method: 'POST',
|
|
2130
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2131
|
+
body: JSON.stringify({ model: 'llama3.2', messages, tools, stream: true }),
|
|
2132
|
+
});
|
|
2133
|
+
|
|
2134
|
+
// FORWARD THE STATUS. Returning 200 for a failed upstream is how "ollama is
|
|
2135
|
+
// not running" or "that model was never pulled" turns into silence: the error
|
|
2136
|
+
// body is JSON, it goes out labelled text/event-stream, the SSE reader finds
|
|
2137
|
+
// no frame, and the turn resolves empty with nothing logged and no bubble.
|
|
2138
|
+
if (!upstream.ok) {
|
|
2139
|
+
return new Response(await upstream.text(), {
|
|
2140
|
+
status: upstream.status,
|
|
2141
|
+
headers: {
|
|
2142
|
+
'Content-Type': upstream.headers.get('content-type') ?? 'application/json',
|
|
2143
|
+
},
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
if (!upstream.body) {
|
|
2147
|
+
return new Response(JSON.stringify({ error: { message: 'Ollama returned no body to stream.' } }), {
|
|
2148
|
+
status: 502,
|
|
2149
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
// Ollama returns OpenAI-format SSE. Stream it straight to the browser.
|
|
2154
|
+
return new Response(upstream.body, {
|
|
2155
|
+
status: 200,
|
|
2156
|
+
headers: {
|
|
2157
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
2158
|
+
// no-transform stops a proxy buffering the stream into one blob.
|
|
2159
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
2160
|
+
Connection: 'keep-alive',
|
|
2161
|
+
},
|
|
2162
|
+
});
|
|
2163
|
+
}`,
|
|
2164
|
+
streamMapping: "Ollama's OpenAI-compatible endpoint (http://localhost:11434/v1/chat/completions) returns OpenAI-format SSE. Pipe upstream.body straight to the browser; readOpenAIStream from @kitn.ai/ui/wire parses it, including tool calls and reasoning. No API key needed; pass any string if a client requires one (Ollama ignores it).",
|
|
2165
|
+
runNote: "No API key required. Run: ollama serve (starts on 127.0.0.1:11434), then ollama pull <model>. For browser-direct access, set OLLAMA_ORIGINS to allow the page origin; restart Ollama after any env change.",
|
|
2166
|
+
docsSlug: "integrations/ollama",
|
|
2167
|
+
// No 'model': the route pins llama3.2, so a front-end model const would be an
|
|
2168
|
+
// editable value the route throws away. Change the model in the route. 'tools'
|
|
2169
|
+
// IS forwarded, which is what lets an agentic scaffold fill its kai-tool panel.
|
|
2170
|
+
forwardsFromClient: ["tools"],
|
|
2171
|
+
// Ollama's OpenAI-compatible endpoint, and the handler forwards `tools`
|
|
2172
|
+
// verbatim into that body — so the client sends OpenAI's function envelope.
|
|
2173
|
+
clientToolFormat: "openai",
|
|
2174
|
+
// Nothing. The route is global `fetch` and imports no module; the `ollama`
|
|
2175
|
+
// npm package is not used, and the server itself is installed out of band.
|
|
2176
|
+
deps: { npm: [], pip: [] },
|
|
2177
|
+
// One of the two 'frontend-safe' entries, and it is read off the route rather
|
|
2178
|
+
// than off "it's local, so it's fine": the fetch to localhost:11434 sends
|
|
2179
|
+
// Content-Type and NOTHING else — no Authorization, no key — and envVars is
|
|
2180
|
+
// empty, so there is no secret for a bundle to leak. runNote already documents
|
|
2181
|
+
// the browser-direct path (set OLLAMA_ORIGINS to allow the page origin), which
|
|
2182
|
+
// is the path this value unlocks.
|
|
2183
|
+
keyExposure: "frontend-safe",
|
|
2184
|
+
// The route fetches http://localhost:11434, so Ollama must ALREADY be running:
|
|
2185
|
+
// `ollama serve`, then `ollama pull <model>` for the model the route pins.
|
|
2186
|
+
// Nothing in package.json installs or starts it — `deps.npm` is empty because
|
|
2187
|
+
// the route is bare `fetch`, which is exactly why this cannot be inferred from
|
|
2188
|
+
// the dependency list. The schema's LOOPBACK_FETCH net catches this one
|
|
2189
|
+
// independently, so a later edit to 'none' fails at the catalog boundary.
|
|
2190
|
+
outOfBand: "local-server"
|
|
2191
|
+
};
|
|
2192
|
+
var ollama_default = ollama;
|
|
2193
|
+
|
|
2194
|
+
// ../ui/src/agent-tooling/integrations/mastra.ts
|
|
2195
|
+
var mastra = {
|
|
2196
|
+
id: "mastra",
|
|
2197
|
+
title: "Mastra",
|
|
2198
|
+
category: "harness",
|
|
2199
|
+
language: "ts",
|
|
2200
|
+
streamFormat: "openai-sse",
|
|
2201
|
+
envVars: ["MASTRA_URL"],
|
|
2202
|
+
// No per-framework templates. The old `express` entry was a fragment, not a
|
|
2203
|
+
// route: it referenced `messages` and `res` without declaring either, and
|
|
2204
|
+
// every non-express framework inherited it anyway. The handler below is
|
|
2205
|
+
// web-standard, so the scaffolder wraps it per framework.
|
|
2206
|
+
routeTemplates: {},
|
|
2207
|
+
webRoute: `import { MastraClient } from '@mastra/client-js';
|
|
2208
|
+
|
|
2209
|
+
// MASTRA_URL is required, not defaulted to localhost: a deployed consumer app
|
|
2210
|
+
// that forgets to set it would otherwise construct a client that silently
|
|
2211
|
+
// points a production Worker/serverless function at 127.0.0.1, then fail deep
|
|
2212
|
+
// inside a fetch call with a confusing connection error. Failing at import
|
|
2213
|
+
// time, with a message that names the fix, is louder and cheaper to debug.
|
|
2214
|
+
const MASTRA_URL = process.env.MASTRA_URL;
|
|
2215
|
+
if (!MASTRA_URL) {
|
|
2216
|
+
throw new Error('MASTRA_URL is not set. Point it at your Mastra server, e.g. http://localhost:4111 for \`mastra dev\`.');
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
const mastra = new MastraClient({ baseUrl: MASTRA_URL });
|
|
2220
|
+
|
|
2221
|
+
// Proxy a Mastra agent to the browser as OpenAI-format SSE.
|
|
2222
|
+
async function chatHandler(request: Request): Promise<Response> {
|
|
2223
|
+
const { messages } = await readChatRequest(request);
|
|
2224
|
+
|
|
2225
|
+
// MastraClient's Agent.stream() takes AI-SDK CoreMessage[], not the OpenAI
|
|
2226
|
+
// wire format: each message has ONE literal role (not a union) and content
|
|
2227
|
+
// is never null. A 'tool' wire message is OUR client-side tool bookkeeping;
|
|
2228
|
+
// this agent owns its tools server-side (forwardsFromClient is empty), so it
|
|
2229
|
+
// has no schema for that shape and the entry is dropped rather than guessed.
|
|
2230
|
+
//
|
|
2231
|
+
// Only the USER role takes content parts: a system prompt is text by
|
|
2232
|
+
// definition, and an assistant turn here is replayed history, not a new
|
|
2233
|
+
// upload. \`data\` uses the bare base64-or-URL shorthand, which every AI SDK
|
|
2234
|
+
// major Mastra accepts understands.
|
|
2235
|
+
type MastraFilePart = { type: 'file'; data: string | URL; mediaType: string; filename?: string };
|
|
2236
|
+
type MastraUserContent = string | Array<{ type: 'text'; text: string } | MastraFilePart>;
|
|
2237
|
+
type MastraMessage = { role: 'system'; content: string } | { role: 'user'; content: MastraUserContent } | { role: 'assistant'; content: string };
|
|
2238
|
+
const mastraMessages: MastraMessage[] = [];
|
|
2239
|
+
for (const m of messages) {
|
|
2240
|
+
// Each branch constructs a literal with ONE fixed role, not m.role (which is
|
|
2241
|
+
// still typed as the 4-way union): a union-VALUED field on a single object
|
|
2242
|
+
// does not structurally match a union of role-discriminated objects, so
|
|
2243
|
+
// widening back to m.role here would reintroduce the original TS2345.
|
|
2244
|
+
if (m.role === 'system') mastraMessages.push({ role: 'system', content: wireText(m.content) });
|
|
2245
|
+
else if (m.role === 'assistant') mastraMessages.push({ role: 'assistant', content: wireText(m.content) });
|
|
2246
|
+
else if (m.role === 'user') {
|
|
2247
|
+
const parts = wireParts(m.content);
|
|
2248
|
+
// Plain string unless the turn actually carries an attachment.
|
|
2249
|
+
if (parts.every((p) => p.kind === 'text')) {
|
|
2250
|
+
mastraMessages.push({ role: 'user', content: wireText(m.content) });
|
|
2251
|
+
} else {
|
|
2252
|
+
mastraMessages.push({
|
|
2253
|
+
role: 'user',
|
|
2254
|
+
content: parts.map((p) =>
|
|
2255
|
+
p.kind === 'text'
|
|
2256
|
+
? { type: 'text' as const, text: p.text }
|
|
2257
|
+
: {
|
|
2258
|
+
type: 'file' as const,
|
|
2259
|
+
mediaType: p.mediaType,
|
|
2260
|
+
...(p.filename === undefined ? {} : { filename: p.filename }),
|
|
2261
|
+
data: p.source.type === 'data' ? p.source.data : new URL(p.source.url),
|
|
2262
|
+
},
|
|
2263
|
+
),
|
|
2264
|
+
});
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
let agentStream: Awaited<ReturnType<ReturnType<typeof mastra.getAgent>['stream']>>;
|
|
2270
|
+
try {
|
|
2271
|
+
agentStream = await mastra.getAgent('supportAgent').stream(mastraMessages);
|
|
2272
|
+
} catch (err) {
|
|
2273
|
+
// A REAL status, before a byte is streamed: an unreachable MASTRA_URL or an
|
|
2274
|
+
// unknown agent id fails here. Returning 200 would send this JSON out
|
|
2275
|
+
// labelled text/event-stream, the SSE reader would find no frame, and the
|
|
2276
|
+
// turn would resolve empty with nothing logged and no bubble.
|
|
2277
|
+
return new Response(
|
|
2278
|
+
JSON.stringify({ error: { message: err instanceof Error ? err.message : 'Mastra agent failed to start' } }),
|
|
2279
|
+
{ status: 502, headers: { 'Content-Type': 'application/json' } },
|
|
2280
|
+
);
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
const encoder = new TextEncoder();
|
|
2284
|
+
const body = new ReadableStream({
|
|
2285
|
+
async start(controller) {
|
|
2286
|
+
try {
|
|
2287
|
+
// The client SDK has no textStream: it hands back a Response wrapped
|
|
2288
|
+
// with processDataStream, which replays the agent's SSE body as typed
|
|
2289
|
+
// chunks. Only text-delta carries browser-facing content; tool-call and
|
|
2290
|
+
// reasoning chunks exist here too but are out of scope for this route
|
|
2291
|
+
// (see streamMapping) and are left unhandled rather than half-wired.
|
|
2292
|
+
await agentStream.processDataStream({
|
|
2293
|
+
onChunk: (chunk) => {
|
|
2294
|
+
if (chunk.type === 'text-delta') {
|
|
2295
|
+
const openaiChunk = { choices: [{ delta: { content: chunk.payload.text } }] };
|
|
2296
|
+
controller.enqueue(encoder.encode(\`data: \${JSON.stringify(openaiChunk)}\\n\\n\`));
|
|
2297
|
+
} else if (chunk.type === 'error') {
|
|
2298
|
+
// Mastra reports some failures IN BAND as an 'error' chunk instead
|
|
2299
|
+
// of rejecting the stream; fold it into the same frame the catch
|
|
2300
|
+
// below emits so readOpenAIStream lands it on turn.error either way.
|
|
2301
|
+
const message = chunk.payload.error instanceof Error ? chunk.payload.error.message : 'Mastra agent reported an error';
|
|
2302
|
+
controller.enqueue(encoder.encode(\`data: \${JSON.stringify({ error: { message } })}\\n\\n\`));
|
|
2303
|
+
}
|
|
2304
|
+
},
|
|
2305
|
+
});
|
|
2306
|
+
} catch (err) {
|
|
2307
|
+
// The status is spent once the stream started, so report IN BAND:
|
|
2308
|
+
// readOpenAIStream lands this on turn.error and keeps what streamed.
|
|
2309
|
+
const message = err instanceof Error ? err.message : 'Mastra stream failed';
|
|
2310
|
+
controller.enqueue(encoder.encode(\`data: \${JSON.stringify({ error: { message } })}\\n\\n\`));
|
|
2311
|
+
}
|
|
2312
|
+
controller.enqueue(encoder.encode('data: [DONE]\\n\\n'));
|
|
2313
|
+
controller.close();
|
|
2314
|
+
},
|
|
2315
|
+
});
|
|
2316
|
+
|
|
2317
|
+
return new Response(body, {
|
|
2318
|
+
status: 200,
|
|
2319
|
+
headers: {
|
|
2320
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
2321
|
+
// no-transform stops a proxy buffering the stream into one blob.
|
|
2322
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
2323
|
+
Connection: 'keep-alive',
|
|
2324
|
+
},
|
|
2325
|
+
});
|
|
2326
|
+
}`,
|
|
2327
|
+
streamMapping: "Mastra's client SDK has no textStream: agent.stream() returns a Response wrapped with processDataStream({ onChunk }), which replays the agent's SSE body as typed chunks (text-delta, tool-call, reasoning-delta, error, finish, ...). Filter to type === 'text-delta' and emit data: {choices:[{delta:{content:payload.text}}]} frames; close with data: [DONE]. readOpenAIStream from @kitn.ai/ui/wire parses that, including tool calls and reasoning, but this route only wires text-delta: re-frame tool-call/tool-result and reasoning-delta chunks onto delta.tool_calls and delta.reasoning to get them too.",
|
|
2328
|
+
// No install list here: `deps` below is the one, and the scaffolder emits it.
|
|
2329
|
+
// This sentence used to end "Install @mastra/client-js." — right on the day it
|
|
2330
|
+
// was written, and unchecked against the route's imports ever after.
|
|
2331
|
+
runNote: "Set MASTRA_URL to your Mastra server base URL (mastra dev exposes POST /api/agents/:agentId/stream on port 4111).",
|
|
2332
|
+
docsSlug: "integrations/harnesses",
|
|
2333
|
+
// Nothing. The Mastra agent owns its model and its tools server-side.
|
|
2334
|
+
forwardsFromClient: [],
|
|
2335
|
+
// The route's one import.
|
|
2336
|
+
deps: { npm: ["@mastra/client-js"], pip: [] },
|
|
2337
|
+
// THE JUDGEMENT CALL IN THIS TABLE, so it is argued rather than asserted.
|
|
2338
|
+
//
|
|
2339
|
+
// MASTRA_URL is not a secret — it is a base URL, and the schema's SECRET_ENV_VAR
|
|
2340
|
+
// net deliberately does not match it. So the automatic check cannot decide this
|
|
2341
|
+
// one, and 'frontend-safe' would parse cleanly.
|
|
2342
|
+
//
|
|
2343
|
+
// It is still 'needs-proxy', on what the route actually is: this integration
|
|
2344
|
+
// ships a server `chatHandler` that reads `process.env` at module scope, and
|
|
2345
|
+
// nothing else. There is no browser-direct path in it. Declaring it
|
|
2346
|
+
// 'frontend-safe' would have the CLI point a public bundle at an unauthenticated
|
|
2347
|
+
// agent endpoint — a decision a scaffolder must not make silently, even though
|
|
2348
|
+
// no key leaks. The conservative direction is the cheap one: a needless server
|
|
2349
|
+
// hop costs a process, the other error costs the endpoint.
|
|
2350
|
+
keyExposure: "needs-proxy",
|
|
2351
|
+
// THE JUDGEMENT CALL FOR THIS FIELD, exactly as keyExposure above is, and for
|
|
2352
|
+
// a related reason: no automatic check can decide it. MASTRA_URL is a base URL
|
|
2353
|
+
// for a server this integration does not ship and cannot start; the route's
|
|
2354
|
+
// only mention of loopback is inside a `throw new Error(...)` string, which
|
|
2355
|
+
// LOOPBACK_FETCH deliberately does not match (a net that fired on prose would
|
|
2356
|
+
// be right here by accident and wrong elsewhere). So 'none' would parse
|
|
2357
|
+
// cleanly.
|
|
2358
|
+
//
|
|
2359
|
+
// It is 'local-server' on what the integration actually requires: a Mastra
|
|
2360
|
+
// server has to be reachable at MASTRA_URL before the first message works,
|
|
2361
|
+
// `mastra dev` is how a developer gets one (port 4111), and `@mastra/client-js`
|
|
2362
|
+
// is a CLIENT for it, not the server itself. Printing "start your Mastra
|
|
2363
|
+
// server" costs a line; omitting it produces a scaffold that fetches a URL
|
|
2364
|
+
// nothing answers.
|
|
2365
|
+
outOfBand: "local-server"
|
|
2366
|
+
};
|
|
2367
|
+
var mastra_default = mastra;
|
|
2368
|
+
|
|
2369
|
+
// ../ui/src/agent-tooling/integrations/pi.ts
|
|
2370
|
+
var pi = {
|
|
2371
|
+
id: "pi",
|
|
2372
|
+
title: "Pi",
|
|
2373
|
+
category: "harness",
|
|
2374
|
+
language: "ts",
|
|
2375
|
+
streamFormat: "native",
|
|
2376
|
+
envVars: [],
|
|
2377
|
+
routeTemplates: {
|
|
2378
|
+
// Express only, and genuinely so: the bridge spawns a local process, which a
|
|
2379
|
+
// Worker or an edge runtime cannot do. Every other framework gets this under
|
|
2380
|
+
// the scaffolder's "cannot host" warning, the same way pydantic-ai emits a
|
|
2381
|
+
// whole FastAPI service.
|
|
2382
|
+
express: `// server.ts
|
|
2383
|
+
import express from 'express';
|
|
2384
|
+
import { spawn } from 'node:child_process';
|
|
2385
|
+
import type { OpenAIWireMessage } from '@kitn.ai/ui/wire';
|
|
2386
|
+
|
|
2387
|
+
const app = express();
|
|
2388
|
+
app.use(express.json());
|
|
2389
|
+
|
|
2390
|
+
// POST /api/chat: bridge a Pi RPC session to the browser as SSE.
|
|
2391
|
+
app.post('/api/chat', (req, res) => {
|
|
2392
|
+
const { messages } = req.body as { messages: OpenAIWireMessage[] };
|
|
2393
|
+
const last = messages.at(-1)?.content;
|
|
2394
|
+
// \`content\` is a plain string until the turn carries an attachment, at which
|
|
2395
|
+
// point it is an ARRAY of content parts. Pi's RPC mode takes a TEXT prompt and
|
|
2396
|
+
// has no channel for a file, so an attachment is REFUSED here. Passing the
|
|
2397
|
+
// array straight through would JSON.stringify an object graph into the prompt
|
|
2398
|
+
// \u2014 no type error, no crash, just a model reading serialised noise.
|
|
2399
|
+
if (Array.isArray(last) && last.some((part) => part.type !== 'text')) {
|
|
2400
|
+
res.status(400).json({
|
|
2401
|
+
error: {
|
|
2402
|
+
message:
|
|
2403
|
+
'This Pi bridge forwards a text prompt only and has no channel for an attachment. Extract the file content into the message text, or send it through a tool.',
|
|
2404
|
+
},
|
|
2405
|
+
});
|
|
2406
|
+
return;
|
|
2407
|
+
}
|
|
2408
|
+
const prompt =
|
|
2409
|
+
typeof last === 'string'
|
|
2410
|
+
? last
|
|
2411
|
+
: (last ?? []).map((part) => (part.type === 'text' ? part.text : '')).join('');
|
|
2412
|
+
|
|
2413
|
+
// Headers before the first frame: without text/event-stream the browser
|
|
2414
|
+
// buffers the body and readOpenAIStream never sees a frame.
|
|
2415
|
+
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
|
2416
|
+
// no-transform stops a proxy buffering the stream into one blob.
|
|
2417
|
+
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
|
2418
|
+
res.setHeader('Connection', 'keep-alive');
|
|
2419
|
+
|
|
2420
|
+
// ONE PROCESS PER REQUEST. Pi in RPC mode holds conversation state, so a
|
|
2421
|
+
// module-level process would leak one caller's turns into the next caller's
|
|
2422
|
+
// reply and serialise every request behind a single stdin.
|
|
2423
|
+
const pi = spawn('pi', ['--mode', 'rpc', '--no-session']);
|
|
2424
|
+
|
|
2425
|
+
// A spawn failure (pi not on PATH) arrives asynchronously, by which point the
|
|
2426
|
+
// SSE headers are out and the status is spent, so report IN BAND:
|
|
2427
|
+
// readOpenAIStream lands this on turn.error instead of leaving a blank bubble.
|
|
2428
|
+
pi.on('error', (err) => {
|
|
2429
|
+
res.write(\`data: \${JSON.stringify({ error: { message: err.message } })}\\n\\n\`);
|
|
2430
|
+
res.write('data: [DONE]\\n\\n');
|
|
2431
|
+
res.end();
|
|
2432
|
+
});
|
|
2433
|
+
|
|
2434
|
+
// Send the user's turn. Pi commands are { type, message }.
|
|
2435
|
+
pi.stdin.write(JSON.stringify({ type: 'prompt', message: prompt }) + '\\n');
|
|
2436
|
+
|
|
2437
|
+
let buffer = '';
|
|
2438
|
+
pi.stdout.on('data', (chunk: Buffer) => {
|
|
2439
|
+
buffer += chunk.toString();
|
|
2440
|
+
// Split on \\n rather than readline: readline also breaks on the Unicode
|
|
2441
|
+
// separators U+2028/U+2029, which appear inside model output.
|
|
2442
|
+
const lines = buffer.split('\\n');
|
|
2443
|
+
// pop() is \`string | undefined\`, and the buffer has to stay a string.
|
|
2444
|
+
buffer = lines.pop() ?? ''; // hold the partial line for the next chunk
|
|
2445
|
+
for (const line of lines) {
|
|
2446
|
+
if (!line) continue;
|
|
2447
|
+
const event = JSON.parse(line);
|
|
2448
|
+
const part = event.assistantMessageEvent;
|
|
2449
|
+
if (event.type === 'message_update' && part?.type === 'text_delta') {
|
|
2450
|
+
res.write(\`data: \${JSON.stringify({ choices: [{ delta: { content: part.delta } }] })}\\n\\n\`);
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
});
|
|
2454
|
+
pi.on('close', () => { res.write('data: [DONE]\\n\\n'); res.end(); });
|
|
2455
|
+
});
|
|
2456
|
+
|
|
2457
|
+
app.listen(3001, () => console.log('chat api: http://localhost:3001/api/chat'));
|
|
2458
|
+
// The front end fetches a RELATIVE /api/chat, so proxy that path to this port
|
|
2459
|
+
// from your dev server, or serve both from one origin.`
|
|
2460
|
+
},
|
|
2461
|
+
streamMapping: "Pi runs as a local stdio process in RPC mode (pi --mode rpc --no-session). It emits newline-delimited JSON events on stdout. Map message_update events where assistantMessageEvent.type === 'text_delta' to data: {choices:[{delta:{content:part.delta}}]} SSE frames; send data: [DONE] on close. Split stdout on \\n (not readline, which breaks on Unicode separators U+2028/U+2029). Pi also emits thinking_delta and toolcall_* events; re-frame them onto delta.reasoning and delta.tool_calls in the same frames. readOpenAIStream from @kitn.ai/ui/wire parses it, including tool calls and reasoning.",
|
|
2462
|
+
runNote: "Pi must be installed locally and available on PATH as 'pi'. No API key is required by the bridge itself; Pi uses its own credentials. Pi runs with full user permissions, so sandbox it before exposing to a public endpoint. See the RPC reference: https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/rpc.md",
|
|
2463
|
+
docsSlug: "integrations/harnesses",
|
|
2464
|
+
// Nothing. Pi runs with its own credentials, model and tools; the bridge
|
|
2465
|
+
// forwards a prompt, not a model id.
|
|
2466
|
+
forwardsFromClient: [],
|
|
2467
|
+
// `express` only. The bridge's other two imports are excluded by the rule:
|
|
2468
|
+
// `node:child_process` is a builtin and '@kitn.ai/ui/wire' is the kit. Pi
|
|
2469
|
+
// itself is NOT an npm dep of the app — runNote requires it on PATH, and the
|
|
2470
|
+
// route reaches it with spawn('pi'), not an import.
|
|
2471
|
+
deps: { npm: ["express"], pip: [] },
|
|
2472
|
+
// No key anywhere: envVars is empty and Pi uses its own credentials on disk.
|
|
2473
|
+
// This is 'needs-proxy' for the OTHER reason the flag covers — a server-only
|
|
2474
|
+
// capability. The bridge calls spawn(), which no browser can do, and runNote
|
|
2475
|
+
// warns that Pi runs with full user permissions and wants sandboxing before it
|
|
2476
|
+
// is exposed. "No key" is not the same fact as "safe in the browser", and
|
|
2477
|
+
// collapsing the two is exactly how this flag would be got wrong.
|
|
2478
|
+
keyExposure: "needs-proxy",
|
|
2479
|
+
// Not 'local-server': nothing is listening in advance. The bridge spawns
|
|
2480
|
+
// `pi --mode rpc` per request, so what the developer supplies is an EXECUTABLE
|
|
2481
|
+
// on PATH ("Pi must be installed locally and available on PATH as 'pi'"), and
|
|
2482
|
+
// "start the server first" would be the wrong instruction to print. Pi is not
|
|
2483
|
+
// in `deps` for the same reason — the route reaches it with spawn(), not an
|
|
2484
|
+
// import. The schema's SPAWNS_PROCESS net catches this one independently.
|
|
2485
|
+
outOfBand: "local-binary"
|
|
2486
|
+
};
|
|
2487
|
+
var pi_default = pi;
|
|
2488
|
+
|
|
2489
|
+
// ../ui/src/agent-tooling/integrations/pydantic-ai.ts
|
|
2490
|
+
var pydanticAi = {
|
|
2491
|
+
id: "pydantic-ai",
|
|
2492
|
+
title: "Pydantic AI",
|
|
2493
|
+
category: "framework",
|
|
2494
|
+
language: "python",
|
|
2495
|
+
streamFormat: "openai-sse",
|
|
2496
|
+
envVars: ["OPENAI_API_KEY"],
|
|
2497
|
+
routeTemplates: {
|
|
2498
|
+
fastapi: `# main.py
|
|
2499
|
+
import json
|
|
2500
|
+
from fastapi import FastAPI, HTTPException
|
|
2501
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
2502
|
+
from fastapi.responses import StreamingResponse
|
|
2503
|
+
from pydantic import BaseModel
|
|
2504
|
+
from pydantic_ai import Agent
|
|
2505
|
+
|
|
2506
|
+
agent = Agent('openai:gpt-4o')
|
|
2507
|
+
|
|
2508
|
+
app = FastAPI()
|
|
2509
|
+
app.add_middleware(
|
|
2510
|
+
CORSMiddleware, allow_origins=['*'], allow_methods=['*'], allow_headers=['*']
|
|
2511
|
+
)
|
|
2512
|
+
|
|
2513
|
+
class ContentPart(BaseModel):
|
|
2514
|
+
type: str
|
|
2515
|
+
text: str | None = None
|
|
2516
|
+
|
|
2517
|
+
class Message(BaseModel):
|
|
2518
|
+
role: str
|
|
2519
|
+
# A plain string until the turn carries an attachment, at which point the
|
|
2520
|
+
# wire sends an ARRAY of content parts. Declaring this \`str\` alone made a
|
|
2521
|
+
# message with a file a 422 that named pydantic rather than the cause.
|
|
2522
|
+
content: str | list[ContentPart] | None = None
|
|
2523
|
+
|
|
2524
|
+
class ChatRequest(BaseModel):
|
|
2525
|
+
messages: list[Message]
|
|
2526
|
+
|
|
2527
|
+
def prompt_text(message: Message) -> str:
|
|
2528
|
+
"""The text of a turn, refusing what this route cannot carry.
|
|
2529
|
+
|
|
2530
|
+
agent.run_stream() takes a TEXT prompt, so an image or a document has no
|
|
2531
|
+
channel here. Dropping it would send the model a turn that silently lost the
|
|
2532
|
+
user's file; a 400 that names the reason is the honest failure.
|
|
2533
|
+
"""
|
|
2534
|
+
if message.content is None:
|
|
2535
|
+
return ''
|
|
2536
|
+
if isinstance(message.content, str):
|
|
2537
|
+
return message.content
|
|
2538
|
+
if any(part.type != 'text' for part in message.content):
|
|
2539
|
+
raise HTTPException(
|
|
2540
|
+
status_code=400,
|
|
2541
|
+
detail='This route forwards a text prompt only and has no channel for an attachment. '
|
|
2542
|
+
'Extract the file content into the message text, or send it through a tool.',
|
|
2543
|
+
)
|
|
2544
|
+
return ''.join(part.text or '' for part in message.content)
|
|
2545
|
+
|
|
2546
|
+
async def openai_sse(messages: list[Message]):
|
|
2547
|
+
prompt = prompt_text(messages[-1]) if messages else ''
|
|
2548
|
+
async with agent.run_stream(prompt) as result:
|
|
2549
|
+
async for delta in result.stream_text(delta=True):
|
|
2550
|
+
chunk = {'choices': [{'delta': {'content': delta}}]}
|
|
2551
|
+
yield f'data: {json.dumps(chunk)}\\n\\n'
|
|
2552
|
+
yield 'data: [DONE]\\n\\n'
|
|
2553
|
+
|
|
2554
|
+
@app.post('/api/chat')
|
|
2555
|
+
async def chat(req: ChatRequest):
|
|
2556
|
+
return StreamingResponse(openai_sse(req.messages), media_type='text/event-stream')`
|
|
2557
|
+
},
|
|
2558
|
+
streamMapping: "Pydantic AI's agent.run_stream() yields text deltas via result.stream_text(delta=True). Each delta is re-framed as a data: {choices:[{delta:{content}}]} SSE line and the stream closes with data: [DONE]. readOpenAIStream from @kitn.ai/ui/wire parses tool calls and reasoning too, but stream_text() yields text only: iterate the run's events instead and re-frame its tool-call events onto delta.tool_calls to fill kai-tool.",
|
|
2559
|
+
// No install list here: `deps` below is the one, and the scaffolder emits it.
|
|
2560
|
+
// This sentence used to open "Install: pip install pydantic-ai fastapi
|
|
2561
|
+
// uvicorn." — three of the four packages, missing the `pydantic` its own route
|
|
2562
|
+
// imports on the next line.
|
|
2563
|
+
runNote: "Set OPENAI_API_KEY. Run: uvicorn main:app --reload (default port 8000). Point kai-chat at http://localhost:8000/api/chat.",
|
|
2564
|
+
docsSlug: "integrations/pydantic-ai",
|
|
2565
|
+
// Nothing. Agent('openai:gpt-4o') pins the model and the agent registers its
|
|
2566
|
+
// own tools, both server-side.
|
|
2567
|
+
forwardsFromClient: [],
|
|
2568
|
+
// The only python integration, so the only non-empty `pip`. Three of the four
|
|
2569
|
+
// are the route's own imports (`pydantic_ai` is imported under its module
|
|
2570
|
+
// name and installed under its hyphenated one); `uvicorn` is the ASGI server
|
|
2571
|
+
// the run note starts, which nothing imports and without which the app cannot
|
|
2572
|
+
// run. That asymmetry is why the pip guard checks imports ⊆ declared and not
|
|
2573
|
+
// the reverse.
|
|
2574
|
+
deps: { npm: [], pip: ["pydantic-ai", "fastapi", "pydantic", "uvicorn"] },
|
|
2575
|
+
// Agent('openai:gpt-4o') reads OPENAI_API_KEY inside the python process.
|
|
2576
|
+
//
|
|
2577
|
+
// Worth being precise about, because this one looks like an exception: the
|
|
2578
|
+
// FastAPI app sets CORS to allow_origins=['*'] and the browser fetches it
|
|
2579
|
+
// DIRECTLY on :8000, with no JS proxy in front. That still is not
|
|
2580
|
+
// 'frontend-safe' — the flag asks where the SECRET lives, and it lives in the
|
|
2581
|
+
// python process. The FastAPI service IS the server hop.
|
|
2582
|
+
keyExposure: "needs-proxy",
|
|
2583
|
+
// The emitted backend is a FastAPI service: a python interpreter, the four
|
|
2584
|
+
// `deps.pip` packages, and `uvicorn main:app` on its own port — none of which a
|
|
2585
|
+
// node toolchain provides or starts. It also needs OPENAI_API_KEY, so this is
|
|
2586
|
+
// the entry that proves the groups are not mutually exclusive: a runtime
|
|
2587
|
+
// prerequisite and a key at once. The prompt should lead with the runtime,
|
|
2588
|
+
// because a key is useless until the service runs. The schema's `language ===
|
|
2589
|
+
// 'python'` net catches this one independently.
|
|
2590
|
+
outOfBand: "language-runtime"
|
|
2591
|
+
};
|
|
2592
|
+
var pydantic_ai_default = pydanticAi;
|
|
2593
|
+
|
|
2594
|
+
// ../ui/src/agent-tooling/integrations/mock.ts
|
|
2595
|
+
var mock = {
|
|
2596
|
+
id: "mock",
|
|
2597
|
+
title: "Mock (local preview)",
|
|
2598
|
+
category: "mock",
|
|
2599
|
+
language: "ts",
|
|
2600
|
+
streamFormat: "native",
|
|
2601
|
+
envVars: [],
|
|
2602
|
+
routeTemplates: {},
|
|
2603
|
+
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.",
|
|
2604
|
+
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.",
|
|
2605
|
+
docsSlug: "integrations/mock",
|
|
2606
|
+
// Nothing: there is no HTTP request at all. The frames are produced in-process
|
|
2607
|
+
// by createMockResponder(), so there is no body to forward a model or a tools
|
|
2608
|
+
// array on, and no route that would read one.
|
|
2609
|
+
forwardsFromClient: [],
|
|
2610
|
+
// Nothing to install: there is no route to install anything for.
|
|
2611
|
+
deps: { npm: [], pip: [] },
|
|
2612
|
+
// The other 'frontend-safe' entry, and the only one that is true by absence:
|
|
2613
|
+
// no routeTemplates, no webRoute, no envVars. There is no request, no upstream
|
|
2614
|
+
// and no secret, so there is nothing a public bundle could give away. This is
|
|
2615
|
+
// the one place where "declares nothing" genuinely means safe — which is
|
|
2616
|
+
// precisely why it still has to SAY so rather than be left blank.
|
|
2617
|
+
keyExposure: "frontend-safe",
|
|
2618
|
+
// Nothing at all, and here that is the literal truth rather than a shorthand:
|
|
2619
|
+
// there is no route, no upstream and no process. This is the "No backend"
|
|
2620
|
+
// group of create-kai's gateway prompt all by itself.
|
|
2621
|
+
outOfBand: "none"
|
|
2622
|
+
};
|
|
2623
|
+
var mock_default = mock;
|
|
2624
|
+
|
|
2625
|
+
// ../ui/src/agent-tooling/archetypes.ts
|
|
2626
|
+
var archetypes = [
|
|
2627
|
+
{
|
|
2628
|
+
id: "drop-in-chat",
|
|
2629
|
+
title: "Drop-in chat",
|
|
2630
|
+
components: ["kai-chat"],
|
|
2631
|
+
defaultPlacement: "full-page",
|
|
2632
|
+
docsSlug: "examples/drop-in-chat"
|
|
2633
|
+
},
|
|
2634
|
+
{
|
|
2635
|
+
id: "support-widget",
|
|
2636
|
+
title: "Support widget",
|
|
2637
|
+
components: ["kai-chat"],
|
|
2638
|
+
defaultPlacement: "docked-widget",
|
|
2639
|
+
docsSlug: "examples/support-widget"
|
|
2640
|
+
},
|
|
2641
|
+
{
|
|
2642
|
+
id: "knowledge-base",
|
|
2643
|
+
title: "Knowledge base / RAG",
|
|
2644
|
+
components: ["kai-chat", "kai-sources"],
|
|
2645
|
+
defaultPlacement: "full-page",
|
|
2646
|
+
docsSlug: "examples/knowledge-base"
|
|
2647
|
+
},
|
|
2648
|
+
{
|
|
2649
|
+
id: "agentic",
|
|
2650
|
+
title: "Agentic assistant",
|
|
2651
|
+
components: ["kai-chat", "kai-tool", "kai-reasoning"],
|
|
2652
|
+
defaultPlacement: "side",
|
|
2653
|
+
docsSlug: "examples/agentic-assistant"
|
|
2654
|
+
},
|
|
2655
|
+
{
|
|
2656
|
+
id: "workspace",
|
|
2657
|
+
title: "Agentic workspace",
|
|
2658
|
+
components: ["kai-chat", "kai-artifact", "kai-resizable"],
|
|
2659
|
+
defaultPlacement: "side",
|
|
2660
|
+
docsSlug: "examples/workspace"
|
|
2661
|
+
},
|
|
2662
|
+
{
|
|
2663
|
+
id: "voice",
|
|
2664
|
+
title: "Voice assistant",
|
|
2665
|
+
components: ["kai-chat", "kai-voice-input"],
|
|
2666
|
+
defaultPlacement: "full-page",
|
|
2667
|
+
docsSlug: "examples/voice-assistant"
|
|
2668
|
+
},
|
|
2669
|
+
/**
|
|
2670
|
+
* THE PRESET THAT EXISTS TO MAKE A CAPABILITY REACHABLE, not to name a product
|
|
2671
|
+
* shape someone asked for.
|
|
2672
|
+
*
|
|
2673
|
+
* `kai-file-upload` and `kai-attachments` were registered elements that NO
|
|
2674
|
+
* preset composed, and `listCapabilityGroups` derives its answer from this
|
|
2675
|
+
* table — so the attachments capability reached neither `listSurfaceProbes`
|
|
2676
|
+
* nor `verify:scaffold`, and `renderSurface` had no branch that emitted either
|
|
2677
|
+
* tag. A caller could pass them in `components` (the axis takes any list) and
|
|
2678
|
+
* get two bare `<kai-file-upload></kai-file-upload>` / `<kai-attachments>`
|
|
2679
|
+
* siblings with nothing wired to them: on screen, inert, silent.
|
|
2680
|
+
*
|
|
2681
|
+
* The pair is ONE capability for the same reason `kai-artifact` +
|
|
2682
|
+
* `kai-resizable` are: a dropzone with no list is a black hole, and a list
|
|
2683
|
+
* with no dropzone can never fill. `hasAttachments` in scaffold.ts requires
|
|
2684
|
+
* both for exactly that reason.
|
|
2685
|
+
*
|
|
2686
|
+
* The components match the create-kai spec's own feature table
|
|
2687
|
+
* (`attachments` -> `kai-file-upload`, `kai-attachments`), so the CLI's
|
|
2688
|
+
* multi-select and this preset name the same surface rather than two.
|
|
2689
|
+
*/
|
|
2690
|
+
{
|
|
2691
|
+
id: "attachments",
|
|
2692
|
+
title: "File attachments",
|
|
2693
|
+
components: ["kai-chat", "kai-file-upload", "kai-attachments"],
|
|
2694
|
+
defaultPlacement: "full-page",
|
|
2695
|
+
docsSlug: "examples/attachments"
|
|
2696
|
+
}
|
|
2697
|
+
];
|
|
2698
|
+
var BASE_COMPONENT = "kai-chat";
|
|
2699
|
+
function listCapabilityGroups() {
|
|
2700
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2701
|
+
for (const a of archetypes) {
|
|
2702
|
+
const components = a.components.filter((c) => c !== BASE_COMPONENT);
|
|
2703
|
+
if (components.length === 0) continue;
|
|
2704
|
+
const key = [...components].sort().join(",");
|
|
2705
|
+
if (seen.has(key)) continue;
|
|
2706
|
+
seen.set(key, { id: components.map((c) => c.replace(/^kai-/, "")).join("+"), components });
|
|
2707
|
+
}
|
|
2708
|
+
return [...seen.values()];
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
// ../ui/src/agent-tooling/registry.ts
|
|
2712
|
+
var integrations = [
|
|
2713
|
+
openai_default,
|
|
2714
|
+
anthropic_default,
|
|
2715
|
+
openrouter_default,
|
|
2716
|
+
vercel_ai_sdk_default,
|
|
2717
|
+
langgraph_default,
|
|
2718
|
+
cloudflare_default,
|
|
2719
|
+
ollama_default,
|
|
2720
|
+
mastra_default,
|
|
2721
|
+
pi_default,
|
|
2722
|
+
pydantic_ai_default,
|
|
2723
|
+
mock_default
|
|
2724
|
+
];
|
|
2725
|
+
function getIntegration(id) {
|
|
2726
|
+
return integrations.find((i) => i.id === id);
|
|
2727
|
+
}
|
|
2728
|
+
function listIntegrations() {
|
|
2729
|
+
return integrations;
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
// ../ui/src/agent-tooling/route-emit.ts
|
|
2733
|
+
var CLIENT_MODEL_IDS = {
|
|
2734
|
+
// Vendor-prefixed `vendor/model`: OpenRouter's own id space, and the ONLY one
|
|
2735
|
+
// of the three where the prefix belongs.
|
|
2736
|
+
openrouter: "openai/gpt-4o-mini",
|
|
2737
|
+
// No vendor prefix. This is what the route already pinned, so moving the knob
|
|
2738
|
+
// to the client changes the wire not at all.
|
|
2739
|
+
openai: "gpt-4o-mini",
|
|
2740
|
+
// Anthropic's id space. Matches what the route pinned; 'claude-sonnet-5' and
|
|
2741
|
+
// 'claude-haiku-4-5' are the cheaper swaps (see this integration's runNote).
|
|
2742
|
+
anthropic: "claude-opus-5"
|
|
2743
|
+
};
|
|
2744
|
+
function defaultModelFor(integration) {
|
|
2745
|
+
if (!integration.forwardsFromClient.includes("model")) return void 0;
|
|
2746
|
+
const id = CLIENT_MODEL_IDS[integration.id];
|
|
2747
|
+
if (id === void 0) {
|
|
2748
|
+
throw new Error(
|
|
2749
|
+
`Integration '${integration.id}' forwards the client's 'model' but has no CLIENT_MODEL_IDS entry, so the scaffold would emit a model id that is not valid for the host its route POSTs to. Add one in agent-tooling/route-emit.ts.`
|
|
2750
|
+
);
|
|
2751
|
+
}
|
|
2752
|
+
return id;
|
|
2753
|
+
}
|
|
2754
|
+
var CHAT_REQUEST_BODY_IMPORT = `import type { OpenAIWireMessage } from '@kitn.ai/ui/wire';`;
|
|
2755
|
+
var CHAT_REQUEST_BODY_DECL = [
|
|
2756
|
+
`/**`,
|
|
2757
|
+
` * What the front end POSTs. \`request.json()\` is \`unknown\` (it is whatever the`,
|
|
2758
|
+
` * client sent), so the body is narrowed once here instead of at every use \u2014`,
|
|
2759
|
+
` * without it this route does not compile under a server tsconfig. Widen it as`,
|
|
2760
|
+
` * you add fields of your own.`,
|
|
2761
|
+
` */`,
|
|
2762
|
+
`type ChatRequestBody = {`,
|
|
2763
|
+
` messages: OpenAIWireMessage[];`,
|
|
2764
|
+
` model?: string;`,
|
|
2765
|
+
` tools?: unknown[];`,
|
|
2766
|
+
`};`,
|
|
2767
|
+
``,
|
|
2768
|
+
`/** Narrow the JSON body once, at the edge. */`,
|
|
2769
|
+
`async function readChatRequest(request: Request): Promise<ChatRequestBody> {`,
|
|
2770
|
+
` return (await request.json()) as ChatRequestBody;`,
|
|
2771
|
+
`}`
|
|
2772
|
+
];
|
|
2773
|
+
var CONTENT_PARTS_DECL = [
|
|
2774
|
+
`/** Where an attachment's bytes are: inline base64, or an address the PROVIDER`,
|
|
2775
|
+
` * fetches. Never both. */`,
|
|
2776
|
+
`type WireFileSource = { type: 'data'; data: string } | { type: 'url'; url: string };`,
|
|
2777
|
+
``,
|
|
2778
|
+
`/** One piece of a turn, with the string and array content forms flattened into`,
|
|
2779
|
+
` * a single shape. */`,
|
|
2780
|
+
`type WirePart =`,
|
|
2781
|
+
` | { kind: 'text'; text: string }`,
|
|
2782
|
+
` | { kind: 'file'; mediaType: string; filename?: string; source: WireFileSource };`,
|
|
2783
|
+
``,
|
|
2784
|
+
`const DATA_URI = /^data:([^;,]+);base64,([\\s\\S]*)$/;`,
|
|
2785
|
+
``,
|
|
2786
|
+
`/**`,
|
|
2787
|
+
` * Flatten a wire message's content into parts.`,
|
|
2788
|
+
` *`,
|
|
2789
|
+
` * An image sent by URL has no media type here \u2014 \`image_url\` carries only the`,
|
|
2790
|
+
` * address \u2014 so it reports the top-level segment \`'image'\`, which is all a URL`,
|
|
2791
|
+
` * source needs. Only images can reach that branch: the kit refuses to encode a`,
|
|
2792
|
+
` * remote PDF rather than guess at one.`,
|
|
2793
|
+
` */`,
|
|
2794
|
+
`function wireParts(content: OpenAIWireMessage['content']): WirePart[] {`,
|
|
2795
|
+
` if (content == null) return [];`,
|
|
2796
|
+
` if (typeof content === 'string') return content === '' ? [] : [{ kind: 'text', text: content }];`,
|
|
2797
|
+
` return content.map((part): WirePart => {`,
|
|
2798
|
+
` if (part.type === 'text') return { kind: 'text', text: part.text };`,
|
|
2799
|
+
` if (part.type === 'image_url') {`,
|
|
2800
|
+
` const asData = DATA_URI.exec(part.image_url.url);`,
|
|
2801
|
+
` return asData`,
|
|
2802
|
+
` ? { kind: 'file', mediaType: asData[1], source: { type: 'data', data: asData[2] } }`,
|
|
2803
|
+
` : { kind: 'file', mediaType: 'image', source: { type: 'url', url: part.image_url.url } };`,
|
|
2804
|
+
` }`,
|
|
2805
|
+
` const asData = DATA_URI.exec(part.file.file_data);`,
|
|
2806
|
+
` if (!asData) {`,
|
|
2807
|
+
` // LOUD on purpose. \`file_data\` is a data URI on this wire; anything else`,
|
|
2808
|
+
` // cannot be turned into bytes without fetching it, and forwarding a turn`,
|
|
2809
|
+
` // with the attachment quietly missing is the bug this whole path exists`,
|
|
2810
|
+
` // to prevent.`,
|
|
2811
|
+
` throw new Error(`,
|
|
2812
|
+
` 'Unsupported file content part: file_data must be a data: URI of the form data:<media type>;base64,<data>.',`,
|
|
2813
|
+
` );`,
|
|
2814
|
+
` }`,
|
|
2815
|
+
` return {`,
|
|
2816
|
+
` kind: 'file',`,
|
|
2817
|
+
` mediaType: asData[1],`,
|
|
2818
|
+
` filename: part.file.filename,`,
|
|
2819
|
+
` source: { type: 'data', data: asData[2] },`,
|
|
2820
|
+
` };`,
|
|
2821
|
+
` });`,
|
|
2822
|
+
`}`,
|
|
2823
|
+
``,
|
|
2824
|
+
`/** Just the text of a turn. System, assistant and tool messages are text-only`,
|
|
2825
|
+
` * on this wire, so this collapses the array form for them. */`,
|
|
2826
|
+
`function wireText(content: OpenAIWireMessage['content']): string {`,
|
|
2827
|
+
` return wireParts(content)`,
|
|
2828
|
+
` .map((p) => (p.kind === 'text' ? p.text : ''))`,
|
|
2829
|
+
` .join('');`,
|
|
2830
|
+
`}`
|
|
2831
|
+
];
|
|
2832
|
+
var PREAMBLE_DECLARATION = /^(?:export\s+)?(?:async\s+)?(?:function|type|interface|const|class)\s+([A-Za-z_$][\w$]*)/;
|
|
2833
|
+
function chatRoutePreamble(fragment) {
|
|
2834
|
+
const decl = /\bwire(?:Parts|Text)\s*\(/.test(fragment) ? [...CHAT_REQUEST_BODY_DECL, ``, ...CONTENT_PARTS_DECL] : [...CHAT_REQUEST_BODY_DECL];
|
|
2835
|
+
return {
|
|
2836
|
+
imports: [CHAT_REQUEST_BODY_IMPORT],
|
|
2837
|
+
decl,
|
|
2838
|
+
symbols: decl.flatMap((line) => PREAMBLE_DECLARATION.exec(line)?.[1] ?? [])
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
// src/catalog.ts
|
|
2843
|
+
var WIRED_GATEWAYS = /* @__PURE__ */ new Set(["mock", "openrouter", "anthropic"]);
|
|
2844
|
+
function wirableGateway(gatewayId, framework) {
|
|
2845
|
+
if (gatewayId === "mock") return null;
|
|
2846
|
+
if (!WIRED_GATEWAYS.has(gatewayId)) {
|
|
2847
|
+
return `gateway '${gatewayId}' is in the kit catalog but is not wired by this release yet. Available: ${[...WIRED_GATEWAYS].map((g2) => g2 === "mock" ? "none" : g2).join(", ")}`;
|
|
2848
|
+
}
|
|
2849
|
+
if (framework.route === null) {
|
|
2850
|
+
return `'${framework.id}' has no route destination yet, so gateway '${gatewayId}' cannot be wired for it. A keyed gateway needs a server route, and where that goes differs per framework. Run \`--list --json\` to see which frameworks declare one.`;
|
|
2851
|
+
}
|
|
2852
|
+
return null;
|
|
2853
|
+
}
|
|
2854
|
+
function mockIntegration() {
|
|
2855
|
+
const mock2 = getIntegration("mock");
|
|
2856
|
+
if (!mock2) {
|
|
2857
|
+
throw new Error("create-kai: the `mock` integration is missing from the kit catalog");
|
|
2858
|
+
}
|
|
2859
|
+
return mock2;
|
|
2860
|
+
}
|
|
2861
|
+
function rendererComponents() {
|
|
2862
|
+
const known = /* @__PURE__ */ new Set([BASE_COMPONENT]);
|
|
2863
|
+
for (const group of listCapabilityGroups()) {
|
|
2864
|
+
for (const component of group.components) known.add(component);
|
|
2865
|
+
}
|
|
2866
|
+
return known;
|
|
2867
|
+
}
|
|
2868
|
+
function listGateways() {
|
|
2869
|
+
const all = listIntegrations();
|
|
2870
|
+
const mock2 = all.filter((i) => i.id === "mock");
|
|
2871
|
+
const rest = all.filter((i) => i.id !== "mock");
|
|
2872
|
+
return [...mock2, ...rest].map((integration) => ({
|
|
2873
|
+
integration,
|
|
2874
|
+
wired: WIRED_GATEWAYS.has(integration.id)
|
|
2875
|
+
}));
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
// src/features.ts
|
|
2879
|
+
var FEATURES = [
|
|
2880
|
+
{
|
|
2881
|
+
id: "conversations",
|
|
2882
|
+
label: "Conversation history",
|
|
2883
|
+
hint: "sidebar of past chats, new-chat, switching",
|
|
2884
|
+
components: ["kai-conversations", "kai-resizable"],
|
|
2885
|
+
default: true
|
|
2886
|
+
},
|
|
2887
|
+
{
|
|
2888
|
+
id: "sources",
|
|
2889
|
+
label: "Sources and citations",
|
|
2890
|
+
hint: "inline citations plus a sources panel",
|
|
2891
|
+
components: ["kai-sources"],
|
|
2892
|
+
default: false
|
|
2893
|
+
},
|
|
2894
|
+
{
|
|
2895
|
+
id: "agentic",
|
|
2896
|
+
label: "Tools and reasoning",
|
|
2897
|
+
hint: "tool-call panels plus reasoning disclosure",
|
|
2898
|
+
components: ["kai-tool", "kai-reasoning"],
|
|
2899
|
+
default: false
|
|
2900
|
+
},
|
|
2901
|
+
{
|
|
2902
|
+
id: "artifacts",
|
|
2903
|
+
label: "Artifacts and preview",
|
|
2904
|
+
hint: "split view with a live artifact pane",
|
|
2905
|
+
components: ["kai-artifact", "kai-resizable"],
|
|
2906
|
+
default: false
|
|
2907
|
+
},
|
|
2908
|
+
{
|
|
2909
|
+
id: "voice",
|
|
2910
|
+
label: "Voice",
|
|
2911
|
+
hint: "mic input",
|
|
2912
|
+
components: ["kai-voice-input"],
|
|
2913
|
+
default: false
|
|
2914
|
+
},
|
|
2915
|
+
{
|
|
2916
|
+
id: "attachments",
|
|
2917
|
+
label: "Attachments",
|
|
2918
|
+
hint: "file upload plus attachment chips",
|
|
2919
|
+
components: ["kai-file-upload", "kai-attachments"],
|
|
2920
|
+
default: false
|
|
2921
|
+
}
|
|
2922
|
+
];
|
|
2923
|
+
function getFeature(id) {
|
|
2924
|
+
return FEATURES.find((f) => f.id === id);
|
|
2925
|
+
}
|
|
2926
|
+
var DEFAULT_FEATURES = FEATURES.filter((f) => f.default).map(
|
|
2927
|
+
(f) => f.id
|
|
2928
|
+
);
|
|
2929
|
+
var COMPOSED_ONLY = /* @__PURE__ */ new Set(["conversations"]);
|
|
2930
|
+
function featureEmit(feature, framework) {
|
|
2931
|
+
if (COMPOSED_ONLY.has(feature.id)) {
|
|
2932
|
+
return framework.composedWorkspace ? "composed" : "unavailable";
|
|
2933
|
+
}
|
|
2934
|
+
const known = rendererComponents();
|
|
2935
|
+
return feature.components.every((c) => known.has(c)) ? "renderer" : "unavailable";
|
|
2936
|
+
}
|
|
2937
|
+
function featureUnavailableReason(feature, framework) {
|
|
2938
|
+
if (featureEmit(feature, framework) !== "unavailable") return null;
|
|
2939
|
+
if (COMPOSED_ONLY.has(feature.id)) {
|
|
2940
|
+
return `feature '${feature.id}' cannot be emitted for ${framework.label}: it comes from the hand-composed workspace starter and ${framework.label} has none. No renderer emits a ${feature.components[0]} surface, so there is nothing to generate in its place.`;
|
|
2941
|
+
}
|
|
2942
|
+
const known = rendererComponents();
|
|
2943
|
+
const missing = feature.components.filter((c) => !known.has(c));
|
|
2944
|
+
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.`;
|
|
2945
|
+
}
|
|
2946
|
+
function availableFeatures(framework) {
|
|
2947
|
+
return FEATURES.filter((f) => featureEmit(f, framework) !== "unavailable");
|
|
2948
|
+
}
|
|
2949
|
+
function resolveSurface(featureIds, framework) {
|
|
2950
|
+
const chosen = [];
|
|
2951
|
+
for (const id of featureIds) {
|
|
2952
|
+
const feature = getFeature(id);
|
|
2953
|
+
if (!feature) return { ok: false, reason: `unknown feature '${id}'` };
|
|
2954
|
+
const unavailable = featureUnavailableReason(feature, framework);
|
|
2955
|
+
if (unavailable) return { ok: false, reason: unavailable };
|
|
2956
|
+
chosen.push(feature);
|
|
2957
|
+
}
|
|
2958
|
+
const composed = chosen.filter((f) => featureEmit(f, framework) === "composed");
|
|
2959
|
+
if (composed.length > 0) {
|
|
2960
|
+
if (chosen.length > composed.length) {
|
|
2961
|
+
const extra = chosen.filter((f) => !composed.includes(f)).map((f) => f.id);
|
|
2962
|
+
return {
|
|
2963
|
+
ok: false,
|
|
2964
|
+
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
|
+
};
|
|
2966
|
+
}
|
|
2967
|
+
return { ok: true, surface: { kind: "composed", features: featureIds } };
|
|
2968
|
+
}
|
|
2969
|
+
const components = ["kai-chat", ...new Set(chosen.flatMap((f) => f.components))];
|
|
2970
|
+
return { ok: true, surface: { kind: "generated", features: featureIds, components } };
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
// src/routes.ts
|
|
2974
|
+
var clientModelFor = defaultModelFor;
|
|
2975
|
+
var nextHost = {
|
|
2976
|
+
file: "app/api/chat/route.ts",
|
|
2977
|
+
runtime: "Next.js route handler (Node)",
|
|
2978
|
+
production: true,
|
|
2979
|
+
after: [
|
|
2980
|
+
``,
|
|
2981
|
+
`// Next.js App Router: the file exports the HTTP method.`,
|
|
2982
|
+
`export async function POST(req: Request): Promise<Response> {`,
|
|
2983
|
+
` return chatHandler(req);`,
|
|
2984
|
+
`}`
|
|
2985
|
+
]
|
|
2986
|
+
};
|
|
2987
|
+
function viteSpaHost() {
|
|
2988
|
+
return {
|
|
2989
|
+
file: "server/chat.ts",
|
|
2990
|
+
runtime: "Vite dev-server middleware (Node) \u2014 development only",
|
|
2991
|
+
production: false,
|
|
2992
|
+
after: [
|
|
2993
|
+
``,
|
|
2994
|
+
`// vite.config.ts reaches the handler through this export.`,
|
|
2995
|
+
`export { chatHandler };`
|
|
2996
|
+
],
|
|
2997
|
+
extra: [
|
|
2998
|
+
{
|
|
2999
|
+
path: "vite-chat-api.ts",
|
|
3000
|
+
contents: [
|
|
3001
|
+
`import type { Plugin } from 'vite';`,
|
|
3002
|
+
``,
|
|
3003
|
+
`import { chatHandler } from './server/chat';`,
|
|
3004
|
+
``,
|
|
3005
|
+
`/**`,
|
|
3006
|
+
` * Mount the chat handler on Vite's dev server.`,
|
|
3007
|
+
` *`,
|
|
3008
|
+
` * A Vite SPA has no server routes, so \`fetch('/api/chat')\` has nothing to`,
|
|
3009
|
+
` * answer it: the dev server serves static files, the request 404s with an`,
|
|
3010
|
+
` * HTML body, and the SSE reader fails on the first frame.`,
|
|
3011
|
+
` *`,
|
|
3012
|
+
` * DEV ONLY. \`vite build\` emits static assets and no server, so a deployed`,
|
|
3013
|
+
` * build has nothing behind this path. To ship, deploy \`server/chat.ts\` to a`,
|
|
3014
|
+
` * real server (a Next route, a SvelteKit endpoint, a Worker, Express) and`,
|
|
3015
|
+
` * point the fetch at it.`,
|
|
3016
|
+
` */`,
|
|
3017
|
+
`export function chatApiPlugin(): Plugin {`,
|
|
3018
|
+
` return {`,
|
|
3019
|
+
` name: 'chat-api',`,
|
|
3020
|
+
` configureServer(server) {`,
|
|
3021
|
+
` server.middlewares.use('/api/chat', async (req, res) => {`,
|
|
3022
|
+
` // THE try/catch IS NOT DEFENSIVE PADDING. Connect does not await this`,
|
|
3023
|
+
` // handler, so a rejection here is an unhandled promise rejection, and`,
|
|
3024
|
+
` // Node's default for that is to kill the process \u2014 \`npm run dev\` exits`,
|
|
3025
|
+
` // on the first upstream network failure and the browser is left with a`,
|
|
3026
|
+
` // dead server. Observed, not theorised: a POST with the provider`,
|
|
3027
|
+
` // unreachable took the dev server down with a TypeError.`,
|
|
3028
|
+
` try {`,
|
|
3029
|
+
` let body = '';`,
|
|
3030
|
+
` req.setEncoding('utf8');`,
|
|
3031
|
+
` for await (const chunk of req) body += chunk;`,
|
|
3032
|
+
``,
|
|
3033
|
+
` const response = await chatHandler(`,
|
|
3034
|
+
` new Request('http://localhost/api/chat', {`,
|
|
3035
|
+
` method: 'POST',`,
|
|
3036
|
+
` headers: { 'Content-Type': 'application/json' },`,
|
|
3037
|
+
` body,`,
|
|
3038
|
+
` }),`,
|
|
3039
|
+
` );`,
|
|
3040
|
+
``,
|
|
3041
|
+
` // The STATUS has to survive the bridge: a 401 from the provider that`,
|
|
3042
|
+
` // arrives at the browser as a 200 is a blank bubble and no error.`,
|
|
3043
|
+
` res.statusCode = response.status;`,
|
|
3044
|
+
` // Annotated because this tsconfig has no DOM lib, so Headers comes from`,
|
|
3045
|
+
` // @types/node and these params are implicitly \`any\` under noImplicitAny.`,
|
|
3046
|
+
` response.headers.forEach((value: string, key: string) => res.setHeader(key, value));`,
|
|
3047
|
+
` if (!response.body) {`,
|
|
3048
|
+
` res.end();`,
|
|
3049
|
+
` return;`,
|
|
3050
|
+
` }`,
|
|
3051
|
+
``,
|
|
3052
|
+
` // Write each chunk as it lands \u2014 buffering here defeats streaming.`,
|
|
3053
|
+
` const reader = response.body.getReader();`,
|
|
3054
|
+
` for (;;) {`,
|
|
3055
|
+
` const { value, done } = await reader.read();`,
|
|
3056
|
+
` if (done) break;`,
|
|
3057
|
+
` res.write(value);`,
|
|
3058
|
+
` }`,
|
|
3059
|
+
` res.end();`,
|
|
3060
|
+
` } catch (error) {`,
|
|
3061
|
+
` // Loudly, and as JSON: readOpenAIStream throws a WireError carrying`,
|
|
3062
|
+
` // this message, so the failure reaches the UI instead of being a`,
|
|
3063
|
+
` // bubble that never fills.`,
|
|
3064
|
+
` console.error('[chat-api]', error);`,
|
|
3065
|
+
` if (!res.headersSent) {`,
|
|
3066
|
+
` res.statusCode = 502;`,
|
|
3067
|
+
` res.setHeader('Content-Type', 'application/json');`,
|
|
3068
|
+
` res.end(`,
|
|
3069
|
+
` JSON.stringify({`,
|
|
3070
|
+
` error: { message: error instanceof Error ? error.message : String(error) },`,
|
|
3071
|
+
` }),`,
|
|
3072
|
+
` );`,
|
|
3073
|
+
` } else {`,
|
|
3074
|
+
` // Mid-stream: the status is already out, so the only honest move is`,
|
|
3075
|
+
` // to stop rather than append an error frame the parser would read`,
|
|
3076
|
+
` // as content.`,
|
|
3077
|
+
` res.end();`,
|
|
3078
|
+
` }`,
|
|
3079
|
+
` }`,
|
|
3080
|
+
` });`,
|
|
3081
|
+
` },`,
|
|
3082
|
+
` };`,
|
|
3083
|
+
`}`,
|
|
3084
|
+
``
|
|
3085
|
+
].join("\n")
|
|
3086
|
+
}
|
|
3087
|
+
]
|
|
3088
|
+
};
|
|
3089
|
+
}
|
|
3090
|
+
var REACT_ROUTE_HOST = viteSpaHost();
|
|
3091
|
+
var NEXT_ROUTE_HOST = nextHost;
|
|
3092
|
+
function emitRoute(integration, framework) {
|
|
3093
|
+
const host = framework.route;
|
|
3094
|
+
if (!host) return [];
|
|
3095
|
+
const fragment = integration.webRoute;
|
|
3096
|
+
if (!fragment) return [];
|
|
3097
|
+
const preamble = chatRoutePreamble(fragment);
|
|
3098
|
+
return [
|
|
3099
|
+
{
|
|
3100
|
+
path: host.file,
|
|
3101
|
+
contents: [
|
|
3102
|
+
`// ${host.file} \u2014 ${host.runtime}`,
|
|
3103
|
+
...preamble.imports,
|
|
3104
|
+
...host.before ?? [],
|
|
3105
|
+
``,
|
|
3106
|
+
...preamble.decl,
|
|
3107
|
+
``,
|
|
3108
|
+
fragment,
|
|
3109
|
+
...host.after,
|
|
3110
|
+
``
|
|
3111
|
+
].join("\n")
|
|
3112
|
+
},
|
|
3113
|
+
...host.extra ?? []
|
|
3114
|
+
];
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
// src/frameworks.ts
|
|
3118
|
+
var FRAMEWORKS = [
|
|
3119
|
+
{
|
|
3120
|
+
id: "react",
|
|
3121
|
+
label: "React",
|
|
3122
|
+
templateDir: "react",
|
|
3123
|
+
renderer: "react",
|
|
3124
|
+
registration: "elements",
|
|
3125
|
+
composedWorkspace: true,
|
|
3126
|
+
status: "ready",
|
|
3127
|
+
paths: {
|
|
3128
|
+
entry: "src/main.tsx",
|
|
3129
|
+
app: "src/App.tsx",
|
|
3130
|
+
components: "src/components",
|
|
3131
|
+
css: "src/index.css",
|
|
3132
|
+
env: ".env.local"
|
|
3133
|
+
},
|
|
3134
|
+
// A Vite SPA has no server, so the handler needs a dev-server plugin beside it.
|
|
3135
|
+
route: REACT_ROUTE_HOST
|
|
3136
|
+
},
|
|
3137
|
+
{
|
|
3138
|
+
id: "vue",
|
|
3139
|
+
label: "Vue",
|
|
3140
|
+
templateDir: "vue",
|
|
3141
|
+
renderer: "vue",
|
|
3142
|
+
registration: "elements",
|
|
3143
|
+
composedWorkspace: true,
|
|
3144
|
+
status: "ready",
|
|
3145
|
+
paths: {
|
|
3146
|
+
entry: "src/main.ts",
|
|
3147
|
+
app: "src/App.vue",
|
|
3148
|
+
components: "src/components",
|
|
3149
|
+
css: "src/index.css",
|
|
3150
|
+
env: ".env.local"
|
|
3151
|
+
},
|
|
3152
|
+
route: null
|
|
3153
|
+
},
|
|
3154
|
+
{
|
|
3155
|
+
id: "svelte",
|
|
3156
|
+
label: "Svelte",
|
|
3157
|
+
templateDir: "svelte",
|
|
3158
|
+
renderer: "svelte",
|
|
3159
|
+
registration: "elements",
|
|
3160
|
+
composedWorkspace: true,
|
|
3161
|
+
status: "ready",
|
|
3162
|
+
paths: {
|
|
3163
|
+
entry: "src/main.ts",
|
|
3164
|
+
app: "src/App.svelte",
|
|
3165
|
+
components: "src/components",
|
|
3166
|
+
css: "src/index.css",
|
|
3167
|
+
env: ".env.local"
|
|
3168
|
+
},
|
|
3169
|
+
route: null
|
|
3170
|
+
},
|
|
3171
|
+
{
|
|
3172
|
+
id: "solid",
|
|
3173
|
+
label: "SolidJS",
|
|
3174
|
+
templateDir: "solid",
|
|
3175
|
+
renderer: "solid",
|
|
3176
|
+
// The one target that imports the SolidJS components directly instead of
|
|
3177
|
+
// registering `kai-*` web components. Any future codegen has to branch on
|
|
3178
|
+
// this, which is why `kai.json` records it rather than leaving it to be
|
|
3179
|
+
// re-derived by parsing the entry file.
|
|
3180
|
+
registration: "solid",
|
|
3181
|
+
/**
|
|
3182
|
+
* TRUE since the starter became a hand-composed chat workspace — a sidebar
|
|
3183
|
+
* rail, a scrolling thread and a composer, wired to `createMockResponder()`
|
|
3184
|
+
* through `readOpenAIStream`, the same gateway the other five run.
|
|
3185
|
+
*
|
|
3186
|
+
* It is composed HARDER than they are, which is the point of this row rather
|
|
3187
|
+
* than an aside: the other five reach the thread through one `<kai-thread>`
|
|
3188
|
+
* tag, and Solid spells the list out as `<ChatContainer>` + a `<Message>` /
|
|
3189
|
+
* `<MessageBody>` per turn, because Solid is the kit's authored layer and
|
|
3190
|
+
* renders the components directly. So `conversations` is emittable here for
|
|
3191
|
+
* the same reason it is for React — there is a real rail to emit into.
|
|
3192
|
+
*/
|
|
3193
|
+
composedWorkspace: true,
|
|
3194
|
+
status: "ready",
|
|
3195
|
+
paths: {
|
|
3196
|
+
entry: "src/index.tsx",
|
|
3197
|
+
app: "src/App.tsx",
|
|
3198
|
+
// `src/components`, not the `src` this said while the starter was a single
|
|
3199
|
+
// 533-line `App.tsx` with nowhere else to put anything. It now has the same
|
|
3200
|
+
// `components/` seam every other composed starter has, and this path is
|
|
3201
|
+
// what a v2 `add` reads out of `kai.json` to decide where to WRITE a
|
|
3202
|
+
// generated component — so pointing it at `src` would have scattered
|
|
3203
|
+
// generated files next to the entry point.
|
|
3204
|
+
components: "src/components",
|
|
3205
|
+
// `src/styles.css`, not the `src/index.css` every other row carries. This
|
|
3206
|
+
// was wrong from the day the row was written and nothing caught it,
|
|
3207
|
+
// because `verifyDeclaredPaths` only runs against a READY framework's
|
|
3208
|
+
// template. Fixed here so the row is true before it is ever offered.
|
|
3209
|
+
css: "src/styles.css",
|
|
3210
|
+
env: ".env.local"
|
|
3211
|
+
},
|
|
3212
|
+
route: null
|
|
3213
|
+
},
|
|
3214
|
+
{
|
|
3215
|
+
id: "angular",
|
|
3216
|
+
label: "Angular",
|
|
3217
|
+
templateDir: "angular",
|
|
3218
|
+
renderer: "angular",
|
|
3219
|
+
registration: "elements",
|
|
3220
|
+
composedWorkspace: true,
|
|
3221
|
+
status: "ready",
|
|
3222
|
+
paths: {
|
|
3223
|
+
entry: "src/main.ts",
|
|
3224
|
+
app: "src/app/app.ts",
|
|
3225
|
+
components: "src/app",
|
|
3226
|
+
css: "src/styles.css",
|
|
3227
|
+
env: ".env.local"
|
|
3228
|
+
},
|
|
3229
|
+
route: null
|
|
3230
|
+
},
|
|
3231
|
+
{
|
|
3232
|
+
id: "html",
|
|
3233
|
+
label: "HTML (plain, Vite)",
|
|
3234
|
+
templateDir: "vanilla",
|
|
3235
|
+
renderer: "html",
|
|
3236
|
+
registration: "elements",
|
|
3237
|
+
composedWorkspace: true,
|
|
3238
|
+
status: "ready",
|
|
3239
|
+
paths: {
|
|
3240
|
+
entry: "src/main.ts",
|
|
3241
|
+
app: "src/main.ts",
|
|
3242
|
+
components: "src",
|
|
3243
|
+
css: "src/index.css",
|
|
3244
|
+
env: ".env.local"
|
|
3245
|
+
},
|
|
3246
|
+
route: null
|
|
3247
|
+
},
|
|
3248
|
+
{
|
|
3249
|
+
id: "nextjs",
|
|
3250
|
+
label: "Next.js",
|
|
3251
|
+
templateDir: "nextjs",
|
|
3252
|
+
renderer: "next",
|
|
3253
|
+
registration: "elements",
|
|
3254
|
+
// The starter is now the hand-composed workspace, prerendered by the App
|
|
3255
|
+
// Router: a `<Resizable>` split, `<Conversations>` in the rail, `<Thread>`
|
|
3256
|
+
// fed by `useKaiChat`, `<PromptInput>` below it, and the kit's
|
|
3257
|
+
// `createMockResponder` streaming through `readOpenAIStream`. So
|
|
3258
|
+
// `conversations` is emittable here exactly as it is for the six Vite rows.
|
|
3259
|
+
//
|
|
3260
|
+
// It was the last row to flip, and BOTH of the defects its old note predicted
|
|
3261
|
+
// were real. Recorded here because each was a class, not a typo:
|
|
3262
|
+
//
|
|
3263
|
+
// · `paths.css` named `app/globals.css`, which the starter did not have —
|
|
3264
|
+
// the third instance of a `css` entry copied down from the row above and
|
|
3265
|
+
// never opened, after Solid's and TanStack's. `declaredPathsProblem` only
|
|
3266
|
+
// runs on a READY framework, so a `planned` row is never graded against
|
|
3267
|
+
// its template and the defect waits for the flip. The composed starter
|
|
3268
|
+
// now HAS `app/globals.css`, so the path became true rather than being
|
|
3269
|
+
// edited to match a wrong file.
|
|
3270
|
+
//
|
|
3271
|
+
// · `app/layout.tsx` imported `@kitn.ai/ui/theme.css` while
|
|
3272
|
+
// `postcss.config.mjs` declared `plugins: {}` — the THIRD instance of the
|
|
3273
|
+
// Tailwind-source import (#216 hit it in TanStack, #217 proved Solid is
|
|
3274
|
+
// safe). Measured on the emitted asset before the fix: one raw `@theme {`
|
|
3275
|
+
// at-rule in `.next/static/css/*.css`, with `--color-background` defined
|
|
3276
|
+
// ONLY inside it. A browser discards an unknown at-rule whole, and
|
|
3277
|
+
// `next build` was green throughout. It now imports the pre-compiled
|
|
3278
|
+
// `theme.tokens.css` and the same count is zero.
|
|
3279
|
+
//
|
|
3280
|
+
// ITS BLAST RADIUS IS NARROWER THAN "the app renders on fallbacks",
|
|
3281
|
+
// which is what the audit note said and what a reader would repeat.
|
|
3282
|
+
// Measured in Chromium against a scaffolded project, defect twin beside
|
|
3283
|
+
// fixed: DARK mode is unaffected, because those tokens are a plain
|
|
3284
|
+
// `.dark` rule rather than `@theme`. In LIGHT mode `:root` carries no
|
|
3285
|
+
// `--color-*` at all, but chrome INSIDE a `kai-*` element still resolves
|
|
3286
|
+
// — the elements re-scope their own tokens onto slotted content, so the
|
|
3287
|
+
// sidebar still computed `rgb(244, 244, 245)`. What actually broke is the
|
|
3288
|
+
// chrome OUTSIDE every element: `.app` computed
|
|
3289
|
+
// `background-color: rgba(0, 0, 0, 0)` against `rgb(255, 255, 255)`
|
|
3290
|
+
// fixed. That is precisely what `theme.tokens.css` is documented to be
|
|
3291
|
+
// for, and the narrowness is why it survived review — the page looks
|
|
3292
|
+
// nearly right.
|
|
3293
|
+
//
|
|
3294
|
+
// THE RULE IS "DOES TAILWIND PROCESS THIS FILE", NOT "NEVER IMPORT
|
|
3295
|
+
// theme.css" — the second is the obvious lesson and it is wrong. Solid
|
|
3296
|
+
// imports the very same file and is correct, because its `styles.css`
|
|
3297
|
+
// says `@import "tailwindcss"` above it and `@tailwindcss/vite` is in the
|
|
3298
|
+
// pipeline, so `@theme` is COMPILED rather than discarded. A starter with
|
|
3299
|
+
// Tailwind imports `theme.css`; one without imports the pre-compiled
|
|
3300
|
+
// `theme.tokens.css`. This row has no Tailwind, so it wants the latter.
|
|
3301
|
+
composedWorkspace: true,
|
|
3302
|
+
status: "ready",
|
|
3303
|
+
paths: {
|
|
3304
|
+
entry: "app/layout.tsx",
|
|
3305
|
+
// `app/workspace.tsx`, NOT `app/page.tsx`. The page is a Server Component
|
|
3306
|
+
// that renders the client island next to it, which is the idiomatic App
|
|
3307
|
+
// Router shape and the starter's answer to "where does `'use client'` go".
|
|
3308
|
+
// The composed workspace — and so the `toOpenAIMessages(...)` expression the
|
|
3309
|
+
// emitted README quotes — lives in the island.
|
|
3310
|
+
app: "app/workspace.tsx",
|
|
3311
|
+
// `app/components`, not the bare `app` this row used to claim. Everything
|
|
3312
|
+
// under `app/` that is not a `page`/`layout`/`route` file is ignored by the
|
|
3313
|
+
// router, so colocation is fine — but a v2 `add` reads this to decide where
|
|
3314
|
+
// to WRITE a generated component, and `app` would scatter them next to the
|
|
3315
|
+
// route files.
|
|
3316
|
+
components: "app/components",
|
|
3317
|
+
css: "app/globals.css",
|
|
3318
|
+
env: ".env.local"
|
|
3319
|
+
},
|
|
3320
|
+
// The cheapest correct destination in the table: one file, no config edit,
|
|
3321
|
+
// and the same route answers `next dev` and `next start`.
|
|
3322
|
+
route: NEXT_ROUTE_HOST
|
|
3323
|
+
},
|
|
3324
|
+
{
|
|
3325
|
+
id: "tanstack-start",
|
|
3326
|
+
label: "TanStack Start",
|
|
3327
|
+
templateDir: "tanstack-start",
|
|
3328
|
+
renderer: "tanstack-start",
|
|
3329
|
+
registration: "elements",
|
|
3330
|
+
// The starter is now the hand-composed workspace, server-rendered: a
|
|
3331
|
+
// `<Resizable>` split, `<Conversations>` in the rail, `<Thread>` fed by
|
|
3332
|
+
// `useKaiChat`, `<PromptInput>` below it, and the kit's `createMockResponder`
|
|
3333
|
+
// streaming through `readOpenAIStream`. So `conversations` is emittable here
|
|
3334
|
+
// exactly as it is for the five Vite rows.
|
|
3335
|
+
composedWorkspace: true,
|
|
3336
|
+
status: "ready",
|
|
3337
|
+
paths: {
|
|
3338
|
+
entry: "src/router.tsx",
|
|
3339
|
+
app: "src/routes/index.tsx",
|
|
3340
|
+
// `src/components`, NOT `src/routes`, which this row used to claim. The
|
|
3341
|
+
// route files are compiled by the TanStack Router plugin to build the route
|
|
3342
|
+
// tree, so a v2 `add` dropping a plain component in there would be writing
|
|
3343
|
+
// into generated-routing territory. The starter keeps its shared components
|
|
3344
|
+
// outside `routes/` for the same reason.
|
|
3345
|
+
components: "src/components",
|
|
3346
|
+
css: "src/styles.css",
|
|
3347
|
+
env: ".env.local"
|
|
3348
|
+
},
|
|
3349
|
+
route: null
|
|
3350
|
+
}
|
|
3351
|
+
];
|
|
3352
|
+
function getFramework(id) {
|
|
3353
|
+
return FRAMEWORKS.find((f) => f.id === id);
|
|
3354
|
+
}
|
|
3355
|
+
function readyFrameworks() {
|
|
3356
|
+
return FRAMEWORKS.filter((f) => f.status === "ready");
|
|
3357
|
+
}
|
|
3358
|
+
var DEFAULT_FRAMEWORK = "react";
|
|
3359
|
+
|
|
3360
|
+
// src/args.ts
|
|
3361
|
+
var TAKES_VALUE = /* @__PURE__ */ new Set([
|
|
3362
|
+
"--framework",
|
|
3363
|
+
"--layout",
|
|
3364
|
+
"--widget-style",
|
|
3365
|
+
"--features",
|
|
3366
|
+
"--gateway",
|
|
3367
|
+
"--kit"
|
|
3368
|
+
]);
|
|
3369
|
+
function parseArgs(argv) {
|
|
3370
|
+
const out = { yes: false, list: false, json: false, help: false, version: false, errors: [] };
|
|
3371
|
+
for (let i = 0; i < argv.length; i++) {
|
|
3372
|
+
const arg = argv[i];
|
|
3373
|
+
let flag = arg;
|
|
3374
|
+
let value;
|
|
3375
|
+
if (flag.startsWith("--") && flag.includes("=")) {
|
|
3376
|
+
const eq = flag.indexOf("=");
|
|
3377
|
+
value = flag.slice(eq + 1);
|
|
3378
|
+
flag = flag.slice(0, eq);
|
|
3379
|
+
} else if (TAKES_VALUE.has(flag)) {
|
|
3380
|
+
value = argv[++i];
|
|
3381
|
+
if (value === void 0) {
|
|
3382
|
+
out.errors.push(`${flag} needs a value`);
|
|
3383
|
+
continue;
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
switch (flag) {
|
|
3387
|
+
case "--framework":
|
|
3388
|
+
out.framework = value;
|
|
3389
|
+
break;
|
|
3390
|
+
case "--layout":
|
|
3391
|
+
out.layout = value;
|
|
3392
|
+
break;
|
|
3393
|
+
case "--widget-style":
|
|
3394
|
+
out.widgetStyle = value;
|
|
3395
|
+
break;
|
|
3396
|
+
case "--features":
|
|
3397
|
+
out.features = value === void 0 || value === "" || value === "none" ? [] : value.split(",").map((f) => f.trim()).filter(Boolean);
|
|
3398
|
+
break;
|
|
3399
|
+
case "--gateway":
|
|
3400
|
+
out.gateway = value;
|
|
3401
|
+
break;
|
|
3402
|
+
case "--kit":
|
|
3403
|
+
out.kit = value;
|
|
3404
|
+
break;
|
|
3405
|
+
case "-y":
|
|
3406
|
+
case "--yes":
|
|
3407
|
+
out.yes = true;
|
|
3408
|
+
break;
|
|
3409
|
+
case "--install":
|
|
3410
|
+
out.install = true;
|
|
3411
|
+
break;
|
|
3412
|
+
case "--no-install":
|
|
3413
|
+
out.install = false;
|
|
3414
|
+
break;
|
|
3415
|
+
case "--list":
|
|
3416
|
+
out.list = true;
|
|
3417
|
+
break;
|
|
3418
|
+
case "--json":
|
|
3419
|
+
out.json = true;
|
|
3420
|
+
break;
|
|
3421
|
+
case "-h":
|
|
3422
|
+
case "--help":
|
|
3423
|
+
out.help = true;
|
|
3424
|
+
break;
|
|
3425
|
+
case "-v":
|
|
3426
|
+
case "--version":
|
|
3427
|
+
out.version = true;
|
|
3428
|
+
break;
|
|
3429
|
+
default:
|
|
3430
|
+
if (flag.startsWith("-")) out.errors.push(`unknown flag ${flag}`);
|
|
3431
|
+
else if (out.dir === void 0) out.dir = flag;
|
|
3432
|
+
else out.errors.push(`unexpected argument ${flag}`);
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
return out;
|
|
3436
|
+
}
|
|
3437
|
+
var ZERO_CONFIG = {
|
|
3438
|
+
name: "kai-app",
|
|
3439
|
+
framework: DEFAULT_FRAMEWORK,
|
|
3440
|
+
layout: "full-screen",
|
|
3441
|
+
features: DEFAULT_FEATURES,
|
|
3442
|
+
gateway: "mock"
|
|
3443
|
+
};
|
|
3444
|
+
function normalizeGateway(value) {
|
|
3445
|
+
if (value === void 0) return void 0;
|
|
3446
|
+
return value === "none" ? "mock" : value;
|
|
3447
|
+
}
|
|
3448
|
+
function validateProjectName(name) {
|
|
3449
|
+
if (name.length === 0) return "Project name cannot be empty";
|
|
3450
|
+
if (name.length > 214) return "Project name must be 214 characters or fewer";
|
|
3451
|
+
if (name.startsWith(".") || name.startsWith("_")) return "Project name cannot start with . or _";
|
|
3452
|
+
if (name !== name.toLowerCase()) return "Project name must be lowercase";
|
|
3453
|
+
if (!/^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(name)) {
|
|
3454
|
+
return "Project name may only contain letters, digits, and - . _ ~";
|
|
3455
|
+
}
|
|
3456
|
+
return null;
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3459
|
+
// src/generate.ts
|
|
3460
|
+
import { cp, mkdir, readFile, readdir, rename, stat, writeFile } from "node:fs/promises";
|
|
3461
|
+
import { existsSync } from "node:fs";
|
|
3462
|
+
import path from "node:path";
|
|
3463
|
+
import { fileURLToPath } from "node:url";
|
|
3464
|
+
|
|
3465
|
+
// src/kai-json.ts
|
|
3466
|
+
var KAI_JSON_VERSION = 1;
|
|
3467
|
+
var KAI_JSON_SCHEMA_URL = "https://ui.kitn.ai/schema/kai.json";
|
|
3468
|
+
function buildKaiJson(plan, framework) {
|
|
3469
|
+
return {
|
|
3470
|
+
$schema: KAI_JSON_SCHEMA_URL,
|
|
3471
|
+
version: KAI_JSON_VERSION,
|
|
3472
|
+
framework: plan.frameworkId,
|
|
3473
|
+
kit: plan.kit,
|
|
3474
|
+
layout: plan.layout,
|
|
3475
|
+
widgetStyle: plan.widgetStyle,
|
|
3476
|
+
features: [...plan.featureIds],
|
|
3477
|
+
gateway: plan.gatewayId,
|
|
3478
|
+
registration: framework.registration,
|
|
3479
|
+
paths: {
|
|
3480
|
+
...framework.paths,
|
|
3481
|
+
// No gateway means no route was written, so reporting where one WOULD go
|
|
3482
|
+
// would be reporting a file that does not exist.
|
|
3483
|
+
route: plan.gatewayId === "mock" ? null : framework.route?.file ?? null
|
|
3484
|
+
},
|
|
3485
|
+
theme: { tokens: "@kitn.ai/ui/theme.tokens.css", default: "dark" }
|
|
3486
|
+
};
|
|
3487
|
+
}
|
|
3488
|
+
function stringifyKaiJson(json) {
|
|
3489
|
+
return `${JSON.stringify(json, null, 2)}
|
|
3490
|
+
`;
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3493
|
+
// src/package-json.ts
|
|
3494
|
+
function rewritePackageJson(source, options) {
|
|
3495
|
+
const json = JSON.parse(source);
|
|
3496
|
+
json.name = options.name;
|
|
3497
|
+
json.version = "0.1.0";
|
|
3498
|
+
delete json.private;
|
|
3499
|
+
const deps = json.dependencies ?? {};
|
|
3500
|
+
const previousKitSpec = deps["@kitn.ai/ui"];
|
|
3501
|
+
if (previousKitSpec === void 0) {
|
|
3502
|
+
throw new Error(
|
|
3503
|
+
"create-kai: template package.json declares no `@kitn.ai/ui` dependency \u2014 refusing to emit it"
|
|
3504
|
+
);
|
|
3505
|
+
}
|
|
3506
|
+
deps["@kitn.ai/ui"] = options.kit;
|
|
3507
|
+
for (const pkg of options.gatewayDeps ?? []) {
|
|
3508
|
+
deps[pkg] = options.gatewayDepSpec?.(pkg) ?? "*";
|
|
3509
|
+
}
|
|
3510
|
+
json.dependencies = deps;
|
|
3511
|
+
return { json, previousKitSpec };
|
|
3512
|
+
}
|
|
3513
|
+
function stringifyPackageJson(json) {
|
|
3514
|
+
return `${JSON.stringify(json, null, 2)}
|
|
3515
|
+
`;
|
|
3516
|
+
}
|
|
3517
|
+
|
|
3518
|
+
// src/patches.ts
|
|
3519
|
+
function baseFlags(patch) {
|
|
3520
|
+
return patch.find.flags.replace(/g/g, "");
|
|
3521
|
+
}
|
|
3522
|
+
function patchRegExp(patch) {
|
|
3523
|
+
const flags = baseFlags(patch);
|
|
3524
|
+
return new RegExp(patch.find.source, patch.multiple === true ? `${flags}g` : flags);
|
|
3525
|
+
}
|
|
3526
|
+
var PATCHES = {
|
|
3527
|
+
react: [
|
|
3528
|
+
{
|
|
3529
|
+
file: "index.html",
|
|
3530
|
+
find: /<title>@kitn\.ai\/ui React example<\/title>/,
|
|
3531
|
+
replace: (name) => `<title>${name}</title>`,
|
|
3532
|
+
why: "the browser tab should name the user's app, not the kit's example"
|
|
3533
|
+
},
|
|
3534
|
+
{
|
|
3535
|
+
file: "vite.config.ts",
|
|
3536
|
+
find: /\/\/ `@kitn\.ai\/ui` is linked into this example via `workspace:\*`[\s\S]*?\n(?=\/\/ https:\/\/vite\.dev\/config\/)/,
|
|
3537
|
+
replace: () => "// `@kitn.ai/ui` ships compiled entry points and resolves through its own\n// `exports` map \u2014 no aliases, no transpile step needed.\n",
|
|
3538
|
+
why: "a scaffolded project is not a workspace member and has no `nx build ui` to run"
|
|
3539
|
+
}
|
|
3540
|
+
],
|
|
3541
|
+
vue: [
|
|
3542
|
+
{
|
|
3543
|
+
file: "index.html",
|
|
3544
|
+
find: /<title>@kitn\.ai\/ui Vue example<\/title>/,
|
|
3545
|
+
replace: (name) => `<title>${name}</title>`,
|
|
3546
|
+
why: "the browser tab should name the user's app, not the kit's example"
|
|
3547
|
+
},
|
|
3548
|
+
{
|
|
3549
|
+
/**
|
|
3550
|
+
* Vue's config carries TWO comment paragraphs where React's carries one:
|
|
3551
|
+
* the `workspace:*` note, then the `isCustomElement` note explaining why
|
|
3552
|
+
* every `kai-*` tag is passed through to the DOM instead of resolved as a
|
|
3553
|
+
* Vue component. Only the first is repo-internal, and the second is the
|
|
3554
|
+
* single most load-bearing line in a Vue consumer's config — drop it and
|
|
3555
|
+
* the app warns "unknown custom element" and renders nothing.
|
|
3556
|
+
*
|
|
3557
|
+
* So the lookahead stops at the blank comment line before it, rather than
|
|
3558
|
+
* at `// https://vite.dev/config/` the way React's does. Copying React's
|
|
3559
|
+
* pattern here would have eaten the Vue-specific note.
|
|
3560
|
+
*/
|
|
3561
|
+
file: "vite.config.ts",
|
|
3562
|
+
find: /\/\/ `@kitn\.ai\/ui` is linked into this example via `workspace:\*`[\s\S]*?\n(?=\/\/\n\/\/ The one Vue-specific bit)/,
|
|
3563
|
+
replace: () => "// `@kitn.ai/ui` ships compiled entry points and resolves through its own\n// `exports` map \u2014 no aliases, no transpile step needed.\n",
|
|
3564
|
+
why: "a scaffolded project is not a workspace member and has no `nx build ui` to run"
|
|
3565
|
+
}
|
|
3566
|
+
],
|
|
3567
|
+
angular: [
|
|
3568
|
+
{
|
|
3569
|
+
/**
|
|
3570
|
+
* Angular's index.html lives at `src/index.html`, not the project root the
|
|
3571
|
+
* way React's and Vue's do — `angular.json` names it as the `index` input
|
|
3572
|
+
* and the builder emits it to `dist/`. Same one-line edit, different path,
|
|
3573
|
+
* and the path is the part that a copy of the row above would get wrong.
|
|
3574
|
+
*/
|
|
3575
|
+
file: "src/index.html",
|
|
3576
|
+
find: /<title>@kitn\.ai\/ui Angular example<\/title>/,
|
|
3577
|
+
replace: (name) => `<title>${name}</title>`,
|
|
3578
|
+
why: "the browser tab should name the user's app, not the kit's example"
|
|
3579
|
+
},
|
|
3580
|
+
{
|
|
3581
|
+
/**
|
|
3582
|
+
* The one patch in this table that renames rather than rewrites, and the
|
|
3583
|
+
* only reason `multiple` exists — see the flag's docblock for why four
|
|
3584
|
+
* separate patches would have been the worse answer.
|
|
3585
|
+
*
|
|
3586
|
+
* `angular.json` is where Angular's builder learns what the project is
|
|
3587
|
+
* called, and unlike every other starter's config it is copied verbatim
|
|
3588
|
+
* with nothing rewriting it: `rewritePackageJson` only touches
|
|
3589
|
+
* package.json, and READMEs never reach a template at all. So a user who
|
|
3590
|
+
* scaffolded `my-app` got `ng build` writing into `dist/ui-example-angular`
|
|
3591
|
+
* and `ng serve` resolving a build target named after our example. Same
|
|
3592
|
+
* class as the "@kitn.ai/ui Vue example" browser tab, in the one file no
|
|
3593
|
+
* patch had ever opened.
|
|
3594
|
+
*
|
|
3595
|
+
* Matching the bare name rather than each quoted string is deliberate:
|
|
3596
|
+
* two of the four sites embed it in a longer value
|
|
3597
|
+
* (`dist/ui-example-angular`, `ui-example-angular:build:production`), so a
|
|
3598
|
+
* `"…"`-anchored find would need two shapes to cover what one substring
|
|
3599
|
+
* covers exactly.
|
|
3600
|
+
*/
|
|
3601
|
+
file: "angular.json",
|
|
3602
|
+
find: /ui-example-angular/,
|
|
3603
|
+
multiple: true,
|
|
3604
|
+
replace: (name) => name,
|
|
3605
|
+
why: "angular.json names the project; a scaffolded app must build into its own dist/<name>, not dist/ui-example-angular"
|
|
3606
|
+
}
|
|
3607
|
+
],
|
|
3608
|
+
svelte: [
|
|
3609
|
+
{
|
|
3610
|
+
file: "index.html",
|
|
3611
|
+
find: /<title>@kitn\.ai\/ui Svelte example<\/title>/,
|
|
3612
|
+
replace: (name) => `<title>${name}</title>`,
|
|
3613
|
+
why: "the browser tab should name the user's app, not the kit's example"
|
|
3614
|
+
},
|
|
3615
|
+
{
|
|
3616
|
+
/**
|
|
3617
|
+
* Same two-paragraph shape as Vue's, and the second paragraph is kept for
|
|
3618
|
+
* the same reason: it is about the FRAMEWORK, not about this repo. It
|
|
3619
|
+
* tells a Svelte reader why `kai-*` needs no `isCustomElement`-style
|
|
3620
|
+
* registration — the compiler treats any hyphenated tag as a native
|
|
3621
|
+
* custom element — which is the first thing someone coming from the Vue
|
|
3622
|
+
* config asks. So the lookahead stops at the blank comment line, exactly
|
|
3623
|
+
* as Vue's does.
|
|
3624
|
+
*/
|
|
3625
|
+
file: "vite.config.ts",
|
|
3626
|
+
find: /\/\/ `@kitn\.ai\/ui` is linked into this example via `workspace:\*`[\s\S]*?\n(?=\/\/\n\/\/ Unlike Vue, Svelte needs no)/,
|
|
3627
|
+
replace: () => "// `@kitn.ai/ui` ships compiled entry points and resolves through its own\n// `exports` map \u2014 no aliases, no transpile step needed.\n",
|
|
3628
|
+
why: "a scaffolded project is not a workspace member and has no `nx build ui` to run"
|
|
3629
|
+
}
|
|
3630
|
+
],
|
|
3631
|
+
solid: [
|
|
3632
|
+
{
|
|
3633
|
+
/**
|
|
3634
|
+
* The ONLY patch this template needs, and worth stating why rather than
|
|
3635
|
+
* leaving a reader to wonder which one went missing: every other Vite
|
|
3636
|
+
* starter also patches its `vite.config.ts` to strip the `workspace:*`
|
|
3637
|
+
* paragraph, and Solid's config has no such paragraph to strip. It is four
|
|
3638
|
+
* lines registering two plugins, with the kit resolved through its own
|
|
3639
|
+
* `exports` map. `emittedContentProblem` is what proves that — it scans the
|
|
3640
|
+
* PATCHED bytes of every file for repo-internal instructions — so this
|
|
3641
|
+
* comment is a signpost, not the evidence.
|
|
3642
|
+
*
|
|
3643
|
+
* WHAT THIS TITLE USED TO BE, because it is the string `titleProblems` was
|
|
3644
|
+
* written against:
|
|
3645
|
+
*
|
|
3646
|
+
* kai-chat — SolidJS Primitives Example
|
|
3647
|
+
*
|
|
3648
|
+
* Two separate things had gone stale in it. `kai-chat` is an ELEMENT name
|
|
3649
|
+
* being used as a product name, and the product it stood for was since
|
|
3650
|
+
* renamed to `@kitn.ai/ui`. The result contained no `@kitn.ai/ui` anywhere,
|
|
3651
|
+
* so the REPO_INTERNAL row could not match it and it would have shipped
|
|
3652
|
+
* into a user's browser tab the day this row flipped to `ready` — which is
|
|
3653
|
+
* exactly the hole `titleProblems` exists to close, and it caught this one
|
|
3654
|
+
* on the first build with the status flipped.
|
|
3655
|
+
*/
|
|
3656
|
+
file: "index.html",
|
|
3657
|
+
find: /<title>@kitn\.ai\/ui SolidJS example<\/title>/,
|
|
3658
|
+
replace: (name) => `<title>${name}</title>`,
|
|
3659
|
+
why: "the browser tab should name the user's app, not the kit's example"
|
|
3660
|
+
}
|
|
3661
|
+
],
|
|
3662
|
+
vanilla: [
|
|
3663
|
+
{
|
|
3664
|
+
file: "index.html",
|
|
3665
|
+
find: /<title>@kitn\.ai\/ui vanilla example<\/title>/,
|
|
3666
|
+
replace: (name) => `<title>${name}</title>`,
|
|
3667
|
+
why: "the browser tab should name the user's app, not the kit's example"
|
|
3668
|
+
},
|
|
3669
|
+
{
|
|
3670
|
+
/**
|
|
3671
|
+
* The one place a patch REWRITES the framework paragraph instead of
|
|
3672
|
+
* stopping short of it.
|
|
3673
|
+
*
|
|
3674
|
+
* Vue's and Svelte's second paragraphs describe their compilers, so they
|
|
3675
|
+
* survive the copy into a user's project unchanged. The vanilla starter's
|
|
3676
|
+
* describes ITSELF — "unlike the React/Vue examples", "this is the pure
|
|
3677
|
+
* web components, zero framework showcase" — which is true of the starter
|
|
3678
|
+
* sitting in `examples/` and false of the app the user just scaffolded.
|
|
3679
|
+
* Nothing in REPO_INTERNAL catches that, because it is not an instruction
|
|
3680
|
+
* the user cannot follow; it is a sentence that describes the wrong
|
|
3681
|
+
* project. So this patch spans both paragraphs and re-emits the half that
|
|
3682
|
+
* is about the browser, dropping the half that is about the repo.
|
|
3683
|
+
*/
|
|
3684
|
+
file: "vite.config.ts",
|
|
3685
|
+
find: /\/\/ `@kitn\.ai\/ui` is linked into this example via `workspace:\*`[\s\S]*?\n(?=\/\/ https:\/\/vite\.dev\/config\/)/,
|
|
3686
|
+
replace: () => "// `@kitn.ai/ui` ships compiled entry points and resolves through its own\n// `exports` map \u2014 no aliases, no transpile step needed.\n//\n// No framework plugin here: the browser upgrades the `kai-*` custom elements\n// natively, so there is nothing for a template compiler to learn about the\n// tags.\n",
|
|
3687
|
+
why: "a scaffolded project is not a workspace member and has no `nx build ui` to run"
|
|
3688
|
+
}
|
|
3689
|
+
],
|
|
3690
|
+
/**
|
|
3691
|
+
* The other STANDALONE starter, and the same pair of patches as
|
|
3692
|
+
* `tanstack-start` below for the same two reasons — a JS-declared document
|
|
3693
|
+
* title, and a `.npmrc` comment naming a monorepo-relative kit path. Different
|
|
3694
|
+
* files, because Next declares its title in `export const metadata` rather than
|
|
3695
|
+
* a route's `head()`.
|
|
3696
|
+
*/
|
|
3697
|
+
nextjs: [
|
|
3698
|
+
{
|
|
3699
|
+
/**
|
|
3700
|
+
* THE BROWSER TAB, VIA NEXT'S METADATA API.
|
|
3701
|
+
*
|
|
3702
|
+
* Unlike the TanStack row below, this one IS graded. `titleProblems` now
|
|
3703
|
+
* parses `export const metadata = { title }` and `generateMetadata()`
|
|
3704
|
+
* (src/template-guards.ts), so a Next starter that stopped being patched —
|
|
3705
|
+
* or that grew a second `metadata` export in a new route — fails the build
|
|
3706
|
+
* on the emitted bytes rather than relying on this patch still matching.
|
|
3707
|
+
* Both hold it: `patchMatchProblem` if the string is reworded,
|
|
3708
|
+
* `emittedContentProblem` if the resulting title is not the project name.
|
|
3709
|
+
*
|
|
3710
|
+
* The one shape to avoid when editing this: anything the parser cannot
|
|
3711
|
+
* read as a static string. A template literal, a variable, a call, or
|
|
3712
|
+
* Next's `title.template` (`'%s | Acme'`) all yield NO title to compare, so
|
|
3713
|
+
* the guard goes quiet rather than failing — see the stated limits on
|
|
3714
|
+
* `titleProblems`. Keep it a plain string literal.
|
|
3715
|
+
*/
|
|
3716
|
+
file: "app/layout.tsx",
|
|
3717
|
+
find: /'@kitn\.ai\/ui — Next\.js App Router example'/,
|
|
3718
|
+
replace: (name) => `'${name}'`,
|
|
3719
|
+
why: "the browser tab should name the user's app, not the kit's example"
|
|
3720
|
+
},
|
|
3721
|
+
{
|
|
3722
|
+
/**
|
|
3723
|
+
* The `.npmrc` comment, which is pure monorepo geography — see the
|
|
3724
|
+
* identical patch on the `tanstack-start` row for the full reasoning. The
|
|
3725
|
+
* replacement keeps this starter's own justification for `install-links`,
|
|
3726
|
+
* which is Next-specific and worth carrying into a user's project: webpack
|
|
3727
|
+
* resolves symlinks to their realpath, which would put the kit's prebuilt
|
|
3728
|
+
* `dist/` outside `node_modules` and hand it to the transpiler.
|
|
3729
|
+
*/
|
|
3730
|
+
file: ".npmrc",
|
|
3731
|
+
find: /# Pack the local `@kitn\.ai\/ui`[\s\S]*?\n(?=install-links=true)/,
|
|
3732
|
+
replace: () => "# A `file:` dependency pointing at a DIRECTORY is copied into node_modules\n# rather than symlinked. Next/webpack resolves a symlink to its realpath, which\n# would put a linked package's prebuilt dist/ outside node_modules and hand it\n# to the transpiler. Harmless if this project has no local dependencies.\n",
|
|
3733
|
+
why: "the comment explains a monorepo-relative kit path that does not exist in a user's project"
|
|
3734
|
+
}
|
|
3735
|
+
],
|
|
3736
|
+
/**
|
|
3737
|
+
* The first STANDALONE starter to go `ready`, and it needs a different pair of
|
|
3738
|
+
* patches from the VITE rows above — neither of the two files below exists in
|
|
3739
|
+
* any of them. (`nextjs`, added later and also standalone, sits directly above
|
|
3740
|
+
* with the same shape.)
|
|
3741
|
+
*
|
|
3742
|
+
* The six linked starters name the kit `workspace:*` and carry the repo-internal
|
|
3743
|
+
* instruction in `vite.config.ts`. This one names it
|
|
3744
|
+
* `file:../../../packages/ui`, and its vite.config says nothing about the
|
|
3745
|
+
* monorepo at all, so the React-shaped `vite.config.ts` patch would find nothing
|
|
3746
|
+
* to match here. Copying the row above is the wrong move; these are the two
|
|
3747
|
+
* files that actually carry the leak.
|
|
3748
|
+
*/
|
|
3749
|
+
"tanstack-start": [
|
|
3750
|
+
{
|
|
3751
|
+
/**
|
|
3752
|
+
* THE BROWSER TAB, SET IN JAVASCRIPT RATHER THAN HTML.
|
|
3753
|
+
*
|
|
3754
|
+
* TanStack Start has no `index.html`: the document title is a `head()` meta
|
|
3755
|
+
* entry on the root route, which is why this patch targets a `.tsx` file
|
|
3756
|
+
* where every other title patch in this table targets HTML.
|
|
3757
|
+
*
|
|
3758
|
+
* THAT DIFFERENCE COSTS REAL COVERAGE, and it is worth knowing rather than
|
|
3759
|
+
* discovering. `verifyTitles` (src/template-guards.ts) only reads `.html`
|
|
3760
|
+
* files, so it cannot grade this one — see the KNOWN LIMIT paragraph on
|
|
3761
|
+
* `titleProblems`, which names this exact case. What holds this title true
|
|
3762
|
+
* is therefore narrower than what holds the other five true:
|
|
3763
|
+
*
|
|
3764
|
+
* · `patchMatchProblem` fails the build if this `find` stops matching or
|
|
3765
|
+
* starts matching twice, so the title cannot be reworded silently.
|
|
3766
|
+
* · `generate.test.ts`'s tanstack-start block asserts the EMITTED
|
|
3767
|
+
* `__root.tsx` carries the project's name and no longer mentions the
|
|
3768
|
+
* kit's example, which is the assertion `verifyTitles` would have made.
|
|
3769
|
+
*
|
|
3770
|
+
* What neither covers is a SECOND document title added elsewhere in the
|
|
3771
|
+
* project — a new route with its own `head()`. Closing that needs
|
|
3772
|
+
* `titleProblems` to read a JS meta array, which needs a parser rather than
|
|
3773
|
+
* a regex (an object field named `title` is card data far more often than
|
|
3774
|
+
* it is a document title). Left as a stated gap rather than a regex that
|
|
3775
|
+
* would half-cover it.
|
|
3776
|
+
*/
|
|
3777
|
+
file: "src/routes/__root.tsx",
|
|
3778
|
+
find: /'@kitn\.ai\/ui — TanStack Start example'/,
|
|
3779
|
+
replace: (name) => `'${name}'`,
|
|
3780
|
+
why: "the browser tab should name the user's app, not the kit's example"
|
|
3781
|
+
},
|
|
3782
|
+
{
|
|
3783
|
+
/**
|
|
3784
|
+
* The `.npmrc` comment, which is pure monorepo geography.
|
|
3785
|
+
*
|
|
3786
|
+
* `install-links=true` itself is worth keeping — it is a general npm setting
|
|
3787
|
+
* with a general justification — but the three comment lines above it
|
|
3788
|
+
* explain why THIS EXAMPLE points at `file:../../../packages/ui`, a path
|
|
3789
|
+
* that climbs out of a user's project and resolves to nothing. That spec is
|
|
3790
|
+
* caught by the `file:` pattern in REPO_INTERNAL; `rewritePackageJson`
|
|
3791
|
+
* scrubs it from package.json, and the starter's package-lock.json never
|
|
3792
|
+
* reaches a template because `scripts/build.mjs` skips lockfiles. This
|
|
3793
|
+
* comment is the third and last place it appears.
|
|
3794
|
+
*
|
|
3795
|
+
* So the patch replaces the explanation rather than the directive, on the
|
|
3796
|
+
* same principle as the vanilla row above: keep the half that is about npm,
|
|
3797
|
+
* drop the half that is about where this repo keeps its packages.
|
|
3798
|
+
*/
|
|
3799
|
+
file: ".npmrc",
|
|
3800
|
+
find: /# Pack the local `@kitn\.ai\/ui`[\s\S]*?\n(?=install-links=true)/,
|
|
3801
|
+
replace: () => "# A `file:` dependency pointing at a DIRECTORY is copied into node_modules\n# rather than symlinked, so a local package resolves the way a published one\n# does. Harmless if this project has none.\n",
|
|
3802
|
+
why: "the comment explains a monorepo-relative kit path that does not exist in a user's project"
|
|
3803
|
+
}
|
|
3804
|
+
]
|
|
3805
|
+
};
|
|
3806
|
+
function goLivePatches() {
|
|
3807
|
+
return [
|
|
3808
|
+
{
|
|
3809
|
+
file: "",
|
|
3810
|
+
find: /import \{ readOpenAIStream \} from '@kitn\.ai\/ui\/wire';/,
|
|
3811
|
+
replace: () => `import { readOpenAIStream, toOpenAIMessages } from '@kitn.ai/ui/wire';`,
|
|
3812
|
+
why: "the live path encodes the thread with toOpenAIMessages before POSTing it"
|
|
3813
|
+
},
|
|
3814
|
+
{
|
|
3815
|
+
/**
|
|
3816
|
+
* Drop `mockResponse` from the import.
|
|
3817
|
+
*
|
|
3818
|
+
* NOT COSMETIC. Both starters compile under `noUnusedLocals: true`, so an
|
|
3819
|
+
* import left behind after its only call site is rewritten is TS6133 and a
|
|
3820
|
+
* hard `npm run build` failure in the emitted project — which is precisely
|
|
3821
|
+
* the class of defect the smoke test exists to catch, and the reason it
|
|
3822
|
+
* runs a real build rather than checking that files exist.
|
|
3823
|
+
*/
|
|
3824
|
+
file: "",
|
|
3825
|
+
find: /, newId, mockResponse \} from '\.\/chat-data';/,
|
|
3826
|
+
replace: () => `, newId } from './chat-data';`,
|
|
3827
|
+
why: "mockResponse is no longer called, and noUnusedLocals makes an unused import a build failure"
|
|
3828
|
+
},
|
|
3829
|
+
{
|
|
3830
|
+
/**
|
|
3831
|
+
* The doc comment above the component, which tells its reader the app runs
|
|
3832
|
+
* on a mock. It does not any more.
|
|
3833
|
+
*
|
|
3834
|
+
* The optional `\n \*` in the middle is not sloppiness: React's starter
|
|
3835
|
+
* wraps this sentence across two comment lines and Next's does not, and a
|
|
3836
|
+
* regex written against either one alone silently leaves the other's stale
|
|
3837
|
+
* sentence in a user's project.
|
|
3838
|
+
*/
|
|
3839
|
+
file: "",
|
|
3840
|
+
find: /Swap(\n \*)? `mockResponse\(text\)` for a real `fetch` to ship a real app\./,
|
|
3841
|
+
replace: ({ routeFile }) => `Its \`send\` posts to \`/api/chat\`, served by \`${routeFile}\`.`,
|
|
3842
|
+
why: "the emitted app no longer runs on the mock, so the comment describing it would be false"
|
|
3843
|
+
},
|
|
3844
|
+
{
|
|
3845
|
+
/**
|
|
3846
|
+
* The line above the append, which calls the reply a mock. It is not one
|
|
3847
|
+
* any more. Small, and included for the same reason the docblock sentence
|
|
3848
|
+
* above is: a comment that describes the wrong program is worse than no
|
|
3849
|
+
* comment, and these are the three places the starters say "mock" outside
|
|
3850
|
+
* the block that gets replaced wholesale.
|
|
3851
|
+
*/
|
|
3852
|
+
file: "",
|
|
3853
|
+
find: /message and stream the \(mock\) assistant reply\./,
|
|
3854
|
+
replace: () => `message and stream the assistant reply.`,
|
|
3855
|
+
why: "the reply is no longer a mock, so the comment calling it one would be false"
|
|
3856
|
+
},
|
|
3857
|
+
{
|
|
3858
|
+
/**
|
|
3859
|
+
* The call site itself — the one expression the README's go-live diff has
|
|
3860
|
+
* always described, applied for the user instead of handed to them.
|
|
3861
|
+
*
|
|
3862
|
+
* The `find` spans the whole NO-BACKEND comment block as well as the call,
|
|
3863
|
+
* because the comment explains a mock that is no longer there. Leaving it
|
|
3864
|
+
* would ship five lines telling the reader their real provider call is
|
|
3865
|
+
* canned SSE frames.
|
|
3866
|
+
*/
|
|
3867
|
+
file: "",
|
|
3868
|
+
find: /\/\/ NO BACKEND AND NO PROVIDER\.[\s\S]*?await readOpenAIStream\(mockResponse\(text\), stream\);/,
|
|
3869
|
+
replace: ({ thread, routeFile, gatewayTitle, model }) => [
|
|
3870
|
+
`// The key never reaches the browser: \`${routeFile}\` reads it from the`,
|
|
3871
|
+
` // environment and forwards this request to ${gatewayTitle}. The reply is`,
|
|
3872
|
+
` // parsed by the SAME reader that parsed the mock's frames.`,
|
|
3873
|
+
` const res = await fetch('/api/chat', {`,
|
|
3874
|
+
` method: 'POST',`,
|
|
3875
|
+
` headers: { 'content-type': 'application/json' },`,
|
|
3876
|
+
...model ? [
|
|
3877
|
+
` // Change this to any model id ${gatewayTitle} accepts. The route forwards`,
|
|
3878
|
+
` // it verbatim, so this is the whole of "use a different model".`,
|
|
3879
|
+
` body: JSON.stringify({ model: '${model}', messages: toOpenAIMessages(${thread}) }),`
|
|
3880
|
+
] : [` body: JSON.stringify({ messages: toOpenAIMessages(${thread}) }),`],
|
|
3881
|
+
` });`,
|
|
3882
|
+
` await readOpenAIStream(res, stream);`
|
|
3883
|
+
].join("\n"),
|
|
3884
|
+
why: "the emitted app must call the route instead of the mock responder"
|
|
3885
|
+
}
|
|
3886
|
+
];
|
|
3887
|
+
}
|
|
3888
|
+
function goLivePatchesFor(appFile) {
|
|
3889
|
+
return goLivePatches().map((patch) => ({ ...patch, file: appFile }));
|
|
3890
|
+
}
|
|
3891
|
+
var GATEWAY_PATCHES = {
|
|
3892
|
+
react: [
|
|
3893
|
+
...goLivePatchesFor("src/App.tsx"),
|
|
3894
|
+
{
|
|
3895
|
+
file: "vite.config.ts",
|
|
3896
|
+
find: /import react from '@vitejs\/plugin-react';/,
|
|
3897
|
+
replace: () => `import react from '@vitejs/plugin-react';
|
|
3898
|
+
// Mounts the chat route on the dev server. A Vite SPA has no server routes,
|
|
3899
|
+
// so without this \`fetch('/api/chat')\` 404s with an HTML body.
|
|
3900
|
+
import { chatApiPlugin } from './vite-chat-api';`,
|
|
3901
|
+
why: "a Vite SPA needs the dev-server plugin imported before it can be registered"
|
|
3902
|
+
},
|
|
3903
|
+
{
|
|
3904
|
+
file: "vite.config.ts",
|
|
3905
|
+
find: /plugins: \[react\(\)\],/,
|
|
3906
|
+
replace: () => `plugins: [react(), chatApiPlugin()],`,
|
|
3907
|
+
why: "the chat-api plugin has to be in the plugin list or the middleware never mounts"
|
|
3908
|
+
}
|
|
3909
|
+
],
|
|
3910
|
+
nextjs: goLivePatchesFor("app/workspace.tsx")
|
|
3911
|
+
};
|
|
3912
|
+
function gatewayPatchesFor(templateDir) {
|
|
3913
|
+
return GATEWAY_PATCHES[templateDir] ?? [];
|
|
3914
|
+
}
|
|
3915
|
+
function applyGatewayPatch(patch, source, context) {
|
|
3916
|
+
const find = patchRegExp(patch);
|
|
3917
|
+
if (!find.test(source)) {
|
|
3918
|
+
throw new Error(
|
|
3919
|
+
`create-kai: gateway patch for ${patch.file} no longer matches its template (${patch.why}). The starter changed; update GATEWAY_PATCHES in src/patches.ts.`
|
|
3920
|
+
);
|
|
3921
|
+
}
|
|
3922
|
+
find.lastIndex = 0;
|
|
3923
|
+
return source.replace(find, () => patch.replace(context));
|
|
3924
|
+
}
|
|
3925
|
+
function applyPatch(patch, source, projectName) {
|
|
3926
|
+
const find = patchRegExp(patch);
|
|
3927
|
+
if (!find.test(source)) {
|
|
3928
|
+
throw new Error(
|
|
3929
|
+
`create-kai: patch for ${patch.file} no longer matches its template (${patch.why}). The starter changed; update PATCHES in src/patches.ts.`
|
|
3930
|
+
);
|
|
3931
|
+
}
|
|
3932
|
+
find.lastIndex = 0;
|
|
3933
|
+
return source.replace(find, () => patch.replace(projectName));
|
|
3934
|
+
}
|
|
3935
|
+
function patchesFor(templateDir) {
|
|
3936
|
+
return PATCHES[templateDir] ?? [];
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3939
|
+
// src/generate.ts
|
|
3940
|
+
var GITIGNORE_TEMPLATE_NAME = "_gitignore";
|
|
3941
|
+
function defaultTemplateRoot() {
|
|
3942
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), "templates");
|
|
3943
|
+
}
|
|
3944
|
+
async function generate(plan, options = {}) {
|
|
3945
|
+
const framework = getFramework(plan.frameworkId);
|
|
3946
|
+
if (!framework) throw new Error(`create-kai: unknown framework '${plan.frameworkId}'`);
|
|
3947
|
+
const integration = plan.gatewayId === "mock" ? mockIntegration() : getIntegration(plan.gatewayId);
|
|
3948
|
+
if (!integration) throw new Error(`create-kai: unknown gateway '${plan.gatewayId}'`);
|
|
3949
|
+
const surface = resolveSurface(plan.featureIds, framework);
|
|
3950
|
+
if (!surface.ok) throw new Error(`create-kai: ${surface.reason}`);
|
|
3951
|
+
if (surface.surface.kind === "generated") {
|
|
3952
|
+
throw new Error(
|
|
3953
|
+
"create-kai: generated feature surfaces are not wired in this release \u2014 the composed workspace (conversation history) is the path that runs today"
|
|
3954
|
+
);
|
|
3955
|
+
}
|
|
3956
|
+
const templateRoot = options.templateRoot ?? defaultTemplateRoot();
|
|
3957
|
+
const templateDir = path.join(templateRoot, framework.templateDir);
|
|
3958
|
+
if (!existsSync(templateDir)) {
|
|
3959
|
+
throw new Error(
|
|
3960
|
+
`create-kai: no template for '${framework.id}' at ${templateDir}. Run the package build (it copies examples/starters/* into dist/templates).`
|
|
3961
|
+
);
|
|
3962
|
+
}
|
|
3963
|
+
await mkdir(plan.dir, { recursive: true });
|
|
3964
|
+
await cp(templateDir, plan.dir, { recursive: true });
|
|
3965
|
+
const underscored = path.join(plan.dir, GITIGNORE_TEMPLATE_NAME);
|
|
3966
|
+
if (existsSync(underscored)) {
|
|
3967
|
+
await rename(underscored, path.join(plan.dir, ".gitignore"));
|
|
3968
|
+
}
|
|
3969
|
+
for (const patch of patchesFor(framework.templateDir)) {
|
|
3970
|
+
const file = path.join(plan.dir, patch.file);
|
|
3971
|
+
await writeFile(file, applyPatch(patch, await readFile(file, "utf8"), plan.name), "utf8");
|
|
3972
|
+
}
|
|
3973
|
+
const pkgPath = path.join(plan.dir, "package.json");
|
|
3974
|
+
const { json, previousKitSpec } = rewritePackageJson(await readFile(pkgPath, "utf8"), {
|
|
3975
|
+
name: plan.name,
|
|
3976
|
+
kit: plan.kit,
|
|
3977
|
+
gatewayDeps: integration.deps.npm
|
|
3978
|
+
});
|
|
3979
|
+
await writeFile(pkgPath, stringifyPackageJson(json), "utf8");
|
|
3980
|
+
const appSource = await readFile(path.join(plan.dir, framework.paths.app), "utf8");
|
|
3981
|
+
const thread = goLiveThread(appSource, framework);
|
|
3982
|
+
const routeFiles = plan.gatewayId === "mock" ? [] : emitRoute(integration, framework);
|
|
3983
|
+
if (plan.gatewayId !== "mock") {
|
|
3984
|
+
if (routeFiles.length === 0) {
|
|
3985
|
+
throw new Error(
|
|
3986
|
+
framework.route === null ? `create-kai: '${framework.id}' has no route destination, so gateway '${plan.gatewayId}' cannot be wired for it. Scaffold with --gateway none, or use a framework whose route host is declared (see \`--list --json\`).` : `create-kai: gateway '${plan.gatewayId}' declares no webRoute, so there is no handler to emit into ${framework.route.file}. It should not be in WIRED_GATEWAYS.`
|
|
3987
|
+
);
|
|
3988
|
+
}
|
|
3989
|
+
for (const file of routeFiles) {
|
|
3990
|
+
const abs = path.join(plan.dir, file.path);
|
|
3991
|
+
await mkdir(path.dirname(abs), { recursive: true });
|
|
3992
|
+
await writeFile(abs, file.contents, "utf8");
|
|
3993
|
+
}
|
|
3994
|
+
for (const patch of gatewayPatchesFor(framework.templateDir)) {
|
|
3995
|
+
const file = path.join(plan.dir, patch.file);
|
|
3996
|
+
await writeFile(
|
|
3997
|
+
file,
|
|
3998
|
+
applyGatewayPatch(patch, await readFile(file, "utf8"), {
|
|
3999
|
+
thread,
|
|
4000
|
+
routeFile: routeFiles[0].path,
|
|
4001
|
+
gatewayTitle: integration.title,
|
|
4002
|
+
model: clientModelFor(integration)
|
|
4003
|
+
}),
|
|
4004
|
+
"utf8"
|
|
4005
|
+
);
|
|
4006
|
+
}
|
|
4007
|
+
if (integration.envVars.length > 0) {
|
|
4008
|
+
await writeFile(
|
|
4009
|
+
path.join(plan.dir, framework.paths.env),
|
|
4010
|
+
renderEnvFile(integration.envVars, integration.runNote),
|
|
4011
|
+
"utf8"
|
|
4012
|
+
);
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
4015
|
+
await writeFile(
|
|
4016
|
+
path.join(plan.dir, "kai.json"),
|
|
4017
|
+
stringifyKaiJson(buildKaiJson(plan, framework)),
|
|
4018
|
+
"utf8"
|
|
4019
|
+
);
|
|
4020
|
+
await writeFile(
|
|
4021
|
+
path.join(plan.dir, "README.md"),
|
|
4022
|
+
plan.gatewayId === "mock" ? renderMockReadme(plan, framework, thread, integration.docsSlug) : renderGatewayReadme(plan, framework, integration, routeFiles[0].path),
|
|
4023
|
+
"utf8"
|
|
4024
|
+
);
|
|
4025
|
+
return {
|
|
4026
|
+
dir: plan.dir,
|
|
4027
|
+
files: await listFiles(plan.dir),
|
|
4028
|
+
previousKitSpec,
|
|
4029
|
+
runNote: integration.runNote,
|
|
4030
|
+
docsSlug: integration.docsSlug
|
|
4031
|
+
};
|
|
4032
|
+
}
|
|
4033
|
+
function renderEnvFile(envVars, runNote) {
|
|
4034
|
+
return [
|
|
4035
|
+
`# ${runNote}`,
|
|
4036
|
+
`#`,
|
|
4037
|
+
`# Read by the chat route on the server. This file is gitignored \u2014 the key`,
|
|
4038
|
+
`# never reaches the browser and must never be committed.`,
|
|
4039
|
+
``,
|
|
4040
|
+
...envVars.map((name) => `${name}=replace-me`),
|
|
4041
|
+
``
|
|
4042
|
+
].join("\n");
|
|
4043
|
+
}
|
|
4044
|
+
var GO_LIVE_CALL = "toOpenAIMessages(";
|
|
4045
|
+
function goLiveThread(appSource, framework) {
|
|
4046
|
+
const call = appSource.indexOf(GO_LIVE_CALL);
|
|
4047
|
+
if (call >= 0) {
|
|
4048
|
+
const from = call + GO_LIVE_CALL.length;
|
|
4049
|
+
let depth = 1;
|
|
4050
|
+
for (let i = from; i < appSource.length; i++) {
|
|
4051
|
+
const ch = appSource[i];
|
|
4052
|
+
if (ch === "(") depth++;
|
|
4053
|
+
else if (ch === ")" && --depth === 0) return appSource.slice(from, i);
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
throw new Error(
|
|
4057
|
+
`create-kai: ${framework.paths.app} carries no balanced toOpenAIMessages(...) expression, so the README cannot state how to go live without inventing one. Restore the comment in the examples/starters/${framework.templateDir} starter.`
|
|
4058
|
+
);
|
|
4059
|
+
}
|
|
4060
|
+
function renderGatewayReadme(plan, framework, integration, routeFile) {
|
|
4061
|
+
const host = framework.route;
|
|
4062
|
+
return `# ${plan.name}
|
|
4063
|
+
|
|
4064
|
+
A chat app built with [\`@kitn.ai/ui\`](https://ui.kitn.ai), scaffolded by \`create-kai\`
|
|
4065
|
+
and wired to ${integration.title}.
|
|
4066
|
+
|
|
4067
|
+
\`\`\`bash
|
|
4068
|
+
npm install
|
|
4069
|
+
npm run dev
|
|
4070
|
+
\`\`\`
|
|
4071
|
+
|
|
4072
|
+
## Your key
|
|
4073
|
+
|
|
4074
|
+
${integration.runNote}
|
|
4075
|
+
|
|
4076
|
+
\`${framework.paths.env}\` was created with ${integration.envVars.length === 1 ? "the variable" : "the variables"} ${integration.envVars.map((v2) => `\`${v2}\``).join(", ")} set to a placeholder.
|
|
4077
|
+
Replace ${integration.envVars.length === 1 ? "it" : "them"} with your own before \`npm run dev\`.
|
|
4078
|
+
That file is gitignored, and only the server route reads it \u2014 the key never
|
|
4079
|
+
reaches the browser.
|
|
4080
|
+
|
|
4081
|
+
## The route
|
|
4082
|
+
|
|
4083
|
+
\`${routeFile}\` holds the provider call. The front end in
|
|
4084
|
+
\`${framework.paths.app}\` POSTs the thread to \`/api/chat\` and streams the
|
|
4085
|
+
reply back through \`readOpenAIStream\`, the same parser the mock path used.
|
|
4086
|
+
|
|
4087
|
+
Runtime: ${host?.runtime ?? "unknown"}.
|
|
4088
|
+
${host?.production === false ? `
|
|
4089
|
+
**Development only.** \`vite build\` emits static assets and no server, so a
|
|
4090
|
+
deployed build has nothing behind \`/api/chat\`. To ship, deploy the handler in
|
|
4091
|
+
\`${routeFile}\` to a real server \u2014 a Next route, a SvelteKit endpoint, a Worker,
|
|
4092
|
+
Express \u2014 and point the fetch at it.
|
|
4093
|
+
` : ""}
|
|
4094
|
+
\`kai.json\` records what was scaffolded, including where the route lives.
|
|
4095
|
+
|
|
4096
|
+
Docs: https://ui.kitn.ai/${integration.docsSlug}
|
|
4097
|
+
`;
|
|
4098
|
+
}
|
|
4099
|
+
function renderMockReadme(plan, framework, thread, docsSlug) {
|
|
4100
|
+
return `# ${plan.name}
|
|
4101
|
+
|
|
4102
|
+
A chat app built with [\`@kitn.ai/ui\`](https://ui.kitn.ai), scaffolded by \`create-kai\`.
|
|
4103
|
+
|
|
4104
|
+
\`\`\`bash
|
|
4105
|
+
npm install
|
|
4106
|
+
npm run dev
|
|
4107
|
+
\`\`\`
|
|
4108
|
+
|
|
4109
|
+
The reply you see on first run comes from the kit's mock responder, not a model.
|
|
4110
|
+
It streams canned SSE frames through \`readOpenAIStream\` \u2014 the same parser a real
|
|
4111
|
+
provider's response goes through \u2014 so what you are looking at is the real
|
|
4112
|
+
rendering path with a fake reply. Every frame is tagged \`_kai_mock\`, the stream
|
|
4113
|
+
opens with a \`: kai-mock\` comment and usage reports zero tokens, so nothing here
|
|
4114
|
+
can be mistaken for a real turn.
|
|
4115
|
+
|
|
4116
|
+
To go live, one expression in \`${framework.paths.app}\` changes:
|
|
4117
|
+
|
|
4118
|
+
\`\`\`diff
|
|
4119
|
+
- import { readOpenAIStream } from '@kitn.ai/ui/wire';
|
|
4120
|
+
+ import { readOpenAIStream, toOpenAIMessages } from '@kitn.ai/ui/wire';
|
|
4121
|
+
|
|
4122
|
+
- await readOpenAIStream(mockResponse(text), stream);
|
|
4123
|
+
+ const res = await fetch('/api/chat', {
|
|
4124
|
+
+ method: 'POST',
|
|
4125
|
+
+ headers: { 'content-type': 'application/json' },
|
|
4126
|
+
+ body: JSON.stringify({ messages: toOpenAIMessages(${thread}) }),
|
|
4127
|
+
+ });
|
|
4128
|
+
+ await readOpenAIStream(res, stream);
|
|
4129
|
+
\`\`\`
|
|
4130
|
+
|
|
4131
|
+
\`kai.json\` records what was scaffolded.
|
|
4132
|
+
|
|
4133
|
+
Docs: https://ui.kitn.ai/${docsSlug}
|
|
4134
|
+
`;
|
|
4135
|
+
}
|
|
4136
|
+
async function listFiles(dir, prefix = "") {
|
|
4137
|
+
const out = [];
|
|
4138
|
+
for (const entry of await readdir(dir)) {
|
|
4139
|
+
if (entry === "node_modules") continue;
|
|
4140
|
+
const abs = path.join(dir, entry);
|
|
4141
|
+
const rel = prefix ? `${prefix}/${entry}` : entry;
|
|
4142
|
+
if ((await stat(abs)).isDirectory()) out.push(...await listFiles(abs, rel));
|
|
4143
|
+
else out.push(rel);
|
|
4144
|
+
}
|
|
4145
|
+
return out.sort();
|
|
4146
|
+
}
|
|
4147
|
+
|
|
4148
|
+
// src/layouts.ts
|
|
4149
|
+
var LAYOUTS = [
|
|
4150
|
+
{
|
|
4151
|
+
id: "full-screen",
|
|
4152
|
+
label: "Full-screen app",
|
|
4153
|
+
hint: "the chat is the page",
|
|
4154
|
+
placement: "full-page",
|
|
4155
|
+
status: "ready"
|
|
4156
|
+
},
|
|
4157
|
+
{
|
|
4158
|
+
id: "widget",
|
|
4159
|
+
label: "Embedded widget",
|
|
4160
|
+
hint: "the chat sits on top of an existing page",
|
|
4161
|
+
placement: "docked-widget",
|
|
4162
|
+
// A widget has no composed-workspace starter — it is a generated surface at
|
|
4163
|
+
// `docked-widget` placement — so it lands on the same unwired path the
|
|
4164
|
+
// generated features do. Offered once that path has been run.
|
|
4165
|
+
status: "planned",
|
|
4166
|
+
note: "needs the generated-surface path, not wired in this release"
|
|
4167
|
+
}
|
|
4168
|
+
];
|
|
4169
|
+
function getLayout(id) {
|
|
4170
|
+
return LAYOUTS.find((l2) => l2.id === id);
|
|
4171
|
+
}
|
|
4172
|
+
function readyLayouts() {
|
|
4173
|
+
return LAYOUTS.filter((l2) => l2.status === "ready");
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4176
|
+
// src/pm.ts
|
|
4177
|
+
var KNOWN = {
|
|
4178
|
+
npm: { name: "npm", install: ["npm", "install"], run: "npm run dev" },
|
|
4179
|
+
pnpm: { name: "pnpm", install: ["pnpm", "install"], run: "pnpm dev" },
|
|
4180
|
+
yarn: { name: "yarn", install: ["yarn"], run: "yarn dev" },
|
|
4181
|
+
bun: { name: "bun", install: ["bun", "install"], run: "bun run dev" }
|
|
4182
|
+
};
|
|
4183
|
+
function detectPackageManager(userAgent = process.env.npm_config_user_agent) {
|
|
4184
|
+
const name = userAgent?.split(" ")[0]?.split("/")[0];
|
|
4185
|
+
if (name && name in KNOWN) return KNOWN[name];
|
|
4186
|
+
return KNOWN.npm;
|
|
4187
|
+
}
|
|
4188
|
+
|
|
4189
|
+
// src/index.ts
|
|
4190
|
+
var DEFAULT_KIT_RANGE = `^${"0.23.0"}`;
|
|
4191
|
+
var HELP = `
|
|
4192
|
+
${import_picocolors3.default.bold("create-kai")} \u2014 scaffold a runnable @kitn.ai/ui chat app
|
|
4193
|
+
|
|
4194
|
+
${import_picocolors3.default.dim("npm create kai@latest")}
|
|
4195
|
+
${import_picocolors3.default.dim("npx create-kai my-app")}
|
|
4196
|
+
|
|
4197
|
+
Options
|
|
4198
|
+
--framework <id> ${readyFrameworks().map((f) => f.id).join(", ")}
|
|
4199
|
+
--layout <id> ${readyLayouts().map((l2) => l2.id).join(", ")}
|
|
4200
|
+
--features <a,b> ${FEATURES.map((f) => f.id).join(", ")} (or 'none')
|
|
4201
|
+
--gateway <id> none${[...WIRED_GATEWAYS].filter((g2) => g2 !== "mock").map((g2) => `, ${g2}`).join("")}
|
|
4202
|
+
--kit <spec> @kitn.ai/ui spec to pin (default ${DEFAULT_KIT_RANGE})
|
|
4203
|
+
-y, --yes accept every default (zero-config: React + full-screen + mock)
|
|
4204
|
+
--no-install skip installing dependencies
|
|
4205
|
+
--list [--json] print the framework / feature / gateway matrix and exit
|
|
4206
|
+
-h, --help this
|
|
4207
|
+
-v, --version print version
|
|
4208
|
+
`;
|
|
4209
|
+
async function main() {
|
|
4210
|
+
const args = parseArgs(process2.argv.slice(2));
|
|
4211
|
+
if (args.errors.length > 0) {
|
|
4212
|
+
for (const error of args.errors) console.error(import_picocolors3.default.red(`create-kai: ${error}`));
|
|
4213
|
+
console.error(HELP);
|
|
4214
|
+
return 1;
|
|
4215
|
+
}
|
|
4216
|
+
if (args.help) {
|
|
4217
|
+
console.log(HELP);
|
|
4218
|
+
return 0;
|
|
4219
|
+
}
|
|
4220
|
+
if (args.version) {
|
|
4221
|
+
console.log("0.1.0");
|
|
4222
|
+
return 0;
|
|
4223
|
+
}
|
|
4224
|
+
if (args.list) {
|
|
4225
|
+
printMatrix(args.json);
|
|
4226
|
+
return 0;
|
|
4227
|
+
}
|
|
4228
|
+
const nonInteractive = args.yes || !process2.stdout.isTTY;
|
|
4229
|
+
Ie(import_picocolors3.default.bgMagenta(import_picocolors3.default.black(" create-kai ")));
|
|
4230
|
+
const defaultName = args.dir ?? ZERO_CONFIG.name;
|
|
4231
|
+
const name = nonInteractive ? defaultName : await ask(
|
|
4232
|
+
he({
|
|
4233
|
+
message: "Project name",
|
|
4234
|
+
placeholder: defaultName,
|
|
4235
|
+
defaultValue: defaultName,
|
|
4236
|
+
validate: (value) => validateProjectName(value || defaultName) ?? void 0
|
|
4237
|
+
})
|
|
4238
|
+
);
|
|
4239
|
+
const nameError = validateProjectName(name);
|
|
4240
|
+
if (nameError) return fail(nameError);
|
|
4241
|
+
const dir = path2.resolve(process2.cwd(), args.dir ?? name);
|
|
4242
|
+
if (existsSync2(dir) && (await readdir2(dir)).length > 0) {
|
|
4243
|
+
return fail(`${dir} already exists and is not empty`);
|
|
4244
|
+
}
|
|
4245
|
+
const frameworkId = args.framework ?? (nonInteractive ? ZERO_CONFIG.framework : await ask(
|
|
4246
|
+
ve({
|
|
4247
|
+
message: "Which framework?",
|
|
4248
|
+
initialValue: ZERO_CONFIG.framework,
|
|
4249
|
+
options: readyFrameworks().map((f) => ({ value: f.id, label: f.label }))
|
|
4250
|
+
})
|
|
4251
|
+
));
|
|
4252
|
+
const framework = getFramework(frameworkId);
|
|
4253
|
+
if (!framework) return fail(`unknown framework '${frameworkId}'`);
|
|
4254
|
+
if (framework.status !== "ready") {
|
|
4255
|
+
return fail(
|
|
4256
|
+
`'${framework.id}' is not scaffoldable yet (${framework.note ?? "no template"}). Available: ${readyFrameworks().map((f) => f.id).join(", ")}`
|
|
4257
|
+
);
|
|
4258
|
+
}
|
|
4259
|
+
const layoutId = args.layout ?? (nonInteractive ? ZERO_CONFIG.layout : await ask(
|
|
4260
|
+
ve({
|
|
4261
|
+
message: "Where does the chat live?",
|
|
4262
|
+
initialValue: ZERO_CONFIG.layout,
|
|
4263
|
+
options: readyLayouts().map((l2) => ({ value: l2.id, label: l2.label, hint: l2.hint }))
|
|
4264
|
+
})
|
|
4265
|
+
));
|
|
4266
|
+
const layout = getLayout(layoutId);
|
|
4267
|
+
if (!layout) return fail(`unknown layout '${layoutId}'`);
|
|
4268
|
+
if (layout.status !== "ready") {
|
|
4269
|
+
return fail(`layout '${layout.id}' is not scaffoldable yet (${layout.note ?? "no template"})`);
|
|
4270
|
+
}
|
|
4271
|
+
const offered = availableFeatures(framework);
|
|
4272
|
+
const featureIds = args.features ?? (nonInteractive ? DEFAULT_FEATURES.filter((id) => offered.some((f) => f.id === id)) : await ask(
|
|
4273
|
+
fe({
|
|
4274
|
+
message: "Which features?",
|
|
4275
|
+
required: false,
|
|
4276
|
+
initialValues: DEFAULT_FEATURES.filter((id) => offered.some((f) => f.id === id)),
|
|
4277
|
+
options: offered.map((f) => ({ value: f.id, label: f.label, hint: f.hint }))
|
|
4278
|
+
})
|
|
4279
|
+
));
|
|
4280
|
+
for (const id of featureIds) {
|
|
4281
|
+
if (!getFeature(id)) return fail(`unknown feature '${id}'`);
|
|
4282
|
+
}
|
|
4283
|
+
const gateways = listGateways();
|
|
4284
|
+
const wired = gateways.filter(
|
|
4285
|
+
(g2) => g2.wired && wirableGateway(g2.integration.id, framework) === null
|
|
4286
|
+
);
|
|
4287
|
+
const gatewayId = normalizeGateway(args.gateway) ?? (nonInteractive || wired.length === 1 ? ZERO_CONFIG.gateway : await ask(
|
|
4288
|
+
ve({
|
|
4289
|
+
message: "Wire a model gateway?",
|
|
4290
|
+
initialValue: ZERO_CONFIG.gateway,
|
|
4291
|
+
options: wired.map((g2) => ({
|
|
4292
|
+
value: g2.integration.id,
|
|
4293
|
+
label: g2.integration.id === "mock" ? "None" : g2.integration.title,
|
|
4294
|
+
hint: g2.integration.id === "mock" ? "local mock, no key, no backend" : `${g2.integration.envVars.join(", ")} \u2014 a server route is scaffolded for you`
|
|
4295
|
+
}))
|
|
4296
|
+
})
|
|
4297
|
+
));
|
|
4298
|
+
if (!gateways.some((g2) => g2.integration.id === gatewayId)) {
|
|
4299
|
+
return fail(`unknown gateway '${gatewayId}'`);
|
|
4300
|
+
}
|
|
4301
|
+
const gatewayProblem = wirableGateway(gatewayId, framework);
|
|
4302
|
+
if (gatewayProblem) return fail(gatewayProblem);
|
|
4303
|
+
const plan = {
|
|
4304
|
+
dir,
|
|
4305
|
+
name,
|
|
4306
|
+
frameworkId: framework.id,
|
|
4307
|
+
layout: layout.id,
|
|
4308
|
+
widgetStyle: null,
|
|
4309
|
+
featureIds,
|
|
4310
|
+
gatewayId,
|
|
4311
|
+
kit: args.kit ?? DEFAULT_KIT_RANGE
|
|
4312
|
+
};
|
|
4313
|
+
const spinner = Y2();
|
|
4314
|
+
spinner.start("Scaffolding");
|
|
4315
|
+
let result;
|
|
4316
|
+
try {
|
|
4317
|
+
result = await generate(plan);
|
|
4318
|
+
} catch (error) {
|
|
4319
|
+
spinner.stop("Scaffolding failed");
|
|
4320
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
4321
|
+
}
|
|
4322
|
+
spinner.stop(`Scaffolded ${import_picocolors3.default.cyan(path2.relative(process2.cwd(), dir) || ".")}`);
|
|
4323
|
+
const pm = detectPackageManager();
|
|
4324
|
+
const shouldInstall = args.install ?? (nonInteractive ? false : await ask(ye({ message: `Install dependencies with ${pm.name}?`, initialValue: true })));
|
|
4325
|
+
let installed = false;
|
|
4326
|
+
if (shouldInstall) {
|
|
4327
|
+
const install = Y2();
|
|
4328
|
+
install.start(`Installing with ${pm.name}`);
|
|
4329
|
+
const code = await run(pm.install, dir);
|
|
4330
|
+
if (code === 0) {
|
|
4331
|
+
installed = true;
|
|
4332
|
+
install.stop("Dependencies installed");
|
|
4333
|
+
} else {
|
|
4334
|
+
install.stop(import_picocolors3.default.yellow(`${pm.name} install exited ${code} \u2014 run it yourself before ${pm.run}`));
|
|
4335
|
+
}
|
|
4336
|
+
}
|
|
4337
|
+
const relative = path2.relative(process2.cwd(), dir);
|
|
4338
|
+
const integration = gateways.find((g2) => g2.integration.id === gatewayId)?.integration;
|
|
4339
|
+
const keyStep = gatewayId !== "mock" && integration && integration.envVars.length > 0 ? `# put your key in ${framework.paths.env} (${integration.envVars.join(", ")})` : null;
|
|
4340
|
+
const steps = [
|
|
4341
|
+
relative ? `cd ${relative}` : null,
|
|
4342
|
+
installed ? null : pm.install.join(" "),
|
|
4343
|
+
keyStep,
|
|
4344
|
+
pm.run
|
|
4345
|
+
].filter(Boolean);
|
|
4346
|
+
Me(steps.join("\n"), "Next steps");
|
|
4347
|
+
Se(
|
|
4348
|
+
[
|
|
4349
|
+
`${import_picocolors3.default.dim("Gateway:")} ${gatewayId === "mock" ? "none \u2014 the kit's local mock responder. No key, no backend." : `${integration?.title ?? gatewayId} \u2014 route at ${framework.route?.file}`}`,
|
|
4350
|
+
`${import_picocolors3.default.dim("Files:")} ${result.files.length} written, including kai.json`,
|
|
4351
|
+
`${import_picocolors3.default.dim("Docs:")} https://ui.kitn.ai/${result.docsSlug}`
|
|
4352
|
+
].join("\n")
|
|
4353
|
+
);
|
|
4354
|
+
return 0;
|
|
4355
|
+
}
|
|
4356
|
+
async function ask(prompt) {
|
|
4357
|
+
const value = await prompt;
|
|
4358
|
+
if (pD(value)) {
|
|
4359
|
+
xe("Cancelled.");
|
|
4360
|
+
process2.exit(130);
|
|
4361
|
+
}
|
|
4362
|
+
return value;
|
|
4363
|
+
}
|
|
4364
|
+
function fail(message) {
|
|
4365
|
+
xe(import_picocolors3.default.red(message));
|
|
4366
|
+
return 1;
|
|
4367
|
+
}
|
|
4368
|
+
function run(command, cwd) {
|
|
4369
|
+
return new Promise((resolve) => {
|
|
4370
|
+
const child = spawn(command[0], command.slice(1), { cwd, stdio: "ignore", shell: false });
|
|
4371
|
+
child.on("error", () => resolve(-1));
|
|
4372
|
+
child.on("close", (code) => resolve(code ?? -1));
|
|
4373
|
+
});
|
|
4374
|
+
}
|
|
4375
|
+
function printMatrix(asJson) {
|
|
4376
|
+
const matrix = {
|
|
4377
|
+
cli: "0.1.0",
|
|
4378
|
+
kit: DEFAULT_KIT_RANGE,
|
|
4379
|
+
frameworks: FRAMEWORKS.map((f) => ({
|
|
4380
|
+
id: f.id,
|
|
4381
|
+
label: f.label,
|
|
4382
|
+
status: f.status,
|
|
4383
|
+
registration: f.registration,
|
|
4384
|
+
composedWorkspace: f.composedWorkspace,
|
|
4385
|
+
// Where a keyed gateway's route would go, or null when this framework has
|
|
4386
|
+
// no destination declared yet. An agent reading this can tell "gateway not
|
|
4387
|
+
// wired" from "gateway wired but not for this framework" without guessing.
|
|
4388
|
+
route: f.route ? { file: f.route.file, runtime: f.route.runtime, production: f.route.production } : null,
|
|
4389
|
+
...f.note ? { note: f.note } : {}
|
|
4390
|
+
})),
|
|
4391
|
+
layouts: LAYOUTS.map((l2) => ({ id: l2.id, status: l2.status, ...l2.note ? { note: l2.note } : {} })),
|
|
4392
|
+
features: FEATURES.map((f) => ({ id: f.id, components: f.components, default: f.default })),
|
|
4393
|
+
gateways: listGateways().map((g2) => ({
|
|
4394
|
+
id: g2.integration.id,
|
|
4395
|
+
title: g2.integration.title,
|
|
4396
|
+
wired: g2.wired,
|
|
4397
|
+
envVars: g2.integration.envVars,
|
|
4398
|
+
keyExposure: g2.integration.keyExposure,
|
|
4399
|
+
// What a scaffold cannot provide for you: 'none' means a key is the whole
|
|
4400
|
+
// of it, anything else means a process has to already be running.
|
|
4401
|
+
outOfBand: g2.integration.outOfBand,
|
|
4402
|
+
language: g2.integration.language,
|
|
4403
|
+
// Derived, never restated: the frameworks this gateway can actually be
|
|
4404
|
+
// scaffolded onto today.
|
|
4405
|
+
frameworks: g2.wired ? FRAMEWORKS.filter((f) => f.status === "ready" && !wirableGateway(g2.integration.id, f)).map((f) => f.id) : []
|
|
4406
|
+
}))
|
|
4407
|
+
};
|
|
4408
|
+
if (asJson) {
|
|
4409
|
+
console.log(JSON.stringify(matrix, null, 2));
|
|
4410
|
+
return;
|
|
4411
|
+
}
|
|
4412
|
+
console.log(import_picocolors3.default.bold("\nFrameworks"));
|
|
4413
|
+
for (const f of matrix.frameworks) {
|
|
4414
|
+
console.log(` ${mark(f.status === "ready")} ${f.id.padEnd(16)}${f.note ?? ""}`);
|
|
4415
|
+
}
|
|
4416
|
+
console.log(import_picocolors3.default.bold("\nLayouts"));
|
|
4417
|
+
for (const l2 of matrix.layouts) {
|
|
4418
|
+
console.log(` ${mark(l2.status === "ready")} ${l2.id.padEnd(16)}${l2.note ?? ""}`);
|
|
4419
|
+
}
|
|
4420
|
+
console.log(import_picocolors3.default.bold("\nGateways"));
|
|
4421
|
+
for (const g2 of matrix.gateways) {
|
|
4422
|
+
console.log(` ${mark(g2.wired)} ${g2.id.padEnd(16)}${g2.envVars.join(", ")}`);
|
|
4423
|
+
}
|
|
4424
|
+
console.log("");
|
|
4425
|
+
}
|
|
4426
|
+
var mark = (ok) => ok ? import_picocolors3.default.green("\u2022") : import_picocolors3.default.dim("\xB7");
|
|
4427
|
+
main().then(
|
|
4428
|
+
(code) => process2.exit(code),
|
|
4429
|
+
(error) => {
|
|
4430
|
+
console.error(import_picocolors3.default.red(error instanceof Error ? error.stack ?? error.message : String(error)));
|
|
4431
|
+
process2.exit(1);
|
|
4432
|
+
}
|
|
4433
|
+
);
|