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.
- package/dist/cli/src/plugins/external.js +19 -7
- package/dist/cli/src/site/server-plugins.d.ts +1 -4
- package/dist/core/api-client.js +3 -11
- package/dist/core/runtime.js +84 -695
- package/dist/package/preview.js +99 -729
- package/dist/plugins/external.js +19 -7
- package/dist/site/server-plugins.d.ts +1 -4
- package/dist/site/server-plugins.js +28 -537
- package/kernel/apps/web/package.json +0 -1
- package/kernel/apps/web/src/app.css +0 -1
- package/kernel/apps/web/src/app.tsx +7 -24
- package/kernel/apps/web/src/folder-plugin-view.tsx +6 -3
- package/kernel/apps/web/src/plugins.ts +2 -14
- package/kernel/apps/web/vite.config.ts +16 -3
- package/kernel/packages/api/openapi.json +85 -224
- package/kernel/packages/api/package.json +0 -1
- package/kernel/packages/api/src/contracts/schemas.ts +1 -9
- package/kernel/packages/api/src/server/folder-metadata-plugin.ts +9 -151
- package/kernel/packages/api/src/server/plugins.ts +11 -26
- package/kernel/packages/api/src/shared/viewer-route.ts +3 -5
- package/kernel/packages/api-client/src/generated.d.ts +16 -53
- package/kernel/packages/markdown/README.md +2 -2
- package/kernel/packages/notes/package.json +1 -0
- package/kernel/packages/notes/src/client.tsx +0 -1
- package/kernel/packages/notes/src/manifest.ts +7 -0
- package/kernel/packages/notes/src/pages/notes-page.tsx +1 -1
- package/kernel/packages/notes/src/ui/folder-notes-list.tsx +52 -7
- package/kernel/packages/plugins/SKILL.md +3 -2
- package/kernel/packages/plugins/package.json +5 -1
- package/kernel/packages/plugins/src/hooks.ts +24 -0
- package/kernel/packages/plugins/src/index.ts +1 -7
- package/kernel/packages/plugins/src/page.ts +0 -7
- package/kernel/packages/plugins/src/types.ts +8 -0
- package/kernel/packages/plugins/tsconfig.json +1 -1
- package/kernel/packages/plugins/tsup.config.ts +1 -0
- package/kernel/packages/ui/src/components/folder-activity.tsx +16 -98
- package/kernel/packages/ui/src/components/folder-landing.tsx +4 -0
- package/kernel/packages/ui/src/components/pagination.tsx +88 -2
- package/kernel/packages/ui/src/components/plugin-icon.tsx +19 -0
- package/kernel/packages/ui/src/lib/folder-activity.ts +4 -17
- package/kernel/packages/ui/src/styles/folder-activity.css +1 -31
- package/package.json +2 -2
- package/web/assets/{abnfDiagram-VRR7QNED-Brm1vcGG.js → abnfDiagram-VRR7QNED-Dt7QEF5H.js} +1 -1
- package/web/assets/app-C1j063QX.js +2 -0
- package/web/assets/app-options-98-a6ivv.js +25 -0
- package/web/assets/{app-view-header-BSOI-wB6.js → app-view-header-BJ9CWTE1.js} +1 -1
- package/web/assets/{arc-zngHsMKV.js → arc-PFB0H7oD.js} +1 -1
- package/web/assets/{architectureDiagram-ZJ3FMSHR-BLklLqRP.js → architectureDiagram-ZJ3FMSHR-BHLqEXlZ.js} +1 -1
- package/web/assets/{ask-tray-B4SPj-fH.js → ask-tray-C3gBesHJ.js} +1 -1
- package/web/assets/{blockDiagram-677ZJIJ3-yhC2fEtu.js → blockDiagram-677ZJIJ3-UbnYoYDg.js} +1 -1
- package/web/assets/{c4Diagram-LMCZKHZV-DMcweQme.js → c4Diagram-LMCZKHZV-4OSx1V4W.js} +1 -1
- package/web/assets/channel-BuvY5cQN.js +1 -0
- package/web/assets/{chunk-2Q5K7J3B-DD-S6n8h.js → chunk-2Q5K7J3B-CgxJ1aJb.js} +1 -1
- package/web/assets/{chunk-32BRIVSS-DFIAKRv_.js → chunk-32BRIVSS-Bfwxg2LM.js} +1 -1
- package/web/assets/{chunk-5VM5RSS4-B23APb3c.js → chunk-5VM5RSS4-BZN9a2hI.js} +1 -1
- package/web/assets/{chunk-EX3LRPZG-BSVEB7db.js → chunk-EX3LRPZG-CrH6hhqq.js} +1 -1
- package/web/assets/{chunk-JWPE2WC7-B6qGd0ng.js → chunk-JWPE2WC7-Ddn7ZSp8.js} +1 -1
- package/web/assets/{chunk-MOJQB5TN-N4T1-ew1.js → chunk-MOJQB5TN-9lJ7f_hI.js} +1 -1
- package/web/assets/{chunk-RYQCIY6F-6oc6i_Xw.js → chunk-RYQCIY6F-QrFo_00s.js} +1 -1
- package/web/assets/{chunk-V7JOEXUC-Dftgm9ck.js → chunk-V7JOEXUC-Dn0lQjVf.js} +1 -1
- package/web/assets/{chunk-VR4S4FIN-DfjqwEsO.js → chunk-VR4S4FIN-DXVPySkB.js} +1 -1
- package/web/assets/{chunk-XXDRQBXY-CsFsQYow.js → chunk-XXDRQBXY-CBiFDOdZ.js} +1 -1
- package/web/assets/classDiagram-OUVF2IWQ-DCYNCJhe.js +1 -0
- package/web/assets/classDiagram-v2-EOCWNBFH-DCYNCJhe.js +1 -0
- package/web/assets/client-DBJY3XDL.js +43 -0
- package/web/assets/client-DKJs0w4y.js +6 -0
- package/web/assets/{command-D_9V8ejS.js → command-onjTnuXT.js} +1 -1
- package/web/assets/{copy-context-CevDWX59.js → copy-context-CciRaLNN.js} +1 -1
- package/web/assets/{cose-bilkent-JH36ORCC-DzKsIuDb.js → cose-bilkent-JH36ORCC-CGRic2um.js} +1 -1
- package/web/assets/{cynefin-VYW2F7L2-AL45ZHbw.js → cynefin-VYW2F7L2-Blk0Lu0l.js} +1 -1
- package/web/assets/{cynefinDiagram-TSTJHNR4-DSbBegxZ.js → cynefinDiagram-TSTJHNR4-DpnnHRDK.js} +1 -1
- package/web/assets/{dagre-VKFMJZFB-BaG6MGUE.js → dagre-VKFMJZFB-llzMokKp.js} +1 -1
- package/web/assets/{diagram-FQU43EPY-WbDotTX5.js → diagram-FQU43EPY-DC6xHovR.js} +1 -1
- package/web/assets/{diagram-G47NLZAW-DMbouYVI.js → diagram-G47NLZAW-B8GVS3EA.js} +1 -1
- package/web/assets/{diagram-NH7WQ7WH-DoEuaWd_.js → diagram-NH7WQ7WH-DWUaARfh.js} +1 -1
- package/web/assets/{diagram-OA4YK3LP-BjGd-PoH.js → diagram-OA4YK3LP-9Uaes_Yi.js} +1 -1
- package/web/assets/{diagram-WEI45ONY-CXHi27jF.js → diagram-WEI45ONY-DPPTBWud.js} +1 -1
- package/web/assets/{document-CVJCfWiQ.js → document-D23WzFsW.js} +3 -3
- package/web/assets/{ebnfDiagram-CCIWWBDH-B5vMUDOs.js → ebnfDiagram-CCIWWBDH-BPrKneNw.js} +1 -1
- package/web/assets/{entities-BTKzWJ-r.js → entities-DPg0B8vd.js} +1 -1
- package/web/assets/{erDiagram-Q63AITRT-DhO7ayzF.js → erDiagram-Q63AITRT-CFqYBcnb.js} +1 -1
- package/web/assets/{file-viewer-D1HK9J3A.js → file-viewer-Ck73SsLA.js} +1 -1
- package/web/assets/{flowDiagram-23GEKE2U-CublTUlC.js → flowDiagram-23GEKE2U-C7RpYqaT.js} +1 -1
- package/web/assets/folder-filter-bar-DzejUj87.js +4 -0
- package/web/assets/{folder-image-L_AjkRfI.js → folder-image-BjIicmQ6.js} +1 -1
- package/web/assets/folder-landing-CCIXZfkp.js +55 -0
- package/web/assets/folder-list-TuRwuTss.js +1 -0
- package/web/assets/folder-mobile-bar-Bqnfwi-z.js +1 -0
- package/web/assets/folder-plugin-view-CrinCX1o.js +2 -0
- package/web/assets/{ganttDiagram-NO4QXBWP-BLMJWerM.js → ganttDiagram-NO4QXBWP-Cz2sxU8F.js} +1 -1
- package/web/assets/{gitGraphDiagram-IHSO6WYX-Br0E6qyZ.js → gitGraphDiagram-IHSO6WYX-BSuz0yF-.js} +1 -1
- package/web/assets/index-DLn_2LPp.js +2 -0
- package/web/assets/index-uY9jLiL5.css +1 -0
- package/web/assets/{infoDiagram-FWYZ7A6U-Dgz0npJO.js → infoDiagram-FWYZ7A6U-CpeKgqoS.js} +1 -1
- package/web/assets/input-Bz_puamS.js +9 -0
- package/web/assets/{ishikawaDiagram-FXEZZL3T-rY5pdS3b.js → ishikawaDiagram-FXEZZL3T-5c2Tmknb.js} +1 -1
- package/web/assets/{journeyDiagram-5HDEW3XC-CKZALyR6.js → journeyDiagram-5HDEW3XC-Cu-wRkR4.js} +1 -1
- package/web/assets/{kanban-definition-HUTT4EX6-EGPY9Vda.js → kanban-definition-HUTT4EX6-BunSqkPe.js} +1 -1
- package/web/assets/{linear-DkJW9OwI.js → linear-C8e8bGVv.js} +1 -1
- package/web/assets/{mermaid.core-DVEpXYiF.js → mermaid.core-DLgjY1g2.js} +4 -4
- package/web/assets/{metadata-Bu_ffLUd.js → metadata-DEfJKlnd.js} +1 -1
- package/web/assets/{mindmap-definition-LN4V7U3C-BEOUVnDN.js → mindmap-definition-LN4V7U3C-W9ENYFOD.js} +1 -1
- package/web/assets/{pegDiagram-2B236MQR-DThjl3eK.js → pegDiagram-2B236MQR-0vPlHCFp.js} +1 -1
- package/web/assets/{pieDiagram-ENE6RG2P-DzmoGgw2.js → pieDiagram-ENE6RG2P-CakaveRV.js} +1 -1
- package/web/assets/{project-search-dialog-e2Jmhvfq.js → project-search-dialog-BSKA4peX.js} +1 -1
- package/web/assets/{quadrantDiagram-ABIIQ3AL-BqT8U3FS.js → quadrantDiagram-ABIIQ3AL-jxHw_egj.js} +1 -1
- package/web/assets/{railroadDiagram-RFXS5EU6-1k1tfPk1.js → railroadDiagram-RFXS5EU6-lIjDXPZt.js} +1 -1
- package/web/assets/{requirementDiagram-TGXJPOKE-bM4RTd2J.js → requirementDiagram-TGXJPOKE-C-JiT4bc.js} +1 -1
- package/web/assets/{sankeyDiagram-HTMAVEWB-TCgfqDLy.js → sankeyDiagram-HTMAVEWB-SuLPAw9X.js} +1 -1
- package/web/assets/{search-DQiFH2up.js → search-CKujW14f.js} +1 -1
- package/web/assets/{selection-popup-bqfgVd-L.js → selection-popup-CtxT9SWn.js} +3 -3
- package/web/assets/{sequenceDiagram-DBY2YBRQ-BRhGHI0i.js → sequenceDiagram-DBY2YBRQ-o9UwTkZR.js} +1 -1
- package/web/assets/{sizeCapture-X5ZJPWSS-EeUWgn0N.js → sizeCapture-X5ZJPWSS-ClbWGybF.js} +1 -1
- package/web/assets/{stateDiagram-2N3HPSRC-BSOG758d.js → stateDiagram-2N3HPSRC-Ce4avoWx.js} +1 -1
- package/web/assets/stateDiagram-v2-6OUMAXLB-CsrPTZoJ.js +1 -0
- package/web/assets/{swimlanes-5IMT3BWC-DpCo4aEm.js → swimlanes-5IMT3BWC-DMZI4XZT.js} +2 -2
- package/web/assets/swimlanesDiagram-G3AALYLV-pShSVcTd.js +8 -0
- package/web/assets/{timeline-definition-FHXFAJF6-BpaObe6u.js → timeline-definition-FHXFAJF6-CPIpIgPN.js} +1 -1
- package/web/assets/{vennDiagram-L72KCM5P-ClvUzW8B.js → vennDiagram-L72KCM5P-_rha8uDZ.js} +1 -1
- package/web/assets/view-context-BDtIUHQy.js +229 -0
- package/web/assets/{viewer-capabilities-CFBwvpcI.js → viewer-capabilities-YYZqMs56.js} +1 -1
- package/web/assets/{wardleyDiagram-EHGQE667-Dg1CX0Fl.js → wardleyDiagram-EHGQE667-D4EAOS35.js} +1 -1
- package/web/assets/{xychartDiagram-FW5EYKEG-C0c9zQTr.js → xychartDiagram-FW5EYKEG-DR8PwXpe.js} +1 -1
- package/web/index.html +2 -2
- package/kernel/packages/agents/eslint.config.js +0 -10
- package/kernel/packages/agents/package.json +0 -75
- package/kernel/packages/agents/src/agent-card.tsx +0 -334
- package/kernel/packages/agents/src/client.tsx +0 -13
- package/kernel/packages/agents/src/index.ts +0 -18
- package/kernel/packages/agents/src/manifest.ts +0 -7
- package/kernel/packages/agents/src/node.ts +0 -447
- package/kernel/packages/agents/src/profiles.ts +0 -137
- package/kernel/packages/agents/src/resolve.ts +0 -69
- package/kernel/packages/agents/src/server.ts +0 -60
- package/kernel/packages/agents/src/types.ts +0 -82
- package/kernel/packages/agents/src/viewer.tsx +0 -370
- package/kernel/packages/agents/test/profiles.test.mjs +0 -88
- package/kernel/packages/agents/test/resolve.test.mjs +0 -729
- package/kernel/packages/agents/tsconfig.json +0 -17
- package/kernel/packages/api/test/agents-loading.test.ts +0 -94
- package/kernel/packages/ui/src/components/folder-skill-list.tsx +0 -62
- package/web/assets/app-Debw-lUT.js +0 -2
- package/web/assets/app-options-UU_JlJQv.js +0 -25
- package/web/assets/channel-BgYLn5Cm.js +0 -1
- package/web/assets/classDiagram-OUVF2IWQ-Fzhmfbkt.js +0 -1
- package/web/assets/classDiagram-v2-EOCWNBFH-Fzhmfbkt.js +0 -1
- package/web/assets/client-C1WdY7jQ.js +0 -22
- package/web/assets/client-D__L4Hl0.js +0 -17
- package/web/assets/client-DoMhF9Tv.js +0 -5
- package/web/assets/folder-filter-bar-BHz7s9Vb.js +0 -4
- package/web/assets/folder-landing-Byh8W67L.js +0 -54
- package/web/assets/folder-list-jMyo-7pV.js +0 -1
- package/web/assets/folder-metadata-bCocvxpP.js +0 -2
- package/web/assets/folder-mobile-bar-DvvyOS-a.js +0 -1
- package/web/assets/folder-plugin-view-sib0Dwfw.js +0 -3
- package/web/assets/index-4Xh9UKim.js +0 -1
- package/web/assets/index-Bu75S2qS.js +0 -1
- package/web/assets/index-CPcMcCUY.js +0 -2
- package/web/assets/index-CXxkvzNJ.css +0 -1
- package/web/assets/select-CkOwL81C.js +0 -30
- package/web/assets/stateDiagram-v2-6OUMAXLB-DUeGDz1r.js +0 -1
- package/web/assets/swimlanesDiagram-G3AALYLV-BGOaCfyV.js +0 -8
- package/web/assets/use-folder-filter-hA6fa7nm.js +0 -1
- 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
|
-
}
|