kennel 0.2.7 → 0.2.9

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/README.md CHANGED
@@ -96,6 +96,28 @@ package metadata, not from URL text. The transcripts plugin contract is package
96
96
  When this file is absent, entity search allows only the workspace opened by
97
97
  `knl`. Invalid config fails closed.
98
98
 
99
+ The config format is published as a JSON Schema (draft 2020-12):
100
+
101
+ - URL: `https://raw.githubusercontent.com/raggle-ai/kennel/main/cli/schemas/config.schema.json`
102
+ - npm: `kennel/config.schema.json` (file `schemas/config.schema.json` in the package)
103
+
104
+ Add `$schema` to the file for editor completion and validation:
105
+
106
+ ```json
107
+ {
108
+ "$schema": "https://raw.githubusercontent.com/raggle-ai/kennel/main/cli/schemas/config.schema.json",
109
+ "allowedRoots": ["/path/to/workspace"]
110
+ }
111
+ ```
112
+
113
+ The CLI validates the file against this schema each time it reads the config
114
+ and stops with the failing field paths when a value is malformed. Unknown keys
115
+ are rejected. `externalPlugins` records are resolved and written by
116
+ `kennel plugin add` (pinned commit, absolute entry files, export symbols); do
117
+ not write or edit them by hand. To change one, remove the record and add the
118
+ plugin again. The schema checks only the record shape: the install-time checks
119
+ for the cache path, package exports, and imports stay in the CLI.
120
+
99
121
  Suggested convention for agents: write generated HTML components to
100
122
  `.kennel/preview/<name>.html` and pop them with `knl pop
101
123
  .kennel/preview/<name>.html --watch` (reuses 5180), so preview items share one
@@ -13,6 +13,7 @@ import { homedir, tmpdir } from "node:os";
13
13
  import { basename, dirname, isAbsolute, join, relative, resolve, } from "node:path";
14
14
  import { pathToFileURL, fileURLToPath } from "node:url";
15
15
  import { spawnSync } from "node:child_process";
16
+ import { z } from "zod";
16
17
  const CONFIG_FILE = join("kennel", "config.json");
17
18
  const VALID_ID = /^[a-z][a-z0-9-]*$/u;
18
19
  const CORE_IDS = new Set(["agents", "notes"]);
@@ -57,10 +58,51 @@ function run(command, args, options = {}) {
57
58
  }
58
59
  return result.stdout.trim();
59
60
  }
61
+ let configSchema;
62
+ function userConfigSchema() {
63
+ if (!configSchema) {
64
+ const json = JSON.parse(readFileSync(join(cliRoot(), "schemas", "config.schema.json"), "utf8"));
65
+ configSchema = { id: json.$id, schema: z.fromJSONSchema(json) };
66
+ }
67
+ return configSchema;
68
+ }
69
+ function assertUserConfig(value, path) {
70
+ const { id, schema } = userConfigSchema();
71
+ const result = schema.safeParse(value);
72
+ if (result.success)
73
+ return;
74
+ throw new Error(`Invalid Kennel config at ${path} (schema ${id}):\n${describeIssues(result.error.issues).join("\n")}\nexternalPlugins records are managed by \`kennel plugin add\`; remove a malformed record and add the plugin again.`);
75
+ }
76
+ function issuePath(path) {
77
+ return path.reduce((text, key) => typeof key === "number" ? `${text}[${key}]` : text ? `${text}.${String(key)}` : String(key), "") || "(root)";
78
+ }
79
+ function describeIssues(issues, base = [], indent = " ") {
80
+ return issues.flatMap((issue) => {
81
+ const path = [...base, ...issue.path];
82
+ if (issue.code !== "invalid_union" || issue.errors.length === 0)
83
+ return [`${indent}${issuePath(path)}: ${issue.message}`];
84
+ return [
85
+ `${indent}${issuePath(path)}: matches none of the allowed shapes`,
86
+ ...issue.errors.flatMap((branch, index) => [
87
+ `${indent} shape ${index + 1}:`,
88
+ ...describeIssues(branch, path, `${indent} `),
89
+ ]),
90
+ ];
91
+ });
92
+ }
60
93
  function readJson(path) {
61
94
  if (!existsSync(path))
62
95
  return {};
63
- return JSON.parse(readFileSync(path, "utf8"));
96
+ let value;
97
+ try {
98
+ value = JSON.parse(readFileSync(path, "utf8"));
99
+ }
100
+ catch (error) {
101
+ const message = error instanceof Error ? error.message : String(error);
102
+ throw new Error(`Invalid Kennel config at ${path}: ${message}`);
103
+ }
104
+ assertUserConfig(value, path);
105
+ return value;
64
106
  }
