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.
@@ -102,6 +102,11 @@ export async function dispatch(argv) {
102
102
  deploy(args);
103
103
  return;
104
104
  }
105
+ if (command === 'llm') {
106
+ const { llm } = await import('../llm/command.js');
107
+ await llm(args);
108
+ return;
109
+ }
105
110
  // Built-in commands win; use an explicit path (./tasks) to open a folder
106
111
  // whose name is also an installed plugin command.
107
112
  if (/^[a-z][a-z0-9-]*$/u.test(command)) {
package/dist/cli/usage.js CHANGED
@@ -12,6 +12,7 @@ export function usage() {
12
12
  kennel view [folder] [--host <host>] [--port <port>] [--dry-run]
13
13
  kennel open|o [folder] [--open] [--host <host>] [--port <port>] [--dry-run]
14
14
  knl <plugin-id|alias> [folder] [--item <id>] [--open] [--dry-run]
15
+ knl llm [folder|--folder <path>] [full|--depth <count>] [--profile <name>|--no-instructions] [--no-plugins] [--copy]
15
16
  kennel serve [folder] [--host <host>] [--port <port>] [--lan|--tailscale]
16
17
  kennel share|display [folder] [--lan|--tailscale] [--port <port>] [--dry-run]
17
18
  kennel dev [path] [--config <json-file>] [--port <port>] [--script <script>] [--dry-run] [-- <args...>]
@@ -34,5 +35,8 @@ Without an interactive terminal, the viewer prints its URL without a menu.
34
35
  Installed plugin ids are commands, for example knl tasks or knl calendar.
35
36
  Plugins can declare extra aliases, for example knl mail for plugin id email.
36
37
  Use --item <id> for a plugin detail view. Built-in commands take priority.
38
+
39
+ knl llm prints only agent instructions, profiles, and the research write destination.
40
+ Use knl llm full for repository details and plugin sections. Run knl llm --help for details.
37
41
  `);
38
42
  }
@@ -17027,7 +17027,7 @@ var require__2020 = __commonJS(function(exports, module) {
17027
17027
  import {
17028
17028
  createServer
17029
17029
  } from "node:http";
17030
- import { readFile as readFile9, realpath as realpath5, stat as stat8 } from "node:fs/promises";
17030
+ import { readFile as readFile10, realpath as realpath5, stat as stat9 } from "node:fs/promises";
17031
17031
  import { extname as extname7, isAbsolute as isAbsolute7, relative as relative11, resolve as resolve12 } from "node:path";
17032
17032
 
17033
17033
  // packages/api/src/server/watcher.ts
@@ -21993,7 +21993,7 @@ function sendContract(response, url, body, headers = {}) {
21993
21993
 
21994
21994
  // packages/api/src/server/folder-metadata-plugin.ts
21995
21995
  import { realpathSync as realpathSync2 } from "node:fs";
21996
- import { readdir as readdir5, readFile as readFile8, stat as stat7 } from "node:fs/promises";
21996
+ import { readdir as readdir5, readFile as readFile9, stat as stat8 } from "node:fs/promises";
21997
21997
  import { homedir as homedir3 } from "node:os";
21998
21998
  import {
21999
21999
  basename as basename6,
@@ -37086,12 +37086,26 @@ var iconExtensions = new Map([
37086
37086
  ["image/jpeg", "jpg"],
37087
37087
  ["image/png", "png"]
37088
37088
  ]);
37089
+ var maxSearchIconBytes = 512 * 1024;
37090
+ var searchIconTypes = new Map([
37091
+ [".gif", "image/gif"],
37092
+ [".ico", "image/x-icon"],
37093
+ [".jpeg", "image/jpeg"],
37094
+ [".jpg", "image/jpeg"],
37095
+ [".png", "image/png"],
37096
+ [".svg", "image/svg+xml"],
37097
+ [".webp", "image/webp"]
37098
+ ]);
37089
37099
  function assetKey(value) {
37090
37100
  return createHash4("sha256").update(value).digest("hex");
37091
37101
  }
37092
37102
  function encodeBase64Url(value) {
37093
37103
  return Buffer.from(value, "utf8").toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
37094
37104
  }
37105
+ function decodeBase64Url(value) {
37106
+ const padded = `${value}${"=".repeat((4 - value.length % 4) % 4)}`;
37107
+ return Buffer.from(padded.replaceAll("-", "+").replaceAll("_", "/"), "base64").toString("utf8");
37108
+ }
37095
37109
  function iconUrl(folder, hash, mediaType) {
37096
37110
  return `/api/folders/${encodeBase64Url(folder)}/assets/${hash}.${iconExtensions.get(mediaType)}`;
37097
37111
  }
@@ -37115,6 +37129,54 @@ async function folderIcon(folder, assets, assetFiles, fallback) {
37115
37129
  };
37116
37130
  return key;
37117
37131
  }
37132
+ async function searchIconDataUrl(icon) {
37133
+ if (!icon)
37134
+ return;
37135
+ if (icon.startsWith("data:"))
37136
+ return icon;
37137
+ const mediaType = searchIconTypes.get(extname4(icon).toLowerCase());
37138
+ if (!mediaType)
37139
+ return;
37140
+ const details = await stat5(icon).catch(() => {
37141
+ return;
37142
+ });
37143
+ if (!details?.size || details.size > maxSearchIconBytes)
37144
+ return;
37145
+ return `data:${mediaType};base64,${(await readFile7(icon)).toString("base64")}`;
37146
+ }
37147
+ async function faviconAsset(folderName, assets) {
37148
+ if (process.env.KENNEL_EXTERNAL_ICONS !== "1")
37149
+ return;
37150
+ const host = await folderWebsite(folderName);
37151
+ if (!host)
37152
+ return;
37153
+ const key = `favicon:${host}`;
37154
+ assets[key] = {
37155
+ mediaType: "image/png",
37156
+ source: "favicon",
37157
+ url: `/api/folder-favicon?path=${encodeURIComponent(folderName)}&host=${encodeURIComponent(host)}`
37158
+ };
37159
+ return key;
37160
+ }
37161
+ async function iconWithFaviconFallback(folder, assets, assetFiles) {
37162
+ return folderIcon(folder, assets, assetFiles, (folderName) => faviconAsset(folderName, assets));
37163
+ }
37164
+ async function hydrateFolderIcons(metadata, assetFiles) {
37165
+ const assets = {};
37166
+ const projects = metadata.projects?.items ?? [];
37167
+ const [iconAssetKey, projectItems] = await Promise.all([
37168
+ iconWithFaviconFallback(metadata.folder, assets, assetFiles),
37169
+ Promise.all(projects.map(async (project) => ({
37170
+ ...project,
37171
+ iconAssetKey: await iconWithFaviconFallback(project.worktree, assets, assetFiles)
37172
+ })))
37173
+ ]);
37174
+ return {
37175
+ assets,
37176
+ iconAssetKey,
37177
+ projects: { items: projectItems }
37178
+ };
37179
+ }
37118
37180
  async function workspaceBreadcrumbIcons(breadcrumbs, assetFiles, assets = {}) {
37119
37181
  return Promise.all(breadcrumbs.map(async (crumb) => {
37120
37182
  const iconAssetKey = await folderIcon(crumb.path, assets, assetFiles);
@@ -37126,6 +37188,7 @@ async function workspaceBreadcrumbIcons(breadcrumbs, assetFiles, assets = {}) {
37126
37188
  // packages/api/src/server/plugins.ts
37127
37189
  import { pathToFileURL } from "node:url";
37128
37190
  import { existsSync as existsSync3 } from "node:fs";
37191
+ import { mkdir as mkdir2, readFile as readFile8, stat as stat7, writeFile as writeFile4 } from "node:fs/promises";
37129
37192
  import { dirname as dirname7, resolve as resolve10, sep as sep6 } from "node:path";
37130
37193
 
37131
37194
  // packages/notes/src/server.ts
@@ -37147,6 +37210,7 @@ var notesManifest = {
37147
37210
 
37148
37211
  // packages/notes/src/server.ts
37149
37212
  var markdownExtensions = new Set([".md", ".markdown"]);
37213
+ var llmNoteLimit = 20;
37150
37214
  async function listMarkdownFiles(folder) {
37151
37215
  const files = [];
37152
37216
  const visit = async (current) => {
@@ -37192,6 +37256,45 @@ async function loadNotes(folder, linkedFolders = []) {
37192
37256
  status: "available"
37193
37257
  };
37194
37258
  }
37259
+ function codeSpan(text) {
37260
+ const escaped = text.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/gu, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
37261
+ const longest = Math.max(0, ...(escaped.match(/`+/g) ?? []).map((run) => run.length));
37262
+ const fence = "`".repeat(longest + 1);
37263
+ const pad = escaped.startsWith("`") || escaped.endsWith("`") ? " " : "";
37264
+ return `${fence}${pad}${escaped}${pad}${fence}`;
37265
+ }
37266
+ async function notesLlmSections({
37267
+ folder,
37268
+ linkedFolders = [],
37269
+ signal
37270
+ }) {
37271
+ const groups = [];
37272
+ const notes = [];
37273
+ for (const source of [folder, ...linkedFolders]) {
37274
+ signal.throwIfAborted();
37275
+ const files = await listMarkdownFiles(source);
37276
+ const paths = files.map((file) => resolve9(source, file.relativePath));
37277
+ groups.push({ folder: source, paths });
37278
+ files.forEach((file, index) => notes.push({ activityAt: file.activityAt, path: paths[index] }));
37279
+ }
37280
+ if (notes.length === 0)
37281
+ return [];
37282
+ const recent = notes.sort((left, right) => right.activityAt - left.activityAt || (left.path < right.path ? -1 : left.path > right.path ? 1 : 0)).slice(0, llmNoteLimit);
37283
+ return [
37284
+ {
37285
+ id: "notes",
37286
+ title: "Markdown notes",
37287
+ lines: [
37288
+ ...groups.filter((group) => group.paths.length > 0).map((group) => `- Source: ${codeSpan(group.folder)} (${group.paths.length} ${group.paths.length === 1 ? "note" : "notes"})`),
37289
+ `- Total notes: ${notes.length}`,
37290
+ "",
37291
+ `Most recently changed (${recent.length} of ${notes.length}):`,
37292
+ "",
37293
+ ...recent.map((note) => `- ${codeSpan(note.path)}`)
37294
+ ]
37295
+ }
37296
+ ];
37297
+ }
37195
37298
  function createNotesPlugin() {
37196
37299
  return {
37197
37300
  ...notesManifest,
@@ -37206,7 +37309,8 @@ function createNotesPlugin() {
37206
37309
  linkedFolders
37207
37310
  }) {
37208
37311
  return { data: await loadNotes(folder, linkedFolders), kind: "available" };
37209
- }
37312
+ },
37313
+ llm: notesLlmSections
37210
37314
  };
