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.
@@ -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,
@@ -37188,6 +37188,7 @@ async function workspaceBreadcrumbIcons(breadcrumbs, assetFiles, assets = {}) {
37188
37188
  // packages/api/src/server/plugins.ts
37189
37189
  import { pathToFileURL } from "node:url";
37190
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";
37191
37192
  import { dirname as dirname7, resolve as resolve10, sep as sep6 } from "node:path";
37192
37193
 
37193
37194
  // packages/notes/src/server.ts
@@ -37209,6 +37210,7 @@ var notesManifest = {
37209
37210
 
37210
37211
  // packages/notes/src/server.ts
37211
37212
  var markdownExtensions = new Set([".md", ".markdown"]);
37213
+ var llmNoteLimit = 20;
37212
37214
  async function listMarkdownFiles(folder) {
37213
37215
  const files = [];
37214
37216
  const visit = async (current) => {
@@ -37254,6 +37256,45 @@ async function loadNotes(folder, linkedFolders = []) {
37254
37256
  status: "available"
37255
37257
  };
37256
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
+ }
37257
37298
  function createNotesPlugin() {
37258
37299
  return {
37259
37300
  ...notesManifest,
@@ -37268,7 +37309,8 @@ function createNotesPlugin() {
37268
37309
  linkedFolders
37269
37310
  }) {
37270
37311
  return { data: await loadNotes(folder, linkedFolders), kind: "available" };
37271
- }
37312
+ },
37313
+ llm: notesLlmSections
37272
37314
  };
37273
37315
  }
37274
37316
 
@@ -37282,6 +37324,44 @@ function configuredFolderServerRegistry(registry, resolvedConfig) {
37282
37324
  ];
37283
37325
  return new Map(ids.map((id) => [id, registry.get(id)]));
37284
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
+ }
37285
37365
  function createFolderServerPlugins() {
37286
37366
  return [createNotesPlugin()];
37287
37367
  }
