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.
Files changed (176) hide show
  1. package/README.md +311 -0
  2. package/dist/index.js +4433 -0
  3. package/dist/templates/angular/_gitignore +7 -0
  4. package/dist/templates/angular/angular.json +53 -0
  5. package/dist/templates/angular/package.json +30 -0
  6. package/dist/templates/angular/src/app/app.config.ts +11 -0
  7. package/dist/templates/angular/src/app/app.css +36 -0
  8. package/dist/templates/angular/src/app/app.html +51 -0
  9. package/dist/templates/angular/src/app/app.ts +78 -0
  10. package/dist/templates/angular/src/app/components/composer/composer.css +31 -0
  11. package/dist/templates/angular/src/app/components/composer/composer.html +18 -0
  12. package/dist/templates/angular/src/app/components/composer/composer.ts +81 -0
  13. package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.css +3 -0
  14. package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.html +12 -0
  15. package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.ts +15 -0
  16. package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.css +3 -0
  17. package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.html +14 -0
  18. package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.ts +13 -0
  19. package/dist/templates/angular/src/app/components/sidebar/sidebar.css +24 -0
  20. package/dist/templates/angular/src/app/components/sidebar/sidebar.html +12 -0
  21. package/dist/templates/angular/src/app/components/sidebar/sidebar.ts +43 -0
  22. package/dist/templates/angular/src/app/components/theme-toggle/theme-toggle.html +13 -0
  23. package/dist/templates/angular/src/app/components/theme-toggle/theme-toggle.ts +21 -0
  24. package/dist/templates/angular/src/app/components/thread-view/thread-view.css +6 -0
  25. package/dist/templates/angular/src/app/components/thread-view/thread-view.html +6 -0
  26. package/dist/templates/angular/src/app/components/thread-view/thread-view.ts +45 -0
  27. package/dist/templates/angular/src/app/state/chat.store.ts +47 -0
  28. package/dist/templates/angular/src/app/state/conversations.store.ts +42 -0
  29. package/dist/templates/angular/src/app/state/voice-input.ts +57 -0
  30. package/dist/templates/angular/src/app/types.ts +2 -0
  31. package/dist/templates/angular/src/chat-data.ts +106 -0
  32. package/dist/templates/angular/src/index.html +16 -0
  33. package/dist/templates/angular/src/main.ts +16 -0
  34. package/dist/templates/angular/src/styles.css +10 -0
  35. package/dist/templates/angular/tsconfig.app.json +9 -0
  36. package/dist/templates/angular/tsconfig.json +22 -0
  37. package/dist/templates/nextjs/_gitignore +11 -0
  38. package/dist/templates/nextjs/app/chat-data.ts +125 -0
  39. package/dist/templates/nextjs/app/components/Composer.tsx +88 -0
  40. package/dist/templates/nextjs/app/components/HydrationBadge.tsx +68 -0
  41. package/dist/templates/nextjs/app/components/Sidebar.tsx +48 -0
  42. package/dist/templates/nextjs/app/components/ThemeToggle.tsx +27 -0
  43. package/dist/templates/nextjs/app/components/ThreadView.tsx +48 -0
  44. package/dist/templates/nextjs/app/components/icons/MoonIcon.tsx +25 -0
  45. package/dist/templates/nextjs/app/components/icons/SunIcon.tsx +25 -0
  46. package/dist/templates/nextjs/app/components/icons/index.ts +2 -0
  47. package/dist/templates/nextjs/app/globals.css +153 -0
  48. package/dist/templates/nextjs/app/hooks/index.ts +1 -0
  49. package/dist/templates/nextjs/app/hooks/useConversations.ts +42 -0
  50. package/dist/templates/nextjs/app/layout.tsx +50 -0
  51. package/dist/templates/nextjs/app/page.tsx +17 -0
  52. package/dist/templates/nextjs/app/theme.ts +10 -0
  53. package/dist/templates/nextjs/app/workspace.tsx +148 -0
  54. package/dist/templates/nextjs/next.config.mjs +22 -0
  55. package/dist/templates/nextjs/package.json +23 -0
  56. package/dist/templates/nextjs/postcss.config.mjs +16 -0
  57. package/dist/templates/nextjs/tsconfig.json +23 -0
  58. package/dist/templates/react/_gitignore +6 -0
  59. package/dist/templates/react/index.html +16 -0
  60. package/dist/templates/react/package.json +25 -0
  61. package/dist/templates/react/src/App.tsx +108 -0
  62. package/dist/templates/react/src/chat-data.ts +106 -0
  63. package/dist/templates/react/src/components/Composer.tsx +87 -0
  64. package/dist/templates/react/src/components/Sidebar.tsx +38 -0
  65. package/dist/templates/react/src/components/ThemeToggle.tsx +22 -0
  66. package/dist/templates/react/src/components/ThreadView.tsx +40 -0
  67. package/dist/templates/react/src/components/icons/MoonIcon.tsx +25 -0
  68. package/dist/templates/react/src/components/icons/SunIcon.tsx +25 -0
  69. package/dist/templates/react/src/components/icons/index.ts +2 -0
  70. package/dist/templates/react/src/hooks/index.ts +1 -0
  71. package/dist/templates/react/src/hooks/useConversations.ts +37 -0
  72. package/dist/templates/react/src/index.css +111 -0
  73. package/dist/templates/react/src/main.tsx +15 -0
  74. package/dist/templates/react/src/vite-env.d.ts +1 -0
  75. package/dist/templates/react/tsconfig.app.json +28 -0
  76. package/dist/templates/react/tsconfig.json +7 -0
  77. package/dist/templates/react/tsconfig.node.json +25 -0
  78. package/dist/templates/react/vite.config.ts +11 -0
  79. package/dist/templates/solid/_gitignore +6 -0
  80. package/dist/templates/solid/index.html +13 -0
  81. package/dist/templates/solid/package.json +23 -0
  82. package/dist/templates/solid/src/App.tsx +151 -0
  83. package/dist/templates/solid/src/chat-data.ts +234 -0
  84. package/dist/templates/solid/src/components/Composer.tsx +90 -0
  85. package/dist/templates/solid/src/components/Sidebar.tsx +36 -0
  86. package/dist/templates/solid/src/components/ThemeToggle.tsx +25 -0
  87. package/dist/templates/solid/src/components/ThreadView.tsx +122 -0
  88. package/dist/templates/solid/src/components/icons/index.tsx +84 -0
  89. package/dist/templates/solid/src/index.tsx +5 -0
  90. package/dist/templates/solid/src/lib/chat.ts +64 -0
  91. package/dist/templates/solid/src/lib/conversations.ts +54 -0
  92. package/dist/templates/solid/src/lib/types.ts +2 -0
  93. package/dist/templates/solid/src/styles.css +11 -0
  94. package/dist/templates/solid/tsconfig.json +18 -0
  95. package/dist/templates/solid/vite.config.ts +10 -0
  96. package/dist/templates/svelte/_gitignore +6 -0
  97. package/dist/templates/svelte/index.html +16 -0
  98. package/dist/templates/svelte/package.json +23 -0
  99. package/dist/templates/svelte/src/App.svelte +116 -0
  100. package/dist/templates/svelte/src/app.d.ts +20 -0
  101. package/dist/templates/svelte/src/chat-data.ts +106 -0
  102. package/dist/templates/svelte/src/components/Composer.svelte +102 -0
  103. package/dist/templates/svelte/src/components/Sidebar.svelte +61 -0
  104. package/dist/templates/svelte/src/components/ThemeToggle.svelte +45 -0
  105. package/dist/templates/svelte/src/components/ThreadView.svelte +49 -0
  106. package/dist/templates/svelte/src/components/icons/MoonIcon.svelte +24 -0
  107. package/dist/templates/svelte/src/components/icons/SunIcon.svelte +24 -0
  108. package/dist/templates/svelte/src/index.css +111 -0
  109. package/dist/templates/svelte/src/lib/chat.svelte.ts +52 -0
  110. package/dist/templates/svelte/src/lib/conversations.svelte.ts +46 -0
  111. package/dist/templates/svelte/src/lib/types.ts +2 -0
  112. package/dist/templates/svelte/src/lib/voiceInput.ts +57 -0
  113. package/dist/templates/svelte/src/main.ts +18 -0
  114. package/dist/templates/svelte/svelte.config.js +8 -0
  115. package/dist/templates/svelte/tsconfig.app.json +28 -0
  116. package/dist/templates/svelte/tsconfig.json +7 -0
  117. package/dist/templates/svelte/tsconfig.node.json +25 -0
  118. package/dist/templates/svelte/vite.config.ts +15 -0
  119. package/dist/templates/tanstack-start/_gitignore +14 -0
  120. package/dist/templates/tanstack-start/package.json +28 -0
  121. package/dist/templates/tanstack-start/serve.mjs +129 -0
  122. package/dist/templates/tanstack-start/serve.traversal.test.mjs +445 -0
  123. package/dist/templates/tanstack-start/src/chat-data.ts +124 -0
  124. package/dist/templates/tanstack-start/src/components/Composer.tsx +87 -0
  125. package/dist/templates/tanstack-start/src/components/HydrationBadge.tsx +63 -0
  126. package/dist/templates/tanstack-start/src/components/Sidebar.tsx +43 -0
  127. package/dist/templates/tanstack-start/src/components/ThemeToggle.tsx +26 -0
  128. package/dist/templates/tanstack-start/src/components/ThreadView.tsx +48 -0
  129. package/dist/templates/tanstack-start/src/components/icons/MoonIcon.tsx +25 -0
  130. package/dist/templates/tanstack-start/src/components/icons/SunIcon.tsx +25 -0
  131. package/dist/templates/tanstack-start/src/components/icons/index.ts +2 -0
  132. package/dist/templates/tanstack-start/src/hooks/index.ts +1 -0
  133. package/dist/templates/tanstack-start/src/hooks/useConversations.ts +42 -0
  134. package/dist/templates/tanstack-start/src/router.tsx +16 -0
  135. package/dist/templates/tanstack-start/src/routes/__root.tsx +62 -0
  136. package/dist/templates/tanstack-start/src/routes/index.tsx +135 -0
  137. package/dist/templates/tanstack-start/src/styles.css +153 -0
  138. package/dist/templates/tanstack-start/src/theme.ts +10 -0
  139. package/dist/templates/tanstack-start/tsconfig.json +16 -0
  140. package/dist/templates/tanstack-start/vite.config.ts +28 -0
  141. package/dist/templates/vanilla/_gitignore +6 -0
  142. package/dist/templates/vanilla/index.html +16 -0
  143. package/dist/templates/vanilla/package.json +19 -0
  144. package/dist/templates/vanilla/src/chat-data.ts +106 -0
  145. package/dist/templates/vanilla/src/index.css +111 -0
  146. package/dist/templates/vanilla/src/main.ts +53 -0
  147. package/dist/templates/vanilla/src/state.ts +119 -0
  148. package/dist/templates/vanilla/src/view.ts +222 -0
  149. package/dist/templates/vanilla/src/vite-env.d.ts +1 -0
  150. package/dist/templates/vanilla/src/voice-input.ts +55 -0
  151. package/dist/templates/vanilla/tsconfig.json +24 -0
  152. package/dist/templates/vanilla/vite.config.ts +14 -0
  153. package/dist/templates/vue/_gitignore +6 -0
  154. package/dist/templates/vue/index.html +16 -0
  155. package/dist/templates/vue/package.json +23 -0
  156. package/dist/templates/vue/src/App.vue +104 -0
  157. package/dist/templates/vue/src/chat-data.ts +106 -0
  158. package/dist/templates/vue/src/components/Composer.vue +96 -0
  159. package/dist/templates/vue/src/components/Sidebar.vue +48 -0
  160. package/dist/templates/vue/src/components/ThemeToggle.vue +27 -0
  161. package/dist/templates/vue/src/components/ThreadView.vue +50 -0
  162. package/dist/templates/vue/src/components/icons/MoonIcon.vue +24 -0
  163. package/dist/templates/vue/src/components/icons/SunIcon.vue +24 -0
  164. package/dist/templates/vue/src/composables/index.ts +3 -0
  165. package/dist/templates/vue/src/composables/useChat.ts +47 -0
  166. package/dist/templates/vue/src/composables/useConversations.ts +42 -0
  167. package/dist/templates/vue/src/composables/useVoiceInput.ts +57 -0
  168. package/dist/templates/vue/src/index.css +111 -0
  169. package/dist/templates/vue/src/main.ts +18 -0
  170. package/dist/templates/vue/src/types.ts +4 -0
  171. package/dist/templates/vue/src/vite-env.d.ts +1 -0
  172. package/dist/templates/vue/tsconfig.app.json +29 -0
  173. package/dist/templates/vue/tsconfig.json +7 -0
  174. package/dist/templates/vue/tsconfig.node.json +25 -0
  175. package/dist/templates/vue/vite.config.ts +24 -0
  176. 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
+ );