37211
37315
  }
37212
37316
 
@@ -37220,6 +37324,44 @@ function configuredFolderServerRegistry(registry, resolvedConfig) {
37220
37324
  ];
37221
37325
  return new Map(ids.map((id) => [id, registry.get(id)]));
37222
37326
  }
37327
+ var defaultPluginRegistryConfig = {
37328
+ plugins: { disable: [], order: [] }
37329
+ };
37330
+ function stringArray(value) {
37331
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
37332
+ }
37333
+ function parsePluginRegistryConfig(content) {
37334
+ const parsed = JSON.parse(content);
37335
+ const config = parsed && typeof parsed === "object" ? parsed : {};
37336
+ return {
37337
+ plugins: {
37338
+ disable: stringArray(config.plugins?.disable),
37339
+ order: stringArray(config.plugins?.order)
37340
+ }
37341
+ };
37342
+ }
37343
+ async function nearestKennelConfig(folder) {
37344
+ for (let current = folder;; current = dirname7(current)) {
37345
+ const candidate = resolve10(current, "kennel.json");
37346
+ const details = await stat7(candidate).catch(() => {
37347
+ return;
37348
+ });
37349
+ if (details?.isFile())
37350
+ return candidate;
37351
+ if (dirname7(current) === current)
37352
+ return;
37353
+ }
37354
+ }
37355
+ async function folderPluginRegistryConfig(folder) {
37356
+ const path = await nearestKennelConfig(folder);
37357
+ if (!path)
37358
+ return defaultPluginRegistryConfig;
37359
+ try {
37360
+ return parsePluginRegistryConfig(await readFile8(path, "utf8"));
37361
+ } catch {
37362
+ return defaultPluginRegistryConfig;
37363
+ }
37364
+ }
37223
37365
  function createFolderServerPlugins() {
37224
37366
  return [createNotesPlugin()];
37225
37367
  }
