@lovelybunch/api 1.0.75-alpha.0 → 1.0.75-alpha.10

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 (179) hide show
  1. package/dist/lib/auth/auth-manager.d.ts +5 -0
  2. package/dist/lib/auth/auth-manager.js +9 -0
  3. package/dist/lib/storage/file-storage.d.ts +16 -16
  4. package/dist/lib/storage/file-storage.js +84 -79
  5. package/dist/lib/terminal/terminal-manager.d.ts +3 -5
  6. package/dist/lib/terminal/terminal-manager.js +19 -63
  7. package/dist/middleware/auth.js +36 -0
  8. package/dist/routes/api/v1/ai/index.js +0 -2
  9. package/dist/routes/api/v1/ai/route.js +445 -259
  10. package/dist/routes/api/v1/chats/[id]/index.js +2 -1
  11. package/dist/routes/api/v1/chats/[id]/route.d.ts +7 -0
  12. package/dist/routes/api/v1/chats/[id]/route.js +30 -1
  13. package/dist/routes/api/v1/context/index.js +0 -2
  14. package/dist/routes/api/v1/events/route.js +1 -1
  15. package/dist/routes/api/v1/knowledge/[filename]/index.d.ts +1 -0
  16. package/dist/routes/api/v1/knowledge/[filename]/index.js +1 -0
  17. package/dist/routes/api/v1/knowledge/[filename]/route.d.ts +3 -0
  18. package/dist/routes/api/v1/knowledge/[filename]/route.js +254 -0
  19. package/dist/routes/api/v1/knowledge/index.d.ts +1 -0
  20. package/dist/routes/api/v1/knowledge/index.js +1 -0
  21. package/dist/routes/api/v1/knowledge/route.d.ts +3 -0
  22. package/dist/routes/api/v1/knowledge/route.js +176 -0
  23. package/dist/routes/api/v1/mcp/index.js +109 -34
  24. package/dist/routes/api/v1/skills/[id]/index.d.ts +1 -0
  25. package/dist/routes/api/v1/skills/[id]/index.js +1 -0
  26. package/dist/routes/api/v1/skills/[id]/route.d.ts +3 -0
  27. package/dist/routes/api/v1/skills/[id]/route.js +199 -0
  28. package/dist/routes/api/v1/skills/index.d.ts +1 -0
  29. package/dist/routes/api/v1/skills/index.js +1 -0
  30. package/dist/routes/api/v1/skills/route.d.ts +3 -0
  31. package/dist/routes/api/v1/skills/route.js +329 -0
  32. package/dist/routes/api/v1/tasks/[id]/route.d.ts +351 -0
  33. package/dist/routes/api/v1/tasks/[id]/route.js +156 -0
  34. package/dist/routes/api/v1/tasks/index.d.ts +3 -0
  35. package/dist/routes/api/v1/tasks/index.js +10 -0
  36. package/dist/routes/api/v1/tasks/route.d.ts +329 -0
  37. package/dist/routes/api/v1/tasks/route.js +126 -0
  38. package/dist/routes/api/v1/terminal/[proposalId]/create/route.js +2 -2
  39. package/dist/routes/api/v1/terminal/[taskId]/create/index.d.ts +3 -0
  40. package/dist/routes/api/v1/terminal/[taskId]/create/index.js +5 -0
  41. package/dist/routes/api/v1/terminal/[taskId]/create/route.d.ts +10 -0
  42. package/dist/routes/api/v1/terminal/[taskId]/create/route.js +27 -0
  43. package/dist/routes/api/v1/terminal/[taskId]/destroy/index.d.ts +3 -0
  44. package/dist/routes/api/v1/terminal/[taskId]/destroy/index.js +5 -0
  45. package/dist/routes/api/v1/terminal/[taskId]/destroy/route.d.ts +10 -0
  46. package/dist/routes/api/v1/terminal/[taskId]/destroy/route.js +21 -0
  47. package/dist/routes/api/v1/terminal/[taskId]/resize/index.d.ts +3 -0
  48. package/dist/routes/api/v1/terminal/[taskId]/resize/index.js +5 -0
  49. package/dist/routes/api/v1/terminal/[taskId]/resize/route.d.ts +10 -0
  50. package/dist/routes/api/v1/terminal/[taskId]/resize/route.js +21 -0
  51. package/dist/routes/api/v1/terminal/sessions/route.js +4 -4
  52. package/dist/server-with-static.js +18 -14
  53. package/dist/server.js +15 -13
  54. package/package.json +8 -4
  55. package/static/assets/{ActivityPage-Dl3u61DF.js → ActivityPage-CbmEnYhg.js} +1 -1
  56. package/static/assets/ApiKeysSettingsPage-DLxQIqTT.js +2 -0
  57. package/static/assets/{ArchitectureEditPage-CVxBD88N.js → ArchitectureEditPage-CbtzgIQv.js} +4 -4
  58. package/static/assets/{ArchitecturePage-GchbFR8L.js → ArchitecturePage-CcAMfFZ8.js} +1 -1
  59. package/static/assets/{AuthSettingsPage-B7a-i1_T.js → AuthSettingsPage-4Prb7nAt.js} +2 -2
  60. package/static/assets/{CallbackPage-xYgfYT1Z.js → CallbackPage-CmWv_5Nh.js} +1 -1
  61. package/static/assets/CodePage-COC7rcwM.js +2 -0
  62. package/static/assets/{CollapsibleSection-ka7CEJhS.js → CollapsibleSection-C_VVeVSc.js} +1 -1
  63. package/static/assets/DashboardPage-BACFVIaW.js +41 -0
  64. package/static/assets/{GitPage-B-aSKe9Z.js → GitPage-rFngEr_A.js} +2 -2
  65. package/static/assets/GitSettingsPage-CDQovOqx.js +6 -0
  66. package/static/assets/IdentityPage-CS4REq2E.js +11 -0
  67. package/static/assets/{ImplementationStepsEditor-BC8bzHNj.js → ImplementationStepsEditor-CBIgppkZ.js} +2 -2
  68. package/static/assets/IntegrationsSettingsPage-B7HoraBH.js +1 -0
  69. package/static/assets/JobDetailPage-QUhWsaWV.js +1 -0
  70. package/static/assets/KnowledgeDetailPage-DCJstmIr.js +1 -0
  71. package/static/assets/KnowledgeEditPage-EYpFPfcJ.js +1 -0
  72. package/static/assets/KnowledgePage-D-7skvn5.js +8 -0
  73. package/static/assets/{LoginPage-Dp6lQSZW.js → LoginPage-L2aoqSDT.js} +1 -1
  74. package/static/assets/McpSettingsPage-CLwqDjw_.js +1 -0
  75. package/static/assets/NewKnowledgePage-Fq_QD8um.js +9 -0
  76. package/static/assets/NewSkillPage-DRcgovk0.js +1 -0
  77. package/static/assets/NewTaskPage-CleA8rH5.js +90 -0
  78. package/static/assets/ProjectEditPage-GIMOKgmh.js +11 -0
  79. package/static/assets/ProjectPage-DlWZOqnb.js +1 -0
  80. package/static/assets/PromptsSettingsPage-DFagGfo-.js +1 -0
  81. package/static/assets/ResourceDetailPage-CnFRpDec.js +1 -0
  82. package/static/assets/ResourcesPage-Cw3fu0JE.js +41 -0
  83. package/static/assets/{RoleEditPage-Ne8Dh8Tt.js → RoleEditPage-ARXq_eCs.js} +1 -1
  84. package/static/assets/{RolePage-3sb7lhw8.js → RolePage--XBBIrq8.js} +1 -1
  85. package/static/assets/{RulesSettingsPage-CEWEvNFC.js → RulesSettingsPage-DEqaRH96.js} +3 -3
  86. package/static/assets/SchedulePage-C_B2k9P6.js +4 -0
  87. package/static/assets/SkillDetailPage-Dr6xyKAX.js +1 -0
  88. package/static/assets/SkillEditPage-BsHXmfEZ.js +1 -0
  89. package/static/assets/SkillsPage-BrDdAAPx.js +8 -0
  90. package/static/assets/SkillsSettingsPage-Cvi7xaDE.js +1 -0
  91. package/static/assets/SourceInput-DxF_GGj8.js +1 -0
  92. package/static/assets/{TagInput-DRSgjTau.js → TagInput-DYvHbVZq.js} +1 -1
  93. package/static/assets/TaskDetailPage-DzcD6t03.js +1 -0
  94. package/static/assets/TaskEditPage-C7a6FOeJ.js +1 -0
  95. package/static/assets/TasksPage-ChWRSO_S.js +17 -0
  96. package/static/assets/TerminalPage-CoPwn8cU.js +1 -0
  97. package/static/assets/TerminalSessionPage-BQBZrOJa.js +13 -0
  98. package/static/assets/UserPreferencesPage-DjtU7veO.js +1 -0
  99. package/static/assets/UserSettingsPage-CfU8boJQ.js +1 -0
  100. package/static/assets/UtilitiesPage-sP1Crg-X.js +1 -0
  101. package/static/assets/{alert-CBWqAyb9.js → alert-BqZa-crG.js} +1 -1
  102. package/static/assets/{arrow-down-PY3QZW33.js → arrow-down-3faV_GyO.js} +1 -1
  103. package/static/assets/{arrow-left-JpEq5x30.js → arrow-left-FD3wQmzH.js} +1 -1
  104. package/static/assets/{arrow-up-qJ-_eNlY.js → arrow-up-BzP0YNVk.js} +1 -1
  105. package/static/assets/{badge-D6QJCkgl.js → badge-DRyeFib9.js} +1 -1
  106. package/static/assets/{browser-modal-DT5zGtM2.js → browser-modal-vnePkRfO.js} +2 -2
  107. package/static/assets/{card-CjY6k9qN.js → card-CuQs3dpy.js} +1 -1
  108. package/static/assets/{chevron-left-DiMUBSvf.js → chevron-left-QZIoYcVa.js} +1 -1
  109. package/static/assets/{plus-BCs8PAn6.js → chevron-up-DreyvhRd.js} +2 -2
  110. package/static/assets/{chevrons-up-CKVPmJ25.js → chevrons-up-CsAkc9vE.js} +1 -1
  111. package/static/assets/{circle-alert-Bc4Eo-aL.js → circle-alert-ewz28SE3.js} +1 -1
  112. package/static/assets/{circle-check-C8DZc7FJ.js → circle-check-2TuD-EHs.js} +1 -1
  113. package/static/assets/{circle-check-big-D_PvuS7-.js → circle-check-big-1xNuBPkR.js} +1 -1
  114. package/static/assets/{circle-play-Ba60SPvN.js → circle-play-C_w-qCn4.js} +1 -1
  115. package/static/assets/{circle-x-BYe1-ctZ.js → circle-x-B_d4wjG-.js} +1 -1
  116. package/static/assets/{clipboard-DZHDxSAN.js → clipboard-B6vBm1BP.js} +1 -1
  117. package/static/assets/{clock-E0FNqZih.js → clock-BIzEsx1g.js} +1 -1
  118. package/static/assets/{download-DbHXGD3a.js → download-Dv-RIvKK.js} +1 -1
  119. package/static/assets/droid-GYYyVzN-.js +18 -0
  120. package/static/assets/external-link-Cr8wjV6X.js +6 -0
  121. package/static/assets/{eye-CvcvqrS0.js → eye-DN958vyL.js} +1 -1
  122. package/static/assets/{folder-git-2-DK9b1q2x.js → folder-git-2-BproRzAR.js} +1 -1
  123. package/static/assets/index-Co_SJV3n.js +472 -0
  124. package/static/assets/index-GFQ5RqVh.css +2 -0
  125. package/static/assets/info-CzKk8mbR.js +6 -0
  126. package/static/assets/{label-DGhQh35t.js → label-h5GIKGcJ.js} +1 -1
  127. package/static/assets/{markdown-editor-52vxb0rp.js → markdown-editor-C6il4XWv.js} +1 -1
  128. package/static/assets/{pause-B7dyLcuH.js → pause-BDsjEmXM.js} +1 -1
  129. package/static/assets/{play-C3zkOZiK.js → play-CGsVQUJG.js} +1 -1
  130. package/static/assets/{radio-group-BseQ4OVV.js → radio-group-Bsd75ahK.js} +1 -1
  131. package/static/assets/{refresh-cw-ucF4I6xx.js → refresh-cw-qE1iNkL_.js} +1 -1
  132. package/static/assets/{search-TqAjHy3_.js → search-dvi0J4Dr.js} +1 -1
  133. package/static/assets/select-DylRS99W.js +1 -0
  134. package/static/assets/status-utils-CDkPeVfP.js +1 -0
  135. package/static/assets/{switch-B6px80Oh.js → switch-CsB3wpq9.js} +1 -1
  136. package/static/assets/{tabs-BHSWuVOa.js → tabs-Bw_4k2Rs.js} +1 -1
  137. package/static/assets/{tag-DjI_1POr.js → tag-Dh5PraRd.js} +1 -1
  138. package/static/assets/{terminal-preview-C0DTE3G7.js → terminal-preview-CfOb7xMx.js} +1 -1
  139. package/static/assets/use-terminal-T_tdJTCU.js +1 -0
  140. package/static/assets/video-bO6uuAjA.js +36 -0
  141. package/static/assets/{zap-DruahTJJ.js → zap-DHZ91NcK.js} +1 -1
  142. package/static/index.html +2 -2
  143. package/static/assets/AgentDetailPage-BJD22FSv.js +0 -1
  144. package/static/assets/AgentEditPage-ZYd0Rode.js +0 -1
  145. package/static/assets/AgentsPage-CUuxxQ2o.js +0 -3
  146. package/static/assets/AgentsSettingsPage-Dd8MrsKx.js +0 -6
  147. package/static/assets/ApiKeysSettingsPage-DTZStkII.js +0 -7
  148. package/static/assets/CodePage-xpXX0hlR.js +0 -2
  149. package/static/assets/DashboardPage-DQFlhP54.js +0 -41
  150. package/static/assets/GitSettingsPage-vLNJs4r3.js +0 -6
  151. package/static/assets/IdentityPage-ZAp_iARt.js +0 -11
  152. package/static/assets/IntegrationsSettingsPage-CqWZ8GUI.js +0 -1
  153. package/static/assets/JobDetailPage-BWq4XLO2.js +0 -1
  154. package/static/assets/KnowledgeDetailPage-C8evStGp.js +0 -1
  155. package/static/assets/KnowledgeEditPage-DquJprU_.js +0 -1
  156. package/static/assets/KnowledgePage-BBUKBhzS.js +0 -8
  157. package/static/assets/McpSettingsPage-BGIEripM.js +0 -1
  158. package/static/assets/NewAgentPage-D9hL7NY6.js +0 -1
  159. package/static/assets/NewKnowledgePage-CEdd8Yns.js +0 -9
  160. package/static/assets/NewProposalPage-B152OXnT.js +0 -90
  161. package/static/assets/ProjectEditPage-D0uSaGy0.js +0 -11
  162. package/static/assets/ProjectPage-DzWJky4Y.js +0 -1
  163. package/static/assets/PromptsSettingsPage-DH-4vcbF.js +0 -1
  164. package/static/assets/ProposalDetailPage-BI5HDp5I.js +0 -1
  165. package/static/assets/ProposalEditPage-D4412azZ.js +0 -1
  166. package/static/assets/ProposalsPage-D2gMbCBV.js +0 -17
  167. package/static/assets/ResourcesPage-7tbvXxDb.js +0 -71
  168. package/static/assets/SchedulePage-Crd-VMiL.js +0 -4
  169. package/static/assets/SourceInput-fvbAHpzT.js +0 -1
  170. package/static/assets/TerminalPage-BGZJDqvR.js +0 -1
  171. package/static/assets/TerminalSessionPage-DILvcnLG.js +0 -13
  172. package/static/assets/UserPreferencesPage-ChHrBs8H.js +0 -1
  173. package/static/assets/UserSettingsPage-Cv60YInV.js +0 -1
  174. package/static/assets/UtilitiesPage-ChLTn_u9.js +0 -1
  175. package/static/assets/calendar-sFw_mHQb.js +0 -6
  176. package/static/assets/droid-Caom7ttu.js +0 -4
  177. package/static/assets/index-C4LGSoHO.js +0 -468
  178. package/static/assets/index-CzjbtPHw.css +0 -2
  179. package/static/assets/use-terminal-KpWB08HL.js +0 -1
