@bevel-software/platform-shared 0.14.0 → 0.15.1
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/git/types.d.ts +47 -1
- package/dist/git/types.d.ts.map +1 -1
- package/dist/workflow/events.d.ts +7 -0
- package/dist/workflow/events.d.ts.map +1 -1
- package/dist/workflow/events.js.map +1 -1
- package/dist/workflow/interface.d.ts +35 -2
- package/dist/workflow/interface.d.ts.map +1 -1
- package/dist/workflow/types.d.ts +44 -0
- package/dist/workflow/types.d.ts.map +1 -1
- package/dist/workspace/filename.d.ts.map +1 -1
- package/dist/workspace/filename.js +7 -3
- package/dist/workspace/filename.js.map +1 -1
- package/dist/workspace/kb-layout.d.ts +160 -21
- package/dist/workspace/kb-layout.d.ts.map +1 -1
- package/dist/workspace/kb-layout.js +291 -22
- package/dist/workspace/kb-layout.js.map +1 -1
- package/package.json +1 -1
- package/src/git/types.ts +46 -2
- package/src/workflow/events.ts +7 -0
- package/src/workflow/interface.ts +35 -2
- package/src/workflow/types.ts +27 -0
- package/src/workspace/filename.ts +7 -3
- package/src/workspace/kb-layout.ts +299 -26
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { branchSegment } from '../git/branchAuthor.js';
|
|
2
|
+
import { validateFilename } from './filename.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Top-level layout of the KB repo (inside the `KB_DIR_NAME` clone).
|
|
@@ -8,7 +9,8 @@ import { branchSegment } from '../git/branchAuthor.js';
|
|
|
8
9
|
*
|
|
9
10
|
* <kbDirName>/
|
|
10
11
|
* ├── KnowledgeBase/ ← all team ontologies live here (the knowledge graph)
|
|
11
|
-
* ├──
|
|
12
|
+
* ├── Skills/ ← shared skills, organised by ownership; plugins LINK to them
|
|
13
|
+
* ├── Plugins/ ← one folder per plugin: manifest, MCP servers, tools, links
|
|
12
14
|
* ├── Data/ ← agent-produced records; parsed like KnowledgeBase/
|
|
13
15
|
* ├── Agents/ ← .agent files — agent role configurations (not the graph)
|
|
14
16
|
* ├── Pipelines/ ← .pipeline files — execution-layer processes (not the graph)
|
|
@@ -24,23 +26,47 @@ import { branchSegment } from '../git/branchAuthor.js';
|
|
|
24
26
|
*
|
|
25
27
|
* These names are the single source of truth for both sides of the app:
|
|
26
28
|
* - Backend: the graph parser discovers ontologies under the
|
|
27
|
-
* {@link
|
|
29
|
+
* {@link ontologyRoots} (`KnowledgeBase/` and `Data/`); `Plugins/`,
|
|
28
30
|
* `Agents/`, `Pipelines/` (and anything else at the root) are ignored by
|
|
29
31
|
* parsing, validation, and the diagram.
|
|
30
32
|
* - Frontend: the file tree renders these root folders as distinct
|
|
31
33
|
* top-level sections.
|
|
32
34
|
*
|
|
33
35
|
* Don't hard-code these strings elsewhere — import them from here.
|
|
36
|
+
*
|
|
37
|
+
* CONFIGURABLE, WITH DEFAULTS. The three roots a deployment may rename
|
|
38
|
+
* (`KnowledgeBase/`, `Skills/`, `Plugins/`) are `let` bindings applied by
|
|
39
|
+
* {@link configureKbLayout} — the backend from its deployment settings, the
|
|
40
|
+
* browser from `GET /api/config` — the same live-binding pattern as the branch
|
|
41
|
+
* model in `git/protected.ts`. Unlike the branch model they carry defaults, so
|
|
42
|
+
* nothing has to wait for configuration; but the same rule applies: read them
|
|
43
|
+
* inside a function body, never capture one at module scope.
|
|
34
44
|
*/
|
|
35
45
|
|
|
36
46
|
/** Folder under the repo root that contains all team ontologies. */
|
|
37
|
-
export
|
|
47
|
+
export let KNOWLEDGE_BASE_DIR = 'KnowledgeBase';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Folder under the repo root that holds SHARED skills, organised by ownership:
|
|
51
|
+
*
|
|
52
|
+
* Skills/<scope>/…/<skill>/SKILL.md a skill, at any depth
|
|
53
|
+
* Skills/<scope>/access.md who owns / may read the scope
|
|
54
|
+
*
|
|
55
|
+
* A skill's readability comes from ITS OWN path walk — the scope folders'
|
|
56
|
+
* `access.md` files — never from the plugins that link it. Plugins point at
|
|
57
|
+
* skills here by path (see `HEXIS_LINKED_SKILLS_KEY`), so one definition can
|
|
58
|
+
* ship in several plugins, and a skill in no plugin at all is a normal state.
|
|
59
|
+
* Inline skills under `Plugins/<Plugin>/skills/` remain supported (personal
|
|
60
|
+
* folders, legacy layouts); the catalog is the union of both trees.
|
|
61
|
+
*/
|
|
62
|
+
export let SKILLS_DIR = 'Skills';
|
|
38
63
|
|
|
39
64
|
/**
|
|
40
65
|
* Folder under the repo root that holds the plugins.
|
|
41
66
|
*
|
|
42
|
-
* Plugins/<Plugin>/plugin.json the Agent Plugins manifest
|
|
43
|
-
*
|
|
67
|
+
* Plugins/<Plugin>/plugin.json the Agent Plugins manifest; its
|
|
68
|
+
* hexis extension lists LINKED skills
|
|
69
|
+
* Plugins/<Plugin>/skills/<skill>/SKILL.md an inline skill
|
|
44
70
|
* Plugins/<Plugin>/mcp.json MCP servers
|
|
45
71
|
* Plugins/<Plugin>/software.bevel.hexis/tools/ http + inline `.tool` manuals
|
|
46
72
|
* Plugins/<Plugin>/access.md who can read/write the plugin
|
|
@@ -59,10 +85,11 @@ export const KNOWLEDGE_BASE_DIR = 'KnowledgeBase';
|
|
|
59
85
|
* `inline` types the spec has no slot for. `mcp`-type manuals are emitted as
|
|
60
86
|
* real `mcp.json` entries instead, so the portable half stays portable.
|
|
61
87
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
88
|
+
* A plugin's own `access.md` governs what the plugin FOLDER holds: the
|
|
89
|
+
* manifest, the MCP servers, the tools, and any inline skills. Shared skills
|
|
90
|
+
* under `Skills/` are governed by their own scope and are made visible to a
|
|
91
|
+
* plugin's members by granting the plugin's principal (`plugin/<Name>/read`)
|
|
92
|
+
* on the skill — ownership decides, the plugin is a view.
|
|
66
93
|
*
|
|
67
94
|
* A plugin is not a registry of unique names — it is a folder. The same
|
|
68
95
|
* integration may exist in several plugins as separate files (`Everyone/…/
|
|
@@ -74,7 +101,106 @@ export const KNOWLEDGE_BASE_DIR = 'KnowledgeBase';
|
|
|
74
101
|
* display casing. The lowercase slug the spec does constrain lives in the
|
|
75
102
|
* manifest's `name` field.
|
|
76
103
|
*/
|
|
77
|
-
export
|
|
104
|
+
export let PLUGINS_DIR = 'Plugins';
|
|
105
|
+
|
|
106
|
+
/** The three renameable roots, as a deployment declares them and `/api/config` serves them. */
|
|
107
|
+
export interface KbLayout {
|
|
108
|
+
knowledgeBaseDir: string;
|
|
109
|
+
skillsDir: string;
|
|
110
|
+
pluginsDir: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The layout a deployment gets when it names nothing. */
|
|
114
|
+
export const DEFAULT_KB_LAYOUT: Readonly<KbLayout> = Object.freeze({
|
|
115
|
+
knowledgeBaseDir: 'KnowledgeBase',
|
|
116
|
+
skillsDir: 'Skills',
|
|
117
|
+
pluginsDir: 'Plugins',
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* What is wrong with one root name, or null. A root is joined onto the repo
|
|
122
|
+
* root and onto `<dir>/.gitkeep`, so a separator or `..` would write outside
|
|
123
|
+
* the repository; a dot-prefixed name would be skipped by every scanner that
|
|
124
|
+
* treats dot-entries as bookkeeping; `.git` in any case would corrupt the clone.
|
|
125
|
+
*/
|
|
126
|
+
export function validateKbRootName(name: string): string | null {
|
|
127
|
+
const v = name.trim();
|
|
128
|
+
if (!v) return 'A folder name is required.';
|
|
129
|
+
if (v.includes('/') || v.includes('\\')) return 'Use a single folder name — no slashes.';
|
|
130
|
+
// The ONE rule for what a path segment may be called — the same one every
|
|
131
|
+
// file and folder made through the platform passes (reserved Windows
|
|
132
|
+
// names, trailing dots, forbidden characters, length) — plus what a ROOT
|
|
133
|
+
// must not be: dot-prefixed, which every scanner skips as bookkeeping.
|
|
134
|
+
const asName = validateFilename(v);
|
|
135
|
+
if (asName) return asName;
|
|
136
|
+
if (v.startsWith('.')) return 'The name can\'t start with a dot.';
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* What is wrong with a layout, or null — the same rule {@link configureKbLayout}
|
|
142
|
+
* enforces, without applying anything. Separate so the setup screen can judge a
|
|
143
|
+
* proposed layout before it is saved. The three names must differ, compared
|
|
144
|
+
* case-insensitively: the workspaces live on case-insensitive filesystems too,
|
|
145
|
+
* where `Skills` and `skills` are one folder.
|
|
146
|
+
*/
|
|
147
|
+
export function validateKbLayout(layout: KbLayout): string | null {
|
|
148
|
+
for (const [label, value] of [
|
|
149
|
+
['knowledge base', layout.knowledgeBaseDir],
|
|
150
|
+
['skills', layout.skillsDir],
|
|
151
|
+
['plugins', layout.pluginsDir],
|
|
152
|
+
] as const) {
|
|
153
|
+
const problem = validateKbRootName(value ?? '');
|
|
154
|
+
if (problem) return `The ${label} folder: ${problem}`;
|
|
155
|
+
}
|
|
156
|
+
const names = [layout.knowledgeBaseDir, layout.skillsDir, layout.pluginsDir].map((n) =>
|
|
157
|
+
n.trim().toLowerCase(),
|
|
158
|
+
);
|
|
159
|
+
if (new Set(names).size !== names.length) {
|
|
160
|
+
return 'The knowledge base, skills and plugins folders must have three different names.';
|
|
161
|
+
}
|
|
162
|
+
// The fixed reserved roots are taken too: naming the skills folder `Data`
|
|
163
|
+
// would give one directory two reserved roles.
|
|
164
|
+
const fixed = [DATA_DIR, AGENTS_DIR, PIPELINES_DIR].map((n) => n.toLowerCase());
|
|
165
|
+
const clash = names.find((n) => fixed.includes(n));
|
|
166
|
+
if (clash) return `"${clash}" is a reserved folder name (${[DATA_DIR, AGENTS_DIR, PIPELINES_DIR].join(', ')}).`;
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Apply the layout. Called once during boot on each side; throws on an invalid
|
|
172
|
+
* one so a bad deployment setting fails beside the rest of the wiring rather
|
|
173
|
+
* than scattering a half-renamed tree. Applying the defaults is a no-op.
|
|
174
|
+
*/
|
|
175
|
+
export function configureKbLayout(layout: KbLayout): void {
|
|
176
|
+
const problem = validateKbLayout(layout);
|
|
177
|
+
if (problem) throw new Error(problem);
|
|
178
|
+
KNOWLEDGE_BASE_DIR = layout.knowledgeBaseDir.trim();
|
|
179
|
+
SKILLS_DIR = layout.skillsDir.trim();
|
|
180
|
+
PLUGINS_DIR = layout.pluginsDir.trim();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** The layout currently in effect. */
|
|
184
|
+
export function currentKbLayout(): KbLayout {
|
|
185
|
+
return { knowledgeBaseDir: KNOWLEDGE_BASE_DIR, skillsDir: SKILLS_DIR, pluginsDir: PLUGINS_DIR };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Render the layout placeholders a managed template carries —
|
|
190
|
+
* `{{knowledgeBaseDir}}`, `{{skillsDir}}`, `{{pluginsDir}}` — with the
|
|
191
|
+
* names in effect. The packaged `AGENTS.md` and `.bevelignore` are written
|
|
192
|
+
* this way so a deployment that renamed its roots hands the agent a guide
|
|
193
|
+
* that names the folders it will actually find. Text without placeholders
|
|
194
|
+
* passes through unchanged.
|
|
195
|
+
*/
|
|
196
|
+
export function renderKbLayoutPlaceholders(text: string, layout: KbLayout = currentKbLayout()): string {
|
|
197
|
+
// Replacer FUNCTIONS: a string replacement would interpret `$&`, `$$` and
|
|
198
|
+
// friends inside a folder name, and `$` is a legal character in one.
|
|
199
|
+
return text
|
|
200
|
+
.replaceAll('{{knowledgeBaseDir}}', () => layout.knowledgeBaseDir)
|
|
201
|
+
.replaceAll('{{skillsDir}}', () => layout.skillsDir)
|
|
202
|
+
.replaceAll('{{pluginsDir}}', () => layout.pluginsDir);
|
|
203
|
+
}
|
|
78
204
|
|
|
79
205
|
/**
|
|
80
206
|
* The pre-rename name of {@link PLUGINS_DIR}. Referenced ONLY by the migration
|
|
@@ -104,6 +230,99 @@ export const HEXIS_EXTENSION_NS = 'software.bevel.hexis';
|
|
|
104
230
|
/** UTCP manuals whose `http`/`inline` types the spec cannot express. */
|
|
105
231
|
export const HEXIS_TOOLS_DIR = `${HEXIS_EXTENSION_NS}/tools`;
|
|
106
232
|
|
|
233
|
+
/**
|
|
234
|
+
* The manifest key under which a plugin LINKS shared skills:
|
|
235
|
+
*
|
|
236
|
+
* plugin.json → extensions["software.bevel.hexis"].skills: [
|
|
237
|
+
* "Skills/Engineering/deploy", ← one skill folder
|
|
238
|
+
* "Skills/Sales" ← a folder of skills: every skill beneath
|
|
239
|
+
* ]
|
|
240
|
+
*
|
|
241
|
+
* Entries are repo-root-relative folder paths. A plugin's effective skill set
|
|
242
|
+
* is its inline `skills/` folder PLUS everything these roots resolve to. The
|
|
243
|
+
* spec reserves `extensions` for exactly this kind of client-specific data, so
|
|
244
|
+
* a conformant client that ignores it still gets a valid manifest; the
|
|
245
|
+
* compiled distribution copies the linked skills in for it.
|
|
246
|
+
*
|
|
247
|
+
* Linking is a reference, not a grant: a member of the plugin can read a
|
|
248
|
+
* linked skill only because the skill's own access rules name the plugin's
|
|
249
|
+
* principal (`plugin/<Name>/read`). The link service writes both together.
|
|
250
|
+
*/
|
|
251
|
+
export const HEXIS_LINKED_SKILLS_KEY = 'skills';
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Normalise a linked-skill root, or null when it cannot be one: a
|
|
255
|
+
* repo-root-relative POSIX folder path with no `..`, no leading slash, no
|
|
256
|
+
* backslashes and no empty segments. Trailing slashes are dropped.
|
|
257
|
+
*/
|
|
258
|
+
export function normalizeSkillRoot(raw: string): string | null {
|
|
259
|
+
if (typeof raw !== 'string') return null;
|
|
260
|
+
const trimmed = raw.trim();
|
|
261
|
+
if (!trimmed || trimmed.includes('\\') || trimmed.startsWith('/')) return null;
|
|
262
|
+
const segments = trimmed.split('/');
|
|
263
|
+
// Only TRAILING slashes are forgiven; an empty segment anywhere else
|
|
264
|
+
// (`Skills//deploy`) is a malformed path, not a spelling of a valid one.
|
|
265
|
+
while (segments.length > 0 && segments[segments.length - 1] === '') segments.pop();
|
|
266
|
+
if (segments.length === 0) return null;
|
|
267
|
+
if (segments.some((s) => s === '' || s === '.' || s === '..')) return null;
|
|
268
|
+
return segments.join('/');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* The linked-skill roots a parsed manifest declares — invalid entries are
|
|
273
|
+
* dropped, duplicates collapsed, order kept. A manifest with no extension
|
|
274
|
+
* block links nothing.
|
|
275
|
+
*/
|
|
276
|
+
export function linkedSkillRoots(manifest: unknown): string[] {
|
|
277
|
+
if (typeof manifest !== 'object' || manifest === null || Array.isArray(manifest)) return [];
|
|
278
|
+
const ext = (manifest as Record<string, unknown>).extensions;
|
|
279
|
+
if (typeof ext !== 'object' || ext === null) return [];
|
|
280
|
+
const ns = (ext as Record<string, unknown>)[HEXIS_EXTENSION_NS];
|
|
281
|
+
if (typeof ns !== 'object' || ns === null) return [];
|
|
282
|
+
const raw = (ns as Record<string, unknown>)[HEXIS_LINKED_SKILLS_KEY];
|
|
283
|
+
if (!Array.isArray(raw)) return [];
|
|
284
|
+
const out: string[] = [];
|
|
285
|
+
for (const entry of raw) {
|
|
286
|
+
const root = normalizeSkillRoot(typeof entry === 'string' ? entry : '');
|
|
287
|
+
if (root !== null && !out.includes(root)) out.push(root);
|
|
288
|
+
}
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* The manifest with its linked-skill roots REPLACED by `roots`, every other
|
|
294
|
+
* byte of the object preserved (the MCP extension block beside it, the
|
|
295
|
+
* portable fields above it). An empty list removes the key rather than
|
|
296
|
+
* leaving `skills: []` behind.
|
|
297
|
+
*/
|
|
298
|
+
export function withLinkedSkillRoots(
|
|
299
|
+
manifest: Record<string, unknown>,
|
|
300
|
+
roots: readonly string[],
|
|
301
|
+
): Record<string, unknown> {
|
|
302
|
+
const extensions =
|
|
303
|
+
typeof manifest.extensions === 'object' && manifest.extensions !== null && !Array.isArray(manifest.extensions)
|
|
304
|
+
? { ...(manifest.extensions as Record<string, unknown>) }
|
|
305
|
+
: {};
|
|
306
|
+
const current = extensions[HEXIS_EXTENSION_NS];
|
|
307
|
+
const ns: Record<string, unknown> =
|
|
308
|
+
typeof current === 'object' && current !== null && !Array.isArray(current)
|
|
309
|
+
? { ...(current as Record<string, unknown>) }
|
|
310
|
+
: {};
|
|
311
|
+
if (roots.length > 0) ns[HEXIS_LINKED_SKILLS_KEY] = [...roots];
|
|
312
|
+
else delete ns[HEXIS_LINKED_SKILLS_KEY];
|
|
313
|
+
if (Object.keys(ns).length > 0) extensions[HEXIS_EXTENSION_NS] = ns;
|
|
314
|
+
else delete extensions[HEXIS_EXTENSION_NS];
|
|
315
|
+
const out: Record<string, unknown> = { ...manifest };
|
|
316
|
+
if (Object.keys(extensions).length > 0) out.extensions = extensions;
|
|
317
|
+
else delete out.extensions;
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Whether `skillPath` (a skill folder) falls under `root` (a skill folder or a folder of skills). */
|
|
322
|
+
export function skillUnderRoot(skillPath: string, root: string): boolean {
|
|
323
|
+
return skillPath === root || skillPath.startsWith(`${root}/`);
|
|
324
|
+
}
|
|
325
|
+
|
|
107
326
|
/**
|
|
108
327
|
* The manifest `name` for a plugin folder: lowercased, anything outside
|
|
109
328
|
* `[a-z0-9.-]` folded to `-`, runs collapsed, ends trimmed to alphanumerics.
|
|
@@ -135,23 +354,53 @@ export const PLUGIN_MANIFEST_SCHEMA = `https://agent-plugins.org/schemas/${AGENT
|
|
|
135
354
|
export const PLUGIN_MCP_SCHEMA = `https://agent-plugins.org/schemas/${AGENT_PLUGINS_SCHEMA_VERSION}/mcp.schema.json`;
|
|
136
355
|
|
|
137
356
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
357
|
+
* The Agent Plugins `name`: a kebab-case identifier — lowercase letters and
|
|
358
|
+
* digits in hyphen-separated runs, nothing else. It is the plugin's IDENTITY:
|
|
359
|
+
* what the marketplace publishes it as, what the access principals are
|
|
360
|
+
* spelled from (`plugin/<name>/<verb>`), what the catalog and the URLs key
|
|
361
|
+
* on. `pluginManifestName` folds any spelling into one of these.
|
|
362
|
+
*/
|
|
363
|
+
export const PLUGIN_IDENTIFIER_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
364
|
+
|
|
365
|
+
export function isPluginIdentifier(name: unknown): name is string {
|
|
366
|
+
return typeof name === 'string' && PLUGIN_IDENTIFIER_RE.test(name);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* The identity of a plugin folder: the manifest's `name` when it IS an
|
|
371
|
+
* identifier, else the folder name folded into one. A manifest naming
|
|
372
|
+
* something that cannot be an identifier is not silently reinterpreted; the
|
|
373
|
+
* folder stands in, and discovery says so.
|
|
374
|
+
*/
|
|
375
|
+
export function pluginIdentityOf(manifest: Record<string, unknown> | null, folderName: string): string {
|
|
376
|
+
const declared = manifest?.name;
|
|
377
|
+
return isPluginIdentifier(declared) ? declared : pluginManifestName(folderName);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* What a person sees the plugin called: the manifest's `displayName` (the
|
|
382
|
+
* vendor field Claude Code shows in its picker; any casing, spaces allowed),
|
|
383
|
+
* else the folder name — which is what every plugin made before this field
|
|
384
|
+
* existed was called, so nothing renames itself on upgrade.
|
|
385
|
+
*/
|
|
386
|
+
export function pluginDisplayNameOf(manifest: Record<string, unknown> | null, folderName: string): string {
|
|
387
|
+
const declared = manifest?.displayName;
|
|
388
|
+
return typeof declared === 'string' && declared.trim() ? declared.trim() : folderName;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* A minimal, valid `plugin.json` for a plugin folder: the identifier the
|
|
393
|
+
* folder name folds into, and — when the folder is spelled differently — the
|
|
394
|
+
* folder's spelling as `displayName`, so a client's picker shows "Sales
|
|
395
|
+
* Team" for `sales-team`. Nothing else: `version`, `license` and the rest
|
|
396
|
+
* are metadata about a DISTRIBUTED package, and inventing values for a
|
|
397
|
+
* folder someone just made in the app would be asserting things nobody said.
|
|
148
398
|
*/
|
|
149
399
|
export function renderPluginManifest(folderName: string): string {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
)}\n`;
|
|
400
|
+
const name = pluginManifestName(folderName);
|
|
401
|
+
const manifest: Record<string, unknown> = { $schema: PLUGIN_MANIFEST_SCHEMA, name };
|
|
402
|
+
if (folderName !== name) manifest.displayName = folderName;
|
|
403
|
+
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
155
404
|
}
|
|
156
405
|
|
|
157
406
|
/**
|
|
@@ -184,6 +433,18 @@ export function isPersonalPluginFolder(folderName: string): boolean {
|
|
|
184
433
|
return folderName.startsWith(PERSONAL_PLUGIN_PREFIX);
|
|
185
434
|
}
|
|
186
435
|
|
|
436
|
+
/**
|
|
437
|
+
* THE structural rule for a personal shelf: a repo-relative folder that is a
|
|
438
|
+
* DIRECT child of the plugins root and carries the personal prefix. A deeper
|
|
439
|
+
* folder so named is just a name, and a plugin whose manifest name happens
|
|
440
|
+
* to start with the prefix is a plugin — discovery, the principal picker and
|
|
441
|
+
* the item pages all ask this one question of the FOLDER.
|
|
442
|
+
*/
|
|
443
|
+
export function isPersonalPluginDir(repoRelDir: string): boolean {
|
|
444
|
+
const segments = repoRelDir.split('/').filter(Boolean);
|
|
445
|
+
return segments.length === 2 && segments[0] === PLUGINS_DIR && isPersonalPluginFolder(segments[1]!);
|
|
446
|
+
}
|
|
447
|
+
|
|
187
448
|
/**
|
|
188
449
|
* The plugin a repo-root-relative path belongs to, or `null` for content that
|
|
189
450
|
* sits outside any plugin.
|
|
@@ -222,8 +483,20 @@ export const PIPELINES_DIR = 'Pipelines';
|
|
|
222
483
|
/**
|
|
223
484
|
* The roots whose subfolders are discovered as ontologies by the graph parser
|
|
224
485
|
* (each subfolder with both `NodeTypes/` and `Knowledge/` is an ontology).
|
|
486
|
+
* A function, not a constant: `KNOWLEDGE_BASE_DIR` is configurable, and a
|
|
487
|
+
* module-scope array would snapshot the default before configuration.
|
|
488
|
+
*/
|
|
489
|
+
export function ontologyRoots(): readonly string[] {
|
|
490
|
+
return [KNOWLEDGE_BASE_DIR, DATA_DIR];
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Every reserved root name, as currently configured — the set the file tree
|
|
495
|
+
* renders as its own sections rather than folding into Knowledge.
|
|
225
496
|
*/
|
|
226
|
-
export
|
|
497
|
+
export function reservedRootDirNames(): ReadonlySet<string> {
|
|
498
|
+
return new Set([KNOWLEDGE_BASE_DIR, SKILLS_DIR, PLUGINS_DIR, DATA_DIR, AGENTS_DIR, PIPELINES_DIR]);
|
|
499
|
+
}
|
|
227
500
|
|
|
228
501
|
/** The `Knowledge/` marker subfolder of an ontology (holds the graph nodes). */
|
|
229
502
|
export const KNOWLEDGE_DIR = 'Knowledge';
|