@@ -37691,25 +37833,6 @@ var {
37691
37833
  searchProjectIndex,
37692
37834
  sortProjectList
37693
37835
  } = local;
37694
- var maxIconBytes3 = 512 * 1024;
37695
- var iconTypes2 = new Map([
37696
- [".jpeg", "image/jpeg"],
37697
- [".jpg", "image/jpeg"],
37698
- [".png", "image/png"]
37699
- ]);
37700
- var iconExtensions2 = new Map([
37701
- ["image/jpeg", "jpg"],
37702
- ["image/png", "png"]
37703
- ]);
37704
- var searchIconTypes = new Map([
37705
- [".gif", "image/gif"],
37706
- [".ico", "image/x-icon"],
37707
- [".jpeg", "image/jpeg"],
37708
- [".jpg", "image/jpeg"],
37709
- [".png", "image/png"],
37710
- [".svg", "image/svg+xml"],
37711
- [".webp", "image/webp"]
37712
- ]);
37713
37836
  var projectSearchCacheMs = 5000;
37714
37837
  var metadataExtensions = new Set([
37715
37838
  ".html",
@@ -37785,42 +37908,6 @@ function apiHostAllowed(value, remote) {
37785
37908
  const allowlist = (process.env.KENNEL_VIEWER_HOSTS ?? "").split(",").map((host) => host.trim().toLowerCase()).filter(Boolean);
37786
37909
  return allowlist.includes(hostname);
37787
37910
  }
37788
- function decodeBase64Url(value) {
37789
- const padded = `${value}${"=".repeat((4 - value.length % 4) % 4)}`;
37790
- return Buffer.from(padded.replaceAll("-", "+").replaceAll("_", "/"), "base64").toString("utf8");
37791
- }
37792
- async function searchIconDataUrl(icon) {
37793
- if (!icon)
37794
- return;
37795
- if (icon.startsWith("data:"))
37796
- return icon;
37797
- const mediaType = searchIconTypes.get(extname6(icon).toLowerCase());
37798
- if (!mediaType)
37799
- return;
37800
- const details = await stat7(icon).catch(() => {
37801
- return;
37802
- });
37803
- if (!details?.size || details.size > maxIconBytes3)
37804
- return;
37805
- return `data:${mediaType};base64,${(await readFile8(icon)).toString("base64")}`;
37806
- }
37807
- async function faviconAsset(folderName, assets) {
37808
- if (process.env.KENNEL_EXTERNAL_ICONS !== "1")
37809
- return;
37810
- const host = await folderWebsite(folderName);
37811
- if (!host)
37812
- return;
37813
- const key = `favicon:${host}`;
37814
- assets[key] = {
37815
- mediaType: "image/png",
37816
- source: "favicon",
37817
- url: `/api/folder-favicon?path=${encodeURIComponent(folderName)}&host=${encodeURIComponent(host)}`
37818
- };
37819
- return key;
37820
- }
37821
- async function iconWithFaviconFallback(folder, assets, assetFiles) {
37822
- return folderIcon(folder, assets, assetFiles, (folderName) => faviconAsset(folderName, assets));
37823
- }
37824
37911
  async function repositoryIdentity(folder, assets) {
37825
37912
  const remoteUrl = await gitRemoteUrl(folder).catch(() => {
37826
37913
  return;
@@ -37844,22 +37931,6 @@ async function repositoryIdentity(folder, assets) {
37844
37931
  return;
37845
37932
  }
37846
37933
  }
37847
- async function hydrateAssets(metadata, assetFiles) {
37848
- const assets = {};
37849
- const projects = metadata.projects?.items ?? [];
37850
- const [iconAssetKey, projectItems] = await Promise.all([
37851
- iconWithFaviconFallback(metadata.folder, assets, assetFiles),
37852
- Promise.all(projects.map(async (project) => ({
37853
- ...project,
37854
- iconAssetKey: await iconWithFaviconFallback(project.worktree, assets, assetFiles)
37855
- })))
37856
- ]);
37857
- return {
37858
- assets,
37859
- iconAssetKey,
37860
- projects: { items: projectItems }
37861
- };
37862
- }
37863
37934
  function unique(values) {
37864
37935
  return [...new Set(values)];
37865
37936
  }
@@ -37871,8 +37942,8 @@ async function shallowMetadata(folder) {
37871
37942
  const visible = entries.filter((entry) => !entry.name.startsWith(".") && entry.name !== "node_modules");
37872
37943
  const files = await Promise.all(visible.filter((entry) => entry.isFile() && [".markdown", ".md"].includes(extname6(entry.name).toLowerCase())).map(async (entry) => {
37873
37944
  const path = resolve11(folder, entry.name);
37874
- const details = await stat7(path);
37875
- const content = await readFile8(path, "utf8").catch(() => "");
37945
+ const details = await stat8(path);
37946
+ const content = await readFile9(path, "utf8").catch(() => "");
37876
37947
  const title = markdownMetadata(content).title;
37877
37948
  return {
37878
37949
  activityAt: details.mtimeMs,
@@ -37926,7 +37997,7 @@ async function notesFiles(folder, files, origin) {
37926
37997
  const path = file.path ?? (file.relativePath ? resolve11(folder, file.relativePath) : undefined);
37927
37998
  if (!path)
37928
37999
  return origin ? { ...file, folder: origin } : file;
37929
- const content = [".markdown", ".md"].includes(extname6(path).toLowerCase()) ? await readFile8(path, "utf8").catch(() => "") : "";
38000
+ const content = [".markdown", ".md"].includes(extname6(path).toLowerCase()) ? await readFile9(path, "utf8").catch(() => "") : "";
37930
38001
  const title = content ? markdownMetadata(content).title : undefined;
37931
38002
  return {
37932
38003
  ...file,
@@ -38044,44 +38115,6 @@ async function projectSearchItem(project, match) {
38044
38115
  worktree: project.worktree
38045
38116
  };
38046
38117
  }
38047
- var defaultPluginRegistryConfig = {
38048
- plugins: { disable: [], order: [] }
38049
- };
38050
- function stringArray(value) {
38051
- return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
38052
- }
38053
- function parsePluginRegistryConfig(content) {
38054
- const parsed = JSON.parse(content);
38055
- const config = parsed && typeof parsed === "object" ? parsed : {};
38056
- return {
38057
- plugins: {
38058
- disable: stringArray(config.plugins?.disable),
38059
- order: stringArray(config.plugins?.order)
38060
- }
38061
- };
38062
- }
38063
- async function nearestKennelConfig(folder) {
38064
- for (let current = folder;; current = dirname9(current)) {
38065
- const candidate = resolve11(current, "kennel.json");
38066
- const details = await stat7(candidate).catch(() => {
38067
- return;
38068
- });
38069
- if (details?.isFile())
38070
- return candidate;
38071
- if (dirname9(current) === current)
38072
- return;
38073
- }
38074
- }
38075
- async function folderPluginRegistryConfig(folder) {
38076
- const path = await nearestKennelConfig(folder);
38077
- if (!path)
38078
- return defaultPluginRegistryConfig;
38079
- try {
38080
- return parsePluginRegistryConfig(await readFile8(path, "utf8"));
38081
- } catch {
38082
- return defaultPluginRegistryConfig;
38083
- }
38084
- }
38085
38118
  function pluginConfigWarnings(registry, config) {
38086
38119
  const ids = unique([...config.plugins.disable, ...config.plugins.order]);
38087
38120
  return ids.filter((id) => !registry.has(id)).map((id) => `unknown plugin id "${id}" — not installed in this build`);
@@ -38257,7 +38290,7 @@ function createFolderApi({
38257
38290
  "Content-Type": asset.mediaType,
38258
38291
  ETag: `"${hash}"`
38259
38292
  });
38260
- response.end(await readFile8(asset.path));
38293
+ response.end(await readFile9(asset.path));
38261
38294
  } catch (error) {
38262
38295
  response.writeHead(404, {
38263
38296
  "Content-Type": "text/plain; charset=utf-8"
@@ -38336,7 +38369,7 @@ function createFolderApi({
38336
38369
  {
38337
38370
  path: relative10(folder, target.path),
38338
38371
  kind: target.kind,
38339
- content: previewTextKind(target.kind) ? await readFile8(target.path, "utf8") : ""
38372
+ content: previewTextKind(target.kind) ? await readFile9(target.path, "utf8") : ""
38340
38373
  }
38341
38374
  ] : []
38342
38375
  } : await readFolder(folder);
@@ -38352,7 +38385,7 @@ function createFolderApi({
38352
38385
  documentError(response);
38353
38386
  return;
38354
38387
  }
38355
- const details = await stat7(target.path);
38388
+ const details = await stat8(target.path);
38356
38389
  if (!details.isFile()) {
38357
38390
  documentError(response);
38358
38391
  return;
@@ -38364,7 +38397,7 @@ function createFolderApi({
38364
38397
  return;
38365
38398
  }
38366
38399
  watch(folder);
38367
- const content = previewTextKind(target.kind) ? await readFile8(target.path, "utf8") : "";
38400
+ const content = previewTextKind(target.kind) ? await readFile9(target.path, "utf8") : "";
38368
38401
  sendContract(response, url, {
38369
38402
  content,
38370
38403
  kind: target.kind,
@@ -38393,7 +38426,7 @@ function createFolderApi({
38393
38426
  documentError(response);
38394
38427
  return;
38395
38428
  }
38396
- const details = await stat7(target.path);
38429
+ const details = await stat8(target.path);
38397
38430
  if (!details.isFile()) {
38398
38431
  documentError(response);
38399
38432
  return;
@@ -38417,7 +38450,7 @@ function createFolderApi({
38417
38450
  "Content-Disposition": `attachment; filename="${basename6(target.path)}"`
38418
38451
  } : {}
38419
38452
  });
38420
- response.end(await readFile8(target.path));
38453
+ response.end(await readFile9(target.path));
38421
38454
  } catch (error) {
38422
38455
  documentError(response);
38423
38456
  }
@@ -38431,7 +38464,7 @@ function createFolderApi({
38431
38464
  documentError(response);
38432
38465
  return;
38433
38466
  }
38434
- const details = await stat7(target.path);
38467
+ const details = await stat8(target.path);
38435
38468
  if (!details.isFile()) {
38436
38469
  documentError(response);
38437
38470
  return;
@@ -38443,7 +38476,7 @@ function createFolderApi({
38443
38476
  return;
38444
38477
  }
38445
38478
  watch(folder);
38446
- const data = await readFile8(target.path);
38479
+ const data = await readFile9(target.path);
38447
38480
  const baseHeaders = {
38448
38481
  "Accept-Ranges": "bytes",
38449
38482
  "Cache-Control": "private, max-age=0, must-revalidate",
@@ -38700,7 +38733,7 @@ function createFolderApi({
38700
38733
  response.off("close", cancel);
38701
38734
  }
38702
38735
  const [hydrated, workspace] = await Promise.all([
38703
- hydrateAssets({ folder, projects: { items: projects } }, assetFiles),
38736
+ hydrateFolderIcons({ folder, projects: { items: projects } }, assetFiles),
38704
38737
  readWorkspace(folder)
38705
38738
  ]);
38706
38739
  workspace.breadcrumbs = await workspaceBreadcrumbIcons(workspace.breadcrumbs ?? [], assetFiles, hydrated.assets);
@@ -38713,7 +38746,7 @@ function createFolderApi({
38713
38746
  return;
38714
38747
  }
38715
38748
  const metadata = machine ? await shallowMetadata(folder) : await loadFolderData(folder);
38716
- const assets = await hydrateAssets(metadata, assetFiles);
38749
+ const assets = await hydrateFolderIcons(metadata, assetFiles);
38717
38750
  const body = {
38718
38751
  ...metadata,
38719
38752
  ...assets,
@@ -38760,7 +38793,7 @@ async function sendFile(request, response, directory, pathname, fallback) {
38760
38793
  response.end();
38761
38794
  return;
38762
38795
  }
38763
- if (!(await stat8(target).catch(() => null))?.isFile()) {
38796
+ if (!(await stat9(target).catch(() => null))?.isFile()) {
38764
38797
  if (!fallback) {
38765
38798
  response.writeHead(404);
38766
38799
  response.end();
@@ -38778,7 +38811,7 @@ async function sendFile(request, response, directory, pathname, fallback) {
38778
38811
  "Content-Type": contentTypes[extname7(target)] ?? "application/octet-stream",
38779
38812
  "X-Content-Type-Options": "nosniff"
38780
38813
  });
38781
- response.end(request.method === "HEAD" ? undefined : await readFile9(target));
38814
+ response.end(request.method === "HEAD" ? undefined : await readFile10(target));
38782
38815
  }
38783
38816
  function createKennelApi(options) {
38784
38817
  const remote = options.remoteAccess || !isLoopbackServerHost(options.host);
@@ -0,0 +1 @@
1
+ export declare function copyToClipboard(text: string): Promise<void>;
@@ -0,0 +1,59 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { accessSync, constants } from 'node:fs';
3
+ import { delimiter, join } from 'node:path';
4
+ function onPath(command) {
5
+ return (process.env.PATH ?? '').split(delimiter).some((directory) => {
6
+ if (!directory)
7
+ return false;
8
+ try {
9
+ accessSync(join(directory, command), constants.X_OK);
10
+ return true;
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ });
16
+ }
17
+ /** The platform clipboard tool. No shell runs, so the text is never interpreted. */
18
+ function clipboardTool() {
19
+ if (process.platform === 'darwin')
20
+ return { command: 'pbcopy', args: [], encoding: 'utf8' };
21
+ // clip.exe reads UTF-16LE text that starts with a byte order mark.
22
+ if (process.platform === 'win32')
23
+ return { command: 'clip', args: [], encoding: 'utf16le' };
24
+ if (process.env.WAYLAND_DISPLAY && onPath('wl-copy'))
25
+ return { command: 'wl-copy', args: [], encoding: 'utf8' };
26
+ if (process.env.DISPLAY && onPath('xclip'))
27
+ return { command: 'xclip', args: ['-selection', 'clipboard'], encoding: 'utf8' };
28
+ if (process.env.DISPLAY && onPath('xsel'))
29
+ return { command: 'xsel', args: ['--clipboard', '--input'], encoding: 'utf8' };
30
+ throw new Error('No clipboard tool is available. Install wl-copy (Wayland) or xclip or xsel (X11) in a desktop session, or run knl llm without --copy.');
31
+ }
32
+ export function copyToClipboard(text) {
33
+ const tool = clipboardTool();
34
+ const input = tool.encoding === 'utf16le' ? Buffer.from(`\ufeff${text}`, 'utf16le') : Buffer.from(text, 'utf8');
35
+ return new Promise((resolve, reject) => {
36
+ // stderr is inherited: wl-copy keeps serving the selection in a forked
37
+ // process, and a pipe it holds open would keep this command alive.
38
+ const child = spawn(tool.command, tool.args, { stdio: ['pipe', 'ignore', 'inherit'] });
39
+ const timer = setTimeout(() => {
40
+ child.kill('SIGKILL');
41
+ reject(new Error(`${tool.command} did not finish within 10 seconds`));
42
+ }, 10_000);
43
+ child.once('error', (error) => {
44
+ clearTimeout(timer);
45
+ reject(new Error(`Cannot run ${tool.command}: ${error.message}`));
46
+ });
47
+ child.once('exit', (code, signal) => {
48
+ clearTimeout(timer);
49
+ if (code === 0)
50
+ resolve();
51
+ else
52
+ reject(new Error(`${tool.command} failed ${signal ? `with signal ${signal}` : `with exit code ${code}`}`));
53
+ });
54
+ child.stdin.on('error', () => {
55
+ // The exit status reports the failure.
56
+ });
57
+ child.stdin.end(input);
58
+ });
59
+ }
@@ -0,0 +1 @@
1
+ export declare function llm(args: string[]): Promise<void>;