kennel 0.2.30 → 0.2.32

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.
@@ -38723,6 +38723,10 @@ function assetKey(value) {
38723
38723
  function encodeBase64Url(value) {
38724
38724
  return Buffer.from(value, "utf8").toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
38725
38725
  }
38726
+ function decodeBase64Url2(value) {
38727
+ const padded = `${value}${"=".repeat((4 - value.length % 4) % 4)}`;
38728
+ return Buffer.from(padded.replaceAll("-", "+").replaceAll("_", "/"), "base64").toString("utf8");
38729
+ }
38726
38730
  function iconUrl(folder, hash, mediaType) {
38727
38731
  return `/api/folders/${encodeBase64Url(folder)}/assets/${hash}.${iconExtensions.get(mediaType)}`;
38728
38732
  }
@@ -38746,6 +38750,54 @@ async function folderIcon(folder, assets, assetFiles, fallback) {
38746
38750
  };
38747
38751
  return key;
38748
38752
  }
38753
+ async function searchIconDataUrl(icon) {
38754
+ if (!icon)
38755
+ return;
38756
+ if (icon.startsWith("data:"))
38757
+ return icon;
38758
+ const mediaType = searchIconTypes.get(extname4(icon).toLowerCase());
38759
+ if (!mediaType)
38760
+ return;
38761
+ const details = await stat5(icon).catch(() => {
38762
+ return;
38763
+ });
38764
+ if (!details?.size || details.size > maxSearchIconBytes)
38765
+ return;
38766
+ return `data:${mediaType};base64,${(await readFile8(icon)).toString("base64")}`;
38767
+ }
38768
+ async function faviconAsset(folderName, assets) {
38769
+ if (process.env.KENNEL_EXTERNAL_ICONS !== "1")
38770
+ return;
38771
+ const host = await folderWebsite(folderName);
38772
+ if (!host)
38773
+ return;
38774
+ const key = `favicon:${host}`;
38775
+ assets[key] = {
38776
+ mediaType: "image/png",
38777
+ source: "favicon",
38778
+ url: `/api/folder-favicon?path=${encodeURIComponent(folderName)}&host=${encodeURIComponent(host)}`
38779
+ };
38780
+ return key;
38781
+ }
38782
+ async function iconWithFaviconFallback(folder, assets, assetFiles) {
38783
+ return folderIcon(folder, assets, assetFiles, (folderName) => faviconAsset(folderName, assets));
38784
+ }
38785
+ async function hydrateFolderIcons(metadata, assetFiles) {
38786
+ const assets = {};
38787
+ const projects = metadata.projects?.items ?? [];
38788
+ const [iconAssetKey, projectItems] = await Promise.all([
38789
+ iconWithFaviconFallback(metadata.folder, assets, assetFiles),
38790
+ Promise.all(projects.map(async (project) => ({
38791
+ ...project,
38792
+ iconAssetKey: await iconWithFaviconFallback(project.worktree, assets, assetFiles)
38793
+ })))
38794
+ ]);
38795
+ return {
38796
+ assets,
38797
+ iconAssetKey,
38798
+ projects: { items: projectItems }
38799
+ };
38800
+ }
38749
38801
  async function workspaceBreadcrumbIcons(breadcrumbs, assetFiles, assets = {}) {
38750
38802
  return Promise.all(breadcrumbs.map(async (crumb) => {
38751
38803
  const iconAssetKey = await folderIcon(crumb.path, assets, assetFiles);
@@ -38753,8 +38805,9 @@ async function workspaceBreadcrumbIcons(breadcrumbs, assetFiles, assets = {}) {
38753
38805
  return icon ? { ...crumb, icon: { mediaType: icon.mediaType, url: icon.url } } : crumb;
38754
38806
  }));
38755
38807
  }
38756
- var maxIconBytes2, iconTypes, iconExtensions;
38808
+ var maxIconBytes2, iconTypes, iconExtensions, maxSearchIconBytes, searchIconTypes;
38757
38809
  var init_folder_icons = __esm(() => {
38810
+ init_folder_favicon();
38758
38811
  maxIconBytes2 = 4 * 1024 * 1024;
38759
38812
  iconTypes = new Map([
38760
38813
  [".jpeg", "image/jpeg"],
@@ -38765,6 +38818,16 @@ var init_folder_icons = __esm(() => {
38765
38818
  ["image/jpeg", "jpg"],
38766
38819
  ["image/png", "png"]
38767
38820
  ]);
38821
+ maxSearchIconBytes = 512 * 1024;
38822
+ searchIconTypes = new Map([
38823
+ [".gif", "image/gif"],
38824
+ [".ico", "image/x-icon"],
38825
+ [".jpeg", "image/jpeg"],
38826
+ [".jpg", "image/jpeg"],
38827
+ [".png", "image/png"],
38828
+ [".svg", "image/svg+xml"],
38829
+ [".webp", "image/webp"]
38830
+ ]);
38768
38831
  });
38769
38832
 
38770
38833
  // packages/notes/src/manifest.ts
@@ -38831,6 +38894,45 @@ async function loadNotes(folder, linkedFolders = []) {
38831
38894
  status: "available"
38832
38895
  };
38833
38896
  }
38897
+ function codeSpan(text) {
38898
+ const escaped = text.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/gu, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
38899
+ const longest = Math.max(0, ...(escaped.match(/`+/g) ?? []).map((run) => run.length));
38900
+ const fence = "`".repeat(longest + 1);
38901
+ const pad = escaped.startsWith("`") || escaped.endsWith("`") ? " " : "";
38902
+ return `${fence}${pad}${escaped}${pad}${fence}`;
38903
+ }
38904
+ async function notesLlmSections({
38905
+ folder,
38906
+ linkedFolders = [],
38907
+ signal
38908
+ }) {
38909
+ const groups = [];
38910
+ const notes = [];
38911
+ for (const source of [folder, ...linkedFolders]) {
38912
+ signal.throwIfAborted();
38913
+ const files = await listMarkdownFiles(source);
38914
+ const paths = files.map((file) => resolve10(source, file.relativePath));
38915
+ groups.push({ folder: source, paths });
38916
+ files.forEach((file, index) => notes.push({ activityAt: file.activityAt, path: paths[index] }));
38917
+ }
38918
+ if (notes.length === 0)
38919
+ return [];
38920
+ const recent = notes.sort((left, right) => right.activityAt - left.activityAt || (left.path < right.path ? -1 : left.path > right.path ? 1 : 0)).slice(0, llmNoteLimit);
38921
+ return [
38922
+ {
38923
+ id: "notes",
38924
+ title: "Markdown notes",
38925
+ lines: [
38926
+ ...groups.filter((group) => group.paths.length > 0).map((group) => `- Source: ${codeSpan(group.folder)} (${group.paths.length} ${group.paths.length === 1 ? "note" : "notes"})`),
38927
+ `- Total notes: ${notes.length}`,
38928
+ "",
38929
+ `Most recently changed (${recent.length} of ${notes.length}):`,
38930
+ "",
38931
+ ...recent.map((note) => `- ${codeSpan(note.path)}`)
38932
+ ]
38933
+ }
38934
+ ];
38935
+ }
38834
38936
  function createNotesPlugin() {
38835
38937
  return {
38836
38938
  ...notesManifest,
@@ -38845,10 +38947,11 @@ function createNotesPlugin() {
38845
38947
  linkedFolders
38846
38948
  }) {
38847
38949
  return { data: await loadNotes(folder, linkedFolders), kind: "available" };
38848
- }
38950
+ },
38951
+ llm: notesLlmSections
38849
38952
  };
38850
38953
  }
38851
- var markdownExtensions;
38954
+ var markdownExtensions, llmNoteLimit = 20;
38852
38955
  var init_server3 = __esm(() => {
38853
38956
  init_manifest();
38854
38957
  markdownExtensions = new Set([".md", ".markdown"]);
@@ -38857,6 +38960,7 @@ var init_server3 = __esm(() => {
38857
38960
  // packages/api/src/server/plugins.ts
38858
38961
  import { pathToFileURL } from "node:url";
38859
38962
  import { existsSync as existsSync4 } from "node:fs";
38963
+ import { mkdir as mkdir3, readFile as readFile9, stat as stat7, writeFile as writeFile5 } from "node:fs/promises";
38860
38964
  import { dirname as dirname8, resolve as resolve11, sep as sep6 } from "node:path";
38861
38965
  function configuredFolderServerRegistry(registry, resolvedConfig) {
38862
38966
  const disabled = new Set(resolvedConfig.plugins.disable);
@@ -38867,6 +38971,41 @@ function configuredFolderServerRegistry(registry, resolvedConfig) {
38867
38971
  ];
38868
38972
  return new Map(ids.map((id) => [id, registry.get(id)]));
38869
38973
  }
38974
+ function stringArray(value) {
38975
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
38976
+ }
38977
+ function parsePluginRegistryConfig(content) {
38978
+ const parsed = JSON.parse(content);
38979
+ const config = parsed && typeof parsed === "object" ? parsed : {};
38980
+ return {
38981
+ plugins: {
38982
+ disable: stringArray(config.plugins?.disable),
38983
+ order: stringArray(config.plugins?.order)
38984
+ }
38985
+ };
38986
+ }
38987
+ async function nearestKennelConfig(folder) {
38988
+ for (let current = folder;; current = dirname8(current)) {
38989
+ const candidate = resolve11(current, "kennel.json");
38990
+ const details = await stat7(candidate).catch(() => {
38991
+ return;
38992
+ });
38993
+ if (details?.isFile())
38994
+ return candidate;
38995
+ if (dirname8(current) === current)
38996
+ return;
38997
+ }
38998
+ }
38999
+ async function folderPluginRegistryConfig(folder) {
39000
+ const path = await nearestKennelConfig(folder);
39001
+ if (!path)
39002
+ return defaultPluginRegistryConfig;
39003
+ try {
39004
+ return parsePluginRegistryConfig(await readFile9(path, "utf8"));
39005
+ } catch {
39006
+ return defaultPluginRegistryConfig;
39007
+ }
39008
+ }
38870
39009
  function createFolderServerPlugins() {
38871
39010
  return [createNotesPlugin()];
38872
39011
  }
@@ -38947,8 +39086,12 @@ async function externalApiHandlers() {
38947
39086
  }
38948
39087
  return handlers;
38949
39088
  }
39089
+ var defaultPluginRegistryConfig;
38950
39090
  var init_plugins = __esm(() => {
38951
39091
  init_server3();
39092
+ defaultPluginRegistryConfig = {
39093
+ plugins: { disable: [], order: [] }
39094
+ };
38952
39095
  });
38953
39096
 
38954
39097
  // packages/api/src/server/linked-folder-discovery.ts
@@ -39174,7 +39317,7 @@ var init_project_discovery = __esm(() => {
39174
39317
  // packages/api/src/server/folder-metadata-plugin.ts
39175
39318
  import { createHash as createHash5 } from "node:crypto";
39176
39319
  import { realpathSync as realpathSync2 } from "node:fs";
39177
- import { readdir as readdir5, readFile as readFile9, stat as stat7 } from "node:fs/promises";
39320
+ import { readdir as readdir5, readFile as readFile10, stat as stat8 } from "node:fs/promises";
39178
39321
  import { homedir as homedir3 } from "node:os";
39179
39322
  import {
39180
39323
  basename as basename7,
@@ -39243,42 +39386,6 @@ function apiHostAllowed(value, remote) {
39243
39386
  const allowlist = (process.env.KENNEL_VIEWER_HOSTS ?? "").split(",").map((host) => host.trim().toLowerCase()).filter(Boolean);
39244
39387
  return allowlist.includes(hostname);
39245
39388
  }
39246
- function decodeBase64Url2(value) {
39247
- const padded = `${value}${"=".repeat((4 - value.length % 4) % 4)}`;
39248
- return Buffer.from(padded.replaceAll("-", "+").replaceAll("_", "/"), "base64").toString("utf8");
39249
- }
39250
- async function searchIconDataUrl(icon) {
39251
- if (!icon)
39252
- return;
39253
- if (icon.startsWith("data:"))
39254
- return icon;
39255
- const mediaType = searchIconTypes.get(extname6(icon).toLowerCase());
39256
- if (!mediaType)
39257
- return;
39258
- const details = await stat7(icon).catch(() => {
39259
- return;
39260
- });
39261
- if (!details?.size || details.size > maxIconBytes3)
39262
- return;
39263
- return `data:${mediaType};base64,${(await readFile9(icon)).toString("base64")}`;
39264
- }
39265
- async function faviconAsset(folderName, assets) {
39266
- if (process.env.KENNEL_EXTERNAL_ICONS !== "1")
39267
- return;
39268
- const host = await folderWebsite(folderName);
39269
- if (!host)
39270
- return;
39271
- const key = `favicon:${host}`;
39272
- assets[key] = {
39273
- mediaType: "image/png",
39274
- source: "favicon",
39275
- url: `/api/folder-favicon?path=${encodeURIComponent(folderName)}&host=${encodeURIComponent(host)}`
39276
- };
39277
- return key;
39278
- }
39279
- async function iconWithFaviconFallback(folder, assets, assetFiles) {
39280
- return folderIcon(folder, assets, assetFiles, (folderName) => faviconAsset(folderName, assets));
39281
- }
39282
39389
  async function repositoryIdentity(folder, assets) {
39283
39390
  const remoteUrl = await gitRemoteUrl(folder).catch(() => {
39284
39391
  return;
@@ -39302,22 +39409,6 @@ async function repositoryIdentity(folder, assets) {
39302
39409
  return;
39303
39410
  }
39304
39411
  }
39305
- async function hydrateAssets(metadata, assetFiles) {
39306
- const assets = {};
39307
- const projects = metadata.projects?.items ?? [];
39308
- const [iconAssetKey, projectItems] = await Promise.all([
39309
- iconWithFaviconFallback(metadata.folder, assets, assetFiles),
39310
- Promise.all(projects.map(async (project) => ({
39311
- ...project,
39312
- iconAssetKey: await iconWithFaviconFallback(project.worktree, assets, assetFiles)
39313
- })))
39314
- ]);
39315
- return {
39316
- assets,
39317
- iconAssetKey,
39318
- projects: { items: projectItems }
39319
- };
39320
- }
39321
39412
  function unique(values) {
39322
39413
  return [...new Set(values)];
39323
39414
  }
@@ -39329,8 +39420,8 @@ async function shallowMetadata(folder) {
39329
39420
  const visible = entries.filter((entry) => !entry.name.startsWith(".") && entry.name !== "node_modules");
39330
39421
  const files = await Promise.all(visible.filter((entry) => entry.isFile() && [".markdown", ".md"].includes(extname6(entry.name).toLowerCase())).map(async (entry) => {
39331
39422
  const path = resolve12(folder, entry.name);
39332
- const details = await stat7(path);
39333
- const content = await readFile9(path, "utf8").catch(() => "");
39423
+ const details = await stat8(path);
39424
+ const content = await readFile10(path, "utf8").catch(() => "");
39334
39425
  const title = markdownMetadata(content).title;
39335
39426
  return {
39336
39427
  activityAt: details.mtimeMs,
@@ -39384,7 +39475,7 @@ async function notesFiles(folder, files, origin) {
39384
39475
  const path = file.path ?? (file.relativePath ? resolve12(folder, file.relativePath) : undefined);
39385
39476
  if (!path)
39386
39477
  return origin ? { ...file, folder: origin } : file;
39387
- const content = [".markdown", ".md"].includes(extname6(path).toLowerCase()) ? await readFile9(path, "utf8").catch(() => "") : "";
39478
+ const content = [".markdown", ".md"].includes(extname6(path).toLowerCase()) ? await readFile10(path, "utf8").catch(() => "") : "";
39388
39479
  const title = content ? markdownMetadata(content).title : undefined;
39389
39480
  return {
39390
39481
  ...file,
@@ -39502,41 +39593,6 @@ async function projectSearchItem(project, match) {
39502
39593
  worktree: project.worktree
39503
39594
  };
39504
39595
  }
39505
- function stringArray(value) {
39506
- return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
39507
- }
39508
- function parsePluginRegistryConfig(content) {
39509
- const parsed = JSON.parse(content);
39510
- const config = parsed && typeof parsed === "object" ? parsed : {};
39511
- return {
39512
- plugins: {
39513
- disable: stringArray(config.plugins?.disable),
39514
- order: stringArray(config.plugins?.order)
39515
- }
39516
- };
39517
- }
39518
- async function nearestKennelConfig(folder) {
39519
- for (let current = folder;; current = dirname10(current)) {
39520
- const candidate = resolve12(current, "kennel.json");
39521
- const details = await stat7(candidate).catch(() => {
39522
- return;
39523
- });
39524
- if (details?.isFile())
39525
- return candidate;
39526
- if (dirname10(current) === current)
39527
- return;
39528
- }
39529
- }
39530
- async function folderPluginRegistryConfig(folder) {
39531
- const path = await nearestKennelConfig(folder);
39532
- if (!path)
39533
- return defaultPluginRegistryConfig;
39534
- try {
39535
- return parsePluginRegistryConfig(await readFile9(path, "utf8"));
39536
- } catch {
39537
- return defaultPluginRegistryConfig;
39538
- }
39539
- }
39540
39596
  function pluginConfigWarnings(registry, config) {
39541
39597
  const ids = unique([...config.plugins.disable, ...config.plugins.order]);
39542
39598
  return ids.filter((id) => !registry.has(id)).map((id) => `unknown plugin id "${id}" — not installed in this build`);
@@ -39712,7 +39768,7 @@ function createFolderApi({
39712
39768
  "Content-Type": asset.mediaType,
39713
39769
  ETag: `"${hash}"`
39714
39770
  });
39715
- response.end(await readFile9(asset.path));
39771
+ response.end(await readFile10(asset.path));
39716
39772
  } catch (error) {
39717
39773
  response.writeHead(404, {
39718
39774
  "Content-Type": "text/plain; charset=utf-8"
@@ -39791,7 +39847,7 @@ function createFolderApi({
39791
39847
  {
39792
39848
  path: relative11(folder, target.path),
39793
39849
  kind: target.kind,
39794
- content: previewTextKind(target.kind) ? await readFile9(target.path, "utf8") : ""
39850
+ content: previewTextKind(target.kind) ? await readFile10(target.path, "utf8") : ""
39795
39851
  }
39796
39852
  ] : []
39797
39853
  } : await readFolder(folder);
@@ -39807,7 +39863,7 @@ function createFolderApi({
39807
39863
  documentError(response);
39808
39864
  return;
39809
39865
  }
39810
- const details = await stat7(target.path);
39866
+ const details = await stat8(target.path);
39811
39867
  if (!details.isFile()) {
39812
39868
  documentError(response);
39813
39869
  return;
@@ -39819,7 +39875,7 @@ function createFolderApi({
39819
39875
  return;
39820
39876
  }
39821
39877
  watch(folder);
39822
- const content = previewTextKind(target.kind) ? await readFile9(target.path, "utf8") : "";
39878
+ const content = previewTextKind(target.kind) ? await readFile10(target.path, "utf8") : "";
39823
39879
  sendContract(response, url, {
39824
39880
  content,
39825
39881
  kind: target.kind,
@@ -39848,7 +39904,7 @@ function createFolderApi({
39848
39904
  documentError(response);
39849
39905
  return;
39850
39906
  }
39851
- const details = await stat7(target.path);
39907
+ const details = await stat8(target.path);
39852
39908
  if (!details.isFile()) {
39853
39909
  documentError(response);
39854
39910
  return;
@@ -39872,7 +39928,7 @@ function createFolderApi({
39872
39928
  "Content-Disposition": `attachment; filename="${basename7(target.path)}"`
39873
39929
  } : {}
39874
39930
  });
39875
- response.end(await readFile9(target.path));
39931
+ response.end(await readFile10(target.path));
39876
39932
  } catch (error) {
39877
39933
  documentError(response);
39878
39934
  }
@@ -39886,7 +39942,7 @@ function createFolderApi({
39886
39942
  documentError(response);
39887
39943
  return;
39888
39944
  }
39889
- const details = await stat7(target.path);
39945
+ const details = await stat8(target.path);
39890
39946
  if (!details.isFile()) {
39891
39947
  documentError(response);
39892
39948
  return;
@@ -39898,7 +39954,7 @@ function createFolderApi({
39898
39954
  return;
39899
39955
  }
39900
39956
  watch(folder);
39901
- const data = await readFile9(target.path);
39957
+ const data = await readFile10(target.path);
39902
39958
  const baseHeaders = {
39903
39959
  "Accept-Ranges": "bytes",
39904
39960
  "Cache-Control": "private, max-age=0, must-revalidate",
@@ -40155,7 +40211,7 @@ function createFolderApi({
40155
40211
  response.off("close", cancel);
40156
40212
  }
40157
40213
  const [hydrated, workspace] = await Promise.all([
40158
- hydrateAssets({ folder, projects: { items: projects } }, assetFiles),
40214
+ hydrateFolderIcons({ folder, projects: { items: projects } }, assetFiles),
40159
40215
  readWorkspace(folder)
40160
40216
  ]);
40161
40217
  workspace.breadcrumbs = await workspaceBreadcrumbIcons(workspace.breadcrumbs ?? [], assetFiles, hydrated.assets);
@@ -40168,7 +40224,7 @@ function createFolderApi({
40168
40224
  return;
40169
40225
  }
40170
40226
  const metadata = machine ? await shallowMetadata(folder) : await loadFolderData(folder);
40171
- const assets = await hydrateAssets(metadata, assetFiles);
40227
+ const assets = await hydrateFolderIcons(metadata, assetFiles);
40172
40228
  const body = {
40173
40229
  ...metadata,
40174
40230
  ...assets,
@@ -40186,7 +40242,7 @@ function createFolderApi({
40186
40242
  }
40187
40243
  };
40188
40244
  }
40189
- var applyProjectFavorites, buildProjectCatalog, buildProjectSearchIndex, catalogProjectToLocalProject, discoverProjectIcon, discoverProjectsUnderFolder, listLocalMarkdownFiles, projectRepositoryAccessory, projectSubtitle, projectTitle, readRaggleUserSettings, gitRemoteUrl, githubRepositoryPath, repositoryDisplayName, searchProjectIndex, sortProjectList, maxIconBytes3, iconTypes2, iconExtensions2, searchIconTypes, projectSearchCacheMs = 5000, metadataExtensions, loopbackServerHosts, defaultPluginRegistryConfig;
40245
+ var applyProjectFavorites, buildProjectCatalog, buildProjectSearchIndex, catalogProjectToLocalProject, discoverProjectIcon, discoverProjectsUnderFolder, listLocalMarkdownFiles, projectRepositoryAccessory, projectSubtitle, projectTitle, readRaggleUserSettings, gitRemoteUrl, githubRepositoryPath, repositoryDisplayName, searchProjectIndex, sortProjectList, projectSearchCacheMs = 5000, metadataExtensions, loopbackServerHosts;
40190
40246
  var init_folder_metadata_plugin = __esm(() => {
40191
40247
  init_contracts();
40192
40248
  init_markdown();
@@ -40225,25 +40281,6 @@ var init_folder_metadata_plugin = __esm(() => {
40225
40281
  searchProjectIndex,
40226
40282
  sortProjectList
40227
40283
  } = local);
40228
- maxIconBytes3 = 512 * 1024;
40229
- iconTypes2 = new Map([
40230
- [".jpeg", "image/jpeg"],
40231
- [".jpg", "image/jpeg"],
40232
- [".png", "image/png"]
40233
- ]);
40234
- iconExtensions2 = new Map([
40235
- ["image/jpeg", "jpg"],
40236
- ["image/png", "png"]
40237
- ]);
40238
- searchIconTypes = new Map([
40239
- [".gif", "image/gif"],
40240
- [".ico", "image/x-icon"],
40241
- [".jpeg", "image/jpeg"],
40242
- [".jpg", "image/jpeg"],
40243
- [".png", "image/png"],
40244
- [".svg", "image/svg+xml"],
40245
- [".webp", "image/webp"]
40246
- ]);
40247
40284
  metadataExtensions = new Set([
40248
40285
  ".html",
40249
40286
  ".jpeg",
@@ -40262,16 +40299,13 @@ var init_folder_metadata_plugin = __esm(() => {
40262
40299
  undefined,
40263
40300
  false
40264
40301
  ]);
40265
- defaultPluginRegistryConfig = {
40266
- plugins: { disable: [], order: [] }
40267
- };
40268
40302
  });
40269
40303
 
40270
40304
  // packages/api/src/server/index.ts
40271
40305
  import {
40272
40306
  createServer
40273
40307
  } from "node:http";
40274
- import { readFile as readFile10, realpath as realpath5, stat as stat8 } from "node:fs/promises";
40308
+ import { readFile as readFile11, realpath as realpath5, stat as stat9 } from "node:fs/promises";
40275
40309
  import { extname as extname7, isAbsolute as isAbsolute7, relative as relative12, resolve as resolve13 } from "node:path";
40276
40310
  async function sendFile(request, response, directory, pathname, fallback) {
40277
40311
  const root = await realpath5(directory);
@@ -40285,7 +40319,7 @@ async function sendFile(request, response, directory, pathname, fallback) {
40285
40319
  response.end();
40286
40320
  return;
40287
40321
  }
40288
- if (!(await stat8(target).catch(() => null))?.isFile()) {
40322
+ if (!(await stat9(target).catch(() => null))?.isFile()) {
40289
40323
  if (!fallback) {
40290
40324
  response.writeHead(404);
40291
40325
  response.end();
@@ -40303,7 +40337,7 @@ async function sendFile(request, response, directory, pathname, fallback) {
40303
40337
  "Content-Type": contentTypes[extname7(target)] ?? "application/octet-stream",
40304
40338
  "X-Content-Type-Options": "nosniff"
40305
40339
  });
40306
- response.end(request.method === "HEAD" ? undefined : await readFile10(target));
40340
+ response.end(request.method === "HEAD" ? undefined : await readFile11(target));
40307
40341
  }
40308
40342
  function createKennelApi(options) {
40309
40343
  const remote = options.remoteAccess || !isLoopbackServerHost(options.host);
@@ -83,6 +83,12 @@ export type PluginBuild = {
83
83
  };
84
84
  /** Points the core runtime at a plugin build. */
85
85
  export declare function usePluginBuild(build: PluginBuild): void;
86
+ /**
87
+ * The pinned build of the installed plugins, read without building. `knl llm`
88
+ * uses it: a missing or stale build is an error, never a browser build or an
89
+ * install during context generation.
90
+ */
91
+ export declare function readPluginBuild(plugins?: ExternalPluginEntry[]): Promise<PluginBuild>;
86
92
  /**
87
93
  * Compiles the installed external plugins once per plugin set. The host web
88
94
  * app is never rebuilt: plugin browser modules load at runtime against the
@@ -350,6 +350,8 @@ async function validateImports(entry) {
350
350
  throw new Error("server id mismatch");
351
351
  if (typeof server.detect !== "function" || typeof server.handle !== "function")
352
352
  throw new Error("server export missing detect/handle");
353
+ if (server.llm !== undefined && typeof server.llm !== "function")
354
+ throw new Error("server export llm hook must be a function");
353
355
  if (!client || typeof client !== "object" || client.id !== entry.id)
354
356
  throw new Error("client id mismatch");
355
357
  if (typeof client.component !== "function")
@@ -570,6 +572,20 @@ async function readBuildMarker(marker, key) {
570
572
  return undefined;
571
573
  }
572
574
  }
575
+ /**
576
+ * The pinned build of the installed plugins, read without building. `knl llm`
577
+ * uses it: a missing or stale build is an error, never a browser build or an
578
+ * install during context generation.
579
+ */
580
+ export async function readPluginBuild(plugins = readExternalPlugins()) {
581
+ const key = pluginSetKey(plugins);
582
+ if (plugins.length === 0)
583
+ return { key, serverEntries: [], apiEntries: [] };
584
+ const build = await readBuildMarker(join(buildRoot(key), "build.json"), key);
585
+ if (build)
586
+ return build;
587
+ throw new Error(`The installed plugin build is missing or stale (${plugins.map((plugin) => plugin.id).join(", ")}). Refresh each changed plugin with \`kennel plugin add <source>\`, or run knl llm --no-plugins.`);
588
+ }
573
589
  function acquireBuildLock(key) {
574
590
  const locks = join(cacheRoot(), "plugin-builds", ".locks");
575
591
  mkdirSync(locks, { recursive: true });
@@ -19,6 +19,18 @@ export declare class FolderPluginFailure extends Error {
19
19
  constructor(message: string);
20
20
  }
21
21
  export declare function folderServerRegistry(): Map<string, FolderPluginServer>;
22
+ /** Core plugins plus the installed plugins named by the active plugin build. */
23
+ export declare function folderServerRegistryAsync(): Promise<Map<string, FolderPluginServer>>;
24
+ export type FolderPluginRegistryConfig = {
25
+ plugins: {
26
+ disable: string[];
27
+ order: string[];
28
+ };
29
+ };
30
+ /** Applies kennel.json disable/order settings to a registry; never adds plugins. */
31
+ export declare function configuredFolderServerRegistry(registry: Map<string, FolderPluginServer>, config: FolderPluginRegistryConfig): Map<string, FolderPluginServer>;
32
+ /** Reads the plugin settings from the folder's nearest kennel.json. */
33
+ export declare function folderPluginRegistryConfig(folder: string): Promise<FolderPluginRegistryConfig>;
22
34
  export type FolderPluginScaffoldFile = {
23
35
  plugin: string;
24
36
  path: string;