65
107
  function writeJsonAtomic(path, value) {
66
108
  mkdirSync(dirname(path), { recursive: true });
@@ -350,19 +392,20 @@ export async function validateExternalPlugin(inputUrl, existing = [], options =
350
392
  return entry;
351
393
  }
352
394
  export function readExternalPlugins() {
353
- const config = readJson(kennelConfigPath());
354
- return Array.isArray(config.externalPlugins) ? config.externalPlugins : [];
395
+ return readJson(kennelConfigPath()).externalPlugins ?? [];
355
396
  }
356
397
  export async function installExternalPlugin(url, options = {}) {
357
398
  const path = kennelConfigPath();
358
399
  const config = readJson(path);
359
- const externalPlugins = Array.isArray(config.externalPlugins) ? config.externalPlugins : [];
400
+ const externalPlugins = config.externalPlugins ?? [];
360
401
  let entry;
361
402
  try {
362
403
  entry = await validateExternalPlugin(url, externalPlugins, options);
363
404
  const nextPlugins = [...externalPlugins, entry];
405
+ const nextConfig = { ...config, externalPlugins: nextPlugins };
406
+ assertUserConfig(nextConfig, path);
364
407
  await ensureKernel(nextPlugins);
365
- writeJsonAtomic(path, { ...config, externalPlugins: nextPlugins });
408
+ writeJsonAtomic(path, nextConfig);
366
409
  }
367
410
  catch (error) {
368
411
  const message = error instanceof Error ? error.message : String(error);
@@ -13,6 +13,7 @@ import { homedir, tmpdir } from "node:os";
13
13
  import { basename, dirname, isAbsolute, join, relative, resolve, } from "node:path";
14
14
  import { pathToFileURL, fileURLToPath } from "node:url";
15
15
  import { spawnSync } from "node:child_process";
16
+ import { z } from "zod";
16
17
  const CONFIG_FILE = join("kennel", "config.json");
17
18
  const VALID_ID = /^[a-z][a-z0-9-]*$/u;
18
19
  const CORE_IDS = new Set(["agents", "notes"]);
@@ -57,10 +58,51 @@ function run(command, args, options = {}) {
57
58
  }
58
59
  return result.stdout.trim();
59
60
  }
61
+ let configSchema;
62
+ function userConfigSchema() {
63
+ if (!configSchema) {
64
+ const json = JSON.parse(readFileSync(join(cliRoot(), "schemas", "config.schema.json"), "utf8"));
65
+ configSchema = { id: json.$id, schema: z.fromJSONSchema(json) };
66
+ }
67
+ return configSchema;
68
+ }
69
+ function assertUserConfig(value, path) {
70
+ const { id, schema } = userConfigSchema();
71
+ const result = schema.safeParse(value);
72
+ if (result.success)
73
+ return;
74
+ throw new Error(`Invalid Kennel config at ${path} (schema ${id}):\n${describeIssues(result.error.issues).join("\n")}\nexternalPlugins records are managed by \`kennel plugin add\`; remove a malformed record and add the plugin again.`);
75
+ }
76
+ function issuePath(path) {
77
+ return path.reduce((text, key) => typeof key === "number" ? `${text}[${key}]` : text ? `${text}.${String(key)}` : String(key), "") || "(root)";
78
+ }
79
+ function describeIssues(issues, base = [], indent = " ") {
80
+ return issues.flatMap((issue) => {
81
+ const path = [...base, ...issue.path];
82
+ if (issue.code !== "invalid_union" || issue.errors.length === 0)
83
+ return [`${indent}${issuePath(path)}: ${issue.message}`];
84
+ return [
85
+ `${indent}${issuePath(path)}: matches none of the allowed shapes`,
86
+ ...issue.errors.flatMap((branch, index) => [
87
+ `${indent} shape ${index + 1}:`,
88
+ ...describeIssues(branch, path, `${indent} `),
89
+ ]),
90
+ ];
91
+ });
92
+ }
60
93
  function readJson(path) {
61
94
  if (!existsSync(path))
62
95
  return {};
63
- return JSON.parse(readFileSync(path, "utf8"));
96
+ let value;
97
+ try {
98
+ value = JSON.parse(readFileSync(path, "utf8"));
99
+ }
100
+ catch (error) {
101
+ const message = error instanceof Error ? error.message : String(error);
102
+ throw new Error(`Invalid Kennel config at ${path}: ${message}`);
103
+ }
104
+ assertUserConfig(value, path);
105
+ return value;
64
106
  }
65
107
  function writeJsonAtomic(path, value) {
66
108
  mkdirSync(dirname(path), { recursive: true });
@@ -350,19 +392,20 @@ export async function validateExternalPlugin(inputUrl, existing = [], options =
350
392
  return entry;
351
393
  }
352
394
  export function readExternalPlugins() {
353
- const config = readJson(kennelConfigPath());
354
- return Array.isArray(config.externalPlugins) ? config.externalPlugins : [];
395
+ return readJson(kennelConfigPath()).externalPlugins ?? [];
355
396
  }
356
397
  export async function installExternalPlugin(url, options = {}) {
357
398
  const path = kennelConfigPath();
358
399
  const config = readJson(path);
359
- const externalPlugins = Array.isArray(config.externalPlugins) ? config.externalPlugins : [];
400
+ const externalPlugins = config.externalPlugins ?? [];
360
401
  let entry;
361
402
  try {
362
403
  entry = await validateExternalPlugin(url, externalPlugins, options);
363
404
  const nextPlugins = [...externalPlugins, entry];
405
+ const nextConfig = { ...config, externalPlugins: nextPlugins };
406
+ assertUserConfig(nextConfig, path);
364
407
  await ensureKernel(nextPlugins);
365
- writeJsonAtomic(path, { ...config, externalPlugins: nextPlugins });
408
+ writeJsonAtomic(path, nextConfig);
366
409
  }
367
410
  catch (error) {
368
411
  const message = error instanceof Error ? error.message : String(error);
@@ -1 +1,159 @@
1
- export * from "@raggle-ai/kennel-preview/kinds";
1
+ // packages/kinds/src/kinds.ts
2
+ var previewTextKinds = new Set([
3
+ "code",
4
+ "eml",
5
+ "html",
6
+ "markdown",
7
+ "table",
8
+ "text"
9
+ ]);
10
+ function previewTextKind(kind) {
11
+ return previewTextKinds.has(kind);
12
+ }
13
+ var codeExtensions = new Set([
14
+ ".astro",
15
+ ".bash",
16
+ ".c",
17
+ ".cc",
18
+ ".cpp",
19
+ ".cs",
20
+ ".css",
21
+ ".dart",
22
+ ".ex",
23
+ ".exs",
24
+ ".go",
25
+ ".graphql",
26
+ ".h",
27
+ ".hpp",
28
+ ".hs",
29
+ ".java",
30
+ ".js",
31
+ ".jsx",
32
+ ".json",
33
+ ".kt",
34
+ ".less",
35
+ ".lua",
36
+ ".ml",
37
+ ".mjs",
38
+ ".php",
39
+ ".proto",
40
+ ".py",
41
+ ".rb",
42
+ ".rs",
43
+ ".scss",
44
+ ".sh",
45
+ ".sql",
46
+ ".svelte",
47
+ ".swift",
48
+ ".toml",
49
+ ".ts",
50
+ ".tsx",
51
+ ".vue",
52
+ ".xml",
53
+ ".yaml",
54
+ ".yml",
55
+ ".zsh"
56
+ ]);
57
+ var imageExtensions = new Set([
58
+ ".avif",
59
+ ".bmp",
60
+ ".gif",
61
+ ".ico",
62
+ ".jpeg",
63
+ ".jpg",
64
+ ".png",
65
+ ".svg",
66
+ ".webp"
67
+ ]);
68
+ var knownExtensions = new Set([
69
+ ...codeExtensions,
70
+ ...imageExtensions,
71
+ ".csv",
72
+ ".eml",
73
+ ".htm",
74
+ ".html",
75
+ ".markdown",
76
+ ".md",
77
+ ".mdx",
78
+ ".pdf",
79
+ ".text",
80
+ ".tsv",
81
+ ".txt"
82
+ ]);
83
+ function previewKind(path) {
84
+ const extension = path.slice(path.lastIndexOf(".")).toLowerCase();
85
+ if (extension === ".htm" || extension === ".html")
86
+ return "html";
87
+ if (extension === ".md" || extension === ".markdown" || extension === ".mdx")
88
+ return "markdown";
89
+ if (extension === ".text" || extension === ".txt")
90
+ return "text";
91
+ if (extension === ".eml")
92
+ return "eml";
93
+ if (extension === ".csv" || extension === ".tsv")
94
+ return "table";
95
+ if (imageExtensions.has(extension))
96
+ return "image";
97
+ if (codeExtensions.has(extension))
98
+ return "code";
99
+ if (knownExtensions.has(extension))
100
+ return "binary";
101
+ return;
102
+ }
103
+ var rawMime = {
104
+ ".avif": "image/avif",
105
+ ".bmp": "image/bmp",
106
+ ".gif": "image/gif",
107
+ ".htm": "text/html; charset=utf-8",
108
+ ".html": "text/html; charset=utf-8",
109
+ ".ico": "image/x-icon",
110
+ ".jpeg": "image/jpeg",
111
+ ".jpg": "image/jpeg",
112
+ ".pdf": "application/pdf",
113
+ ".png": "image/png",
114
+ ".svg": "image/svg+xml",
115
+ ".webp": "image/webp"
116
+ };
117
+ function previewRawMime(path) {
118
+ const extension = path.slice(path.lastIndexOf(".")).toLowerCase();
119
+ const mime = rawMime[extension];
120
+ if (mime)
121
+ return mime;
122
+ if (previewKind(path) === "code" || previewKind(path) === "table")
123
+ return "text/plain; charset=utf-8";
124
+ return;
125
+ }
126
+ var mediaMime = {
127
+ ".aac": "audio/aac",
128
+ ".avif": "image/avif",
129
+ ".bmp": "image/bmp",
130
+ ".flac": "audio/flac",
131
+ ".gif": "image/gif",
132
+ ".ico": "image/x-icon",
133
+ ".jpeg": "image/jpeg",
134
+ ".jpg": "image/jpeg",
135
+ ".m4a": "audio/mp4",
136
+ ".m4v": "video/mp4",
137
+ ".mov": "video/quicktime",
138
+ ".mp3": "audio/mpeg",
139
+ ".mp4": "video/mp4",
140
+ ".oga": "audio/ogg",
141
+ ".ogg": "audio/ogg",
142
+ ".ogv": "video/ogg",
143
+ ".opus": "audio/opus",
144
+ ".png": "image/png",
145
+ ".wav": "audio/wav",
146
+ ".webm": "video/webm",
147
+ ".webp": "image/webp"
148
+ };
149
+ function mediaRawMime(path) {
150
+ const extension = path.slice(path.lastIndexOf(".")).toLowerCase();
151
+ return mediaMime[extension];
152
+ }
153
+ export {
154
+ mediaRawMime,
155
+ previewKind,
156
+ previewRawMime,
157
+ previewTextKind,
158
+ previewTextKinds
159
+ };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "kennel-plugins",
4
- "version": "0.1.1",
4
+ "version": "0.1.2",
5
5
  "description": "Small shared contract for trusted folder view plugins in the Kennel web viewer.",
6
6
  "type": "module",
7
7
  "license": "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kennel",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
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",
@@ -32,7 +32,8 @@
32
32
  "./automations/notifications": {
33
33
  "types": "./dist/automations/notifications.d.ts",
34
34
  "import": "./dist/automations/notifications.js"
35
- }
35
+ },
36
+ "./config.schema.json": "./schemas/config.schema.json"
36
37
  },
37
38
  "publishConfig": {
38
39
  "registry": "https://registry.npmjs.org",
@@ -41,6 +42,7 @@
41
42
  "files": [
42
43
  "bin",
43
44
  "dist",
45
+ "schemas",
44
46
  "templates",
45
47
  "web",
46
48
  "kernel"
@@ -69,9 +71,7 @@
69
71
  "@raggle-ai/barkdown": "0.6.1",
70
72
  "@raggle-ai/kennel-knowledge": "^0.1.0",
71
73
  "@raggle-ai/kennel-mcp": "^0.1.8",
72
- "@raggle-ai/kennel-preview": "^0.1.0",
73
74
  "@raggle-ai/local": "0.14.0",
74
- "@raggle-ai/markdown": "^0.1.0",
75
75
  "@raggle-ai/scout-ui": "0.1.3",
76
76
  "@tailwindcss/vite": "4.3.3",
77
77
  "@tanstack/react-query": "^5.66.5",
@@ -115,7 +115,8 @@
115
115
  "zod": "^4.3.6"
116
116
  },
117
117
  "devDependencies": {
118
- "kennel-plugins": "0.1.1",
118
+ "@raggle-ai/kennel-preview": "0.1.0",
119
+ "kennel-plugins": "0.1.2",
119
120
  "@types/node": "^22.13.4",
120
121
  "typescript": "^5.9.3"
121
122
  },
@@ -0,0 +1,114 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/raggle-ai/kennel/main/cli/schemas/config.schema.json",
4
+ "title": "Kennel user config",
5
+ "description": "Kennel user config at $XDG_CONFIG_HOME/kennel/config.json (default ~/.config/kennel/config.json). externalPlugins records are written by `kennel plugin add`; do not edit them by hand.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "$schema": {
10
+ "description": "JSON Schema reference for editor validation.",
11
+ "type": "string",
12
+ "minLength": 1
13
+ },
14
+ "allowedRoots": {
15
+ "description": "Absolute local folders that entity search may read.",
16
+ "type": "array",
17
+ "items": { "$ref": "#/$defs/absolutePath" }
18
+ },
19
+ "externalPlugins": {
20
+ "description": "Resolved external folder plugins. Managed by `kennel plugin add`.",
21
+ "type": "array",
22
+ "items": { "$ref": "#/$defs/externalPlugin" }
23
+ }
24
+ },
25
+ "$defs": {
26
+ "absolutePath": {
27
+ "type": "string",
28
+ "description": "Absolute path: POSIX, Windows drive, or Windows UNC.",
29
+ "pattern": "^(/|[A-Za-z]:[\\\\/]|\\\\\\\\)"
30
+ },
31
+ "symbol": {
32
+ "description": "Export name in the bundled module, or \"default\".",
33
+ "type": "string",
34
+ "minLength": 1
35
+ },
36
+ "externalPlugin": {
37
+ "type": "object",
38
+ "additionalProperties": false,
39
+ "required": ["url", "commit", "packageName", "id", "entries"],
40
+ "properties": {
41
+ "url": {
42
+ "description": "Git repository URL, without the #ref fragment.",
43
+ "type": "string",
44
+ "minLength": 1
45
+ },
46
+ "requestedRef": {
47
+ "description": "Ref from the #ref fragment given at install time.",
48
+ "type": "string",
49
+ "minLength": 1
50
+ },
51
+ "commit": {
52
+ "description": "Full commit SHA the plugin is pinned to.",
53
+ "type": "string",
54
+ "pattern": "^[0-9a-f]{40}$"
55
+ },
56
+ "packageName": {
57
+ "type": "string",
58
+ "minLength": 1
59
+ },
60
+ "id": {
61
+ "type": "string",
62
+ "pattern": "^[a-z][a-z0-9-]*$"
63
+ },
64
+ "directory": {
65
+ "description": "Plugin package root relative to the repository root.",
66
+ "type": "string",
67
+ "minLength": 1
68
+ },
69
+ "trigger": {
70
+ "description": "File name that activates the plugin in a folder.",
71
+ "type": "string",
72
+ "pattern": "^(?!\\.{1,2}$)[^/\\\\]+$"
73
+ },
74
+ "entries": {
75
+ "description": "Resolved absolute entry files and export symbols. api and apiSymbol appear together or not at all.",
76
+ "oneOf": [
77
+ { "$ref": "#/$defs/entriesWithoutApi" },
78
+ { "$ref": "#/$defs/entriesWithApi" }
79
+ ]
80
+ }
81
+ }
82
+ },
83
+ "entriesWithoutApi": {
84
+ "type": "object",
85
+ "additionalProperties": false,
86
+ "required": ["client", "clientSymbol", "server", "serverSymbol", "manifest", "manifestSymbol"],
87
+ "properties": {
88
+ "client": { "$ref": "#/$defs/absolutePath" },
89
+ "clientSymbol": { "$ref": "#/$defs/symbol" },
90
+ "server": { "$ref": "#/$defs/absolutePath" },
91
+ "serverSymbol": { "$ref": "#/$defs/symbol" },
92
+ "manifest": { "$ref": "#/$defs/absolutePath" },
93
+ "manifestSymbol": { "$ref": "#/$defs/symbol" },
94
+ "css": { "$ref": "#/$defs/absolutePath" }
95
+ }
96
+ },
97
+ "entriesWithApi": {
98
+ "type": "object",
99
+ "additionalProperties": false,
100
+ "required": ["client", "clientSymbol", "server", "serverSymbol", "manifest", "manifestSymbol", "api", "apiSymbol"],
101
+ "properties": {
102
+ "client": { "$ref": "#/$defs/absolutePath" },
103
+ "clientSymbol": { "$ref": "#/$defs/symbol" },
104
+ "server": { "$ref": "#/$defs/absolutePath" },
105
+ "serverSymbol": { "$ref": "#/$defs/symbol" },
106
+ "manifest": { "$ref": "#/$defs/absolutePath" },
107
+ "manifestSymbol": { "$ref": "#/$defs/symbol" },
108
+ "api": { "$ref": "#/$defs/absolutePath" },
109
+ "apiSymbol": { "$ref": "#/$defs/symbol" },
110
+ "css": { "$ref": "#/$defs/absolutePath" }
111
+ }
112
+ }
113
+ }
114
+ }