kennel 0.2.5 → 0.2.7

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.
Files changed (163) hide show
  1. package/README.md +9 -6
  2. package/dist/cli/src/plugins/external.d.ts +3 -0
  3. package/dist/cli/src/plugins/external.js +24 -12
  4. package/dist/cli/src/site/hot-viewer.js +45 -38
  5. package/dist/cli/src/site/server-plugins.d.ts +2 -2
  6. package/dist/cli/src/site/viewer.js +13 -8
  7. package/dist/core/api-client.js +576 -10317
  8. package/dist/core/core-folders.js +2313 -12088
  9. package/dist/core/runtime.js +7899 -17525
  10. package/dist/package/preview.js +9838 -19692
  11. package/dist/plugins/external.d.ts +3 -0
  12. package/dist/plugins/external.js +24 -12
  13. package/dist/site/hot-viewer.js +45 -38
  14. package/dist/site/server-plugins.d.ts +2 -2
  15. package/dist/site/server-plugins.js +9 -9
  16. package/dist/site/viewer.js +13 -8
  17. package/kernel/apps/web/package.json +1 -1
  18. package/kernel/apps/web/scripts/dev.mjs +4 -0
  19. package/kernel/apps/web/src/api/events.ts +60 -24
  20. package/kernel/apps/web/src/external-plugins.ts +1 -1
  21. package/kernel/apps/web/src/folder-filter-icons.tsx +1 -1
  22. package/kernel/apps/web/src/folder-plugin-view.tsx +1 -1
  23. package/kernel/apps/web/src/plugins.ts +1 -1
  24. package/kernel/apps/web/vite.config.ts +3 -1
  25. package/kernel/packages/agents/package.json +1 -1
  26. package/kernel/packages/agents/src/agent-card.tsx +1 -1
  27. package/kernel/packages/agents/src/client.tsx +1 -1
  28. package/kernel/packages/agents/src/manifest.ts +1 -1
  29. package/kernel/packages/agents/src/server.ts +1 -1
  30. package/kernel/packages/agents/src/viewer.tsx +1 -1
  31. package/kernel/packages/api/package.json +1 -1
  32. package/kernel/packages/api/src/server/acp-live-plugin.ts +1 -1
  33. package/kernel/packages/api/src/server/api/create-agent-file.ts +2 -2
  34. package/kernel/packages/api/src/server/api/document-edit.ts +2 -2
  35. package/kernel/packages/api/src/server/api/open-path.ts +2 -2
  36. package/kernel/packages/api/src/server/folder-metadata-plugin.ts +4 -4
  37. package/kernel/packages/api/src/server/plugins.ts +3 -3
  38. package/kernel/packages/api/src/shared/viewer-route.ts +1 -1
  39. package/kernel/packages/api/test/core.test.ts +1 -1
  40. package/kernel/packages/notes/package.json +1 -1
  41. package/kernel/packages/notes/src/client.tsx +1 -1
  42. package/kernel/packages/notes/src/manifest.ts +1 -1
  43. package/kernel/packages/notes/src/pages/notes-page.tsx +34 -1
  44. package/kernel/packages/notes/src/server.ts +1 -1
  45. package/kernel/packages/notes/src/ui/folder-notes-list.tsx +10 -74
  46. package/kernel/packages/plugins/SKILL.md +39 -0
  47. package/kernel/packages/{folder-plugin → plugins}/package.json +65 -11
  48. package/kernel/packages/plugins/src/index.ts +52 -0
  49. package/kernel/packages/plugins/src/page.ts +17 -0
  50. package/kernel/packages/{folder-plugin → plugins}/tsconfig.json +5 -1
  51. package/kernel/packages/plugins/tsup.config.ts +25 -0
  52. package/kernel/packages/ui/src/components/content-issues.tsx +1 -1
  53. package/kernel/packages/ui/tsconfig.json +1 -1
  54. package/package.json +2 -3
  55. package/web/assets/{abnfDiagram-VRR7QNED-xd_g1uK3.js → abnfDiagram-VRR7QNED-CDr-U31F.js} +1 -1
  56. package/web/assets/app-DHGorUqZ.js +2 -0
  57. package/web/assets/app-options-CtkiC8GR.js +25 -0
  58. package/web/assets/{app-view-header-CNk5KnA-.js → app-view-header-DbQuD6_F.js} +1 -1
  59. package/web/assets/{arc-B0u5yBJA.js → arc-Cwz5OeLQ.js} +1 -1
  60. package/web/assets/{architectureDiagram-ZJ3FMSHR-DfxOAo8P.js → architectureDiagram-ZJ3FMSHR-igGC0b-J.js} +1 -1
  61. package/web/assets/{ask-tray-SscJecDY.js → ask-tray-y8nkSKNG.js} +1 -1
  62. package/web/assets/{blockDiagram-677ZJIJ3-Ci3Usxh6.js → blockDiagram-677ZJIJ3-D4SDltQ_.js} +1 -1
  63. package/web/assets/{c4Diagram-LMCZKHZV-BqgwIVgh.js → c4Diagram-LMCZKHZV-Br15uN-s.js} +1 -1
  64. package/web/assets/channel-DRnAhH31.js +1 -0
  65. package/web/assets/{chunk-2Q5K7J3B-CfX69l2U.js → chunk-2Q5K7J3B-B0RqrvJA.js} +1 -1
  66. package/web/assets/{chunk-32BRIVSS-CuFlzZkl.js → chunk-32BRIVSS-De9VBEyE.js} +1 -1
  67. package/web/assets/{chunk-5VM5RSS4-DidCMlJa.js → chunk-5VM5RSS4-YIMlZtW3.js} +1 -1
  68. package/web/assets/{chunk-EX3LRPZG-B5CDm1dS.js → chunk-EX3LRPZG-y9kGcrYi.js} +1 -1
  69. package/web/assets/{chunk-JWPE2WC7-D5fGV_sH.js → chunk-JWPE2WC7-D0A13HAu.js} +1 -1
  70. package/web/assets/{chunk-MOJQB5TN-ayhH5Q-H.js → chunk-MOJQB5TN-8v4jL8iu.js} +1 -1
  71. package/web/assets/{chunk-RYQCIY6F-C4BfOOZn.js → chunk-RYQCIY6F-Ri_iNmIp.js} +1 -1
  72. package/web/assets/{chunk-V7JOEXUC-DpUEIM9V.js → chunk-V7JOEXUC-b0mIbG_X.js} +1 -1
  73. package/web/assets/{chunk-VR4S4FIN-DoQuFFRh.js → chunk-VR4S4FIN-BkTZWt1n.js} +1 -1
  74. package/web/assets/{chunk-XXDRQBXY-CI9eiLwn.js → chunk-XXDRQBXY-D7muOXcd.js} +1 -1
  75. package/web/assets/classDiagram-OUVF2IWQ-BPkIpFlc.js +1 -0
  76. package/web/assets/classDiagram-v2-EOCWNBFH-BPkIpFlc.js +1 -0
  77. package/web/assets/{client-Dk-NBvJR.js → client-BMso7lHb.js} +2 -2
  78. package/web/assets/client-Bkl3gwZ5.js +17 -0
  79. package/web/assets/client-soK_bLWT.js +5 -0
  80. package/web/assets/{command-zkzH4hVz.js → command-CsN8TfJg.js} +1 -1
  81. package/web/assets/{copy-context-D5oiWrYC.js → copy-context-DmBNpffM.js} +1 -1
  82. package/web/assets/{cose-bilkent-JH36ORCC-BHRk1mO3.js → cose-bilkent-JH36ORCC-7hnQZylB.js} +1 -1
  83. package/web/assets/{cynefin-VYW2F7L2-C1B09TRE.js → cynefin-VYW2F7L2-CTbqXuvk.js} +1 -1
  84. package/web/assets/{cynefinDiagram-TSTJHNR4-B27qXQbv.js → cynefinDiagram-TSTJHNR4-CmIJtocX.js} +1 -1
  85. package/web/assets/{dagre-VKFMJZFB-B1qEi32g.js → dagre-VKFMJZFB-BLPHNs2Y.js} +1 -1
  86. package/web/assets/{diagram-FQU43EPY-BMkkgADD.js → diagram-FQU43EPY-L6FVWAuj.js} +1 -1
  87. package/web/assets/{diagram-G47NLZAW-Cb8_g1A5.js → diagram-G47NLZAW-Bb26dbIM.js} +1 -1
  88. package/web/assets/{diagram-NH7WQ7WH-B51wuc5m.js → diagram-NH7WQ7WH-BBauwdU-.js} +1 -1
  89. package/web/assets/{diagram-OA4YK3LP-xnsecbbI.js → diagram-OA4YK3LP-Dj_9FE7R.js} +1 -1
  90. package/web/assets/{diagram-WEI45ONY-DSZdyaAV.js → diagram-WEI45ONY-Co1MS5BO.js} +1 -1
  91. package/web/assets/{document-Dc2dftTW.js → document-_ETc5epn.js} +3 -3
  92. package/web/assets/{ebnfDiagram-CCIWWBDH-aTHmcUGy.js → ebnfDiagram-CCIWWBDH-COGAgW5z.js} +1 -1
  93. package/web/assets/{entities-CCUtpskY.js → entities-ByGFSWnQ.js} +1 -1
  94. package/web/assets/{erDiagram-Q63AITRT-CUUPBcvo.js → erDiagram-Q63AITRT-CRMKCr0m.js} +1 -1
  95. package/web/assets/{file-viewer-DJ3rsmef.js → file-viewer-Dqx7DbTw.js} +1 -1
  96. package/web/assets/{flowDiagram-23GEKE2U-aTQXx2Mh.js → flowDiagram-23GEKE2U-Drv72N5g.js} +1 -1
  97. package/web/assets/folder-filter-bar-Bs_uyU0g.js +4 -0
  98. package/web/assets/folder-image-BBNRcu1j.js +1 -0
  99. package/web/assets/{folder-landing-DCnzYXXY.js → folder-landing-0EXAzEpD.js} +3 -3
  100. package/web/assets/{folder-list-DvVkVjiR.js → folder-list-Do8Kl8uk.js} +1 -1
  101. package/web/assets/{folder-metadata-C9hMMzIH.js → folder-metadata-DncP3Vx2.js} +1 -1
  102. package/web/assets/folder-mobile-bar-D-XXKrip.js +1 -0
  103. package/web/assets/folder-plugin-view-DoRQa8Dz.js +3 -0
  104. package/web/assets/{ganttDiagram-NO4QXBWP-DiwdUp7u.js → ganttDiagram-NO4QXBWP-DHEt2_9K.js} +1 -1
  105. package/web/assets/{gitGraphDiagram-IHSO6WYX-BL2gG_IS.js → gitGraphDiagram-IHSO6WYX-Bt6XsYTq.js} +1 -1
  106. package/web/assets/{index-CUTtMSyJ.js → index-7JCizcu7.js} +2 -2
  107. package/web/assets/index-DY0VGBht.js +1 -0
  108. package/web/assets/{infoDiagram-FWYZ7A6U-B3n0zZpt.js → infoDiagram-FWYZ7A6U-961DCJAH.js} +1 -1
  109. package/web/assets/{ishikawaDiagram-FXEZZL3T-D7YytbEv.js → ishikawaDiagram-FXEZZL3T-ibSYK50l.js} +1 -1
  110. package/web/assets/{journeyDiagram-5HDEW3XC-BYwqEl3C.js → journeyDiagram-5HDEW3XC-Cd0ZPqw2.js} +1 -1
  111. package/web/assets/{kanban-definition-HUTT4EX6-BQHNJgXZ.js → kanban-definition-HUTT4EX6-Bu5skY8G.js} +1 -1
  112. package/web/assets/{linear-BPiQCLNk.js → linear-Cf8Xflqn.js} +1 -1
  113. package/web/assets/{mermaid.core-CD-KvBjV.js → mermaid.core-C9iHkrxr.js} +4 -4
  114. package/web/assets/{metadata-COJ7fZul.js → metadata-CLq-ZGmh.js} +1 -1
  115. package/web/assets/{mindmap-definition-LN4V7U3C-C1L_gyoj.js → mindmap-definition-LN4V7U3C-C5bK3O7k.js} +1 -1
  116. package/web/assets/{pegDiagram-2B236MQR-BDAfItqS.js → pegDiagram-2B236MQR-5rN2nkjz.js} +1 -1
  117. package/web/assets/{pieDiagram-ENE6RG2P-CkZYH3Vz.js → pieDiagram-ENE6RG2P-DnSBFuIb.js} +1 -1
  118. package/web/assets/popover-D7MTK-sz.js +9 -0
  119. package/web/assets/{project-search-dialog-B2oqMT6h.js → project-search-dialog-Bo1JWdqR.js} +1 -1
  120. package/web/assets/{quadrantDiagram-ABIIQ3AL-Dy8Al3YZ.js → quadrantDiagram-ABIIQ3AL-xinqVpVM.js} +1 -1
  121. package/web/assets/{railroadDiagram-RFXS5EU6-Dd9_yo7B.js → railroadDiagram-RFXS5EU6-GsASFEVa.js} +1 -1
  122. package/web/assets/{requirementDiagram-TGXJPOKE-DPhIUsbs.js → requirementDiagram-TGXJPOKE-DS5CqNNo.js} +1 -1
  123. package/web/assets/{sankeyDiagram-HTMAVEWB-B2QkYtub.js → sankeyDiagram-HTMAVEWB-DsnvjCXC.js} +1 -1
  124. package/web/assets/{search-C1J0-xiB.js → search-BA4r_Yoa.js} +1 -1
  125. package/web/assets/select-D4TpCgnU.js +30 -0
  126. package/web/assets/{selection-popup-qhlUFzGq.js → selection-popup-DIME-60e.js} +3 -3
  127. package/web/assets/{sequenceDiagram-DBY2YBRQ-Bx7fcZDx.js → sequenceDiagram-DBY2YBRQ-PI0WgJrf.js} +1 -1
  128. package/web/assets/{sizeCapture-X5ZJPWSS-CIXIhl_A.js → sizeCapture-X5ZJPWSS-Bpul3MSt.js} +1 -1
  129. package/web/assets/{stateDiagram-2N3HPSRC-DCrdAflB.js → stateDiagram-2N3HPSRC-DZEEP9Z2.js} +1 -1
  130. package/web/assets/stateDiagram-v2-6OUMAXLB-WAp7-BWX.js +1 -0
  131. package/web/assets/{swimlanes-5IMT3BWC-DA2vc7iZ.js → swimlanes-5IMT3BWC-_qVjNiCh.js} +2 -2
  132. package/web/assets/swimlanesDiagram-G3AALYLV-Dy4wBDSi.js +8 -0
  133. package/web/assets/{timeline-definition-FHXFAJF6-lKfJiV9V.js → timeline-definition-FHXFAJF6-CJev7rIB.js} +1 -1
  134. package/web/assets/use-folder-filter-hA6fa7nm.js +1 -0
  135. package/web/assets/{vennDiagram-L72KCM5P-DO1q2KzG.js → vennDiagram-L72KCM5P-BrTVvREy.js} +1 -1
  136. package/web/assets/{view-context-DXKM3-l1.js → view-context-Co01ON81.js} +35 -35
  137. package/web/assets/{viewer-capabilities-Cig_xM9u.js → viewer-capabilities-C_kjcYcl.js} +1 -1
  138. package/web/assets/{wardleyDiagram-EHGQE667-Dal9LkuZ.js → wardleyDiagram-EHGQE667-XwMSgrT5.js} +1 -1
  139. package/web/assets/{xychartDiagram-FW5EYKEG-B3dPLPII.js → xychartDiagram-FW5EYKEG-Be1kFxeC.js} +1 -1
  140. package/web/index.html +1 -1
  141. package/kernel/packages/folder-plugin/src/index.ts +0 -15
  142. package/web/assets/app-CX20nNIa.js +0 -2
  143. package/web/assets/app-options-CkPJE5ep.js +0 -25
  144. package/web/assets/channel-X7WHyxxT.js +0 -1
  145. package/web/assets/classDiagram-OUVF2IWQ-BjsLQ3dz.js +0 -1
  146. package/web/assets/classDiagram-v2-EOCWNBFH-BjsLQ3dz.js +0 -1
  147. package/web/assets/client-7OWw5kW2.js +0 -6
  148. package/web/assets/client-DYj_td4Q.js +0 -17
  149. package/web/assets/folder-image-Bnv43tzP.js +0 -1
  150. package/web/assets/folder-mobile-bar-VoTL1GS0.js +0 -1
  151. package/web/assets/folder-plugin-view-Cvd804ZU.js +0 -6
  152. package/web/assets/popover-0oA2lLUV.js +0 -9
  153. package/web/assets/select-CtXC_PLC.js +0 -30
  154. package/web/assets/stateDiagram-v2-6OUMAXLB-0KCFLYeQ.js +0 -1
  155. package/web/assets/swimlanesDiagram-G3AALYLV-CAw3AV84.js +0 -8
  156. /package/kernel/packages/{folder-plugin → plugins}/eslint.config.js +0 -0
  157. /package/kernel/packages/{folder-plugin → plugins}/src/client.ts +0 -0
  158. /package/kernel/packages/{folder-plugin → plugins}/src/endpoint.ts +0 -0
  159. /package/kernel/packages/{folder-plugin → plugins}/src/server.ts +0 -0
  160. /package/kernel/packages/{folder-plugin → plugins}/src/subpath.ts +0 -0
  161. /package/kernel/packages/{folder-plugin → plugins}/src/types.ts +0 -0
  162. /package/kernel/packages/{folder-plugin → plugins}/src/validation.ts +0 -0
  163. /package/kernel/packages/{folder-plugin → plugins}/test/contract.test.mjs +0 -0
