kennel 0.2.41 → 0.2.43
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/plugin.js +10 -0
- package/dist/cli/usage.js +2 -0
- package/dist/package/preview.js +21 -15
- package/dist/plugins/definition.d.ts +9 -0
- package/dist/plugins/definition.js +62 -0
- package/dist/plugins/external.d.ts +8 -2
- package/dist/plugins/external.js +59 -104
- package/dist/plugins/skills.d.ts +1 -1
- package/dist/plugins/skills.js +2 -2
- package/dist/site/hot-viewer.js +3 -3
- package/dist/site/terminal.d.ts +6 -1
- package/dist/site/terminal.js +23 -17
- package/dist/site/viewer.d.ts +3 -0
- package/dist/site/viewer.js +19 -2
- package/package.json +2 -2
- package/schemas/config.schema.json +21 -7
- package/templates/plugin/kennel.json +14 -0
- package/templates/plugin/src/index.ts +1 -0
- package/templates/plugin/src/page.tsx +5 -0
- package/templates/plugin/{server.ts → src/server.ts} +2 -5
- package/templates/plugin/tsconfig.json +1 -1
- package/templates/plugin/client.tsx +0 -12
- package/templates/plugin/kennel.plugin.json +0 -3
- package/templates/plugin/manifest.ts +0 -6
package/dist/cli/plugin.js
CHANGED
|
@@ -3,6 +3,7 @@ import { dirname, resolve } from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
const USAGE = `Usage:
|
|
5
5
|
kennel plugin add <git-repository-url-or-local-path> [--path <subdirectory>] [--install-skill]
|
|
6
|
+
kennel plugin remove <plugin-id>
|
|
6
7
|
kennel plugin skill install <plugin-id> [--force]
|
|
7
8
|
kennel plugin list
|
|
8
9
|
kennel plugin new <id>`;
|
|
@@ -77,6 +78,15 @@ export async function plugin(args) {
|
|
|
77
78
|
console.log(`Plugin declares skill ${entry.skill.name}; install it with \`kennel plugin skill install ${entry.id}\``);
|
|
78
79
|
return;
|
|
79
80
|
}
|
|
81
|
+
if (subcommand === 'remove') {
|
|
82
|
+
const [id] = rest;
|
|
83
|
+
if (!id || id.startsWith('-') || rest.length !== 1)
|
|
84
|
+
usageError();
|
|
85
|
+
const { removeExternalPlugin } = await import('../plugins/external.js');
|
|
86
|
+
await removeExternalPlugin(id);
|
|
87
|
+
console.log(`Removed plugin ${id}; source folders, caches, and installed skills were kept.`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
80
90
|
if (subcommand === 'skill' && rest[0] === 'install') {
|
|
81
91
|
const [, id, ...flags] = rest;
|
|
82
92
|
if (!id || id.startsWith('-') || flags.some((flag) => flag !== '--force'))
|
package/dist/cli/usage.js
CHANGED
|
@@ -3,6 +3,7 @@ export function usage() {
|
|
|
3
3
|
knl core [folder] [--host <host>] [--port <port>] [--api-only] [--web-root <dir>]
|
|
4
4
|
kennel init [path] [--plugin <id>] [--force] [--list]
|
|
5
5
|
kennel plugin add <git-repository-url-or-local-path> [--path <subdirectory>] [--install-skill]
|
|
6
|
+
kennel plugin remove <plugin-id>
|
|
6
7
|
kennel plugin skill install <plugin-id> [--force]
|
|
7
8
|
kennel plugin list
|
|
8
9
|
kennel plugin new <id>
|
|
@@ -37,6 +38,7 @@ Without an interactive terminal, the viewer prints its URL without a menu.
|
|
|
37
38
|
|
|
38
39
|
Installed plugin ids are commands, for example knl tasks or knl calendar.
|
|
39
40
|
Plugins can declare extra aliases, for example knl mail for plugin id email.
|
|
41
|
+
Removing a plugin keeps its source folders, caches, and separately installed skills.
|
|
40
42
|
Use --item <id> for a plugin detail view. Built-in commands take priority.
|
|
41
43
|
|
|
42
44
|
knl llm prints only agent instructions, profiles, and the research write destination.
|
package/dist/package/preview.js
CHANGED
|
@@ -242,7 +242,7 @@ function repositoryName(folder) {
|
|
|
242
242
|
return;
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
|
-
function previewTerminal(url, target, startShare) {
|
|
245
|
+
function previewTerminal(url, target, startShare, views) {
|
|
246
246
|
url = new URL(url).href;
|
|
247
247
|
const input = process.stdin;
|
|
248
248
|
const output = process.stdout;
|
|
@@ -253,8 +253,9 @@ function previewTerminal(url, target, startShare) {
|
|
|
253
253
|
const repository = interactive ? repositoryName(target.root) : undefined;
|
|
254
254
|
const color = terminal && process.env.NO_COLOR === undefined && output.hasColors();
|
|
255
255
|
const accent = (text) => color ? `\x1B[32m${text}\x1B[39m` : text;
|
|
256
|
-
const
|
|
257
|
-
|
|
256
|
+
const linkTo = (label, href, plain = `${label}
|
|
257
|
+
${href}`) => terminal ? `\x1B]8;;${href}\x07${accent(label)}\x1B]8;;\x07` : plain;
|
|
258
|
+
const link = (label) => linkTo(label, url);
|
|
258
259
|
const wasRaw = Boolean(input.isRaw);
|
|
259
260
|
const wasFlowing = input.readableFlowing === true;
|
|
260
261
|
let resolveClosed;
|
|
@@ -271,27 +272,28 @@ function previewTerminal(url, target, startShare) {
|
|
|
271
272
|
return;
|
|
272
273
|
cursorTo(output, 0, 0);
|
|
273
274
|
clearScreenDown(output);
|
|
274
|
-
const width = Math.max(1, (output.columns || 80) -
|
|
275
|
+
const width = Math.max(1, (output.columns || 80) - 1);
|
|
275
276
|
const fit = (text) => {
|
|
276
277
|
const characters = Array.from(terminalText(text));
|
|
277
278
|
return characters.length > width ? characters.slice(0, width - 1).join("") + "…" : characters.join("");
|
|
278
279
|
};
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
const choices = actions.slice(start, start + count).map((item, index) => {
|
|
280
|
+
const choices = [];
|
|
281
|
+
actions.forEach((item, index) => {
|
|
282
282
|
const label = fit(item.shortcut ? `[${item.shortcut}] ${item.label}` : item.label);
|
|
283
|
-
|
|
283
|
+
let text = index === selected ? accent(`› ${label}`) : ` ${label}`;
|
|
284
|
+
if (item.view !== undefined)
|
|
285
|
+
text = linkTo(text, item.view, accent(text));
|
|
286
|
+
choices.push(text);
|
|
284
287
|
});
|
|
285
288
|
output.write([
|
|
286
|
-
|
|
287
|
-
...repository ? [
|
|
288
|
-
...target.target ? [`
|
|
289
|
+
`\x1B[1m${link(fit(title))}\x1B[22m`,
|
|
290
|
+
...repository ? [`${color ? "\x1B[2m" : ""}${fit(repository)}${color ? "\x1B[22m" : ""}`] : [],
|
|
291
|
+
...target.target ? [`File: ${fit(name)}`] : [],
|
|
289
292
|
"",
|
|
290
293
|
...choices,
|
|
291
294
|
"",
|
|
292
|
-
"
|
|
293
|
-
...
|
|
294
|
-
...status ? ["", ` ${status}`] : [],
|
|
295
|
+
"↑ ↓ to move · Enter to choose · Esc or Ctrl+C to exit",
|
|
296
|
+
...status ? ["", status] : [],
|
|
295
297
|
""
|
|
296
298
|
].join(`
|
|
297
299
|
`));
|
|
@@ -356,6 +358,10 @@ function previewTerminal(url, target, startShare) {
|
|
|
356
358
|
resolveClosed();
|
|
357
359
|
};
|
|
358
360
|
const actions = [
|
|
361
|
+
...(views ?? []).map((view) => ({ label: view.id, run: () => {
|
|
362
|
+
openPreview(view.url);
|
|
363
|
+
status = "Browser open requested.";
|
|
364
|
+
}, view: view.url })),
|
|
359
365
|
{ label: "Open in browser", shortcut: "o", run: () => {
|
|
360
366
|
openPreview(url);
|
|
361
367
|
status = "Browser open requested.";
|
|
@@ -415,7 +421,7 @@ function previewTerminal(url, target, startShare) {
|
|
|
415
421
|
render();
|
|
416
422
|
else
|
|
417
423
|
console.log(`
|
|
418
|
-
|
|
424
|
+
${link(name)}
|
|
419
425
|
`);
|
|
420
426
|
return { close, closed, interactive };
|
|
421
427
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FolderPluginDefinition, FolderPluginManifest } from "kennel-plugins/types";
|
|
2
|
+
/** Reads metadata only. Discovery must never import plugin code. */
|
|
3
|
+
export declare function readPluginDefinition(path: string): FolderPluginDefinition;
|
|
4
|
+
/** The host's display record has no authoring paths or component bindings. */
|
|
5
|
+
export declare function pluginDisplayManifest(plugin: FolderPluginDefinition): FolderPluginManifest;
|
|
6
|
+
/** Named imports let the compiler reject missing exports without running React. */
|
|
7
|
+
export declare function pluginClientSource(index: string, plugin: FolderPluginDefinition): string;
|
|
8
|
+
/** Compose trusted hooks with JSON metadata; hooks cannot change the identity. */
|
|
9
|
+
export declare function pluginServerSource(server: string, plugin: FolderPluginDefinition): string;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
const schema = z.fromJSONSchema(JSON.parse(readFileSync(fileURLToPath(import.meta.resolve("kennel-plugins/schema.json")), "utf8")));
|
|
5
|
+
/** Reads metadata only. Discovery must never import plugin code. */
|
|
6
|
+
export function readPluginDefinition(path) {
|
|
7
|
+
let value;
|
|
8
|
+
try {
|
|
9
|
+
value = JSON.parse(readFileSync(path, "utf8"));
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
throw new Error(`Cannot read plugin definition ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
13
|
+
}
|
|
14
|
+
const result = schema.safeParse(value);
|
|
15
|
+
if (!result.success) {
|
|
16
|
+
throw new Error(`Invalid plugin definition ${path}:\n${result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("\n")}`);
|
|
17
|
+
}
|
|
18
|
+
const { plugin } = result.data;
|
|
19
|
+
const paths = plugin.views.map((view) => view.subpath);
|
|
20
|
+
if (new Set(paths).size !== paths.length || (paths.length > 1 && paths.includes(""))) {
|
|
21
|
+
throw new Error(`${path}: plugin.views must have unique subpaths; an empty subpath must be the only view`);
|
|
22
|
+
}
|
|
23
|
+
return plugin;
|
|
24
|
+
}
|
|
25
|
+
/** The host's display record has no authoring paths or component bindings. */
|
|
26
|
+
export function pluginDisplayManifest(plugin) {
|
|
27
|
+
const first = plugin.views[0];
|
|
28
|
+
const icon = first.subpath === "" ? first.icon ?? plugin.icon : plugin.icon;
|
|
29
|
+
return {
|
|
30
|
+
id: plugin.id,
|
|
31
|
+
label: first.subpath === "" ? first.label : plugin.label,
|
|
32
|
+
...(icon ? { icon } : {}),
|
|
33
|
+
...(first.subpath === "" ? {} : {
|
|
34
|
+
views: plugin.views.map(({ component: _component, ...view }) => view),
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Named imports let the compiler reject missing exports without running React. */
|
|
39
|
+
export function pluginClientSource(index, plugin) {
|
|
40
|
+
return `import { createElement } from "react";
|
|
41
|
+
import { ${plugin.views.map((view, i) => `${view.component} as Page${i}`).join(", ")} } from ${JSON.stringify(index)};
|
|
42
|
+
const views = [${plugin.views.map((view, i) => `{ subpath: ${JSON.stringify(view.subpath)}, component: Page${i} }`).join(", ")}];
|
|
43
|
+
function PluginPage(props) {
|
|
44
|
+
const route = props.subpath.split("?")[0];
|
|
45
|
+
const view = route ? views.find((view) => view.subpath === "" || view.subpath === route.split("/")[0]) : views[0];
|
|
46
|
+
return view
|
|
47
|
+
? createElement(view.component, { ...props, key: props.folderId + ":" + props.subpath })
|
|
48
|
+
: createElement("p", { role: "alert" }, "That plugin view does not exist.");
|
|
49
|
+
}
|
|
50
|
+
export default { component: PluginPage };
|
|
51
|
+
`;
|
|
52
|
+
}
|
|
53
|
+
/** Compose trusted hooks with JSON metadata; hooks cannot change the identity. */
|
|
54
|
+
export function pluginServerSource(server, plugin) {
|
|
55
|
+
return `import entry from ${JSON.stringify(server)};
|
|
56
|
+
const manifest = ${JSON.stringify(pluginDisplayManifest(plugin))};
|
|
57
|
+
export default (...args) => {
|
|
58
|
+
const hooks = typeof entry === "function" ? entry(...args) : entry;
|
|
59
|
+
return { ...hooks, ...manifest, icon: manifest.icon, views: manifest.views };
|
|
60
|
+
};
|
|
61
|
+
`;
|
|
62
|
+
}
|
|
@@ -10,9 +10,9 @@ export type ExternalPluginEntry = {
|
|
|
10
10
|
path?: string;
|
|
11
11
|
trigger?: string;
|
|
12
12
|
entries: {
|
|
13
|
-
|
|
13
|
+
index: string;
|
|
14
14
|
server: string;
|
|
15
|
-
|
|
15
|
+
definition: string;
|
|
16
16
|
api?: string;
|
|
17
17
|
css?: string;
|
|
18
18
|
};
|
|
@@ -25,6 +25,11 @@ export type ExternalPluginEntry = {
|
|
|
25
25
|
digest: string;
|
|
26
26
|
};
|
|
27
27
|
};
|
|
28
|
+
/** Manifest-declared subviews, recorded at install time. */
|
|
29
|
+
views?: {
|
|
30
|
+
subpath: string;
|
|
31
|
+
label: string;
|
|
32
|
+
}[];
|
|
28
33
|
};
|
|
29
34
|
export declare function kennelConfigPath(): string;
|
|
30
35
|
export declare const PLUGIN_SOURCE_IGNORED: Set<string>;
|
|
@@ -35,6 +40,7 @@ export declare function readExternalPlugins(): ExternalPluginEntry[];
|
|
|
35
40
|
export declare function installExternalPlugin(url: string, options?: {
|
|
36
41
|
directory?: string;
|
|
37
42
|
}): Promise<ExternalPluginEntry>;
|
|
43
|
+
export declare function removeExternalPlugin(id: string): Promise<void>;
|
|
38
44
|
/** Installs a declared skill from local source or the pinned cached checkout. */
|
|
39
45
|
export declare function installPluginSkill(id: string, options?: {
|
|
40
46
|
force?: boolean;
|
package/dist/plugins/external.js
CHANGED
|
@@ -15,8 +15,8 @@ import { pathToFileURL, fileURLToPath } from "node:url";
|
|
|
15
15
|
import { spawnSync } from "node:child_process";
|
|
16
16
|
import { z } from "zod";
|
|
17
17
|
import { duplicateSkills, readPluginSkill, skillDestination, skillState, writeSkill } from "./skills.js";
|
|
18
|
+
import { readPluginDefinition, pluginDisplayManifest, pluginClientSource, pluginServerSource } from "./definition.js";
|
|
18
19
|
const CONFIG_FILE = join("kennel", "config.json");
|
|
19
|
-
const VALID_ID = /^[a-z][a-z0-9-]*$/u;
|
|
20
20
|
const CORE_IDS = new Set(["agents", "notes"]);
|
|
21
21
|
function xdgConfigHome() {
|
|
22
22
|
return process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), ".config");
|
|
@@ -162,7 +162,7 @@ function pluginRoot(repoRoot, directory) {
|
|
|
162
162
|
return repoRoot;
|
|
163
163
|
if (isAbsolute(directory) || directory.includes("\0"))
|
|
164
164
|
throw new Error(`Unsafe plugin directory: ${directory}`);
|
|
165
|
-
const target = resolve(repoRoot, directory);
|
|
165
|
+
const target = realpathSync(resolve(repoRoot, directory));
|
|
166
166
|
const rel = relative(repoRoot, target);
|
|
167
167
|
if (rel === "" || rel.startsWith("..") || isAbsolute(rel))
|
|
168
168
|
throw new Error(`Unsafe plugin directory: ${directory}`);
|
|
@@ -179,9 +179,11 @@ function installPluginDependencies(root, dependencies) {
|
|
|
179
179
|
function safeRelativeFile(root, value, label) {
|
|
180
180
|
if (typeof value !== "string" || !value.trim())
|
|
181
181
|
throw new Error(`Missing ${label}`);
|
|
182
|
-
if (isAbsolute(value) || value.includes("\0"))
|
|
182
|
+
if (isAbsolute(value) || value.includes("\0") || value.includes("\\") || /^[a-z][a-z0-9+.-]*:/iu.test(value) || value.split("/").includes(".."))
|
|
183
183
|
throw new Error(`Unsafe ${label}: ${value}`);
|
|
184
|
-
|
|
184
|
+
if (!existsSync(resolve(root, value)))
|
|
185
|
+
throw new Error(`Missing ${label}: ${value}`);
|
|
186
|
+
const target = realpathSync(resolve(root, value));
|
|
185
187
|
const rel = relative(root, target);
|
|
186
188
|
if (rel === "" || rel.startsWith("..") || isAbsolute(rel))
|
|
187
189
|
throw new Error(`Unsafe ${label}: ${value}`);
|
|
@@ -189,16 +191,11 @@ function safeRelativeFile(root, value, label) {
|
|
|
189
191
|
throw new Error(`Missing ${label}: ${value}`);
|
|
190
192
|
return target;
|
|
191
193
|
}
|
|
192
|
-
function
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (existsSync(srcCandidate) && statSync(srcCandidate).isFile())
|
|
198
|
-
return srcCandidate;
|
|
199
|
-
if (!required)
|
|
200
|
-
return undefined;
|
|
201
|
-
throw new Error(`Missing required plugin entry ${file} (expected ${file} or src/${file})`);
|
|
194
|
+
function pluginDefinition(entry) {
|
|
195
|
+
const definition = readPluginDefinition(entry.entries.definition);
|
|
196
|
+
if (definition.id !== entry.id)
|
|
197
|
+
throw new Error(`Plugin definition id changed: reinstall ${entry.id}`);
|
|
198
|
+
return definition;
|
|
202
199
|
}
|
|
203
200
|
// Generated files do not change the plugin source or trigger development rebuilds.
|
|
204
201
|
export const PLUGIN_SOURCE_IGNORED = new Set([".git", "node_modules", ".cache", ".turbo", "dist", "coverage"]);
|
|
@@ -225,11 +222,6 @@ function contentHash(root) {
|
|
|
225
222
|
visit(root);
|
|
226
223
|
return digest.digest("hex");
|
|
227
224
|
}
|
|
228
|
-
function stringField(value, label) {
|
|
229
|
-
if (typeof value !== "string" || !value.trim())
|
|
230
|
-
throw new Error(`Missing ${label}`);
|
|
231
|
-
return value.trim();
|
|
232
|
-
}
|
|
233
225
|
// Specifiers the host shares with plugin browser bundles at runtime. Keep in
|
|
234
226
|
// sync with pluginHostModules in apps/web/src/plugin-host-modules.ts and
|
|
235
227
|
// apps/desktop/src/renderer/plugin-host-modules.ts.
|
|
@@ -298,14 +290,16 @@ module.exports = host.modules[${JSON.stringify(args.path)}];`,
|
|
|
298
290
|
},
|
|
299
291
|
};
|
|
300
292
|
}
|
|
301
|
-
async function bundleForImport(entry, outfile, platform) {
|
|
293
|
+
async function bundleForImport(entry, outfile, platform, contents) {
|
|
302
294
|
const esbuild = await import("esbuild");
|
|
303
295
|
await esbuild.build({
|
|
304
296
|
banner: platform === "node"
|
|
305
297
|
? { js: 'import { createRequire as __kennelCreateRequire } from "node:module"; const require = __kennelCreateRequire(import.meta.url);' }
|
|
306
298
|
: undefined,
|
|
307
299
|
bundle: true,
|
|
308
|
-
entryPoints: [entry]
|
|
300
|
+
...(contents === undefined ? { entryPoints: [entry] } : {
|
|
301
|
+
stdin: { contents, loader: "js", resolveDir: dirname(entry), sourcefile: "kennel-adapter.js" },
|
|
302
|
+
}),
|
|
309
303
|
external: ["bun:ffi", "koffi"],
|
|
310
304
|
format: "esm",
|
|
311
305
|
jsx: "automatic",
|
|
@@ -318,56 +312,27 @@ async function bundleForImport(entry, outfile, platform) {
|
|
|
318
312
|
target: platform === "node" ? "node20" : "es2022",
|
|
319
313
|
});
|
|
320
314
|
}
|
|
321
|
-
function assertManifest(value, id, label) {
|
|
322
|
-
if (!value || typeof value !== "object")
|
|
323
|
-
throw new Error(`${label} is not an object`);
|
|
324
|
-
if (value.id !== id)
|
|
325
|
-
throw new Error(`${label} id mismatch`);
|
|
326
|
-
if (typeof value.label !== "string" || !value.label.trim())
|
|
327
|
-
throw new Error(`${label} missing label`);
|
|
328
|
-
if (value.views !== undefined) {
|
|
329
|
-
if (!Array.isArray(value.views) || value.views.length === 0)
|
|
330
|
-
throw new Error(`${label} views must be a nonempty array`);
|
|
331
|
-
const paths = new Set();
|
|
332
|
-
for (const view of value.views) {
|
|
333
|
-
if (!view || typeof view.subpath !== "string" || !VALID_ID.test(view.subpath) ||
|
|
334
|
-
typeof view.label !== "string" || !view.label.trim() || paths.has(view.subpath)) {
|
|
335
|
-
throw new Error(`${label} views need unique route segments and labels`);
|
|
336
|
-
}
|
|
337
|
-
paths.add(view.subpath);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
315
|
async function validateImports(entry) {
|
|
342
316
|
const dir = await mkdtemp(join(tmpdir(), "kennel-plugin-validate-"));
|
|
343
317
|
try {
|
|
344
|
-
const
|
|
318
|
+
const definition = pluginDefinition(entry);
|
|
345
319
|
const serverOut = join(dir, "server.mjs");
|
|
346
|
-
const clientOut = join(dir, "client.mjs");
|
|
347
320
|
const apiOut = join(dir, "api.mjs");
|
|
348
|
-
await bundleForImport(entry.entries.manifest, manifestOut, "browser");
|
|
349
321
|
await bundleForImport(entry.entries.server, serverOut, "node");
|
|
350
|
-
await bundleForImport(entry.entries.
|
|
322
|
+
await bundleForImport(entry.entries.index, join(dir, "client.mjs"), "browser", pluginClientSource(entry.entries.index, definition));
|
|
351
323
|
if (entry.entries.api)
|
|
352
324
|
await bundleForImport(entry.entries.api, apiOut, "node");
|
|
353
|
-
const manifestModule = await import(__rewriteRelativeImportExtension(pathToFileURL(manifestOut).href));
|
|
354
325
|
const serverModule = await import(__rewriteRelativeImportExtension(pathToFileURL(serverOut).href));
|
|
355
|
-
const clientModule = await import(__rewriteRelativeImportExtension(pathToFileURL(clientOut).href));
|
|
356
|
-
const manifest = manifestModule.default;
|
|
357
326
|
const serverExport = serverModule.default;
|
|
358
327
|
const server = typeof serverExport === "function" ? serverExport({}) : serverExport;
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if (!server || typeof server !== "object" || server.id !== entry.id)
|
|
362
|
-
throw new Error("server id mismatch");
|
|
328
|
+
if (!server || typeof server !== "object")
|
|
329
|
+
throw new Error("server default export must provide hooks or a hook factory");
|
|
363
330
|
if (typeof server.detect !== "function" || typeof server.handle !== "function")
|
|
364
331
|
throw new Error("server export missing detect/handle");
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
if (typeof client.component !== "function")
|
|
370
|
-
throw new Error("client export missing component");
|
|
332
|
+
for (const hook of ["init", "llm"]) {
|
|
333
|
+
if (server[hook] !== undefined && typeof server[hook] !== "function")
|
|
334
|
+
throw new Error(`server export ${hook} hook must be a function`);
|
|
335
|
+
}
|
|
371
336
|
if (entry.entries.api) {
|
|
372
337
|
const apiModule = await import(__rewriteRelativeImportExtension(pathToFileURL(apiOut).href));
|
|
373
338
|
if (typeof apiModule.default !== "function")
|
|
@@ -386,40 +351,18 @@ export async function validateExternalPlugin(inputUrl, options = {}) {
|
|
|
386
351
|
const root = pluginRoot(checkoutRoot, directory);
|
|
387
352
|
const pkgPath = join(root, "package.json");
|
|
388
353
|
const pkg = existsSync(pkgPath) ? JSON.parse(await readFile(pkgPath, "utf8")) : {};
|
|
389
|
-
const
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
: undefined;
|
|
393
|
-
const metadata = (pkg.kennel?.plugin && typeof pkg.kennel.plugin === "object"
|
|
394
|
-
? pkg.kennel.plugin
|
|
395
|
-
: fileMetadata && typeof fileMetadata === "object"
|
|
396
|
-
? fileMetadata
|
|
397
|
-
: undefined);
|
|
398
|
-
const id = metadata?.id === undefined ? basename(root) : stringField(metadata.id, "plugin id");
|
|
399
|
-
if (!VALID_ID.test(id))
|
|
400
|
-
throw new Error(`Invalid plugin id: ${id}`);
|
|
354
|
+
const definition = safeRelativeFile(root, "kennel.json", "plugin definition");
|
|
355
|
+
const metadata = readPluginDefinition(definition);
|
|
356
|
+
const { id, aliases, trigger } = metadata;
|
|
401
357
|
if (CORE_IDS.has(id))
|
|
402
358
|
throw new Error(`External plugin id collides with core plugin: ${id}`);
|
|
403
359
|
const packageName = typeof pkg.name === "string" && pkg.name.trim() ? pkg.name.trim() : id;
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
throw new Error("Plugin aliases must be unique lowercase command names");
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
const trigger = metadata?.trigger === undefined ? undefined : stringField(metadata.trigger, "trigger file");
|
|
414
|
-
if (trigger && (trigger.includes("/") || trigger.includes("\\") || trigger === "." || trigger === ".."))
|
|
415
|
-
throw new Error(`Invalid trigger file: ${trigger}`);
|
|
416
|
-
const client = entryByConvention(root, "client.tsx", true);
|
|
417
|
-
const server = entryByConvention(root, "server.ts", true);
|
|
418
|
-
const manifest = entryByConvention(root, "manifest.ts", true);
|
|
419
|
-
const api = entryByConvention(root, "api.ts", false);
|
|
420
|
-
const cssValue = metadata?.exports?.css;
|
|
421
|
-
const css = cssValue === undefined ? undefined : safeRelativeFile(root, cssValue, "css export path");
|
|
422
|
-
const skill = metadata?.skill === undefined ? undefined : readPluginSkill(root, metadata.skill);
|
|
360
|
+
const index = safeRelativeFile(root, "src/index.ts", "browser entry");
|
|
361
|
+
const server = safeRelativeFile(root, "src/server.ts", "server entry");
|
|
362
|
+
const api = existsSync(join(root, "src/api.ts")) ? safeRelativeFile(root, "src/api.ts", "API entry") : undefined;
|
|
363
|
+
const css = metadata.css === undefined ? undefined : safeRelativeFile(root, metadata.css, "css export path");
|
|
364
|
+
const skill = metadata.skill === undefined ? undefined : readPluginSkill(root, metadata.skill);
|
|
365
|
+
const views = pluginDisplayManifest(metadata).views?.map(({ subpath, label }) => ({ subpath, label }));
|
|
423
366
|
const entry = {
|
|
424
367
|
url: source.url,
|
|
425
368
|
...(source.requestedRef ? { requestedRef: source.requestedRef } : {}),
|
|
@@ -431,13 +374,14 @@ export async function validateExternalPlugin(inputUrl, options = {}) {
|
|
|
431
374
|
...(source.path ? { path: source.path } : {}),
|
|
432
375
|
...(trigger ? { trigger } : {}),
|
|
433
376
|
entries: {
|
|
434
|
-
|
|
377
|
+
index,
|
|
435
378
|
server,
|
|
436
|
-
|
|
379
|
+
definition,
|
|
437
380
|
...(api ? { api } : {}),
|
|
438
381
|
...(css ? { css } : {}),
|
|
439
382
|
},
|
|
440
383
|
...(skill ? { skill: { entry: skill.entry, name: skill.name } } : {}),
|
|
384
|
+
...(views ? { views } : {}),
|
|
441
385
|
};
|
|
442
386
|
installPluginDependencies(root, pkg.dependencies);
|
|
443
387
|
await validateImports(entry);
|
|
@@ -470,6 +414,18 @@ export async function installExternalPlugin(url, options = {}) {
|
|
|
470
414
|
}
|
|
471
415
|
return entry;
|
|
472
416
|
}
|
|
417
|
+
export async function removeExternalPlugin(id) {
|
|
418
|
+
const path = kennelConfigPath();
|
|
419
|
+
const config = readJson(path);
|
|
420
|
+
const plugins = config.externalPlugins ?? [];
|
|
421
|
+
if (!plugins.some((plugin) => plugin.id === id))
|
|
422
|
+
throw new Error(`No installed plugin with id ${id}`);
|
|
423
|
+
const externalPlugins = plugins.filter((plugin) => plugin.id !== id);
|
|
424
|
+
const nextConfig = { ...config, externalPlugins };
|
|
425
|
+
assertUserConfig(nextConfig, path);
|
|
426
|
+
await ensurePluginBuild(externalPlugins);
|
|
427
|
+
writeJsonAtomic(path, nextConfig);
|
|
428
|
+
}
|
|
473
429
|
/** Uses local source or checks out the pinned cached commit without fetching. */
|
|
474
430
|
function pinnedPackageRoot(plugin) {
|
|
475
431
|
if (plugin.path)
|
|
@@ -491,7 +447,7 @@ export function installPluginSkill(id, options = {}) {
|
|
|
491
447
|
if (!plugin)
|
|
492
448
|
throw new Error(`No installed plugin with id ${id}`);
|
|
493
449
|
if (!plugin.skill)
|
|
494
|
-
throw new Error(`Plugin ${id} declares no skill (kennel.
|
|
450
|
+
throw new Error(`Plugin ${id} declares no skill (kennel.json > plugin.skill)`);
|
|
495
451
|
const skill = readPluginSkill(pinnedPackageRoot(plugin), plugin.skill.entry);
|
|
496
452
|
if (skill.name !== plugin.skill.name)
|
|
497
453
|
throw new Error(`Pinned skill name ${skill.name} does not match recorded name ${plugin.skill.name}`);
|
|
@@ -529,7 +485,7 @@ function cliRoot() {
|
|
|
529
485
|
export function webRoot() {
|
|
530
486
|
return resolve(cliRoot(), "web");
|
|
531
487
|
}
|
|
532
|
-
const BUILD_IDENTITY = "kennel-
|
|
488
|
+
const BUILD_IDENTITY = "kennel-json-plugins-v2";
|
|
533
489
|
function cliVersion() {
|
|
534
490
|
return JSON.parse(readFileSync(join(cliRoot(), "package.json"), "utf8")).version;
|
|
535
491
|
}
|
|
@@ -633,7 +589,7 @@ async function buildPluginStyles(plugin, outdir, theme) {
|
|
|
633
589
|
import("@tailwindcss/oxide"),
|
|
634
590
|
import("esbuild"),
|
|
635
591
|
]);
|
|
636
|
-
const source = dirname(plugin.entries.
|
|
592
|
+
const source = dirname(plugin.entries.index);
|
|
637
593
|
const input = [
|
|
638
594
|
'@import "tailwindcss/theme.css" layer(theme);',
|
|
639
595
|
`@import ${JSON.stringify(theme)};`,
|
|
@@ -713,8 +669,13 @@ async function buildPluginClient(plugin, outdir) {
|
|
|
713
669
|
bundle: true,
|
|
714
670
|
chunkNames: "chunks/[name]-[hash]",
|
|
715
671
|
define: { "process.env.NODE_ENV": '"production"' },
|
|
716
|
-
entryNames: "
|
|
717
|
-
|
|
672
|
+
entryNames: "client",
|
|
673
|
+
stdin: {
|
|
674
|
+
contents: pluginClientSource(plugin.entries.index, pluginDefinition(plugin)),
|
|
675
|
+
loader: "js",
|
|
676
|
+
resolveDir: dirname(plugin.entries.index),
|
|
677
|
+
sourcefile: "client.js",
|
|
678
|
+
},
|
|
718
679
|
format: "esm",
|
|
719
680
|
jsx: "automatic",
|
|
720
681
|
loader: ASSET_LOADERS,
|
|
@@ -731,12 +692,6 @@ async function buildPluginClient(plugin, outdir) {
|
|
|
731
692
|
const output = Object.entries(result.metafile.outputs).find(([, value]) => value.entryPoint)?.[1];
|
|
732
693
|
return output?.cssBundle ? [relative(outdir, resolve(output.cssBundle))] : [];
|
|
733
694
|
}
|
|
734
|
-
async function pluginManifest(plugin, outfile) {
|
|
735
|
-
await bundleForImport(plugin.entries.manifest, outfile, "browser");
|
|
736
|
-
const manifest = (await import(__rewriteRelativeImportExtension(pathToFileURL(outfile).href))).default;
|
|
737
|
-
assertManifest(manifest, plugin.id, "manifest export");
|
|
738
|
-
return JSON.parse(JSON.stringify(manifest));
|
|
739
|
-
}
|
|
740
695
|
async function buildPlugins(temp, target, key, plugins, theme) {
|
|
741
696
|
const serverEntries = [];
|
|
742
697
|
const apiEntries = [];
|
|
@@ -746,11 +701,11 @@ async function buildPlugins(temp, target, key, plugins, theme) {
|
|
|
746
701
|
const clientStyles = await buildPluginClient(plugin, clientDir);
|
|
747
702
|
const styles = [await buildPluginStyles(plugin, clientDir, theme), ...clientStyles];
|
|
748
703
|
registry.push({
|
|
749
|
-
manifest:
|
|
704
|
+
manifest: pluginDisplayManifest(pluginDefinition(plugin)),
|
|
750
705
|
module: `/_kennel/plugins/${plugin.id}/client.js`,
|
|
751
706
|
styles: styles.map((style) => `/_kennel/plugins/${plugin.id}/${style}`),
|
|
752
707
|
});
|
|
753
|
-
await bundleForImport(plugin.entries.server, join(temp, "server", `${plugin.id}.js`), "node");
|
|
708
|
+
await bundleForImport(plugin.entries.server, join(temp, "server", `${plugin.id}.js`), "node", pluginServerSource(plugin.entries.server, pluginDefinition(plugin)));
|
|
754
709
|
serverEntries.push({ id: plugin.id, module: join(target, "server", `${plugin.id}.js`), symbol: "default", trigger: plugin.trigger });
|
|
755
710
|
if (plugin.entries.api) {
|
|
756
711
|
await bundleForImport(plugin.entries.api, join(temp, "server", `${plugin.id}-api.js`), "node");
|
package/dist/plugins/skills.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export type SkillState = "missing" | "installed" | "conflict";
|
|
|
8
8
|
export declare function agentSkillsRoot(): string;
|
|
9
9
|
export declare function skillDestination(name: string): string;
|
|
10
10
|
/**
|
|
11
|
-
* Resolves `
|
|
11
|
+
* Resolves `kennel.json > plugin.skill` inside a plugin package root.
|
|
12
12
|
* The entry must be a contained, non-symlinked `SKILL.md` with a valid name and description.
|
|
13
13
|
*/
|
|
14
14
|
export declare function readPluginSkill(packageRoot: string, entry: unknown): PluginSkill;
|
package/dist/plugins/skills.js
CHANGED
|
@@ -38,12 +38,12 @@ function frontmatter(path) {
|
|
|
38
38
|
return data;
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
|
-
* Resolves `
|
|
41
|
+
* Resolves `kennel.json > plugin.skill` inside a plugin package root.
|
|
42
42
|
* The entry must be a contained, non-symlinked `SKILL.md` with a valid name and description.
|
|
43
43
|
*/
|
|
44
44
|
export function readPluginSkill(packageRoot, entry) {
|
|
45
45
|
if (typeof entry !== "string" || !entry.trim())
|
|
46
|
-
throw new Error("kennel.plugin.skill must be a relative path to SKILL.md");
|
|
46
|
+
throw new Error("kennel.json > plugin.skill must be a relative path to SKILL.md");
|
|
47
47
|
if (isAbsolute(entry) || entry.includes("\0") || entry.includes("\\"))
|
|
48
48
|
throw new Error(`Unsafe skill path: ${entry}`);
|
|
49
49
|
if (basename(entry) !== "SKILL.md")
|
package/dist/site/hot-viewer.js
CHANGED
|
@@ -6,7 +6,7 @@ import { createConnection } from 'node:net';
|
|
|
6
6
|
import { tmpdir } from 'node:os';
|
|
7
7
|
import { dirname, relative, resolve } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
|
-
import { popUrl } from './viewer.js';
|
|
9
|
+
import { popUrl, pluginViewLinks } from './viewer.js';
|
|
10
10
|
import { createKennelClient } from '../core/api-client.js';
|
|
11
11
|
import { pluginSetKey } from '../plugins/external.js';
|
|
12
12
|
import { openPreview, previewTerminal } from './terminal.js';
|
|
@@ -179,7 +179,7 @@ export async function hotViewer(options, restarting = false) {
|
|
|
179
179
|
return hotViewer(options, true);
|
|
180
180
|
throw new Error(stale);
|
|
181
181
|
}
|
|
182
|
-
const terminal = previewTerminal(viewerUrl, options);
|
|
182
|
+
const terminal = previewTerminal(viewerUrl, options, pluginViewLinks(viewerUrl, options.root));
|
|
183
183
|
if (options.open)
|
|
184
184
|
openPreview(viewerUrl);
|
|
185
185
|
try {
|
|
@@ -245,7 +245,7 @@ export async function hotViewer(options, restarting = false) {
|
|
|
245
245
|
},
|
|
246
246
|
stdio: ['ignore', 'inherit', 'inherit'],
|
|
247
247
|
});
|
|
248
|
-
const terminal = previewTerminal(viewerUrl, options);
|
|
248
|
+
const terminal = previewTerminal(viewerUrl, options, pluginViewLinks(viewerUrl, options.root));
|
|
249
249
|
void terminal.closed.then(() => {
|
|
250
250
|
if (child.exitCode === null && child.signalCode === null)
|
|
251
251
|
child.kill('SIGTERM');
|
package/dist/site/terminal.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
export { openPreview } from "kennel-preview/terminal";
|
|
2
|
+
/** Extra list entries rendered as hyperlinked plugin view links. */
|
|
3
|
+
export type PluginView = {
|
|
4
|
+
id: string;
|
|
5
|
+
url: string;
|
|
6
|
+
};
|
|
2
7
|
export declare function previewTerminal(url: string, target: {
|
|
3
8
|
root: string;
|
|
4
9
|
target?: string;
|
|
5
|
-
}): {
|
|
10
|
+
}, views: PluginView[]): {
|
|
6
11
|
close: () => void;
|
|
7
12
|
closed: Promise<void>;
|
|
8
13
|
interactive: boolean;
|
package/dist/site/terminal.js
CHANGED
|
@@ -31,7 +31,7 @@ function repositoryName(folder) {
|
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
function previewTerminal(url, target, startShare) {
|
|
34
|
+
function previewTerminal(url, target, startShare, views) {
|
|
35
35
|
url = new URL(url).href;
|
|
36
36
|
const input = process.stdin;
|
|
37
37
|
const output = process.stdout;
|
|
@@ -42,8 +42,9 @@ function previewTerminal(url, target, startShare) {
|
|
|
42
42
|
const repository = interactive ? repositoryName(target.root) : undefined;
|
|
43
43
|
const color = terminal && process.env.NO_COLOR === undefined && output.hasColors();
|
|
44
44
|
const accent = (text) => color ? `\x1B[32m${text}\x1B[39m` : text;
|
|
45
|
-
const
|
|
46
|
-
|
|
45
|
+
const linkTo = (label, href, plain = `${label}
|
|
46
|
+
${href}`) => terminal ? `\x1B]8;;${href}\x07${accent(label)}\x1B]8;;\x07` : plain;
|
|
47
|
+
const link = (label) => linkTo(label, url);
|
|
47
48
|
const wasRaw = Boolean(input.isRaw);
|
|
48
49
|
const wasFlowing = input.readableFlowing === true;
|
|
49
50
|
let resolveClosed;
|
|
@@ -60,27 +61,28 @@ function previewTerminal(url, target, startShare) {
|
|
|
60
61
|
return;
|
|
61
62
|
cursorTo(output, 0, 0);
|
|
62
63
|
clearScreenDown(output);
|
|
63
|
-
const width = Math.max(1, (output.columns || 80) -
|
|
64
|
+
const width = Math.max(1, (output.columns || 80) - 1);
|
|
64
65
|
const fit = (text) => {
|
|
65
66
|
const characters = Array.from(terminalText(text));
|
|
66
67
|
return characters.length > width ? characters.slice(0, width - 1).join("") + "…" : characters.join("");
|
|
67
68
|
};
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
const choices = actions.slice(start, start + count).map((item, index) => {
|
|
69
|
+
const choices = [];
|
|
70
|
+
actions.forEach((item, index) => {
|
|
71
71
|
const label = fit(item.shortcut ? `[${item.shortcut}] ${item.label}` : item.label);
|
|
72
|
-
|
|
72
|
+
let text = index === selected ? accent(`› ${label}`) : ` ${label}`;
|
|
73
|
+
if (item.view !== undefined)
|
|
74
|
+
text = linkTo(text, item.view, accent(text));
|
|
75
|
+
choices.push(text);
|
|
73
76
|
});
|
|
74
77
|
output.write([
|
|
75
|
-
|
|
76
|
-
...repository ? [
|
|
77
|
-
...target.target ? [`
|
|
78
|
+
`\x1B[1m${link(fit(title))}\x1B[22m`,
|
|
79
|
+
...repository ? [`${color ? "\x1B[2m" : ""}${fit(repository)}${color ? "\x1B[22m" : ""}`] : [],
|
|
80
|
+
...target.target ? [`File: ${fit(name)}`] : [],
|
|
78
81
|
"",
|
|
79
82
|
...choices,
|
|
80
83
|
"",
|
|
81
|
-
"
|
|
82
|
-
...
|
|
83
|
-
...status ? ["", ` ${status}`] : [],
|
|
84
|
+
"↑ ↓ to move · Enter to choose · Esc or Ctrl+C to exit",
|
|
85
|
+
...status ? ["", status] : [],
|
|
84
86
|
""
|
|
85
87
|
].join(`
|
|
86
88
|
`));
|
|
@@ -145,6 +147,10 @@ function previewTerminal(url, target, startShare) {
|
|
|
145
147
|
resolveClosed();
|
|
146
148
|
};
|
|
147
149
|
const actions = [
|
|
150
|
+
...(views ?? []).map((view) => ({ label: view.id, run: () => {
|
|
151
|
+
openPreview(view.url);
|
|
152
|
+
status = "Browser open requested.";
|
|
153
|
+
}, view: view.url })),
|
|
148
154
|
{ label: "Open in browser", shortcut: "o", run: () => {
|
|
149
155
|
openPreview(url);
|
|
150
156
|
status = "Browser open requested.";
|
|
@@ -204,17 +210,17 @@ function previewTerminal(url, target, startShare) {
|
|
|
204
210
|
render();
|
|
205
211
|
else
|
|
206
212
|
console.log(`
|
|
207
|
-
|
|
213
|
+
${link(name)}
|
|
208
214
|
`);
|
|
209
215
|
return { close, closed, interactive };
|
|
210
216
|
}
|
|
211
217
|
|
|
212
218
|
// packages/cli/src/site/terminal.ts
|
|
213
|
-
function previewTerminal2(url, target) {
|
|
219
|
+
function previewTerminal2(url, target, views) {
|
|
214
220
|
return previewTerminal(url, target, async (password = false) => {
|
|
215
221
|
const { startPreviewShare } = await import(new URL("../package/preview.js", import.meta.url).href);
|
|
216
222
|
return startPreviewShare({ ...target, ...password ? { password: true } : {} });
|
|
217
|
-
});
|
|
223
|
+
}, views);
|
|
218
224
|
}
|
|
219
225
|
export {
|
|
220
226
|
openPreview,
|
package/dist/site/viewer.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { type PluginView } from "./terminal.js";
|
|
2
|
+
/** Installed-plugin view links for the terminal menu; each is hyperlinked. */
|
|
3
|
+
export declare function pluginViewLinks(viewerUrl: string, root: string): PluginView[];
|
|
1
4
|
export type ViewerOptions = {
|
|
2
5
|
dryRun: boolean;
|
|
3
6
|
host: string;
|
package/dist/site/viewer.js
CHANGED
|
@@ -2,8 +2,25 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, realpathSync, statSync } from "node:fs";
|
|
3
3
|
import { createServer } from "node:net";
|
|
4
4
|
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
5
|
+
import { readExternalPlugins } from "../plugins/external.js";
|
|
5
6
|
import { previewKind } from "./kinds.js";
|
|
6
7
|
import { openPreview, previewTerminal } from "./terminal.js";
|
|
8
|
+
/** Installed-plugin view links for the terminal menu; each is hyperlinked. */
|
|
9
|
+
export function pluginViewLinks(viewerUrl, root) {
|
|
10
|
+
return readExternalPlugins().flatMap((plugin) => {
|
|
11
|
+
const view = (subpath) => {
|
|
12
|
+
const url = new URL(viewerUrl);
|
|
13
|
+
url.pathname = `/folder/${encodeBase64Url(root)}/views/${encodeURIComponent(plugin.id)}${subpath ? `/${encodeURIComponent(subpath)}` : ""}`;
|
|
14
|
+
// A query `folder` makes the route parser ignore the path and select Notes.
|
|
15
|
+
url.searchParams.delete("folder");
|
|
16
|
+
return url.href;
|
|
17
|
+
};
|
|
18
|
+
const views = [{ id: plugin.id, url: view() }];
|
|
19
|
+
for (const item of plugin.views ?? [])
|
|
20
|
+
views.push({ id: `${plugin.id} · ${item.label}`, url: view(item.subpath) });
|
|
21
|
+
return views;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
7
24
|
export function viewerFolder(root, value) {
|
|
8
25
|
return resolve(value ?? root);
|
|
9
26
|
}
|
|
@@ -312,7 +329,7 @@ export async function viewer(input, plugin) {
|
|
|
312
329
|
existing.kernelKey === plugins.key &&
|
|
313
330
|
isSameOrDescendant(existing.root, options.root)))) {
|
|
314
331
|
const viewerUrl = popUrl(displayHost, options.port, options);
|
|
315
|
-
const terminal = previewTerminal(viewerUrl, options);
|
|
332
|
+
const terminal = previewTerminal(viewerUrl, options, pluginViewLinks(viewerUrl, options.root));
|
|
316
333
|
if (options.open)
|
|
317
334
|
openPreview(viewerUrl);
|
|
318
335
|
try {
|
|
@@ -342,7 +359,7 @@ export async function viewer(input, plugin) {
|
|
|
342
359
|
execFileSync("tailscale", ["serve", "--bg", "--yes", options.port]);
|
|
343
360
|
}
|
|
344
361
|
const viewerUrl = popUrl(displayHost, options.port, options);
|
|
345
|
-
const terminal = previewTerminal(viewerUrl, options);
|
|
362
|
+
const terminal = previewTerminal(viewerUrl, options, pluginViewLinks(viewerUrl, options.root));
|
|
346
363
|
if (options.open)
|
|
347
364
|
openPreview(viewerUrl);
|
|
348
365
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kennel",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.43",
|
|
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",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"effect": "^3.19.7",
|
|
70
70
|
"esbuild": "^0.27.0",
|
|
71
71
|
"kennel-mcp": "https://registry.npmjs.org/%40raggle-ai%2fkennel-mcp/-/kennel-mcp-0.1.8.tgz",
|
|
72
|
-
"kennel-plugins": "0.1.
|
|
72
|
+
"kennel-plugins": "0.1.21",
|
|
73
73
|
"koffi": "^3.3.1",
|
|
74
74
|
"lucide-react": "^0.562.0",
|
|
75
75
|
"radix-ui": "^1.6.7",
|
|
@@ -100,6 +100,20 @@
|
|
|
100
100
|
"pattern": "^(?!\\.{1,2}$)[^/\\\\]+$"
|
|
101
101
|
},
|
|
102
102
|
"skill": { "$ref": "#/$defs/pluginSkill" },
|
|
103
|
+
"views": {
|
|
104
|
+
"description": "Undirected views declared by the plugin manifest, recorded at install time.",
|
|
105
|
+
"type": "array",
|
|
106
|
+
"uniqueItems": true,
|
|
107
|
+
"items": {
|
|
108
|
+
"type": "object",
|
|
109
|
+
"additionalProperties": false,
|
|
110
|
+
"required": ["subpath", "label"],
|
|
111
|
+
"properties": {
|
|
112
|
+
"subpath": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
|
|
113
|
+
"label": { "type": "string", "minLength": 1 }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
},
|
|
103
117
|
"entries": {
|
|
104
118
|
"description": "Resolved absolute convention entry files. api is optional.",
|
|
105
119
|
"oneOf": [
|
|
@@ -110,7 +124,7 @@
|
|
|
110
124
|
}
|
|
111
125
|
},
|
|
112
126
|
"pluginSkill": {
|
|
113
|
-
"description": "Agent skill declared by
|
|
127
|
+
"description": "Agent skill declared by kennel.json > plugin.skill. Managed by `kennel plugin add` and `kennel plugin skill install`.",
|
|
114
128
|
"type": "object",
|
|
115
129
|
"additionalProperties": false,
|
|
116
130
|
"required": ["entry", "name"],
|
|
@@ -141,22 +155,22 @@
|
|
|
141
155
|
"entriesWithoutApi": {
|
|
142
156
|
"type": "object",
|
|
143
157
|
"additionalProperties": false,
|
|
144
|
-
"required": ["
|
|
158
|
+
"required": ["index", "server", "definition"],
|
|
145
159
|
"properties": {
|
|
146
|
-
"
|
|
160
|
+
"index": { "$ref": "#/$defs/absolutePath" },
|
|
147
161
|
"server": { "$ref": "#/$defs/absolutePath" },
|
|
148
|
-
"
|
|
162
|
+
"definition": { "$ref": "#/$defs/absolutePath" },
|
|
149
163
|
"css": { "$ref": "#/$defs/absolutePath" }
|
|
150
164
|
}
|
|
151
165
|
},
|
|
152
166
|
"entriesWithApi": {
|
|
153
167
|
"type": "object",
|
|
154
168
|
"additionalProperties": false,
|
|
155
|
-
"required": ["
|
|
169
|
+
"required": ["index", "server", "definition", "api"],
|
|
156
170
|
"properties": {
|
|
157
|
-
"
|
|
171
|
+
"index": { "$ref": "#/$defs/absolutePath" },
|
|
158
172
|
"server": { "$ref": "#/$defs/absolutePath" },
|
|
159
|
-
"
|
|
173
|
+
"definition": { "$ref": "#/$defs/absolutePath" },
|
|
160
174
|
"api": { "$ref": "#/$defs/absolutePath" },
|
|
161
175
|
"css": { "$ref": "#/$defs/absolutePath" }
|
|
162
176
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PluginPage } from "./page.tsx";
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
import manifest from "./manifest";
|
|
3
|
+
import type { FolderPluginServerHooks } from "kennel-plugins/server";
|
|
6
4
|
|
|
7
5
|
// Example content file read by the `llm` hook below: a JSON array of items
|
|
8
6
|
// with a string `title`, for example [{ "title": "First item" }].
|
|
@@ -25,12 +23,11 @@ async function readItems(folder: string) {
|
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
export default {
|
|
28
|
-
...manifest,
|
|
29
26
|
async detect() {
|
|
30
27
|
return { status: "available" };
|
|
31
28
|
},
|
|
32
29
|
async handle() {
|
|
33
|
-
return { data: { items: [] } };
|
|
30
|
+
return { kind: "available", data: { items: [] } };
|
|
34
31
|
},
|
|
35
32
|
// Optional `knl llm` context. Read only: no writes, network calls, or other
|
|
36
33
|
// side effects. Return [] when the folder has no content for this plugin.
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { FolderPluginClient } from "kennel-plugins/client";
|
|
2
|
-
|
|
3
|
-
import manifest from "./manifest";
|
|
4
|
-
|
|
5
|
-
function ExamplePluginView() {
|
|
6
|
-
return <div>{manifest.label} plugin</div>;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export default {
|
|
10
|
-
...manifest,
|
|
11
|
-
component: ExamplePluginView,
|
|
12
|
-
} satisfies FolderPluginClient;
|