kennel 0.2.31 → 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.
@@ -38894,6 +38894,45 @@ async function loadNotes(folder, linkedFolders = []) {
38894
38894
  status: "available"
38895
38895
  };
38896
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
+ }
38897
38936
  function createNotesPlugin() {
38898
38937
  return {
38899
38938
  ...notesManifest,
@@ -38908,10 +38947,11 @@ function createNotesPlugin() {
38908
38947
  linkedFolders
38909
38948
  }) {
38910
38949
  return { data: await loadNotes(folder, linkedFolders), kind: "available" };
38911
- }
38950
+ },
38951
+ llm: notesLlmSections
38912
38952
  };
38913
38953
  }
38914
- var markdownExtensions;
38954
+ var markdownExtensions, llmNoteLimit = 20;
38915
38955
  var init_server3 = __esm(() => {
38916
38956
  init_manifest();
38917
38957
  markdownExtensions = new Set([".md", ".markdown"]);
@@ -38920,6 +38960,7 @@ var init_server3 = __esm(() => {
38920
38960
  // packages/api/src/server/plugins.ts
38921
38961
  import { pathToFileURL } from "node:url";
38922
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";
38923
38964
  import { dirname as dirname8, resolve as resolve11, sep as sep6 } from "node:path";
38924
38965
  function configuredFolderServerRegistry(registry, resolvedConfig) {
38925
38966
  const disabled = new Set(resolvedConfig.plugins.disable);
@@ -38930,6 +38971,41 @@ function configuredFolderServerRegistry(registry, resolvedConfig) {
38930
38971
  ];
38931
38972
  return new Map(ids.map((id) => [id, registry.get(id)]));
38932
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
+ }
38933
39009
  function createFolderServerPlugins() {
38934
39010
  return [createNotesPlugin()];
38935
39011
  }
@@ -39010,8 +39086,12 @@ async function externalApiHandlers() {
39010
39086
  }
39011
39087
  return handlers;
39012
39088
  }
39089
+ var defaultPluginRegistryConfig;
39013
39090
  var init_plugins = __esm(() => {
39014
39091
  init_server3();
39092
+ defaultPluginRegistryConfig = {
39093
+ plugins: { disable: [], order: [] }
39094
+ };
39015
39095
  });
39016
39096
 
39017
39097
  // packages/api/src/server/linked-folder-discovery.ts
@@ -39237,7 +39317,7 @@ var init_project_discovery = __esm(() => {
39237
39317
  // packages/api/src/server/folder-metadata-plugin.ts
39238
39318
  import { createHash as createHash5 } from "node:crypto";
39239
39319
  import { realpathSync as realpathSync2 } from "node:fs";
39240
- 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";
39241
39321
  import { homedir as homedir3 } from "node:os";
39242
39322
  import {
39243
39323
  basename as basename7,
@@ -39340,8 +39420,8 @@ async function shallowMetadata(folder) {
39340
39420
  const visible = entries.filter((entry) => !entry.name.startsWith(".") && entry.name !== "node_modules");
39341
39421
  const files = await Promise.all(visible.filter((entry) => entry.isFile() && [".markdown", ".md"].includes(extname6(entry.name).toLowerCase())).map(async (entry) => {
39342
39422
  const path = resolve12(folder, entry.name);
39343
- const details = await stat7(path);
39344
- const content = await readFile9(path, "utf8").catch(() => "");
39423
+ const details = await stat8(path);
39424
+ const content = await readFile10(path, "utf8").catch(() => "");
39345
39425
  const title = markdownMetadata(content).title;
39346
39426
  return {
39347
39427
  activityAt: details.mtimeMs,
@@ -39395,7 +39475,7 @@ async function notesFiles(folder, files, origin) {
39395
39475
  const path = file.path ?? (file.relativePath ? resolve12(folder, file.relativePath) : undefined);
39396
39476
  if (!path)
39397
39477
  return origin ? { ...file, folder: origin } : file;
39398
- 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(() => "") : "";
39399
39479
  const title = content ? markdownMetadata(content).title : undefined;
39400
39480
  return {
39401
39481
  ...file,
@@ -39513,41 +39593,6 @@ async function projectSearchItem(project, match) {
39513
39593
  worktree: project.worktree
39514
39594
  };
39515
39595
  }
39516
- function stringArray(value) {
39517
- return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
39518
- }
39519
- function parsePluginRegistryConfig(content) {
39520
- const parsed = JSON.parse(content);
39521
- const config = parsed && typeof parsed === "object" ? parsed : {};
39522
- return {
39523
- plugins: {
39524
- disable: stringArray(config.plugins?.disable),
39525
- order: stringArray(config.plugins?.order)
39526
- }
39527
- };
39528
- }
39529
- async function nearestKennelConfig(folder) {
39530
- for (let current = folder;; current = dirname10(current)) {
39531
- const candidate = resolve12(current, "kennel.json");
39532
- const details = await stat7(candidate).catch(() => {
39533
- return;
39534
- });
39535
- if (details?.isFile())
39536
- return candidate;
39537
- if (dirname10(current) === current)
39538
- return;
39539
- }
39540
- }
39541
- async function folderPluginRegistryConfig(folder) {
39542
- const path = await nearestKennelConfig(folder);
39543
- if (!path)
39544
- return defaultPluginRegistryConfig;
39545
- try {
39546
- return parsePluginRegistryConfig(await readFile9(path, "utf8"));
39547
- } catch {
39548
- return defaultPluginRegistryConfig;
39549
- }
39550
- }
39551
39596
  function pluginConfigWarnings(registry, config) {
39552
39597
  const ids = unique([...config.plugins.disable, ...config.plugins.order]);
39553
39598
  return ids.filter((id) => !registry.has(id)).map((id) => `unknown plugin id "${id}" — not installed in this build`);
@@ -39723,7 +39768,7 @@ function createFolderApi({
39723
39768
  "Content-Type": asset.mediaType,
39724
39769
  ETag: `"${hash}"`
39725
39770
  });
39726
- response.end(await readFile9(asset.path));
39771
+ response.end(await readFile10(asset.path));
39727
39772
  } catch (error) {
39728
39773
  response.writeHead(404, {
39729
39774
  "Content-Type": "text/plain; charset=utf-8"
@@ -39802,7 +39847,7 @@ function createFolderApi({
39802
39847
  {
39803
39848
  path: relative11(folder, target.path),
39804
39849
  kind: target.kind,
39805
- content: previewTextKind(target.kind) ? await readFile9(target.path, "utf8") : ""
39850
+ content: previewTextKind(target.kind) ? await readFile10(target.path, "utf8") : ""
39806
39851
  }
39807
39852
  ] : []
39808
39853
  } : await readFolder(folder);
@@ -39818,7 +39863,7 @@ function createFolderApi({
39818
39863
  documentError(response);
39819
39864
  return;
39820
39865
  }
39821
- const details = await stat7(target.path);
39866
+ const details = await stat8(target.path);
39822
39867
  if (!details.isFile()) {
39823
39868
  documentError(response);
39824
39869
  return;
@@ -39830,7 +39875,7 @@ function createFolderApi({
39830
39875
  return;
39831
39876
  }
39832
39877
  watch(folder);
39833
- const content = previewTextKind(target.kind) ? await readFile9(target.path, "utf8") : "";
39878
+ const content = previewTextKind(target.kind) ? await readFile10(target.path, "utf8") : "";
39834
39879
  sendContract(response, url, {
39835
39880
  content,
39836
39881
  kind: target.kind,
@@ -39859,7 +39904,7 @@ function createFolderApi({
39859
39904
  documentError(response);
39860
39905
  return;
39861
39906
  }
39862
- const details = await stat7(target.path);
39907
+ const details = await stat8(target.path);
39863
39908
  if (!details.isFile()) {
39864
39909
  documentError(response);
39865
39910
  return;
@@ -39883,7 +39928,7 @@ function createFolderApi({
39883
39928
  "Content-Disposition": `attachment; filename="${basename7(target.path)}"`
39884
39929
  } : {}
39885
39930
  });
39886
- response.end(await readFile9(target.path));
39931
+ response.end(await readFile10(target.path));
39887
39932
  } catch (error) {
39888
39933
  documentError(response);
39889
39934
  }
@@ -39897,7 +39942,7 @@ function createFolderApi({
39897
39942
  documentError(response);
39898
39943
  return;
39899
39944
  }
39900
- const details = await stat7(target.path);
39945
+ const details = await stat8(target.path);
39901
39946
  if (!details.isFile()) {
39902
39947
  documentError(response);
39903
39948
  return;
@@ -39909,7 +39954,7 @@ function createFolderApi({
39909
39954
  return;
39910
39955
  }
39911
39956
  watch(folder);
39912
- const data = await readFile9(target.path);
39957
+ const data = await readFile10(target.path);
39913
39958
  const baseHeaders = {
39914
39959
  "Accept-Ranges": "bytes",
39915
39960
  "Cache-Control": "private, max-age=0, must-revalidate",
@@ -40197,7 +40242,7 @@ function createFolderApi({
40197
40242
  }
40198
40243
  };
40199
40244
  }
40200
- var applyProjectFavorites, buildProjectCatalog, buildProjectSearchIndex, catalogProjectToLocalProject, discoverProjectIcon, discoverProjectsUnderFolder, listLocalMarkdownFiles, projectRepositoryAccessory, projectSubtitle, projectTitle, readRaggleUserSettings, gitRemoteUrl, githubRepositoryPath, repositoryDisplayName, searchProjectIndex, sortProjectList, 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;
40201
40246
  var init_folder_metadata_plugin = __esm(() => {
40202
40247
  init_contracts();
40203
40248
  init_markdown();
@@ -40254,16 +40299,13 @@ var init_folder_metadata_plugin = __esm(() => {
40254
40299
  undefined,
40255
40300
  false
40256
40301
  ]);
40257
- defaultPluginRegistryConfig = {
40258
- plugins: { disable: [], order: [] }
40259
- };
40260
40302
  });
40261
40303
 
40262
40304
  // packages/api/src/server/index.ts
40263
40305
  import {
40264
40306
  createServer
40265
40307
  } from "node:http";
40266
- 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";
40267
40309
  import { extname as extname7, isAbsolute as isAbsolute7, relative as relative12, resolve as resolve13 } from "node:path";
40268
40310
  async function sendFile(request, response, directory, pathname, fallback) {
40269
40311
  const root = await realpath5(directory);
@@ -40277,7 +40319,7 @@ async function sendFile(request, response, directory, pathname, fallback) {
40277
40319
  response.end();
40278
40320
  return;
40279
40321
  }
40280
- if (!(await stat8(target).catch(() => null))?.isFile()) {
40322
+ if (!(await stat9(target).catch(() => null))?.isFile()) {
40281
40323
  if (!fallback) {
40282
40324
  response.writeHead(404);
40283
40325
  response.end();
@@ -40295,7 +40337,7 @@ async function sendFile(request, response, directory, pathname, fallback) {
40295
40337
  "Content-Type": contentTypes[extname7(target)] ?? "application/octet-stream",
40296
40338
  "X-Content-Type-Options": "nosniff"
40297
40339
  });
40298
- response.end(request.method === "HEAD" ? undefined : await readFile10(target));
40340
+ response.end(request.method === "HEAD" ? undefined : await readFile11(target));
40299
40341
  }
40300
40342
  function createKennelApi(options) {
40301
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;
@@ -1,7 +1,7 @@
1
1
  // packages/api/src/server/plugins.ts
2
2
  import { pathToFileURL } from "node:url";
3
3
  import { existsSync } from "node:fs";
4
- import { mkdir, stat as stat2, writeFile } from "node:fs/promises";
4
+ import { mkdir, readFile, stat as stat2, writeFile } from "node:fs/promises";
5
5
  import { dirname, resolve as resolve2, sep as sep2 } from "node:path";
6
6
 
7
7
  // packages/plugins/src/server.ts
@@ -31,6 +31,7 @@ var notesManifest = {
31
31
 
32
32
  // packages/notes/src/server.ts
33
33
  var markdownExtensions = new Set([".md", ".markdown"]);
34
+ var llmNoteLimit = 20;
34
35
  async function listMarkdownFiles(folder) {
35
36
  const files = [];
36
37
  const visit = async (current) => {
@@ -76,6 +77,45 @@ async function loadNotes(folder, linkedFolders = []) {
76
77
  status: "available"
77
78
  };
78
79
  }
80
+ function codeSpan(text) {
81
+ const escaped = text.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/gu, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
82
+ const longest = Math.max(0, ...(escaped.match(/`+/g) ?? []).map((run) => run.length));
83
+ const fence = "`".repeat(longest + 1);
84
+ const pad = escaped.startsWith("`") || escaped.endsWith("`") ? " " : "";
85
+ return `${fence}${pad}${escaped}${pad}${fence}`;
86
+ }
87
+ async function notesLlmSections({
88
+ folder,
89
+ linkedFolders = [],
90
+ signal
91
+ }) {
92
+ const groups = [];
93
+ const notes = [];
94
+ for (const source of [folder, ...linkedFolders]) {
95
+ signal.throwIfAborted();
96
+ const files = await listMarkdownFiles(source);
97
+ const paths = files.map((file) => resolve(source, file.relativePath));
98
+ groups.push({ folder: source, paths });
99
+ files.forEach((file, index) => notes.push({ activityAt: file.activityAt, path: paths[index] }));
100
+ }
101
+ if (notes.length === 0)
102
+ return [];
103
+ const recent = notes.sort((left, right) => right.activityAt - left.activityAt || (left.path < right.path ? -1 : left.path > right.path ? 1 : 0)).slice(0, llmNoteLimit);
104
+ return [
105
+ {
106
+ id: "notes",
107
+ title: "Markdown notes",
108
+ lines: [
109
+ ...groups.filter((group) => group.paths.length > 0).map((group) => `- Source: ${codeSpan(group.folder)} (${group.paths.length} ${group.paths.length === 1 ? "note" : "notes"})`),
110
+ `- Total notes: ${notes.length}`,
111
+ "",
112
+ `Most recently changed (${recent.length} of ${notes.length}):`,
113
+ "",
114
+ ...recent.map((note) => `- ${codeSpan(note.path)}`)
115
+ ]
116
+ }
117
+ ];
118
+ }
79
119
  function createNotesPlugin() {
80
120
  return {
81
121
  ...notesManifest,
@@ -90,7 +130,8 @@ function createNotesPlugin() {
90
130
  linkedFolders
91
131
  }) {
92
132
  return { data: await loadNotes(folder, linkedFolders), kind: "available" };
93
- }
133
+ },
134
+ llm: notesLlmSections
94
135
  };
95
136
  }
96
137
 
@@ -104,6 +145,44 @@ function configuredFolderServerRegistry(registry, resolvedConfig) {
104
145
  ];
105
146
  return new Map(ids.map((id) => [id, registry.get(id)]));
106
147
  }
148
+ var defaultPluginRegistryConfig = {
149
+ plugins: { disable: [], order: [] }
150
+ };
151
+ function stringArray(value) {
152
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
153
+ }
154
+ function parsePluginRegistryConfig(content) {
155
+ const parsed = JSON.parse(content);
156
+ const config = parsed && typeof parsed === "object" ? parsed : {};
157
+ return {
158
+ plugins: {
159
+ disable: stringArray(config.plugins?.disable),
160
+ order: stringArray(config.plugins?.order)
161
+ }
162
+ };
163
+ }
164
+ async function nearestKennelConfig(folder) {
165
+ for (let current = folder;; current = dirname(current)) {
166
+ const candidate = resolve2(current, "kennel.json");
167
+ const details = await stat2(candidate).catch(() => {
168
+ return;
169
+ });
170
+ if (details?.isFile())
171
+ return candidate;
172
+ if (dirname(current) === current)
173
+ return;
174
+ }
175
+ }
176
+ async function folderPluginRegistryConfig(folder) {
177
+ const path = await nearestKennelConfig(folder);
178
+ if (!path)
179
+ return defaultPluginRegistryConfig;
180
+ try {
181
+ return parsePluginRegistryConfig(await readFile(path, "utf8"));
182
+ } catch {
183
+ return defaultPluginRegistryConfig;
184
+ }
185
+ }
107
186
  function createFolderServerPlugins() {
108
187
  return [createNotesPlugin()];
109
188
  }
@@ -225,6 +304,7 @@ export {
225
304
  configuredFolderServerRegistry,
226
305
  createFolderServerPlugins,
227
306
  externalApiHandlers,
307
+ folderPluginRegistryConfig,
228
308
  folderServerRegistry,
229
309
  folderServerRegistryAsync,
230
310
  scaffoldFolderPlugins
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kennel",
3
- "version": "0.2.31",
3
+ "version": "0.2.32",
4
4
  "description": "Open-source pop-up previews for AI output: markdown, HTML artifacts, email, images, code, tables, notes, and calendars, from the terminal or your agent session.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -67,7 +67,7 @@
67
67
  "effect": "^3.19.7",
68
68
  "esbuild": "^0.27.0",
69
69
  "kennel-mcp": "https://registry.npmjs.org/%40raggle-ai%2fkennel-mcp/-/kennel-mcp-0.1.8.tgz",
70
- "kennel-plugins": "0.1.14",
70
+ "kennel-plugins": "0.1.15",
71
71
  "koffi": "^3.3.1",
72
72
  "lucide-react": "^0.562.0",
73
73
  "radix-ui": "^1.6.7",
@@ -84,6 +84,7 @@
84
84
  "devDependencies": {
85
85
  "@types/node": "^22.13.4",
86
86
  "kennel-api": "0.1.0",
87
+ "kennel-llm": "0.1.0",
87
88
  "kennel-preview": "0.1.0",
88
89
  "typescript": "^5.9.3"
89
90
  },
@@ -4,6 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "devDependencies": {
7
+ "@types/node": "latest",
7
8
  "@types/react": "latest",
8
9
  "kennel-plugins": "latest",
9
10
  "typescript": "latest"
@@ -1,7 +1,29 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
1
3
  import type { FolderPluginServer } from "kennel-plugins/server";
2
4
 
3
5
  import manifest from "./manifest";
4
6
 
7
+ // Example content file read by the `llm` hook below: a JSON array of items
8
+ // with a string `title`, for example [{ "title": "First item" }].
9
+ const CONTENT_FILE = "__PLUGIN_ID__.json";
10
+
11
+ async function readItems(folder: string) {
12
+ const source = join(folder, CONTENT_FILE);
13
+ let text: string;
14
+ try {
15
+ text = await readFile(source, "utf8");
16
+ } catch (error) {
17
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
18
+ throw error;
19
+ }
20
+ const items: unknown = JSON.parse(text);
21
+ if (!Array.isArray(items) || !items.every((item) => typeof item?.title === "string")) {
22
+ throw new Error(`${source} must be a JSON array of items with a string title`);
23
+ }
24
+ return { source, titles: items.map((item: { title: string }) => item.title) };
25
+ }
26
+
5
27
  export default {
6
28
  ...manifest,
7
29
  async detect() {
@@ -10,4 +32,25 @@ export default {
10
32
  async handle() {
11
33
  return { data: { items: [] } };
12
34
  },
35
+ // Optional `knl llm` context. Read only: no writes, network calls, or other
36
+ // side effects. Return [] when the folder has no content for this plugin.
37
+ // The host adds the headings; each line is one Markdown line without line
38
+ // breaks or headings. Invalid output or a thrown error fails `knl llm`.
39
+ async llm({ folder, signal }) {
40
+ signal.throwIfAborted();
41
+ const items = await readItems(folder);
42
+ if (!items) return [];
43
+ return [
44
+ {
45
+ id: "summary",
46
+ title: "Summary",
47
+ lines: [
48
+ `- Source: ${items.source}`,
49
+ `- Items: ${items.titles.length}`,
50
+ ...items.titles.slice(0, 5).map((title) => `- Item: ${title}`),
51
+ "- Use the source file as the authority for these items.",
52
+ ],
53
+ },
54
+ ];
55
+ },
13
56
  } satisfies FolderPluginServer;
@@ -5,7 +5,7 @@
5
5
  "moduleResolution": "NodeNext",
6
6
  "strict": true,
7
7
  "target": "ES2022",
8
- "types": []
8
+ "types": ["node"]
9
9
  },
10
10
  "include": ["*.ts", "*.tsx"]
11
11
  }