@@ -26,6 +26,9 @@ export declare function readExternalPlugins(): ExternalPluginEntry[];
26
26
  export declare function installExternalPlugin(url: string, options?: {
27
27
  directory?: string;
28
28
  }): Promise<ExternalPluginEntry>;
29
+ /** Identifies the installed plugin set, independent of the Node runtime. */
30
+ export declare function hotKernelKey(plugins?: ExternalPluginEntry[]): string;
31
+ /** Identifies a runtime-specific static kernel cache entry. */
29
32
  export declare function kernelKey(plugins?: ExternalPluginEntry[]): string;
30
33
  export type ActiveKernel = {
31
34
  buildIdentity: string;
@@ -163,7 +163,7 @@ function internalSourceAliases(root = kernelSourceRoot()) {
163
163
  const aliases = {};
164
164
  for (const [name, path] of Object.entries({
165
165
  "@raggle-ai/agents": "packages/agents/src",
166
- "@raggle-ai/folder-plugin": "packages/folder-plugin/src",
166
+ "kennel-plugins": "packages/plugins/src",
167
167
  "@raggle-ai/kennel-api": "packages/api/src",
168
168
  "@raggle-ai/markdown": "packages/markdown/src",
169
169
  "@raggle-ai/notes": "packages/notes/src",
@@ -373,21 +373,33 @@ export async function installExternalPlugin(url, options = {}) {
373
373
  function cliRoot() {
374
374
  return resolve(dirname(fileURLToPath(import.meta.url)), "../..");
375
375
  }
376
+ function kernelPlugins(plugins) {
377
+ return plugins.map((plugin) => ({
378
+ url: plugin.url,
379
+ requestedRef: plugin.requestedRef ?? null,
380
+ packageName: plugin.packageName,
381
+ id: plugin.id,
382
+ directory: plugin.directory ?? null,
383
+ commit: plugin.commit,
384
+ trigger: plugin.trigger,
385
+ entries: plugin.entries,
386
+ }));
387
+ }
388
+ /** Identifies the installed plugin set, independent of the Node runtime. */
389
+ export function hotKernelKey(plugins = readExternalPlugins()) {
390
+ return hash(JSON.stringify({
391
+ buildIdentity: BUILD_IDENTITY,
392
+ schema: KENNEL_KERNEL_SCHEMA,
393
+ plugins: kernelPlugins(plugins),
394
+ }));
395
+ }
396
+ /** Identifies a runtime-specific static kernel cache entry. */
376
397
  export function kernelKey(plugins = readExternalPlugins()) {
377
398
  return hash(JSON.stringify({
378
399
  buildIdentity: BUILD_IDENTITY,
379
400
  schema: KENNEL_KERNEL_SCHEMA,
380
401
  runtime: BUILD_RUNTIME,
381
- plugins: plugins.map((plugin) => ({
382
- url: plugin.url,
383
- requestedRef: plugin.requestedRef ?? null,
384
- packageName: plugin.packageName,
385
- id: plugin.id,
386
- directory: plugin.directory ?? null,
387
- commit: plugin.commit,
388
- trigger: plugin.trigger,
389
- entries: plugin.entries,
390
- })),
402
+ plugins: kernelPlugins(plugins),
391
403
  }));
392
404
  }
393
405
  function kernelRoot(key) {
@@ -448,7 +460,7 @@ export async function prepareExternalPluginModules(temp, target = temp, plugins
448
460
  const cssImports = plugins.map((plugin) => plugin.entries.css ? `import ${JSON.stringify(plugin.entries.css)};` : "").filter(Boolean).join("\n");
449
461
  const clientImports = plugins.map((plugin, index) => `import { ${plugin.entries.manifestSymbol} as manifest${index} } from ${JSON.stringify(plugin.entries.manifest)};`).join("\n");
450
462
  const clientItems = plugins.map((plugin, index) => ` { ...manifest${index}, component: lazy(async () => ({ default: (await import(${JSON.stringify(plugin.entries.client)}))[${JSON.stringify(plugin.entries.clientSymbol)}].component })) }`).join(",\n");
451
- await writeFile(clientModule, `import { lazy } from "react";\nimport type { FolderPluginClient } from "@raggle-ai/folder-plugin/client";\n${cssImports}\n${clientImports}\nexport const externalFolderPlugins: FolderPluginClient[] = [\n${clientItems}\n];\n`);
463
+ await writeFile(clientModule, `import { lazy } from "react";\nimport type { FolderPluginClient } from "kennel-plugins/client";\n${cssImports}\n${clientImports}\nexport const externalFolderPlugins: FolderPluginClient[] = [\n${clientItems}\n];\n`);
452
464
  const serverEntries = [];
453
465
  const apiEntries = [];
454
466
  for (const plugin of plugins) {
@@ -7,12 +7,15 @@ import { tmpdir } from 'node:os';
7
7
  import { dirname, relative, resolve } from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { createKennelClient } from '../core/api-client.js';
10
+ import { hotKernelKey, readExternalPlugins } from '../plugins/external.js';
10
11
  function sourceRoot() {
11
12
  return resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
12
13
  }
13
14
  async function viewerStatus(url) {
14
15
  return createKennelClient({ baseUrl: url }).health({ signal: AbortSignal.timeout(1_000) })
15
- .then((body) => body.viewer === 'kennel-vite' || body.viewer === 'kennel-core' ? 'running' : 'other')
16
+ .then((body) => body.viewer === 'kennel-vite' || body.viewer === 'kennel-core'
17
+ ? { viewer: body.viewer, kernelKey: body.kernelKey }
18
+ : 'other')
16
19
  .catch((error) => {
17
20
  // HTTP rejection means a different service owns the port; connection and
18
21
  // timeout failures mean the viewer is unavailable, so startup may retry.
@@ -20,17 +23,17 @@ async function viewerStatus(url) {
20
23
  ? 'other' : 'unavailable';
21
24
  });
22
25
  }
23
- async function viewerRunning(url, attempts = 1, waitOnOther = false) {
26
+ async function runningViewer(url, attempts = 1, waitOnOther = false) {
24
27
  for (let attempt = 0; attempt < attempts; attempt += 1) {
25
28
  const status = await viewerStatus(url);
26
- if (status === 'running')
27
- return true;
29
+ if (typeof status === 'object')
30
+ return status;
28
31
  if (status === 'other' && !waitOnOther)
29
- return false;
32
+ return undefined;
30
33
  if (attempt + 1 < attempts)
31
34
  await new Promise((done) => setTimeout(done, 250));
32
35
  }
33
- return false;
36
+ return undefined;
34
37
  }
35
38
  async function portOpen(host, port) {
36
39
  return new Promise((done) => {
@@ -132,8 +135,20 @@ export async function hotViewer(options) {
132
135
  console.log(`bun ${[...args, ...(options.open ? ['--open', openPath] : [])].join(' ')}`);
133
136
  return;
134
137
  }
135
- const status = await viewerStatus(url);
136
- if (status === 'running') {
138
+ const plugins = readExternalPlugins();
139
+ // Must match the key apps/web/scripts/dev.mjs gives Vite as KENNEL_KERNEL_KEY.
140
+ const expectedKey = plugins.length > 0 ? hotKernelKey(plugins) : 'core';
141
+ // Reuse only a Vite viewer that serves the same plugin set. Anything else on
142
+ // the port is stale for this kernel and must be restarted, never reused.
143
+ const reuse = (running) => {
144
+ const restart = 'Stop that viewer (Ctrl+C in its terminal) and run this command again, or select another port.';
145
+ if (running.viewer !== 'kennel-vite') {
146
+ throw new Error(`Port ${options.port} runs the static Kennel viewer, not the hot viewer. ${restart}`);
147
+ }
148
+ if (running.kernelKey !== expectedKey) {
149
+ throw new Error(`The Kennel hot viewer on port ${options.port} uses kernel ${running.kernelKey}, `
150
+ + `but the installed plugins need kernel ${expectedKey}. ${restart}`);
151
+ }
137
152
  if (options.open) {
138
153
  // Do not stack another tab: the viewer is already open. Print the URL
139
154
  // so the caller can still get to it deliberately.
@@ -142,21 +157,19 @@ export async function hotViewer(options) {
142
157
  else {
143
158
  console.log('Kennel hot viewer is already running. Reusing it.');
144
159
  }
160
+ };
161
+ const status = await viewerStatus(url);
162
+ if (typeof status === 'object') {
163
+ reuse(status);
145
164
  return;
146
165
  }
147
166
  if (status === 'other') {
148
167
  throw new Error(`Port ${options.port} is used by another service. Stop it or select another port.`);
149
168
  }
150
169
  if (await portOpen(options.host, options.port)) {
151
- if (await viewerRunning(url, 20, true)) {
152
- if (options.open) {
153
- // Do not stack another tab: the viewer is already open. Print the URL
154
- // so the caller can still get to it deliberately.
155
- console.log(`Kennel hot viewer is already running at ${viewerUrl}`);
156
- }
157
- else {
158
- console.log('Kennel hot viewer is already running. Reusing it.');
159
- }
170
+ const running = await runningViewer(url, 20, true);
171
+ if (running) {
172
+ reuse(running);
160
173
  return;
161
174
  }
162
175
  throw new Error(`Port ${options.port} is used by another service. Stop it or select another port.`);
@@ -164,15 +177,9 @@ export async function hotViewer(options) {
164
177
  const lock = startupLock(options.host, options.port);
165
178
  let token = await acquireLock(lock);
166
179
  if (!token) {
167
- if (await viewerRunning(url, 40, true)) {
168
- if (options.open) {
169
- // Do not stack another tab: the viewer is already open. Print the URL
170
- // so the caller can still get to it deliberately.
171
- console.log(`Kennel hot viewer is already running at ${viewerUrl}`);
172
- }
173
- else {
174
- console.log('Kennel hot viewer is already running. Reusing it.');
175
- }
180
+ const running = await runningViewer(url, 40, true);
181
+ if (running) {
182
+ reuse(running);
176
183
  return;
177
184
  }
178
185
  await rm(lock, { force: true, recursive: true });
@@ -180,16 +187,10 @@ export async function hotViewer(options) {
180
187
  }
181
188
  if (!token)
182
189
  throw new Error(`Could not acquire the hot viewer startup lock for port ${options.port}.`);
183
- if (await viewerRunning(url)) {
190
+ const running = await runningViewer(url);
191
+ if (running) {
184
192
  await releaseLock(lock, token);
185
- if (options.open) {
186
- // Do not stack another tab: the viewer is already open. Print the URL
187
- // so the caller can still get to it deliberately.
188
- console.log(`Kennel hot viewer is already running at ${viewerUrl}`);
189
- }
190
- else {
191
- console.log('Kennel hot viewer is already running. Reusing it.');
192
- }
193
+ reuse(running);
193
194
  return;
194
195
  }
195
196
  console.log(`bun ${args.join(' ')}`);
@@ -220,9 +221,15 @@ export async function hotViewer(options) {
220
221
  done();
221
222
  return;
222
223
  }
223
- if (await viewerRunning(url, 20, true)) {
224
- console.log('Kennel hot viewer is already running. Reusing it.');
225
- done();
224
+ const existing = await runningViewer(url, 20, true);
225
+ if (existing) {
226
+ try {
227
+ reuse(existing);
228
+ done();
229
+ }
230
+ catch (error) {
231
+ fail(error);
232
+ }
226
233
  return;
227
234
  }
228
235
  fail(new Error(`Kennel hot viewer stopped with exit code ${code ?? 'unknown'}`));
@@ -8,8 +8,8 @@
8
8
  * plain-`node` CLI never imports the plugin packages' runtime sources, while
9
9
  * staying type-checked against the one source of truth.
10
10
  */
11
- import type { FolderPluginServer } from "@raggle-ai/folder-plugin/server";
12
- export type { FolderPluginContext, FolderPluginData, FolderPluginDetection, FolderPluginServer, } from "@raggle-ai/folder-plugin/server";
11
+ import type { FolderPluginServer } from "kennel-plugins/server";
12
+ export type { FolderPluginContext, FolderPluginData, FolderPluginDetection, FolderPluginServer, } from "kennel-plugins/server";
13
13
  /**
14
14
  * Signal a server-side plugin failure (like a mail preview read error) so the
15
15
  * host maps it to an HTTP 5xx instead of a not-found. All other thrown errors
@@ -515,13 +515,13 @@ function createAgentsPlugin(options = {}) {
515
515
  };
516
516
  }
517
517
 
518
- // packages/folder-plugin/src/server.ts
519
- class FolderPluginFailure extends Error {
518
+ // packages/plugins/dist/chunk-AX4A4GYF.js
519
+ var FolderPluginFailure = class extends Error {
520
520
  constructor(message) {
521
521
  super(message);
522
522
  this.name = "FolderPluginFailure";
523
523
  }
524
- }
524
+ };
525
525
 
526
526
  // packages/notes/src/server.ts
527
527
  import { readdir, stat as stat2 } from "node:fs/promises";
@@ -730,11 +730,11 @@ async function scaffoldFolderPlugins(folder, options = {}) {
730
730
  return { created, skipped, supported };
731
731
  }
732
732
  export {
733
- scaffoldFolderPlugins,
734
- folderServerRegistryAsync,
735
- folderServerRegistry,
736
- externalApiHandlers,
737
- createFolderServerPlugins,
733
+ FolderPluginFailure,
738
734
  configuredFolderServerRegistry,
739
- FolderPluginFailure
735
+ createFolderServerPlugins,
736
+ externalApiHandlers,
737
+ folderServerRegistry,
738
+ folderServerRegistryAsync,
739
+ scaffoldFolderPlugins
740
740
  };
@@ -259,17 +259,15 @@ function tailscaleHost() {
259
259
  export async function viewer(input) {
260
260
  const portExplicit = viewerPortWasExplicit(input);
261
261
  const options = splitViewerArgs(input);
262
- const { ensureKernel, readExternalPlugins } = await import("../plugins/external.js");
263
- const externalPlugins = readExternalPlugins();
264
- if (options.hot && externalPlugins.length === 0) {
262
+ if (options.hot) {
265
263
  const { hotViewer } = await import("./hot-viewer.js");
266
264
  await hotViewer(options);
267
265
  return;
268
266
  }
269
- if (options.hot && externalPlugins.length > 0) {
270
- console.log("Kennel hot viewer is disabled for external plugin kernels; using the composed static kernel.");
271
- }
267
+ const { ensureKernel, hotKernelKey, readExternalPlugins } = await import("../plugins/external.js");
268
+ const externalPlugins = readExternalPlugins();
272
269
  const kernel = await ensureKernel(externalPlugins);
270
+ const viteKernelKey = externalPlugins.length > 0 ? hotKernelKey(externalPlugins) : "core";
273
271
  const web = kernel.webRoot;
274
272
  if (!existsSync(resolve(web, "index.html"))) {
275
273
  throw new Error(`Kennel web app not found. Run the CLI build first.`);
@@ -298,9 +296,16 @@ export async function viewer(input) {
298
296
  .join(",");
299
297
  const initialViewerUrl = `http://${options.host}:${options.port}`;
300
298
  const existing = await runningViewer(initialViewerUrl);
299
+ // A local Vite viewer is not root-restricted, so it can open any local
300
+ // folder. The static kennel-core viewer is root-scoped: reuse it only for
301
+ // its own root or a descendant.
301
302
  if (existing &&
302
- existing.kernelKey === kernel.key &&
303
- isSameOrDescendant(existing.root, options.root)) {
303
+ ((existing.viewer === "kennel-vite" &&
304
+ !remote &&
305
+ existing.kernelKey === viteKernelKey) ||
306
+ (existing.viewer !== "kennel-vite" &&
307
+ existing.kernelKey === kernel.key &&
308
+ isSameOrDescendant(existing.root, options.root)))) {
304
309
  const viewerUrl = popUrl(displayHost, options.port, options);
305
310
  console.log(viewerUrl);
306
311
  console.log(`Kennel is already running at ${viewerUrl}`);
@@ -23,7 +23,7 @@
23
23
  "@lexical/react": "0.49.0",
24
24
  "@lexical/rich-text": "0.49.0",
25
25
  "@raggle-ai/agents": "workspace:*",
26
- "@raggle-ai/folder-plugin": "workspace:*",
26
+ "kennel-plugins": "workspace:*",
27
27
  "@raggle-ai/kennel-api": "workspace:*",
28
28
  "@raggle-ai/kennel-preview": "workspace:*",
29
29
  "@raggle-ai/local": "0.14.0",
@@ -5,6 +5,7 @@ import { dirname, join, resolve } from "node:path"
5
5
  import { fileURLToPath } from "node:url"
6
6
 
7
7
  import {
8
+ hotKernelKey,
8
9
  prepareExternalPluginModules,
9
10
  readExternalPlugins,
10
11
  } from "../../../cli/src/plugins/external.ts"
@@ -30,6 +31,9 @@ try {
30
31
  stdio: "inherit",
31
32
  env: {
32
33
  ...process.env,
34
+ // Reported by /api/health so the CLI reuses this viewer only for the
35
+ // same plugin set; "core" matches the CLI kernel without plugins.
36
+ KENNEL_KERNEL_KEY: plugins.length > 0 ? hotKernelKey(plugins) : "core",
33
37
  KENNEL_KERNEL_SOURCE_ROOT: workspace,
34
38
  KENNEL_HOST_NODE_MODULES: resolve(workspace, "cli/node_modules"),
35
39
  KENNEL_EXTERNAL_CLIENT_MODULE: modules.clientModule,
@@ -4,39 +4,75 @@ export function subscribeFolderEvent(
4
4
  folder: string | undefined,
5
5
  listener: () => void,
6
6
  ) {
7
- return subscribeApiEvent(event, (message) => {
8
- let data: unknown
9
- try {
10
- data = JSON.parse((message as MessageEvent<string>).data)
11
- } catch {
12
- return
13
- }
14
- if (
15
- data &&
16
- typeof data === "object" &&
17
- "path" in data &&
18
- data.path === folder
19
- )
20
- listener()
21
- })
7
+ return subscribeApiEvent(
8
+ event,
9
+ (message) => {
10
+ let data: unknown
11
+ try {
12
+ data = JSON.parse((message as MessageEvent<string>).data)
13
+ } catch {
14
+ return
15
+ }
16
+ if (
17
+ data &&
18
+ typeof data === "object" &&
19
+ "path" in data &&
20
+ data.path === folder
21
+ )
22
+ listener()
23
+ },
24
+ listener,
25
+ )
26
+ }
27
+
28
+ type Subscription = {
29
+ event: string
30
+ listener: (event: Event) => void
31
+ resume: () => void
22
32
  }
23
33
 
24
34
  let source: EventSource | undefined
25
- let subscribers = 0
26
- /** Share one SSE connection across mounted views; browser reconnects automatically. */
35
+ const subscriptions = new Set<Subscription>()
36
+
37
+ function connect() {
38
+ if (source || document.visibilityState !== "visible") return
39
+ source = new EventSource("/api/events")
40
+ for (const { event, listener } of subscriptions) {
41
+ source.addEventListener(event, listener)
42
+ }
43
+ }
44
+
45
+ function visibilityChanged() {
46
+ if (document.visibilityState !== "visible") {
47
+ source?.close()
48
+ source = undefined
49
+ return
50
+ }
51
+ connect()
52
+ // Hidden tabs missed notifications. Refresh each subscriber on return.
53
+ for (const subscription of subscriptions) subscription.resume()
54
+ }
55
+
56
+ /** Hidden tabs must not consume the browser's per-origin HTTP/1 connection limit. */
27
57
  export function subscribeApiEvent(
28
58
  event: string,
29
59
  listener: (event: Event) => void,
60
+ resume = () => listener(new Event(event)),
30
61
  ) {
31
- source ??= new EventSource("/api/events")
32
- const current = source
33
- subscribers++
34
- current.addEventListener(event, listener)
62
+ const subscription = { event, listener, resume }
63
+ subscriptions.add(subscription)
64
+ if (subscriptions.size === 1) {
65
+ document.addEventListener("visibilitychange", visibilityChanged)
66
+ }
67
+ if (source) source.addEventListener(event, listener)
68
+ else connect()
35
69
  return () => {
36
- current.removeEventListener(event, listener)
37
- if (--subscribers === 0) {
38
- current.close()
70
+ if (!subscriptions.delete(subscription)) return
71
+ source?.removeEventListener(event, listener)
72
+ if (subscriptions.size === 0) {
73
+ source?.close()
39
74
  source = undefined
75
+ document.removeEventListener("visibilitychange", visibilityChanged)
40
76
  }
41
77
  }
42
78
  }
@@ -1,3 +1,3 @@
1
- import type { FolderPluginClient } from "@raggle-ai/folder-plugin/client"
1
+ import type { FolderPluginClient } from "kennel-plugins/client"
2
2
 
3
3
  export const externalFolderPlugins: FolderPluginClient[] = []
@@ -2,7 +2,7 @@ import type { ReactNode } from "react"
2
2
  import { useQuery } from "@tanstack/react-query"
3
3
  import { api } from "~/api-client"
4
4
 
5
- import { folderPathId } from "@raggle-ai/folder-plugin/client"
5
+ import { folderPathId } from "kennel-plugins/client"
6
6
  import { FolderFilterIconProvider } from "@raggle-ai/ui/components/folder-filter-bar"
7
7
  import { FolderImage } from "@raggle-ai/ui/components/folder-image"
8
8
 
@@ -1,7 +1,7 @@
1
1
  import { useParams } from "@tanstack/react-router"
2
2
  import { api } from "~/api-client"
3
3
 
4
- import type { FolderPluginViewProps } from "@raggle-ai/folder-plugin/client"
4
+ import type { FolderPluginViewProps } from "kennel-plugins/client"
5
5
 
6
6
  import { FolderFilterMetadataProvider } from "./folder-filter-icons"
7
7
  import { getFolderPlugin } from "./plugins"
@@ -1,7 +1,7 @@
1
1
  import { lazy } from "react"
2
2
  import { externalFolderPlugins } from "@/external-plugins"
3
3
 
4
- import type { FolderPluginClient } from "@raggle-ai/folder-plugin/client"
4
+ import type { FolderPluginClient } from "kennel-plugins/client"
5
5
  import { agentsManifest } from "@raggle-ai/agents/manifest"
6
6
 
7
7
  // Keep navigation metadata cheap. Import only the selected plugin's UI;
@@ -74,7 +74,7 @@ const resolveBarkdown = (): string | undefined => {
74
74
  const barkdown = resolveBarkdown()
75
75
  const internalPackages: Array<[RegExp, string]> = [
76
76
  [/^@raggle-ai\/agents(\/.*)?$/, "packages/agents/src"],
77
- [/^@raggle-ai\/folder-plugin(\/.*)?$/, "packages/folder-plugin/src"],
77
+ [/^kennel-plugins(\/.*)?$/, "packages/plugins/src"],
78
78
  [/^@raggle-ai\/kennel-api(\/.*)?$/, "packages/api/src"],
79
79
  [/^@raggle-ai\/markdown(\/.*)?$/, "packages/markdown/src"],
80
80
  [/^@raggle-ai\/notes(\/.*)?$/, "packages/notes/src"],
@@ -215,6 +215,8 @@ export default defineConfig({
215
215
  corePlugin({
216
216
  remoteAccess,
217
217
  restrictToRoot: remoteAccess,
218
+ // /api/health reports this name so the CLI reuses only a hot viewer.
219
+ viewerName: "kennel-vite",
218
220
  root: process.env.KENNEL_VIEWER_ROOT || root,
219
221
  // `kennel pop <file>` (hot) locks document reads to exactly this file;
220
222
  // the static viewer applies the same lock in-process.
@@ -41,7 +41,7 @@
41
41
  "publish:public:dry-run": "npm --@raggle-ai:registry=https://registry.npmjs.org publish --dry-run --access public --registry=https://registry.npmjs.org"
42
42
  },
43
43
  "dependencies": {
44
- "@raggle-ai/folder-plugin": "workspace:*",
44
+ "kennel-plugins": "workspace:*",
45
45
  "@raggle-ai/markdown": "workspace:*",
46
46
  "@raggle-ai/ui": "workspace:*",
47
47
  "@tanstack/react-query": "catalog:tanstack",
@@ -2,7 +2,7 @@ import type { ReactNode } from "react"
2
2
  import { useMemo, useState } from "react"
3
3
  import { FilePlus2, LoaderCircle, Pencil } from "lucide-react"
4
4
 
5
- import type { FolderPluginHostActions } from "@raggle-ai/folder-plugin/client"
5
+ import type { FolderPluginHostActions } from "kennel-plugins/client"
6
6
  import { BarkdownMarkdown, markdownMetadata } from "@raggle-ai/markdown"
7
7
  import { FolderContextIcon } from "@raggle-ai/ui/components/folder-filter-bar"
8
8
  import {
@@ -1,4 +1,4 @@
1
- import type { FolderPluginViewProps } from "@raggle-ai/folder-plugin/client"
1
+ import type { FolderPluginViewProps } from "kennel-plugins/client"
2
2
 
3
3
  import { agentsManifest } from "./manifest.ts"
4
4
  import { AgentsPluginView } from "./viewer.tsx"
@@ -1,4 +1,4 @@
1
- import type { FolderPluginManifest } from "@raggle-ai/folder-plugin"
1
+ import type { FolderPluginManifest } from "kennel-plugins"
2
2
 
3
3
  /** Stable Agents plugin identity and navigation label. No side effects. */
4
4
  export const agentsManifest: FolderPluginManifest = {
@@ -2,7 +2,7 @@ import type {
2
2
  FolderPluginContext,
3
3
  FolderPluginData,
4
4
  FolderPluginDetection,
5
- } from "@raggle-ai/folder-plugin/server"
5
+ } from "kennel-plugins/server"
6
6
 
7
7
  import type { AgentsResolution, ClientFilter } from "./types.ts"
8
8
  import { agentsManifest } from "./manifest.ts"
@@ -2,7 +2,7 @@ import { useMemo, useRef, useState } from "react"
2
2
  import { useQuery } from "@tanstack/react-query"
3
3
  import { ChevronDown, Info, TriangleAlert } from "lucide-react"
4
4
 
5
- import type { FolderPluginViewProps } from "@raggle-ai/folder-plugin/client"
5
+ import type { FolderPluginViewProps } from "kennel-plugins/client"
6
6
  import {
7
7
  Collapsible,
8
8
  CollapsibleContent,
@@ -19,7 +19,7 @@
19
19
  "dependencies": {
20
20
  "@agentclientprotocol/sdk": "^1.3.0",
21
21
  "@raggle-ai/agents": "workspace:*",
22
- "@raggle-ai/folder-plugin": "workspace:*",
22
+ "kennel-plugins": "workspace:*",
23
23
  "@raggle-ai/kennel-kinds": "workspace:*",
24
24
  "@raggle-ai/local": "0.14.0",
25
25
  "@raggle-ai/markdown": "workspace:*",
@@ -10,7 +10,7 @@ import { Readable, Writable } from "node:stream";
10
10
  import type { IncomingMessage, ServerResponse } from "node:http";
11
11
  import type { ApiHost } from "./host";
12
12
 
13
- import { sameOrigin } from "@raggle-ai/folder-plugin/endpoint";
13
+ import { sameOrigin } from "kennel-plugins/endpoint";
14
14
  import localSessions from "@raggle-ai/local/sessions";
15
15
 
16
16
  import type { AcpTransport } from "./acp/transport";
@@ -1,8 +1,8 @@
1
1
  import { writeFile } from "node:fs/promises";
2
2
  import { resolve } from "node:path";
3
3
 
4
- import type { ApiRouteSpec } from "@raggle-ai/folder-plugin/endpoint";
5
- import { apiError, jsonResponse } from "@raggle-ai/folder-plugin/endpoint";
4
+ import type { ApiRouteSpec } from "kennel-plugins/endpoint";
5
+ import { apiError, jsonResponse } from "kennel-plugins/endpoint";
6
6
 
7
7
  export type CreateAgentFileContext = {
8
8
  /**
@@ -17,13 +17,13 @@ import {
17
17
  sep,
18
18
  } from "node:path";
19
19
 
20
- import type { ApiRouteSpec } from "@raggle-ai/folder-plugin/endpoint";
20
+ import type { ApiRouteSpec } from "kennel-plugins/endpoint";
21
21
  import { replaceSelection } from "../../shared/selection";
22
22
  import {
23
23
  apiError,
24
24
  jsonResponse,
25
25
  parseJsonBody,
26
- } from "@raggle-ai/folder-plugin/endpoint";
26
+ } from "kennel-plugins/endpoint";
27
27
 
28
28
  const pending = new Map<string, Promise<unknown>>();
29
29
 
@@ -1,7 +1,7 @@
1
1
  import { execFile } from "node:child_process";
2
2
 
3
- import type { ApiRouteSpec } from "@raggle-ai/folder-plugin/endpoint";
4
- import { apiError } from "@raggle-ai/folder-plugin/endpoint";
3
+ import type { ApiRouteSpec } from "kennel-plugins/endpoint";
4
+ import { apiError } from "kennel-plugins/endpoint";
5
5
 
6
6
  export type OpenPathContext = {
7
7
  /**
@@ -17,14 +17,14 @@ import type { ApiHost } from "./host";
17
17
  import { markdownMetadata } from "@raggle-ai/markdown";
18
18
  import { readFolder } from "@raggle-ai/markdown/node";
19
19
  import { execFile } from "node:child_process";
20
- import { sameOrigin } from "@raggle-ai/folder-plugin/endpoint";
20
+ import { sameOrigin } from "kennel-plugins/endpoint";
21
21
  import { parse as parseYaml } from "yaml";
22
22
 
23
- import type { FolderPluginServer } from "@raggle-ai/folder-plugin/server";
23
+ import type { FolderPluginServer } from "kennel-plugins/server";
24
24
  import type { LocalMarkdownFileSummary, LocalProject } from "@raggle-ai/local";
25
25
  import type { FolderSkill } from "@raggle-ai/ui/lib/folder-activity";
26
- import { runApiRoute } from "@raggle-ai/folder-plugin/endpoint";
27
- import { FolderPluginFailure } from "@raggle-ai/folder-plugin/server";
26
+ import { runApiRoute } from "kennel-plugins/endpoint";
27
+ import { FolderPluginFailure } from "kennel-plugins/server";
28
28
  import { folderWorkspace } from "@raggle-ai/schemas/workspace";
29
29
  import { createSchemaCollectionServer } from "@raggle-ai/schemas/server";
30
30
  import local from "@raggle-ai/local";