@@ -0,0 +1,329 @@
1
+ import { Hono } from 'hono';
2
+ import { promises as fs } from 'fs';
3
+ import path from 'path';
4
+ import matter from 'gray-matter';
5
+ import AdmZip from 'adm-zip';
6
+ const app = new Hono();
7
+ const ALLOWED_ARCHIVE_HOSTS = [
8
+ 'registry.coconut.dev',
9
+ ];
10
+ const MAX_ARCHIVE_SIZE = 50 * 1024 * 1024; // 50 MB
11
+ /** Paths to skip entirely when extracting zip archives (macOS resource forks, etc.) */
12
+ const IGNORED_ZIP_PREFIXES = ['__MACOSX/', '.DS_Store'];
13
+ function isIgnoredZipEntry(entryName) {
14
+ return IGNORED_ZIP_PREFIXES.some(prefix => entryName.startsWith(prefix) || entryName === prefix);
15
+ }
16
+ /**
17
+ * Detect if all zip entries share a single wrapping directory (e.g., `pptx/...`).
18
+ * Returns the prefix (with trailing slash) to strip, or null if no wrapper.
19
+ * Ignores macOS resource fork entries (__MACOSX/) when detecting the prefix.
20
+ */
21
+ function detectWrapperPrefix(entries) {
22
+ const paths = entries
23
+ .map(e => e.entryName)
24
+ .filter(name => name !== '/' && name !== '' && !isIgnoredZipEntry(name));
25
+ if (paths.length === 0)
26
+ return null;
27
+ const prefixes = new Set(paths.map(p => {
28
+ const firstSlash = p.indexOf('/');
29
+ return firstSlash >= 0 ? p.substring(0, firstSlash + 1) : null;
30
+ }));
31
+ // If all entries share exactly one prefix directory, strip it
32
+ if (prefixes.size === 1) {
33
+ const prefix = [...prefixes][0];
34
+ if (prefix !== null)
35
+ return prefix;
36
+ }
37
+ return null;
38
+ }
39
+ /**
40
+ * Download a zip archive from a trusted URL and extract it into destDir.
41
+ * Returns true if extraction succeeded and SKILL.md exists in the output.
42
+ */
43
+ async function downloadAndExtractArchive(archiveUrl, destDir) {
44
+ // Validate URL against allow-list
45
+ let parsed;
46
+ try {
47
+ parsed = new URL(archiveUrl);
48
+ }
49
+ catch {
50
+ throw new Error('Invalid archive URL');
51
+ }
52
+ if (!ALLOWED_ARCHIVE_HOSTS.includes(parsed.hostname)) {
53
+ throw new Error(`Archive host '${parsed.hostname}' is not trusted`);
54
+ }
55
+ // Download the zip
56
+ const response = await fetch(archiveUrl, {
57
+ headers: { 'User-Agent': 'coconut-api' },
58
+ });
59
+ if (!response.ok) {
60
+ throw new Error(`Failed to download archive: ${response.status} ${response.statusText}`);
61
+ }
62
+ const contentLength = Number(response.headers.get('content-length') || '0');
63
+ if (contentLength > MAX_ARCHIVE_SIZE) {
64
+ throw new Error(`Archive too large (${contentLength} bytes, max ${MAX_ARCHIVE_SIZE})`);
65
+ }
66
+ const buffer = Buffer.from(await response.arrayBuffer());
67
+ if (buffer.length > MAX_ARCHIVE_SIZE) {
68
+ throw new Error(`Archive too large (${buffer.length} bytes, max ${MAX_ARCHIVE_SIZE})`);
69
+ }
70
+ // Extract with path-traversal protection
71
+ const zip = new AdmZip(buffer);
72
+ const entries = zip.getEntries();
73
+ const wrapperPrefix = detectWrapperPrefix(entries);
74
+ await fs.mkdir(destDir, { recursive: true });
75
+ for (const entry of entries) {
76
+ let entryName = entry.entryName;
77
+ // Skip macOS resource fork junk and other ignored entries
78
+ if (isIgnoredZipEntry(entryName))
79
+ continue;
80
+ // Strip single wrapping directory if detected (e.g., pptx/SKILL.md -> SKILL.md)
81
+ if (wrapperPrefix && entryName.startsWith(wrapperPrefix)) {
82
+ entryName = entryName.substring(wrapperPrefix.length);
83
+ }
84
+ if (!entryName || entryName === '/')
85
+ continue;
86
+ // Reject paths that attempt directory traversal
87
+ const resolved = path.resolve(destDir, entryName);
88
+ if (!resolved.startsWith(path.resolve(destDir) + path.sep) && resolved !== path.resolve(destDir)) {
89
+ console.warn(`Skipping zip entry with path traversal: ${entryName}`);
90
+ continue;
91
+ }
92
+ if (entry.isDirectory) {
93
+ await fs.mkdir(resolved, { recursive: true });
94
+ }
95
+ else {
96
+ await fs.mkdir(path.dirname(resolved), { recursive: true });
97
+ await fs.writeFile(resolved, entry.getData());
98
+ }
99
+ }
100
+ // Check if SKILL.md was extracted
101
+ try {
102
+ await fs.access(path.join(destDir, 'SKILL.md'));
103
+ return true;
104
+ }
105
+ catch {
106
+ return false;
107
+ }
108
+ }
109
+ /**
110
+ * Validate skill name per Agent Skills spec:
111
+ * - 1-64 characters
112
+ * - Lowercase letters, numbers, hyphens only
113
+ * - No leading/trailing hyphens
114
+ * - No consecutive hyphens
115
+ */
116
+ function validateSkillName(name) {
117
+ if (!name || name.length === 0)
118
+ return 'Name is required';
119
+ if (name.length > 64)
120
+ return 'Name must be 64 characters or fewer';
121
+ if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(name)) {
122
+ return 'Name must contain only lowercase letters, numbers, and hyphens, and must not start or end with a hyphen';
123
+ }
124
+ if (/--/.test(name))
125
+ return 'Name must not contain consecutive hyphens';
126
+ return null;
127
+ }
128
+ function generateSkillId(name) {
129
+ return name
130
+ .toLowerCase()
131
+ .replace(/[^a-z0-9\s-]/g, '')
132
+ .replace(/\s+/g, '-')
133
+ .replace(/--+/g, '-')
134
+ .replace(/^-|-$/g, '');
135
+ }
136
+ function getSkillsPath() {
137
+ let basePath;
138
+ if (process.env.NODE_ENV === 'development' && process.env.GAIT_DEV_ROOT) {
139
+ basePath = process.env.GAIT_DEV_ROOT;
140
+ }
141
+ else if (process.env.GAIT_DATA_PATH) {
142
+ basePath = path.resolve(process.env.GAIT_DATA_PATH, '.nut');
143
+ }
144
+ else {
145
+ basePath = path.resolve(process.cwd(), '.nut');
146
+ }
147
+ return path.join(basePath, 'skills');
148
+ }
149
+ /**
150
+ * GET /api/v1/skills
151
+ * Load all skill documents
152
+ */
153
+ app.get('/', async (c) => {
154
+ try {
155
+ const skillsPath = getSkillsPath();
156
+ await fs.mkdir(skillsPath, { recursive: true });
157
+ const entries = await fs.readdir(skillsPath, { withFileTypes: true });
158
+ const skills = await Promise.all(entries
159
+ .filter(entry => entry.isDirectory())
160
+ .map(async (entry) => {
161
+ const skillFile = path.join(skillsPath, entry.name, 'SKILL.md');
162
+ try {
163
+ const fileContent = await fs.readFile(skillFile, 'utf-8');
164
+ const { data, content } = matter(fileContent);
165
+ const nestedMeta = typeof data.metadata === 'object' ? data.metadata : undefined;
166
+ const doc = {
167
+ id: entry.name,
168
+ name: data.name || entry.name,
169
+ description: data.description || '',
170
+ ...(data.license ? { license: data.license } : {}),
171
+ ...(data.compatibility ? { compatibility: data.compatibility } : {}),
172
+ ...(data['allowed-tools'] || data.allowedTools ? { allowedTools: data['allowed-tools'] || data.allowedTools } : {}),
173
+ metadata: {
174
+ name: data.name || entry.name,
175
+ description: data.description || '',
176
+ ...(nestedMeta || {}),
177
+ },
178
+ content,
179
+ };
180
+ return doc;
181
+ }
182
+ catch {
183
+ // Skip directories without valid SKILL.md
184
+ return null;
185
+ }
186
+ }));
187
+ const validSkills = skills.filter((s) => s !== null);
188
+ return c.json({
189
+ success: true,
190
+ documents: validSkills.sort((a, b) => a.id.localeCompare(b.id))
191
+ });
192
+ }
193
+ catch (error) {
194
+ console.error('Error loading skill documents:', error);
195
+ return c.json({
196
+ success: false,
197
+ error: 'Failed to load skill documents',
198
+ documents: []
199
+ }, 500);
200
+ }
201
+ });
202
+ /**
203
+ * POST /api/v1/skills
204
+ * Create a new skill document
205
+ */
206
+ app.post('/', async (c) => {
207
+ try {
208
+ const body = await c.req.json();
209
+ if (!body.name || !body.description) {
210
+ return c.json({ success: false, error: 'Name and description are required' }, 400);
211
+ }
212
+ const skillId = generateSkillId(body.name);
213
+ const nameError = validateSkillName(skillId);
214
+ if (nameError) {
215
+ return c.json({ success: false, error: nameError }, 400);
216
+ }
217
+ const skillsPath = getSkillsPath();
218
+ const skillDir = path.join(skillsPath, skillId);
219
+ // Check if skill directory already exists
220
+ try {
221
+ await fs.access(path.join(skillDir, 'SKILL.md'));
222
+ return c.json({ success: false, error: 'A skill with this name already exists' }, 409);
223
+ }
224
+ catch {
225
+ // Doesn't exist, proceed
226
+ }
227
+ // Create skill directory
228
+ await fs.mkdir(skillDir, { recursive: true });
229
+ let finalContent = body.content || '';
230
+ if (body.archiveUrl) {
231
+ // Download and extract archive — SKILL.md comes from the zip
232
+ try {
233
+ const hasSkillMd = await downloadAndExtractArchive(body.archiveUrl, skillDir);
234
+ if (!hasSkillMd) {
235
+ // Fallback: generate SKILL.md from JSON fields if the archive didn't include one
236
+ const frontmatter = {
237
+ name: skillId,
238
+ description: body.description,
239
+ };
240
+ if (body.license)
241
+ frontmatter.license = body.license;
242
+ if (body.compatibility)
243
+ frontmatter.compatibility = body.compatibility;
244
+ if (body.allowedTools)
245
+ frontmatter['allowed-tools'] = body.allowedTools;
246
+ if (body.metadata && Object.keys(body.metadata).length > 0) {
247
+ const meta = {};
248
+ for (const [key, value] of Object.entries(body.metadata)) {
249
+ if (value !== undefined && value !== null) {
250
+ meta[key] = value;
251
+ }
252
+ }
253
+ if (Object.keys(meta).length > 0) {
254
+ frontmatter.metadata = meta;
255
+ }
256
+ }
257
+ const fileContent = matter.stringify(finalContent, frontmatter);
258
+ await fs.writeFile(path.join(skillDir, 'SKILL.md'), fileContent, 'utf-8');
259
+ }
260
+ // Read back SKILL.md content for the response
261
+ try {
262
+ const written = await fs.readFile(path.join(skillDir, 'SKILL.md'), 'utf-8');
263
+ const { data: extractedFrontmatter, content: mdContent } = matter(written);
264
+ finalContent = mdContent;
265
+ // If the extracted SKILL.md body is empty but we have registry content,
266
+ // append it as a fallback and rewrite the file
267
+ if (!mdContent.trim() && body.content?.trim()) {
268
+ const rewritten = matter.stringify(body.content, extractedFrontmatter);
269
+ await fs.writeFile(path.join(skillDir, 'SKILL.md'), rewritten, 'utf-8');
270
+ finalContent = body.content;
271
+ }
272
+ }
273
+ catch {
274
+ // best effort
275
+ }
276
+ }
277
+ catch (archiveError) {
278
+ // Clean up the directory on failure
279
+ await fs.rm(skillDir, { recursive: true, force: true }).catch(() => { });
280
+ return c.json({ success: false, error: `Archive extraction failed: ${archiveError.message}` }, 400);
281
+ }
282
+ }
283
+ else {
284
+ // No archive — build SKILL.md from JSON fields (existing behavior)
285
+ const frontmatter = {
286
+ name: skillId,
287
+ description: body.description,
288
+ };
289
+ if (body.license)
290
+ frontmatter.license = body.license;
291
+ if (body.compatibility)
292
+ frontmatter.compatibility = body.compatibility;
293
+ if (body.allowedTools)
294
+ frontmatter['allowed-tools'] = body.allowedTools;
295
+ if (body.metadata && Object.keys(body.metadata).length > 0) {
296
+ const meta = {};
297
+ for (const [key, value] of Object.entries(body.metadata)) {
298
+ if (value !== undefined && value !== null) {
299
+ meta[key] = value;
300
+ }
301
+ }
302
+ if (Object.keys(meta).length > 0) {
303
+ frontmatter.metadata = meta;
304
+ }
305
+ }
306
+ const fileContent = matter.stringify(finalContent, frontmatter);
307
+ await fs.writeFile(path.join(skillDir, 'SKILL.md'), fileContent, 'utf-8');
308
+ }
309
+ return c.json({
310
+ success: true,
311
+ document: {
312
+ id: skillId,
313
+ name: skillId,
314
+ description: body.description,
315
+ metadata: {
316
+ name: skillId,
317
+ description: body.description,
318
+ ...(body.metadata || {}),
319
+ },
320
+ content: finalContent
321
+ }
322
+ }, 201);
323
+ }
324
+ catch (error) {
325
+ console.error('Error creating skill document:', error);
326
+ return c.json({ success: false, error: 'Failed to create skill document' }, 500);
327
+ }
328
+ });
329
+ export default app;
@@ -0,0 +1,351 @@
1
+ import { Context } from 'hono';
2
+ export declare function GET(c: Context): Promise<(Response & import("hono").TypedResponse<{
3
+ success: false;
4
+ error: {
5
+ code: string;
6
+ message: string;
7
+ };
8
+ }, 404, "json">) | (Response & import("hono").TypedResponse<{
9
+ success: true;
10
+ data: {
11
+ id: string;
12
+ title: string;
13
+ intent?: string;
14
+ content?: string;
15
+ author: {
16
+ type: import("@lovelybunch/core").AuthorType;
17
+ id: string;
18
+ name: string;
19
+ email?: string;
20
+ };
21
+ productSpecRef?: string;
22
+ planSteps: {
23
+ id: string;
24
+ description: string;
25
+ command?: string;
26
+ expectedOutcome?: string;
27
+ status: "pending" | "in-progress" | "completed" | "failed";
28
+ output?: string;
29
+ error?: string;
30
+ executedAt?: string;
31
+ }[];
32
+ evidence: {
33
+ type: "test" | "benchmark" | "screenshot" | "log" | "other";
34
+ description: string;
35
+ data: any;
36
+ timestamp: string;
37
+ }[];
38
+ policies: {
39
+ id: string;
40
+ name: string;
41
+ description: string;
42
+ status: "pending" | "passed" | "failed" | "skipped";
43
+ message?: string;
44
+ checkedAt?: string;
45
+ }[];
46
+ featureFlags: {
47
+ id: string;
48
+ version: string;
49
+ name: string;
50
+ description: string;
51
+ type: import("@lovelybunch/core").FeatureFlagType;
52
+ defaultValue: any;
53
+ scopes: string[];
54
+ targets: {
55
+ audience: string;
56
+ value: any;
57
+ percentage?: number;
58
+ }[];
59
+ killSwitch: boolean;
60
+ dependencies: string[];
61
+ createdAt: string;
62
+ updatedAt: string;
63
+ }[];
64
+ experiments: {
65
+ id: string;
66
+ name: string;
67
+ hypothesis: string;
68
+ variants: {
69
+ id: string;
70
+ name: string;
71
+ description: string;
72
+ allocation: number;
73
+ config: {
74
+ [x: string]: any;
75
+ };
76
+ }[];
77
+ allocation: {
78
+ method: "random" | "deterministic" | "weighted";
79
+ seed?: string;
80
+ totalAllocation: number;
81
+ };
82
+ successMetrics: string[];
83
+ guardrailMetrics: {
84
+ name: string;
85
+ threshold: number;
86
+ comparison: "greater" | "less" | "equal";
87
+ action: "alert" | "pause" | "stop";
88
+ }[];
89
+ stopRules: {
90
+ metric: string;
91
+ condition: string;
92
+ action: "stop" | "pause";
93
+ }[];
94
+ analysisConfig: {
95
+ confidenceLevel: number;
96
+ minimumSampleSize: number;
97
+ testType: "two-tailed" | "one-tailed";
98
+ };
99
+ status: import("@lovelybunch/core").ExperimentStatus;
100
+ startedAt?: string;
101
+ endedAt?: string;
102
+ }[];
103
+ telemetryContracts: {
104
+ eventName: string;
105
+ version: string;
106
+ schema: {
107
+ [x: string]: any;
108
+ };
109
+ piiFields: {
110
+ field: string;
111
+ classification: "none" | "quasi" | "sensitive" | "highly-sensitive";
112
+ handling: "clear" | "hash" | "encrypt" | "remove";
113
+ }[];
114
+ samplingRate: number;
115
+ retentionPolicy: {
116
+ duration: number;
117
+ archiveAfter?: number;
118
+ deleteAfter: number;
119
+ };
120
+ approvals: {
121
+ approver: string;
122
+ approvedAt: string;
123
+ comment?: string;
124
+ }[];
125
+ }[];
126
+ releasePlan: {
127
+ strategy: "immediate" | "gradual" | "scheduled" | "gated";
128
+ stages?: {
129
+ name: string;
130
+ percentage: number;
131
+ duration?: number;
132
+ criteria?: string[];
133
+ }[];
134
+ schedule?: string;
135
+ rollbackPlan?: string;
136
+ };
137
+ status: import("@lovelybunch/core").TaskStatus;
138
+ comments?: {
139
+ id: string;
140
+ author: string;
141
+ content: string;
142
+ createdAt: string;
143
+ }[];
144
+ metadata: {
145
+ createdAt: string;
146
+ updatedAt: string;
147
+ reviewers: string[];
148
+ aiInteractions: {
149
+ id: string;
150
+ timestamp: string;
151
+ model: string;
152
+ prompt: string;
153
+ response: string;
154
+ tokens: {
155
+ prompt: number;
156
+ completion: number;
157
+ total: number;
158
+ };
159
+ cost?: number;
160
+ }[];
161
+ tags?: string[];
162
+ priority?: "low" | "medium" | "high" | "critical";
163
+ };
164
+ };
165
+ }, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
166
+ success: false;
167
+ error: {
168
+ code: string;
169
+ message: any;
170
+ };
171
+ }, 500, "json">)>;
172
+ export declare function PATCH(c: Context): Promise<(Response & import("hono").TypedResponse<{
173
+ success: true;
174
+ data: {
175
+ id: string;
176
+ title: string;
177
+ intent?: string;
178
+ content?: string;
179
+ author: {
180
+ type: import("@lovelybunch/core").AuthorType;
181
+ id: string;
182
+ name: string;
183
+ email?: string;
184
+ };
185
+ productSpecRef?: string;
186
+ planSteps: {
187
+ id: string;
188
+ description: string;
189
+ command?: string;
190
+ expectedOutcome?: string;
191
+ status: "pending" | "in-progress" | "completed" | "failed";
192
+ output?: string;
193
+ error?: string;
194
+ executedAt?: string;
195
+ }[];
196
+ evidence: {
197
+ type: "test" | "benchmark" | "screenshot" | "log" | "other";
198
+ description: string;
199
+ data: any;
200
+ timestamp: string;
201
+ }[];
202
+ policies: {
203
+ id: string;
204
+ name: string;
205
+ description: string;
206
+ status: "pending" | "passed" | "failed" | "skipped";
207
+ message?: string;
208
+ checkedAt?: string;
209
+ }[];
210
+ featureFlags: {
211
+ id: string;
212
+ version: string;
213
+ name: string;
214
+ description: string;
215
+ type: import("@lovelybunch/core").FeatureFlagType;
216
+ defaultValue: any;
217
+ scopes: string[];
218
+ targets: {
219
+ audience: string;
220
+ value: any;
221
+ percentage?: number;
222
+ }[];
223
+ killSwitch: boolean;
224
+ dependencies: string[];
225
+ createdAt: string;
226
+ updatedAt: string;
227
+ }[];
228
+ experiments: {
229
+ id: string;
230
+ name: string;
231
+ hypothesis: string;
232
+ variants: {
233
+ id: string;
234
+ name: string;
235
+ description: string;
236
+ allocation: number;
237
+ config: {
238
+ [x: string]: any;
239
+ };
240
+ }[];
241
+ allocation: {
242
+ method: "random" | "deterministic" | "weighted";
243
+ seed?: string;
244
+ totalAllocation: number;
245
+ };
246
+ successMetrics: string[];
247
+ guardrailMetrics: {
248
+ name: string;
249
+ threshold: number;
250
+ comparison: "greater" | "less" | "equal";
251
+ action: "alert" | "pause" | "stop";
252
+ }[];
253
+ stopRules: {
254
+ metric: string;
255
+ condition: string;
256
+ action: "stop" | "pause";
257
+ }[];
258
+ analysisConfig: {
259
+ confidenceLevel: number;
260
+ minimumSampleSize: number;
261
+ testType: "two-tailed" | "one-tailed";
262
+ };
263
+ status: import("@lovelybunch/core").ExperimentStatus;
264
+ startedAt?: string;
265
+ endedAt?: string;
266
+ }[];
267
+ telemetryContracts: {
268
+ eventName: string;
269
+ version: string;
270
+ schema: {
271
+ [x: string]: any;
272
+ };
273
+ piiFields: {
274
+ field: string;
275
+ classification: "none" | "quasi" | "sensitive" | "highly-sensitive";
276
+ handling: "clear" | "hash" | "encrypt" | "remove";
277
+ }[];
278
+ samplingRate: number;
279
+ retentionPolicy: {
280
+ duration: number;
281
+ archiveAfter?: number;
282
+ deleteAfter: number;
283
+ };
284
+ approvals: {
285
+ approver: string;
286
+ approvedAt: string;
287
+ comment?: string;
288
+ }[];
289
+ }[];
290
+ releasePlan: {
291
+ strategy: "immediate" | "gradual" | "scheduled" | "gated";
292
+ stages?: {
293
+ name: string;
294
+ percentage: number;
295
+ duration?: number;
296
+ criteria?: string[];
297
+ }[];
298
+ schedule?: string;
299
+ rollbackPlan?: string;
300
+ };
301
+ status: import("@lovelybunch/core").TaskStatus;
302
+ comments?: {
303
+ id: string;
304
+ author: string;
305
+ content: string;
306
+ createdAt: string;
307
+ }[];
308
+ metadata: {
309
+ createdAt: string;
310
+ updatedAt: string;
311
+ reviewers: string[];
312
+ aiInteractions: {
313
+ id: string;
314
+ timestamp: string;
315
+ model: string;
316
+ prompt: string;
317
+ response: string;
318
+ tokens: {
319
+ prompt: number;
320
+ completion: number;
321
+ total: number;
322
+ };
323
+ cost?: number;
324
+ }[];
325
+ tags?: string[];
326
+ priority?: "low" | "medium" | "high" | "critical";
327
+ };
328
+ };
329
+ }, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
330
+ success: false;
331
+ error: {
332
+ code: string;
333
+ message: any;
334
+ };
335
+ }, 404, "json">) | (Response & import("hono").TypedResponse<{
336
+ success: false;
337
+ error: {
338
+ code: string;
339
+ message: any;
340
+ };
341
+ }, 500, "json">)>;
342
+ export declare function DELETE(c: Context): Promise<(Response & import("hono").TypedResponse<{
343
+ success: true;
344
+ message: string;
345
+ }, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
346
+ success: false;
347
+ error: {
348
+ code: string;
349
+ message: any;
350
+ };
351
+ }, 500, "json">)>;