kennel 0.2.12 → 0.2.13

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 (164) hide show
  1. package/dist/cli/src/plugins/external.js +19 -7
  2. package/dist/cli/src/site/server-plugins.d.ts +1 -4
  3. package/dist/core/api-client.js +3 -11
  4. package/dist/core/runtime.js +84 -695
  5. package/dist/package/preview.js +99 -729
  6. package/dist/plugins/external.js +19 -7
  7. package/dist/site/server-plugins.d.ts +1 -4
  8. package/dist/site/server-plugins.js +28 -537
  9. package/kernel/apps/web/package.json +0 -1
  10. package/kernel/apps/web/src/app.css +0 -1
  11. package/kernel/apps/web/src/app.tsx +7 -24
  12. package/kernel/apps/web/src/folder-plugin-view.tsx +6 -3
  13. package/kernel/apps/web/src/plugins.ts +2 -14
  14. package/kernel/apps/web/vite.config.ts +16 -3
  15. package/kernel/packages/api/openapi.json +85 -224
  16. package/kernel/packages/api/package.json +0 -1
  17. package/kernel/packages/api/src/contracts/schemas.ts +1 -9
  18. package/kernel/packages/api/src/server/folder-metadata-plugin.ts +9 -151
  19. package/kernel/packages/api/src/server/plugins.ts +11 -26
  20. package/kernel/packages/api/src/shared/viewer-route.ts +3 -5
  21. package/kernel/packages/api-client/src/generated.d.ts +16 -53
  22. package/kernel/packages/markdown/README.md +2 -2
  23. package/kernel/packages/notes/package.json +1 -0
  24. package/kernel/packages/notes/src/client.tsx +0 -1
  25. package/kernel/packages/notes/src/manifest.ts +7 -0
  26. package/kernel/packages/notes/src/pages/notes-page.tsx +1 -1
  27. package/kernel/packages/notes/src/ui/folder-notes-list.tsx +52 -7
  28. package/kernel/packages/plugins/SKILL.md +3 -2
  29. package/kernel/packages/plugins/package.json +5 -1
  30. package/kernel/packages/plugins/src/hooks.ts +24 -0
  31. package/kernel/packages/plugins/src/index.ts +1 -7
  32. package/kernel/packages/plugins/src/page.ts +0 -7
  33. package/kernel/packages/plugins/src/types.ts +8 -0
  34. package/kernel/packages/plugins/tsconfig.json +1 -1
  35. package/kernel/packages/plugins/tsup.config.ts +1 -0
  36. package/kernel/packages/ui/src/components/folder-activity.tsx +16 -98
  37. package/kernel/packages/ui/src/components/folder-landing.tsx +4 -0
  38. package/kernel/packages/ui/src/components/pagination.tsx +88 -2
  39. package/kernel/packages/ui/src/components/plugin-icon.tsx +19 -0
  40. package/kernel/packages/ui/src/lib/folder-activity.ts +4 -17
  41. package/kernel/packages/ui/src/styles/folder-activity.css +1 -31
  42. package/package.json +2 -2
  43. package/web/assets/{abnfDiagram-VRR7QNED-Brm1vcGG.js → abnfDiagram-VRR7QNED-Dt7QEF5H.js} +1 -1
  44. package/web/assets/app-C1j063QX.js +2 -0
  45. package/web/assets/app-options-98-a6ivv.js +25 -0
  46. package/web/assets/{app-view-header-BSOI-wB6.js → app-view-header-BJ9CWTE1.js} +1 -1
  47. package/web/assets/{arc-zngHsMKV.js → arc-PFB0H7oD.js} +1 -1
  48. package/web/assets/{architectureDiagram-ZJ3FMSHR-BLklLqRP.js → architectureDiagram-ZJ3FMSHR-BHLqEXlZ.js} +1 -1
  49. package/web/assets/{ask-tray-B4SPj-fH.js → ask-tray-C3gBesHJ.js} +1 -1
  50. package/web/assets/{blockDiagram-677ZJIJ3-yhC2fEtu.js → blockDiagram-677ZJIJ3-UbnYoYDg.js} +1 -1
  51. package/web/assets/{c4Diagram-LMCZKHZV-DMcweQme.js → c4Diagram-LMCZKHZV-4OSx1V4W.js} +1 -1
  52. package/web/assets/channel-BuvY5cQN.js +1 -0
  53. package/web/assets/{chunk-2Q5K7J3B-DD-S6n8h.js → chunk-2Q5K7J3B-CgxJ1aJb.js} +1 -1
  54. package/web/assets/{chunk-32BRIVSS-DFIAKRv_.js → chunk-32BRIVSS-Bfwxg2LM.js} +1 -1
  55. package/web/assets/{chunk-5VM5RSS4-B23APb3c.js → chunk-5VM5RSS4-BZN9a2hI.js} +1 -1
  56. package/web/assets/{chunk-EX3LRPZG-BSVEB7db.js → chunk-EX3LRPZG-CrH6hhqq.js} +1 -1
  57. package/web/assets/{chunk-JWPE2WC7-B6qGd0ng.js → chunk-JWPE2WC7-Ddn7ZSp8.js} +1 -1
  58. package/web/assets/{chunk-MOJQB5TN-N4T1-ew1.js → chunk-MOJQB5TN-9lJ7f_hI.js} +1 -1
  59. package/web/assets/{chunk-RYQCIY6F-6oc6i_Xw.js → chunk-RYQCIY6F-QrFo_00s.js} +1 -1
  60. package/web/assets/{chunk-V7JOEXUC-Dftgm9ck.js → chunk-V7JOEXUC-Dn0lQjVf.js} +1 -1
  61. package/web/assets/{chunk-VR4S4FIN-DfjqwEsO.js → chunk-VR4S4FIN-DXVPySkB.js} +1 -1
  62. package/web/assets/{chunk-XXDRQBXY-CsFsQYow.js → chunk-XXDRQBXY-CBiFDOdZ.js} +1 -1
  63. package/web/assets/classDiagram-OUVF2IWQ-DCYNCJhe.js +1 -0
  64. package/web/assets/classDiagram-v2-EOCWNBFH-DCYNCJhe.js +1 -0
  65. package/web/assets/client-DBJY3XDL.js +43 -0
  66. package/web/assets/client-DKJs0w4y.js +6 -0
  67. package/web/assets/{command-D_9V8ejS.js → command-onjTnuXT.js} +1 -1
  68. package/web/assets/{copy-context-CevDWX59.js → copy-context-CciRaLNN.js} +1 -1
  69. package/web/assets/{cose-bilkent-JH36ORCC-DzKsIuDb.js → cose-bilkent-JH36ORCC-CGRic2um.js} +1 -1
  70. package/web/assets/{cynefin-VYW2F7L2-AL45ZHbw.js → cynefin-VYW2F7L2-Blk0Lu0l.js} +1 -1
  71. package/web/assets/{cynefinDiagram-TSTJHNR4-DSbBegxZ.js → cynefinDiagram-TSTJHNR4-DpnnHRDK.js} +1 -1
  72. package/web/assets/{dagre-VKFMJZFB-BaG6MGUE.js → dagre-VKFMJZFB-llzMokKp.js} +1 -1
  73. package/web/assets/{diagram-FQU43EPY-WbDotTX5.js → diagram-FQU43EPY-DC6xHovR.js} +1 -1
  74. package/web/assets/{diagram-G47NLZAW-DMbouYVI.js → diagram-G47NLZAW-B8GVS3EA.js} +1 -1
  75. package/web/assets/{diagram-NH7WQ7WH-DoEuaWd_.js → diagram-NH7WQ7WH-DWUaARfh.js} +1 -1
  76. package/web/assets/{diagram-OA4YK3LP-BjGd-PoH.js → diagram-OA4YK3LP-9Uaes_Yi.js} +1 -1
  77. package/web/assets/{diagram-WEI45ONY-CXHi27jF.js → diagram-WEI45ONY-DPPTBWud.js} +1 -1
  78. package/web/assets/{document-CVJCfWiQ.js → document-D23WzFsW.js} +3 -3
  79. package/web/assets/{ebnfDiagram-CCIWWBDH-B5vMUDOs.js → ebnfDiagram-CCIWWBDH-BPrKneNw.js} +1 -1
  80. package/web/assets/{entities-BTKzWJ-r.js → entities-DPg0B8vd.js} +1 -1
  81. package/web/assets/{erDiagram-Q63AITRT-DhO7ayzF.js → erDiagram-Q63AITRT-CFqYBcnb.js} +1 -1
  82. package/web/assets/{file-viewer-D1HK9J3A.js → file-viewer-Ck73SsLA.js} +1 -1
  83. package/web/assets/{flowDiagram-23GEKE2U-CublTUlC.js → flowDiagram-23GEKE2U-C7RpYqaT.js} +1 -1
  84. package/web/assets/folder-filter-bar-DzejUj87.js +4 -0
  85. package/web/assets/{folder-image-L_AjkRfI.js → folder-image-BjIicmQ6.js} +1 -1
  86. package/web/assets/folder-landing-CCIXZfkp.js +55 -0
  87. package/web/assets/folder-list-TuRwuTss.js +1 -0
  88. package/web/assets/folder-mobile-bar-Bqnfwi-z.js +1 -0
  89. package/web/assets/folder-plugin-view-CrinCX1o.js +2 -0
  90. package/web/assets/{ganttDiagram-NO4QXBWP-BLMJWerM.js → ganttDiagram-NO4QXBWP-Cz2sxU8F.js} +1 -1
  91. package/web/assets/{gitGraphDiagram-IHSO6WYX-Br0E6qyZ.js → gitGraphDiagram-IHSO6WYX-BSuz0yF-.js} +1 -1
  92. package/web/assets/index-DLn_2LPp.js +2 -0
  93. package/web/assets/index-uY9jLiL5.css +1 -0
  94. package/web/assets/{infoDiagram-FWYZ7A6U-Dgz0npJO.js → infoDiagram-FWYZ7A6U-CpeKgqoS.js} +1 -1
  95. package/web/assets/input-Bz_puamS.js +9 -0
  96. package/web/assets/{ishikawaDiagram-FXEZZL3T-rY5pdS3b.js → ishikawaDiagram-FXEZZL3T-5c2Tmknb.js} +1 -1
  97. package/web/assets/{journeyDiagram-5HDEW3XC-CKZALyR6.js → journeyDiagram-5HDEW3XC-Cu-wRkR4.js} +1 -1
  98. package/web/assets/{kanban-definition-HUTT4EX6-EGPY9Vda.js → kanban-definition-HUTT4EX6-BunSqkPe.js} +1 -1
  99. package/web/assets/{linear-DkJW9OwI.js → linear-C8e8bGVv.js} +1 -1
  100. package/web/assets/{mermaid.core-DVEpXYiF.js → mermaid.core-DLgjY1g2.js} +4 -4
  101. package/web/assets/{metadata-Bu_ffLUd.js → metadata-DEfJKlnd.js} +1 -1
  102. package/web/assets/{mindmap-definition-LN4V7U3C-BEOUVnDN.js → mindmap-definition-LN4V7U3C-W9ENYFOD.js} +1 -1
  103. package/web/assets/{pegDiagram-2B236MQR-DThjl3eK.js → pegDiagram-2B236MQR-0vPlHCFp.js} +1 -1
  104. package/web/assets/{pieDiagram-ENE6RG2P-DzmoGgw2.js → pieDiagram-ENE6RG2P-CakaveRV.js} +1 -1
  105. package/web/assets/{project-search-dialog-e2Jmhvfq.js → project-search-dialog-BSKA4peX.js} +1 -1
  106. package/web/assets/{quadrantDiagram-ABIIQ3AL-BqT8U3FS.js → quadrantDiagram-ABIIQ3AL-jxHw_egj.js} +1 -1
  107. package/web/assets/{railroadDiagram-RFXS5EU6-1k1tfPk1.js → railroadDiagram-RFXS5EU6-lIjDXPZt.js} +1 -1
  108. package/web/assets/{requirementDiagram-TGXJPOKE-bM4RTd2J.js → requirementDiagram-TGXJPOKE-C-JiT4bc.js} +1 -1
  109. package/web/assets/{sankeyDiagram-HTMAVEWB-TCgfqDLy.js → sankeyDiagram-HTMAVEWB-SuLPAw9X.js} +1 -1
  110. package/web/assets/{search-DQiFH2up.js → search-CKujW14f.js} +1 -1
  111. package/web/assets/{selection-popup-bqfgVd-L.js → selection-popup-CtxT9SWn.js} +3 -3
  112. package/web/assets/{sequenceDiagram-DBY2YBRQ-BRhGHI0i.js → sequenceDiagram-DBY2YBRQ-o9UwTkZR.js} +1 -1
  113. package/web/assets/{sizeCapture-X5ZJPWSS-EeUWgn0N.js → sizeCapture-X5ZJPWSS-ClbWGybF.js} +1 -1
  114. package/web/assets/{stateDiagram-2N3HPSRC-BSOG758d.js → stateDiagram-2N3HPSRC-Ce4avoWx.js} +1 -1
  115. package/web/assets/stateDiagram-v2-6OUMAXLB-CsrPTZoJ.js +1 -0
  116. package/web/assets/{swimlanes-5IMT3BWC-DpCo4aEm.js → swimlanes-5IMT3BWC-DMZI4XZT.js} +2 -2
  117. package/web/assets/swimlanesDiagram-G3AALYLV-pShSVcTd.js +8 -0
  118. package/web/assets/{timeline-definition-FHXFAJF6-BpaObe6u.js → timeline-definition-FHXFAJF6-CPIpIgPN.js} +1 -1
  119. package/web/assets/{vennDiagram-L72KCM5P-ClvUzW8B.js → vennDiagram-L72KCM5P-_rha8uDZ.js} +1 -1
  120. package/web/assets/view-context-BDtIUHQy.js +229 -0
  121. package/web/assets/{viewer-capabilities-CFBwvpcI.js → viewer-capabilities-YYZqMs56.js} +1 -1
  122. package/web/assets/{wardleyDiagram-EHGQE667-Dg1CX0Fl.js → wardleyDiagram-EHGQE667-D4EAOS35.js} +1 -1
  123. package/web/assets/{xychartDiagram-FW5EYKEG-C0c9zQTr.js → xychartDiagram-FW5EYKEG-DR8PwXpe.js} +1 -1
  124. package/web/index.html +2 -2
  125. package/kernel/packages/agents/eslint.config.js +0 -10
  126. package/kernel/packages/agents/package.json +0 -75
  127. package/kernel/packages/agents/src/agent-card.tsx +0 -334
  128. package/kernel/packages/agents/src/client.tsx +0 -13
  129. package/kernel/packages/agents/src/index.ts +0 -18
  130. package/kernel/packages/agents/src/manifest.ts +0 -7
  131. package/kernel/packages/agents/src/node.ts +0 -447
  132. package/kernel/packages/agents/src/profiles.ts +0 -137
  133. package/kernel/packages/agents/src/resolve.ts +0 -69
  134. package/kernel/packages/agents/src/server.ts +0 -60
  135. package/kernel/packages/agents/src/types.ts +0 -82
  136. package/kernel/packages/agents/src/viewer.tsx +0 -370
  137. package/kernel/packages/agents/test/profiles.test.mjs +0 -88
  138. package/kernel/packages/agents/test/resolve.test.mjs +0 -729
  139. package/kernel/packages/agents/tsconfig.json +0 -17
  140. package/kernel/packages/api/test/agents-loading.test.ts +0 -94
  141. package/kernel/packages/ui/src/components/folder-skill-list.tsx +0 -62
  142. package/web/assets/app-Debw-lUT.js +0 -2
  143. package/web/assets/app-options-UU_JlJQv.js +0 -25
  144. package/web/assets/channel-BgYLn5Cm.js +0 -1
  145. package/web/assets/classDiagram-OUVF2IWQ-Fzhmfbkt.js +0 -1
  146. package/web/assets/classDiagram-v2-EOCWNBFH-Fzhmfbkt.js +0 -1
  147. package/web/assets/client-C1WdY7jQ.js +0 -22
  148. package/web/assets/client-D__L4Hl0.js +0 -17
  149. package/web/assets/client-DoMhF9Tv.js +0 -5
  150. package/web/assets/folder-filter-bar-BHz7s9Vb.js +0 -4
  151. package/web/assets/folder-landing-Byh8W67L.js +0 -54
  152. package/web/assets/folder-list-jMyo-7pV.js +0 -1
  153. package/web/assets/folder-metadata-bCocvxpP.js +0 -2
  154. package/web/assets/folder-mobile-bar-DvvyOS-a.js +0 -1
  155. package/web/assets/folder-plugin-view-sib0Dwfw.js +0 -3
  156. package/web/assets/index-4Xh9UKim.js +0 -1
  157. package/web/assets/index-Bu75S2qS.js +0 -1
  158. package/web/assets/index-CPcMcCUY.js +0 -2
  159. package/web/assets/index-CXxkvzNJ.css +0 -1
  160. package/web/assets/select-CkOwL81C.js +0 -30
  161. package/web/assets/stateDiagram-v2-6OUMAXLB-DUeGDz1r.js +0 -1
  162. package/web/assets/swimlanesDiagram-G3AALYLV-BGOaCfyV.js +0 -8
  163. package/web/assets/use-folder-filter-hA6fa7nm.js +0 -1
  164. package/web/assets/view-context-CVvDw2JM.js +0 -229
