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.
- package/dist/cli/dispatch.js +5 -0
- package/dist/cli/usage.js +4 -0
- package/dist/core/runtime.js +96 -54
- package/dist/llm/clipboard.d.ts +1 -0
- package/dist/llm/clipboard.js +59 -0
- package/dist/llm/command.d.ts +1 -0
- package/dist/llm/command.js +219 -0
- package/dist/llm/worker.d.ts +19 -0
- package/dist/llm/worker.js +866 -0
- package/dist/package/preview.js +98 -56
- package/dist/plugins/external.d.ts +6 -0
- package/dist/plugins/external.js +16 -0
- package/dist/site/server-plugins.d.ts +12 -0
- package/dist/site/server-plugins.js +82 -2
- package/package.json +3 -2
- package/templates/plugin/package.json +1 -0
- package/templates/plugin/server.ts +43 -0
- package/templates/plugin/tsconfig.json +1 -1
package/dist/package/preview.js
CHANGED
|
@@ -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
|
|
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
|
|
39344
|
-
const content = await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
package/dist/plugins/external.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
},
|
|
@@ -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;
|