@@ -37862,8 +37942,8 @@ async function shallowMetadata(folder) {
37862
37942
  const visible = entries.filter((entry) => !entry.name.startsWith(".") && entry.name !== "node_modules");
37863
37943
  const files = await Promise.all(visible.filter((entry) => entry.isFile() && [".markdown", ".md"].includes(extname6(entry.name).toLowerCase())).map(async (entry) => {
37864
37944
  const path = resolve11(folder, entry.name);
37865
- const details = await stat7(path);
37866
- const content = await readFile8(path, "utf8").catch(() => "");
37945
+ const details = await stat8(path);
37946
+ const content = await readFile9(path, "utf8").catch(() => "");
37867
37947
  const title = markdownMetadata(content).title;
37868
37948
  return {
37869
37949
  activityAt: details.mtimeMs,
@@ -37917,7 +37997,7 @@ async function notesFiles(folder, files, origin) {
37917
37997
  const path = file.path ?? (file.relativePath ? resolve11(folder, file.relativePath) : undefined);
37918
37998
  if (!path)
37919
37999
  return origin ? { ...file, folder: origin } : file;
37920
- 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(() => "") : "";
37921
38001
  const title = content ? markdownMetadata(content).title : undefined;
37922
38002
  return {
37923
38003
  ...file,
@@ -38035,44 +38115,6 @@ async function projectSearchItem(project, match) {
38035
38115
  worktree: project.worktree
38036
38116
  };
38037
38117
  }
38038
- var defaultPluginRegistryConfig = {
38039
- plugins: { disable: [], order: [] }
38040
- };
38041
- function stringArray(value) {
38042
- return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
38043
- }
38044
- function parsePluginRegistryConfig(content) {
38045
- const parsed = JSON.parse(content);
38046
- const config = parsed && typeof parsed === "object" ? parsed : {};
38047
- return {
38048
- plugins: {
38049
- disable: stringArray(config.plugins?.disable),
38050
- order: stringArray(config.plugins?.order)
38051
- }
38052
- };
38053
- }
38054
- async function nearestKennelConfig(folder) {
38055
- for (let current = folder;; current = dirname9(current)) {
38056
- const candidate = resolve11(current, "kennel.json");
38057
- const details = await stat7(candidate).catch(() => {
38058
- return;
38059
- });
38060
- if (details?.isFile())
38061
- return candidate;
38062
- if (dirname9(current) === current)
38063
- return;
38064
- }
38065
- }
38066
- async function folderPluginRegistryConfig(folder) {
38067
- const path = await nearestKennelConfig(folder);
38068
- if (!path)
38069
- return defaultPluginRegistryConfig;
38070
- try {
38071
- return parsePluginRegistryConfig(await readFile8(path, "utf8"));
38072
- } catch {
38073
- return defaultPluginRegistryConfig;
38074
- }
38075
- }
38076
38118
  function pluginConfigWarnings(registry, config) {
38077
38119
  const ids = unique([...config.plugins.disable, ...config.plugins.order]);
38078
38120
  return ids.filter((id) => !registry.has(id)).map((id) => `unknown plugin id "${id}" — not installed in this build`);
@@ -38248,7 +38290,7 @@ function createFolderApi({
38248
38290
  "Content-Type": asset.mediaType,
38249
38291
  ETag: `"${hash}"`
38250
38292
  });
38251
- response.end(await readFile8(asset.path));
38293
+ response.end(await readFile9(asset.path));
38252
38294
  } catch (error) {
38253
38295
  response.writeHead(404, {
38254
38296
  "Content-Type": "text/plain; charset=utf-8"
@@ -38327,7 +38369,7 @@ function createFolderApi({
38327
38369
  {
38328
38370
  path: relative10(folder, target.path),
38329
38371
  kind: target.kind,
38330
- content: previewTextKind(target.kind) ? await readFile8(target.path, "utf8") : ""
38372
+ content: previewTextKind(target.kind) ? await readFile9(target.path, "utf8") : ""
38331
38373
  }
38332
38374
  ] : []
38333
38375
  } : await readFolder(folder);
@@ -38343,7 +38385,7 @@ function createFolderApi({
38343
38385
  documentError(response);
38344
38386
  return;
38345
38387
  }
38346
- const details = await stat7(target.path);
38388
+ const details = await stat8(target.path);
38347
38389
  if (!details.isFile()) {
38348
38390
  documentError(response);
38349
38391
  return;
@@ -38355,7 +38397,7 @@ function createFolderApi({
38355
38397
  return;
38356
38398
  }
38357
38399
  watch(folder);
38358
- const content = previewTextKind(target.kind) ? await readFile8(target.path, "utf8") : "";
38400
+ const content = previewTextKind(target.kind) ? await readFile9(target.path, "utf8") : "";
38359
38401
  sendContract(response, url, {
38360
38402
  content,
38361
38403
  kind: target.kind,
@@ -38384,7 +38426,7 @@ function createFolderApi({
38384
38426
  documentError(response);
38385
38427
  return;
38386
38428
  }
38387
- const details = await stat7(target.path);
38429
+ const details = await stat8(target.path);
38388
38430
  if (!details.isFile()) {
38389
38431
  documentError(response);
38390
38432
  return;
@@ -38408,7 +38450,7 @@ function createFolderApi({
38408
38450
  "Content-Disposition": `attachment; filename="${basename6(target.path)}"`
38409
38451
  } : {}
38410
38452
  });
38411
- response.end(await readFile8(target.path));
38453
+ response.end(await readFile9(target.path));
38412
38454
  } catch (error) {
38413
38455
  documentError(response);
38414
38456
  }
@@ -38422,7 +38464,7 @@ function createFolderApi({
38422
38464
  documentError(response);
38423
38465
  return;
38424
38466
  }
38425
- const details = await stat7(target.path);
38467
+ const details = await stat8(target.path);
38426
38468
  if (!details.isFile()) {
38427
38469
  documentError(response);
38428
38470
  return;
@@ -38434,7 +38476,7 @@ function createFolderApi({
38434
38476
  return;
38435
38477
  }
38436
38478
  watch(folder);
38437
- const data = await readFile8(target.path);
38479
+ const data = await readFile9(target.path);
38438
38480
  const baseHeaders = {
38439
38481
  "Accept-Ranges": "bytes",
38440
38482
  "Cache-Control": "private, max-age=0, must-revalidate",
@@ -38751,7 +38793,7 @@ async function sendFile(request, response, directory, pathname, fallback) {
38751
38793
  response.end();
38752
38794
  return;
38753
38795
  }
38754
- if (!(await stat8(target).catch(() => null))?.isFile()) {
38796
+ if (!(await stat9(target).catch(() => null))?.isFile()) {
38755
38797
  if (!fallback) {
38756
38798
  response.writeHead(404);
38757
38799
  response.end();
@@ -38769,7 +38811,7 @@ async function sendFile(request, response, directory, pathname, fallback) {
38769
38811
  "Content-Type": contentTypes[extname7(target)] ?? "application/octet-stream",
38770
38812
  "X-Content-Type-Options": "nosniff"
38771
38813
  });
38772
- response.end(request.method === "HEAD" ? undefined : await readFile9(target));
38814
+ response.end(request.method === "HEAD" ? undefined : await readFile10(target));
38773
38815
  }
38774
38816
  function createKennelApi(options) {
38775
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>;
@@ -0,0 +1,219 @@
1
+ import { fork } from 'node:child_process';
2
+ import { realpathSync, statSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ const TIMEOUT_MS = 60_000;
6
+ const LLM_USAGE = `Usage:
7
+ knl llm [folder] [--profile <name>|--no-instructions] [--no-plugins] [--copy]
8
+ knl llm --folder <path> [options]
9
+ knl llm full [--folder <path>] [options]
10
+ knl llm [--folder <path>] --depth <count> [options]
11
+ knl llm --help
12
+
13
+ Prints only applicable AGENTS.md, personal instructions, and the research
14
+ write destination. The folder defaults to the current directory. Startup
15
+ reads registered workspace configs without scanning notes or the project catalog.
16
+
17
+ full Add folder details, all links, local subpaths, and plugin sections.
18
+ --depth <count> Add links and plugin sections, limited to a positive graph depth.
19
+ --profile <name> Use this personal instruction profile.
20
+ --no-instructions Omit personal profiles and all inlined instruction files. Expanded
21
+ modes can still include graph paths and plugin context.
22
+ --no-plugins Omit plugins in expanded modes. Startup never loads plugins.
23
+ --copy Copy the message to the clipboard instead of printing it.
24
+
25
+ Use --folder ./full for a folder named full. Use knl llm full for plugin
26
+ context from the pinned build; refresh it with \`kennel plugin add <source>\`.
27
+ `;
28
+ function usageError(message) {
29
+ throw new Error(`${message}\nRun knl llm --help for usage.`);
30
+ }
31
+ /** Strict argument parser: unknown, repeated, or conflicting input fails. */
32
+ function parseLlmArgs(args) {
33
+ const options = { help: false, includeInstructions: true, includePlugins: true, copy: false };
34
+ const seen = new Set();
35
+ const once = (name) => {
36
+ if (seen.has(name))
37
+ usageError(`Use ${name} once`);
38
+ seen.add(name);
39
+ };
40
+ const positionals = [];
41
+ for (let index = 0; index < args.length; index += 1) {
42
+ const arg = args[index];
43
+ const equals = arg.startsWith('--') ? arg.indexOf('=') : -1;
44
+ const name = equals === -1 ? arg : arg.slice(0, equals);
45
+ const inline = equals === -1 ? undefined : arg.slice(equals + 1);
46
+ const value = (label) => {
47
+ const next = inline ?? args[++index];
48
+ if (next === undefined || next === '' || (inline === undefined && next.startsWith('-'))) {
49
+ usageError(`${name} requires ${label}`);
50
+ }
51
+ return next;
52
+ };
53
+ const flag = () => {
54
+ if (inline !== undefined)
55
+ usageError(`${name} does not take a value`);
56
+ once(name);
57
+ };
58
+ if (name === '-h' || name === '--help') {
59
+ options.help = true;
60
+ }
61
+ else if (name === '--folder') {
62
+ once(name);
63
+ options.folder = value('a folder path');
64
+ }
65
+ else if (name === '--depth') {
66
+ once(name);
67
+ const count = value('a positive count');
68
+ if (!/^[1-9][0-9]{0,8}$/u.test(count))
69
+ usageError(`--depth must be a positive integer: ${count}`);
70
+ options.depth = Number(count);
71
+ }
72
+ else if (name === '--profile') {
73
+ once(name);
74
+ options.profile = value('a profile name');
75
+ }
76
+ else if (name === '--no-instructions') {
77
+ flag();
78
+ options.includeInstructions = false;
79
+ }
80
+ else if (name === '--no-plugins') {
81
+ flag();
82
+ options.includePlugins = false;
83
+ }
84
+ else if (name === '--copy') {
85
+ flag();
86
+ options.copy = true;
87
+ }
88
+ else if (arg.startsWith('-')) {
89
+ usageError(`Unknown option: ${arg}`);
90
+ }
91
+ else {
92
+ positionals.push(arg);
93
+ }
94
+ }
95
+ if (options.help)
96
+ return options;
97
+ if (positionals[0] === 'full') {
98
+ positionals.shift();
99
+ if (options.depth !== undefined)
100
+ usageError('Use either full or --depth, not both');
101
+ options.depth = 'full';
102
+ }
103
+ if (positionals.length > 1)
104
+ usageError(`Unexpected argument: ${positionals[1]}`);
105
+ if (positionals.length === 1) {
106
+ if (options.folder !== undefined)
107
+ usageError('Give the folder once: as an argument or with --folder');
108
+ options.folder = positionals[0];
109
+ }
110
+ if (options.profile !== undefined && !options.includeInstructions) {
111
+ usageError('--profile cannot be combined with --no-instructions');
112
+ }
113
+ return options;
114
+ }
115
+ function resolveFolder(input) {
116
+ let folder;
117
+ try {
118
+ folder = realpathSync(resolve(input));
119
+ }
120
+ catch {
121
+ throw new Error(`Folder does not exist: ${input}`);
122
+ }
123
+ if (!statSync(folder).isDirectory())
124
+ throw new Error(`Not a folder: ${input}. knl llm needs a folder.`);
125
+ return folder;
126
+ }
127
+ class Interrupted extends Error {
128
+ }
129
+ /** Runs one bounded generation in a child process and returns its message. */
130
+ function generate(request) {
131
+ return new Promise((resolvePromise, reject) => {
132
+ const child = fork(fileURLToPath(new URL('./worker.js', import.meta.url)), [JSON.stringify(request)], {
133
+ env: { ...process.env, NODE_OPTIONS: '' },
134
+ execArgv: [],
135
+ // Optional catalog/plugin logs are not startup output. Failures arrive over IPC.
136
+ stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
137
+ });
138
+ let stage = 'context generation';
139
+ let settled = false;
140
+ const finish = (error, text) => {
141
+ if (settled)
142
+ return;
143
+ settled = true;
144
+ clearTimeout(timer);
145
+ process.off('SIGINT', interrupt);
146
+ process.off('SIGTERM', interrupt);
147
+ if (child.exitCode === null && child.signalCode === null)
148
+ child.kill('SIGKILL');
149
+ if (error)
150
+ reject(error);
151
+ else
152
+ resolvePromise(text);
153
+ };
154
+ const interrupt = () => finish(new Interrupted('Cancelled. No context was printed or copied.'));
155
+ const timer = setTimeout(() => {
156
+ finish(new Error(`Context generation timed out after ${TIMEOUT_MS / 1000} seconds during ${stage}. No context was printed or copied.`));
157
+ }, TIMEOUT_MS);
158
+ process.once('SIGINT', interrupt);
159
+ process.once('SIGTERM', interrupt);
160
+ child.on('message', (message) => {
161
+ if (message?.type === 'stage' && typeof message.stage === 'string') {
162
+ stage = message.stage;
163
+ }
164
+ else if (message?.type === 'result' && message.ok && typeof message.text === 'string') {
165
+ finish(undefined, message.text);
166
+ }
167
+ else if (message?.type === 'result' && !message.ok) {
168
+ finish(new Error(String(message.error)));
169
+ }
170
+ else {
171
+ finish(new Error('The context worker sent an invalid message'));
172
+ }
173
+ });
174
+ child.once('error', (error) => finish(new Error(`Cannot start the context worker: ${error.message}`)));
175
+ child.once('exit', (code, signal) => {
176
+ // A terminal Ctrl+C reaches the worker too, sometimes before this process.
177
+ if (signal === 'SIGINT' || signal === 'SIGTERM')
178
+ return interrupt();
179
+ finish(new Error(`The context worker stopped during ${stage} (${signal ? `signal ${signal}` : `exit code ${code}`}) without a result`));
180
+ });
181
+ });
182
+ }
183
+ export async function llm(args) {
184
+ const options = parseLlmArgs(args);
185
+ if (options.help) {
186
+ process.stdout.write(LLM_USAGE);
187
+ return;
188
+ }
189
+ const folder = resolveFolder(options.folder ?? '.');
190
+ let text;
191
+ try {
192
+ text = await generate({
193
+ folder,
194
+ ...(options.depth !== undefined ? { depth: options.depth } : {}),
195
+ ...(options.profile !== undefined ? { profile: options.profile } : {}),
196
+ includeInstructions: options.includeInstructions,
197
+ includePlugins: options.includePlugins,
198
+ });
199
+ }
200
+ catch (error) {
201
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
202
+ process.exitCode = error instanceof Interrupted ? 130 : 1;
203
+ return;
204
+ }
205
+ if (options.copy) {
206
+ const { copyToClipboard } = await import('./clipboard.js');
207
+ try {
208
+ await copyToClipboard(text);
209
+ }
210
+ catch (error) {
211
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
212
+ process.exitCode = 1;
213
+ return;
214
+ }
215
+ console.error(`Copied the LLM context for ${folder} to the clipboard (${Buffer.byteLength(text, 'utf8')} bytes).`);
216
+ return;
217
+ }
218
+ process.stdout.write(text);
219
+ }
@@ -0,0 +1,19 @@
1
+ export type WorkerRequest = {
2
+ folder: string;
3
+ depth?: number | 'full';
4
+ profile?: string;
5
+ includeInstructions: boolean;
6
+ includePlugins: boolean;
7
+ };
8
+ export type WorkerMessage = {
9
+ type: 'stage';
10
+ stage: string;
11
+ } | {
12
+ type: 'result';
13
+ ok: true;
14
+ text: string;
15
+ } | {
16
+ type: 'result';
17
+ ok: false;
18
+ error: string;
19
+ };