@@ -1,447 +0,0 @@
1
- import { lstat, readFile, realpath, stat } from "node:fs/promises"
2
- import { homedir } from "node:os"
3
- import { basename, dirname, resolve } from "node:path"
4
-
5
- import type { ClientProfile } from "./profiles.ts"
6
- import type {
7
- AgentChainEntry,
8
- AgentClientId,
9
- AgentSource,
10
- AgentsResolution,
11
- AgentWarning,
12
- ClientFilter,
13
- SourceLevel,
14
- } from "./types.ts"
15
- import { CLIENT_ORDER, PROFILES } from "./profiles.ts"
16
- import { dedupeSources, sortSources } from "./resolve.ts"
17
-
18
- export type ResolveAgentsOptions = {
19
- folder: string
20
- client?: ClientFilter
21
- /** Home directory used to expand `~` global paths. Defaults to the OS home. */
22
- homeDir?: string
23
- /** Skip reading file content, used for the navigation source count. */
24
- readText?: boolean
25
- /**
26
- * Folders connected to the working folder by repository links, resolved by
27
- * the host. Each linked folder's instruction files are inventoried as an
28
- * explicit `linked` row (never claimed as loaded by any client).
29
- */
30
- linkedFolders?: string[]
31
- }
32
-
33
- type BuildSourceInput = {
34
- clientId: AgentClientId
35
- filename: string
36
- level: SourceLevel
37
- literalPath: string
38
- override: boolean
39
- readText: boolean
40
- }
41
-
42
- type Candidate = {
43
- filename: string
44
- relative: string
45
- }
46
-
47
- async function canonicalize(path: string): Promise<string | null> {
48
- try {
49
- return await realpath(path)
50
- } catch {
51
- return null
52
- }
53
- }
54
-
55
- async function hasGit(folder: string): Promise<boolean> {
56
- const entry = await lstat(resolve(folder, ".git")).catch(() => undefined)
57
- return Boolean(entry)
58
- }
59
-
60
- async function nearestGitRoot(folder: string): Promise<string | null> {
61
- let current = folder
62
- while (true) {
63
- if (await hasGit(current)) return current
64
- const parent = dirname(current)
65
- if (parent === current) return null
66
- current = parent
67
- }
68
- }
69
-
70
- /**
71
- * Nearest ancestor carrying a `raggle.json` workspace marker. Raggle
72
- * workspaces are the project boundary for this preview: the ancestor walk
73
- * stops there, so a folder inside a workspace only ever shows instructions
74
- * within that workspace (plus its global sources).
75
- */
76
- async function nearestRaggleRoot(folder: string): Promise<string | null> {
77
- let current = folder
78
- while (true) {
79
- const entry = await lstat(resolve(current, "raggle.json")).catch(
80
- () => undefined,
81
- )
82
- if (entry) return current
83
- const parent = dirname(current)
84
- if (parent === current) return null
85
- current = parent
86
- }
87
- }
88
-
89
- /**
90
- * Resolve the ancestor boundary for one client. `git` stops at the nearest
91
- * `.git` root. `root` walks to the filesystem root, like Stepladder and the
92
- * Pi and Claude Code context loaders. Only exact instruction filenames are
93
- * read at each ancestor, so the wide root walk never reads arbitrary content.
94
- */
95
- async function ancestorBoundary(
96
- folder: string,
97
- scope: ClientProfile["scope"],
98
- ): Promise<string> {
99
- let current = folder
100
- while (true) {
101
- if (scope === "git" && (await hasGit(current))) return current
102
- const parent = dirname(current)
103
- if (parent === current) return current
104
- current = parent
105
- }
106
- }
107
-
108
- function ancestorChain(folder: string, boundary: string): string[] {
109
- const chain: string[] = []
110
- let current = folder
111
- while (true) {
112
- chain.push(current)
113
- if (current === boundary) break
114
- const parent = dirname(current)
115
- if (parent === current) break
116
- current = parent
117
- }
118
- return chain
119
- }
120
-
121
- function sourceLevel(
122
- index: number,
123
- total: number,
124
- scope: ClientProfile["scope"],
125
- ): SourceLevel {
126
- if (total === 1) return "project"
127
- if (index === 0) return "project"
128
- if (scope === "git" && index === total - 1) return "repository"
129
- return "parent"
130
- }
131
-
132
- async function isRegularFile(path: string): Promise<boolean> {
133
- const details = await stat(path).catch(() => undefined)
134
- return details?.isFile() === true
135
- }
136
-
137
- /**
138
- * Merge the ancestor walks of every profile into one ordered chain. The
139
- * working folder stays first; deeper duplicates keep the filenames found by
140
- * each client. Used for the `all` filter — a single client passes one chain
141
- * straight through.
142
- */
143
- function mergeChains(chains: AgentChainEntry[][]): AgentChainEntry[] {
144
- const byDirectory = new Map<string, AgentChainEntry>()
145
- const order: string[] = []
146
- for (const chain of chains) {
147
- for (const entry of chain) {
148
- const existing = byDirectory.get(entry.directory)
149
- if (!existing) {
150
- byDirectory.set(entry.directory, {
151
- ...entry,
152
- filenames: [...entry.filenames],
153
- })
154
- order.push(entry.directory)
155
- } else {
156
- for (const filename of entry.filenames) {
157
- if (!existing.filenames.includes(filename)) {
158
- existing.filenames.push(filename)
159
- }
160
- }
161
- }
162
- }
163
- }
164
- return order.map((directory) => byDirectory.get(directory)!)
165
- }
166
-
167
- async function selectFiles(
168
- directory: string,
169
- candidates: string[],
170
- mode: ClientProfile["filesMode"],
171
- ): Promise<Candidate[]> {
172
- if (mode === "all") {
173
- const found: Candidate[] = []
174
- for (const relative of candidates) {
175
- if (await isRegularFile(resolve(directory, relative))) {
176
- found.push({ filename: basename(relative), relative })
177
- }
178
- }
179
- return found
180
- }
181
- for (const relative of candidates) {
182
- if (await isRegularFile(resolve(directory, relative))) {
183
- return [{ filename: basename(relative), relative }]
184
- }
185
- }
186
- return []
187
- }
188
-
189
- function expandHome(path: string, homeDir: string): string {
190
- if (path === "~") return homeDir
191
- if (path.startsWith("~/")) return resolve(homeDir, path.slice(2))
192
- return path
193
- }
194
-
195
- function globalDirectory(
196
- profile: ClientProfile,
197
- homeDir: string,
198
- ): string | null {
199
- if (profile.globalDir === null) return null
200
- const envValue = profile.globalEnv
201
- ? process.env[profile.globalEnv]?.trim()
202
- : undefined
203
- if (envValue) return expandHome(envValue, homeDir)
204
- if (profile.globalDir === "") return homeDir
205
- return resolve(homeDir, profile.globalDir)
206
- }
207
-
208
- function readError(error: unknown): string {
209
- return error instanceof Error ? error.message : "Could not read file"
210
- }
211
-
212
- const INSTRUCTION_NAMES = new Set(
213
- Object.values(PROFILES)
214
- .flatMap((profile) => [...profile.files, ...profile.globalFiles])
215
- .map((path) => basename(path).toLowerCase()),
216
- )
217
-
218
- async function codexModelInstructionsFile(
219
- codexHome: string,
220
- ): Promise<string | null> {
221
- try {
222
- const content = await readFile(resolve(codexHome, "config.toml"), "utf8")
223
- const match =
224
- /^\s*model_instructions_file\s*=\s*["']?([^"'\n]+)["']?\s*$/m.exec(
225
- content,
226
- )
227
- return match?.[1]?.trim() ?? null
228
- } catch {
229
- return null
230
- }
231
- }
232
-
233
- /**
234
- * Realpath, authorize, and read one instruction file. The file is only read,
235
- * never executed. A regular file check rejects directories and special files
236
- * that a symlink could otherwise point at. Content is read in full.
237
- */
238
- async function loadSource(input: BuildSourceInput): Promise<AgentSource> {
239
- const real = await canonicalize(input.literalPath)
240
- if (!real) {
241
- return {
242
- id: input.literalPath,
243
- path: input.literalPath,
244
- sourcePaths: [input.literalPath],
245
- filename: input.filename,
246
- directory: dirname(input.literalPath),
247
- level: input.level,
248
- status: "unreadable",
249
- text: "",
250
- size: 0,
251
- clientIds: [input.clientId],
252
- override: input.override,
253
- applies: true,
254
- reason: "Path cannot be resolved",
255
- }
256
- }
257
-
258
- const record: AgentSource = {
259
- id: real,
260
- path: real,
261
- sourcePaths: [input.literalPath],
262
- filename: input.filename,
263
- directory: dirname(real),
264
- level: input.level,
265
- status: "loaded",
266
- text: "",
267
- size: 0,
268
- clientIds: [input.clientId],
269
- override: input.override,
270
- applies: true,
271
- }
272
-
273
- // A symlink may point anywhere; the resolved target must still be an
274
- // instruction file, never an arbitrary non-instruction file like a secret.
275
- if (!INSTRUCTION_NAMES.has(basename(real).toLowerCase())) {
276
- record.status = "unsafe"
277
- record.reason = "Symlink resolves to a non-instruction file"
278
- return record
279
- }
280
-
281
- const details = await stat(real).catch(() => undefined)
282
- if (!details?.isFile()) {
283
- record.status = "unreadable"
284
- record.reason = "Not a regular file"
285
- return record
286
- }
287
- record.size = details.size
288
-
289
- if (input.readText) {
290
- try {
291
- record.text = await readFile(real, "utf8")
292
- } catch (error) {
293
- record.status = "unreadable"
294
- record.reason = readError(error)
295
- }
296
- }
297
-
298
- return record
299
- }
300
-
301
- /**
302
- * Resolve every instruction source that applies to a working folder for one
303
- * client filter. The result is read-only and ordered project → parents →
304
- * global. Child and sibling folders are never scanned. Each client uses its
305
- * own documented ancestor boundary.
306
- */
307
- export async function resolveAgents(
308
- options: ResolveAgentsOptions,
309
- ): Promise<AgentsResolution> {
310
- const homeDir = options.homeDir ?? homedir()
311
- const client = options.client ?? "all"
312
- const readText = options.readText ?? true
313
-
314
- const folder = await canonicalize(options.folder)
315
- if (!folder) throw new Error(`Folder not found: ${options.folder}`)
316
-
317
- const repositoryRoot = await nearestGitRoot(folder)
318
- const workspaceRoot = await nearestRaggleRoot(folder)
319
- const profileIds: AgentClientId[] = client === "all" ? CLIENT_ORDER : [client]
320
-
321
- const sources: AgentSource[] = []
322
- const warnings: AgentWarning[] = []
323
- const profileChains: AgentChainEntry[][] = []
324
-
325
- for (const profileId of profileIds) {
326
- const profile = PROFILES[profileId]
327
- // A Raggle workspace root caps the walk for every client; without one,
328
- // each client falls back to its own documented ancestor boundary.
329
- const boundary =
330
- workspaceRoot ?? (await ancestorBoundary(folder, profile.scope))
331
- const ancestors = ancestorChain(folder, boundary)
332
- const chain: AgentChainEntry[] = []
333
-
334
- for (let index = 0; index < ancestors.length; index += 1) {
335
- const directory = ancestors[index]
336
- if (!directory) continue
337
- const level = sourceLevel(index, ancestors.length, profile.scope)
338
- const files = await selectFiles(
339
- directory,
340
- profile.files,
341
- profile.filesMode,
342
- )
343
- chain.push({
344
- directory,
345
- level,
346
- filenames: files.map((file) => file.filename),
347
- })
348
- for (const file of files) {
349
- const source = await loadSource({
350
- clientId: profileId,
351
- filename: file.filename,
352
- level,
353
- literalPath: resolve(directory, file.relative),
354
- override: file.filename === "AGENTS.override.md",
355
- readText,
356
- })
357
- sources.push(source)
358
- }
359
- }
360
-
361
- profileChains.push(chain)
362
-
363
- // Linked project folders (the host's connected-notes relation graph)
364
- // contribute their root instruction files as an explicit inventory row.
365
- // Real clients do not load these automatically — the row is labelled
366
- // `linked` so the preview never claims a client applies them.
367
- for (const linkedFolder of options.linkedFolders ?? []) {
368
- const canonicalLinked = await canonicalize(linkedFolder)
369
- if (!canonicalLinked || canonicalLinked === folder) continue
370
- const linkedFiles = await selectFiles(
371
- canonicalLinked,
372
- profile.files,
373
- profile.filesMode,
374
- )
375
- chain.push({
376
- directory: canonicalLinked,
377
- level: "linked",
378
- filenames: linkedFiles.map((file) => file.filename),
379
- })
380
- for (const file of linkedFiles) {
381
- const source = await loadSource({
382
- clientId: profileId,
383
- filename: file.filename,
384
- level: "linked",
385
- literalPath: resolve(canonicalLinked, file.relative),
386
- override: file.filename === "AGENTS.override.md",
387
- readText,
388
- })
389
- sources.push(source)
390
- }
391
- }
392
-
393
- const globalDir = globalDirectory(profile, homeDir)
394
- if (globalDir) {
395
- const globals = await selectFiles(
396
- globalDir,
397
- profile.globalFiles,
398
- profile.globalMode,
399
- )
400
- for (const file of globals) {
401
- const source = await loadSource({
402
- clientId: profileId,
403
- filename: file.filename,
404
- level: "global",
405
- literalPath: resolve(globalDir, file.relative),
406
- override: file.filename === "AGENTS.override.md",
407
- readText,
408
- })
409
- sources.push(source)
410
- }
411
- if (globals.length === 0 && profile.globalFiles.length > 0) {
412
- warnings.push({
413
- kind: "missing",
414
- path: globalDir,
415
- clientIds: [profileId],
416
- reason: "No global instruction file found in this directory",
417
- })
418
- }
419
- }
420
-
421
- if (profileId === "codex") {
422
- const codexHome = globalDirectory(profile, homeDir)
423
- if (codexHome) {
424
- const fallback = await codexModelInstructionsFile(codexHome)
425
- if (fallback) {
426
- warnings.push({
427
- kind: "unsupported",
428
- clientIds: [profileId],
429
- reason: `Codex model_instructions_file (${fallback}) is configured but not applied here`,
430
- })
431
- }
432
- }
433
- }
434
- }
435
-
436
- const deduped = dedupeSources(sources)
437
- const ordered = sortSources(deduped)
438
-
439
- return {
440
- folder,
441
- repositoryRoot,
442
- client,
443
- sources: ordered,
444
- warnings,
445
- chain: mergeChains(profileChains),
446
- }
447
- }
@@ -1,137 +0,0 @@
1
- import type { AgentClientId, ClientFilter } from "./types.ts"
2
-
3
- export type ClientProfile = {
4
- id: AgentClientId
5
- label: string
6
- /** Ancestor walk scope: `git` stops at the nearest repository root, `root` walks to the filesystem root. */
7
- scope: "root" | "git"
8
- /**
9
- * Candidate instruction files checked in each ancestor directory, in order.
10
- * A relative path may include a subdirectory, e.g. `.claude/CLAUDE.md`.
11
- */
12
- files: string[]
13
- /** `first` loads only the first existing candidate; `all` loads every candidate as a separate source. */
14
- filesMode: "first" | "all"
15
- /** Global directory relative to home, or `""` for home itself. `null` disables a global source. */
16
- globalDir: string | null
17
- /** Environment variable that replaces the whole global directory when set. */
18
- globalEnv?: string
19
- /** Candidate instruction files checked inside the global directory. */
20
- globalFiles: string[]
21
- /** Same mode as `filesMode`, applied to the global directory. */
22
- globalMode: "first" | "all"
23
- /** True when discovery rules were verified against client documentation or source. */
24
- verified: boolean
25
- /** What was verified and where the rules come from. */
26
- note: string
27
- /** Explicit limits of this preview that are not part of the real client prompt. */
28
- limits: string
29
- }
30
-
31
- export const CLIENT_ORDER: AgentClientId[] = [
32
- "generic",
33
- "codex",
34
- "claude",
35
- "pi",
36
- ]
37
-
38
- export const PROFILES: Record<AgentClientId, ClientProfile> = {
39
- generic: {
40
- id: "generic",
41
- label: "Generic",
42
- scope: "root",
43
- files: ["AGENTS.md"],
44
- filesMode: "first",
45
- globalDir: ".agents",
46
- globalFiles: ["AGENTS.md"],
47
- globalMode: "first",
48
- verified: false,
49
- note: "Reference view of the ancestor AGENTS.md chain. No client execution behavior is claimed.",
50
- limits:
51
- "Walks to the filesystem root like Stepladder; only AGENTS.md files are read.",
52
- },
53
- codex: {
54
- id: "codex",
55
- label: "Codex",
56
- scope: "git",
57
- files: ["AGENTS.override.md", "AGENTS.md"],
58
- filesMode: "first",
59
- globalDir: ".codex",
60
- globalEnv: "CODEX_HOME",
61
- globalFiles: ["AGENTS.override.md", "AGENTS.md"],
62
- globalMode: "first",
63
- verified: true,
64
- note: "Verified from the Codex 0.154.0 binary: reads the repository root plus every directory from the CWD up to the root, and prefers AGENTS.override.md over AGENTS.md per directory.",
65
- limits:
66
- "A configured model_instructions_file fallback is not applied here; a warning appears when it is configured.",
67
- },
68
- claude: {
69
- id: "claude",
70
- label: "Claude Code",
71
- scope: "root",
72
- files: ["CLAUDE.md", ".claude/CLAUDE.md", "CLAUDE.local.md"],
73
- filesMode: "all",
74
- globalDir: ".claude",
75
- globalFiles: ["CLAUDE.md"],
76
- globalMode: "first",
77
- verified: true,
78
- note: "Verified from the Claude Code 2.1.274 binary: loads ~/.claude/CLAUDE.md and, walking to the filesystem root, each ancestor's CLAUDE.md, .claude/CLAUDE.md, and CLAUDE.local.md.",
79
- limits:
80
- "Conditional .claude/rules/*.md files and @path imports are not expanded here.",
81
- },
82
- pi: {
83
- id: "pi",
84
- label: "Pi",
85
- scope: "root",
86
- files: [
87
- "AGENTS.override.md",
88
- "AGENTS.md",
89
- "AGENTS.MD",
90
- "CLAUDE.md",
91
- "CLAUDE.MD",
92
- ],
93
- filesMode: "first",
94
- globalDir: ".pi/agent",
95
- globalEnv: "PI_CODING_AGENT_DIR",
96
- globalFiles: [
97
- "AGENTS.override.md",
98
- "AGENTS.md",
99
- "AGENTS.MD",
100
- "CLAUDE.md",
101
- "CLAUDE.MD",
102
- ],
103
- globalMode: "first",
104
- verified: true,
105
- note: "Verified from the Pi 0.85.1 source (resource-loader.js): first match wins from AGENTS.override.md, AGENTS.md, AGENTS.MD, CLAUDE.md, CLAUDE.MD, in each ancestor directory and in the global directory.",
106
- limits: "Uses the same candidate list for the global directory.",
107
- },
108
- }
109
-
110
- export const FILTERS: Array<{ id: ClientFilter; label: string }> = [
111
- { id: "generic", label: "Generic" },
112
- { id: "codex", label: "Codex" },
113
- { id: "claude", label: "Claude Code" },
114
- { id: "pi", label: "Pi" },
115
- ]
116
-
117
- export function clientLabel(id: AgentClientId) {
118
- return PROFILES[id].label
119
- }
120
-
121
- /**
122
- * Resolves the plugin-relative subpath to a single client. Pi is the default
123
- * client; unknown values (including the legacy `all` mode) fall back to it.
124
- */
125
- export function parseClientFilter(
126
- value: string | null | undefined,
127
- ): AgentClientId {
128
- if (
129
- value === "generic" ||
130
- value === "codex" ||
131
- value === "claude" ||
132
- value === "pi"
133
- ) {
134
- return value
135
- }
136
- return "pi"
137
- }
@@ -1,69 +0,0 @@
1
- import type { AgentSource, SourceLevel } from "./types.ts"
2
-
3
- export const LEVEL_ORDER: Record<SourceLevel, number> = {
4
- project: 0,
5
- parent: 1,
6
- repository: 2,
7
- linked: 3,
8
- global: 4,
9
- }
10
-
11
- function unique<T>(values: T[]): T[] {
12
- return [...new Set(values)]
13
- }
14
-
15
- function pathSegments(path: string): number {
16
- const normalized = path.replaceAll("\\", "/").replace(/\/+$/, "")
17
- if (!normalized) return 0
18
- return normalized.split("/").length
19
- }
20
-
21
- /**
22
- * Merge sources that point at the same physical file. The first occurrence
23
- * keeps its position and status; later occurrences contribute their client ids
24
- * and literal source paths, so a shared instruction file is shown once and
25
- * labelled with every client that loads it.
26
- */
27
- export function dedupeSources(sources: AgentSource[]): AgentSource[] {
28
- const merged = new Map<string, AgentSource>()
29
- for (const source of sources) {
30
- const existing = merged.get(source.id)
31
- if (!existing) {
32
- merged.set(source.id, {
33
- ...source,
34
- clientIds: [...source.clientIds],
35
- sourcePaths: unique([...source.sourcePaths]),
36
- })
37
- continue
38
- }
39
- existing.clientIds = unique([...existing.clientIds, ...source.clientIds])
40
- existing.sourcePaths = unique([
41
- ...existing.sourcePaths,
42
- ...source.sourcePaths,
43
- ])
44
- if (existing.status !== "loaded" && source.status === "loaded") {
45
- existing.status = source.status
46
- existing.text = source.text
47
- existing.size = source.size
48
- existing.reason = source.reason
49
- }
50
- if (source.override) existing.override = true
51
- }
52
- return [...merged.values()]
53
- }
54
-
55
- /**
56
- * Order sources project → parents → repository → global. Deeper directories
57
- * come before shallower ones within the ancestor levels, then the file name
58
- * breaks any remaining tie. This display order never changes a client's own
59
- * loading order.
60
- */
61
- export function sortSources(sources: AgentSource[]): AgentSource[] {
62
- return [...sources].sort((left, right) => {
63
- const level = LEVEL_ORDER[left.level] - LEVEL_ORDER[right.level]
64
- if (level !== 0) return level
65
- const depth = pathSegments(right.directory) - pathSegments(left.directory)
66
- if (depth !== 0) return depth
67
- return left.filename.localeCompare(right.filename)
68
- })
69
- }
@@ -1,60 +0,0 @@
1
- import type {
2
- FolderPluginContext,
3
- FolderPluginData,
4
- FolderPluginDetection,
5
- } from "kennel-plugins/server"
6
-
7
- import type { AgentsResolution, ClientFilter } from "./types.ts"
8
- import { agentsManifest } from "./manifest.ts"
9
- import { resolveAgents } from "./node.ts"
10
- import { parseClientFilter } from "./profiles.ts"
11
-
12
- export type AgentsCreatePluginOptions = {
13
- /** Home directory used to expand `~` global paths. Intended for tests. */
14
- homeDir?: string
15
- }
16
-
17
- export function createAgentsPlugin(options: AgentsCreatePluginOptions = {}) {
18
- return {
19
- ...agentsManifest,
20
- async detect(context: FolderPluginContext): Promise<FolderPluginDetection> {
21
- try {
22
- const result: AgentsResolution = await resolveAgents({
23
- folder: context.folder,
24
- client: "all",
25
- homeDir: options.homeDir,
26
- linkedFolders: context.linkedFolders,
27
- readText: false,
28
- })
29
- return { count: result.sources.length, status: "available" }
30
- } catch {
31
- // The Agents item is always available, even when discovery fails.
32
- return { status: "available" }
33
- }
34
- },
35
- async handle(
36
- context: FolderPluginContext,
37
- subpath: string,
38
- ): Promise<FolderPluginData> {
39
- const client: ClientFilter = parseClientFilter(subpath.trim() || null)
40
- const result: AgentsResolution = await resolveAgents({
41
- folder: context.folder,
42
- client,
43
- homeDir: options.homeDir,
44
- linkedFolders: context.linkedFolders,
45
- })
46
- return {
47
- kind: "available",
48
- data: {
49
- client,
50
- folder: result.folder,
51
- repositoryRoot: result.repositoryRoot,
52
- sources: result.sources,
53
- status: "available",
54
- warnings: result.warnings,
55
- chain: result.chain,
56
- },
57
- }
58
- },
59
- }
60
- }