@b-apm/integration-api 0.1.0
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/LICENSE +21 -0
- package/README.md +76 -0
- package/dist/index.d.mts +378 -0
- package/dist/index.mjs +452 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sdju
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# @b-apm/integration-api
|
|
2
|
+
|
|
3
|
+
> **UNSTABLE:** Early public release. APIs and on-disk layouts may change without a major bump. Not production-ready.
|
|
4
|
+
|
|
5
|
+
Shared **contracts, registry, and materialize helpers** for bapm integrations.
|
|
6
|
+
|
|
7
|
+
## Boundary
|
|
8
|
+
|
|
9
|
+
| Package | May depend on |
|
|
10
|
+
| --------------------------------------------------- | ---------------------------------------------------------------- |
|
|
11
|
+
| `@b-apm/core` | `@b-apm/integration-api` only (no concrete `@b-apm/integration-*`) |
|
|
12
|
+
| `@b-apm/integration-cursor` (and other integrations) | `@b-apm/integration-api` |
|
|
13
|
+
| CLI / tests | Register concrete integrations into a registry created here |
|
|
14
|
+
|
|
15
|
+
Core Install discovers primitives and calls `materialize` on registered integrations through this package. Integration packages implement detection, deploy roots, and disk writes — core never imports them.
|
|
16
|
+
|
|
17
|
+
## Loadable package export contract (object-map)
|
|
18
|
+
|
|
19
|
+
When a project manifest uses object-map `target` / `targets`, the CLI loads each map value as an npm package from the project cwd and registers a runtime `BapmIntegration`. A loadable package MUST expose (first match wins):
|
|
20
|
+
|
|
21
|
+
1. Named **`createIntegration`** — zero-arg factory returning `BapmIntegration` (preferred for third parties); or
|
|
22
|
+
2. Named **`createCursorIntegration`** (or an equivalent documented factory) returning `BapmIntegration`; or
|
|
23
|
+
3. **Default export** that is either a `BapmIntegration` object or a factory returning one.
|
|
24
|
+
|
|
25
|
+
The loaded instance MUST have non-empty `id`, `deployRoots` array, `detect`, and `materialize` (`configureMcp` / `compile` optional). Optional `mcpEnvMode: "bake" | "translate"` tells install whether to bake APM `${VAR}` placeholders before `configureMcp` (omitted ⇒ bake-compatible, Cursor default). `id` MUST equal the map key. Marketplace-output-only packages (no runtime hooks) are rejected. A successful `configureMcp` report MUST include a non-empty `configPath` (project-relative, absolute, or home-tilde form for home-scoped hosts).
|
|
26
|
+
|
|
27
|
+
See `@b-apm/integration-cursor` for a built-in reference and the VitePress architecture guide for the author how-to.
|
|
28
|
+
|
|
29
|
+
## Helpers
|
|
30
|
+
|
|
31
|
+
Optional fs/path helpers for host `materialize` (exported from the package root):
|
|
32
|
+
|
|
33
|
+
| Symbol | Role |
|
|
34
|
+
| --------------------------------- | ------------------------------------------------------------------ |
|
|
35
|
+
| `primitivesList` | Normalize array / `{ primitives }` sets |
|
|
36
|
+
| `primitivesMaterialize` | Dispatch primitives to `skill` / `instruction` / … handlers |
|
|
37
|
+
| `sanitizeName` | Path-safe single segment from a primitive name |
|
|
38
|
+
| `isUnderRoot` | Containment check under a deploy root |
|
|
39
|
+
| `assertUnderDeployRoots` | Refuse writes outside registered roots |
|
|
40
|
+
| `writeDeployedFile` | Assert roots + mkdir + write + `DeployedFile` inventory row |
|
|
41
|
+
| `readPrimitiveContent` | Inline content / source file / stub frontmatter |
|
|
42
|
+
| `toPosixRel` | Absolute → cwd-relative path with `/` separators |
|
|
43
|
+
| `findPackageRoot` | Nearest `apm.yml` / `bapm.yml` / `plugin.json` ancestor |
|
|
44
|
+
| `isWithin` | Path containment (`candidate` under `root`) |
|
|
45
|
+
| `listFiles` | Recursive file listing (absolute paths) |
|
|
46
|
+
| `copyPortableSkillDirectory` | Safe Agent Plugin skill tree copy (no symlink escape) |
|
|
47
|
+
| `materializeSkill` | Shared skill deploy (portable tree / SKILL.md / stub + inventory) |
|
|
48
|
+
| `renderPrimitivesMarkdown` | Deterministic compile markdown (AGENTS.md family) |
|
|
49
|
+
| `compileMarkdownReport` | Preview/write `CompileReport` for host compile hooks |
|
|
50
|
+
| `filterFrontmatterKeys` | Drop non-allowlisted YAML frontmatter keys |
|
|
51
|
+
| `SHARED_COMMAND_FRONTMATTER_KEYS` | Shared command FM allowlist (description/tools/model/…) |
|
|
52
|
+
| `HookOwnershipSidecar` | Type for `.*/bapm-hooks.json` owned map (flexible optional fields) |
|
|
53
|
+
| `readHookOwnershipSidecar` | Read ownership sidecar; missing/malformed → `{ owned: {} }` |
|
|
54
|
+
| `writeHookOwnershipSidecar` | Write `{ owned }` as pretty JSON + trailing newline |
|
|
55
|
+
| `stripOwnedHookCommands` | Filter event arrays by owned entry commands (no disk deletes) |
|
|
56
|
+
| `removeOwnedHookArtifacts` | Best-effort rm of owned `scripts` / `hookFile` / `hookFiles` |
|
|
57
|
+
| `copyHookScript` | Simple script copy + command rewrite (needle + caller `destRel`) |
|
|
58
|
+
|
|
59
|
+
Prefer `primitivesMaterialize({ skill() {…}, … })` over a manual `primitivesList` loop.
|
|
60
|
+
Use `materializeSkill({ destDir })` inside the `skill` handler — hosts only pick the path.
|
|
61
|
+
Use `writeDeployedFile` for single-file markdown/json deploys; use `renderPrimitivesMarkdown` + `compileMarkdownReport` inside `compile`.
|
|
62
|
+
Integrations keep host-specific detect + destinations; shared path/content plumbing lives here.
|
|
63
|
+
|
|
64
|
+
## Materialize report
|
|
65
|
+
|
|
66
|
+
`materialize` MAY return a `MaterializeReport`:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
type DeployedFile = { path: string; hash?: string };
|
|
70
|
+
type MaterializeReport = { deployedFiles: DeployedFile[] };
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- `path` — project-/cwd-relative harness path (e.g. `.agents/skills/hello/SKILL.md`)
|
|
74
|
+
- `hash` — optional; when omitted, core computes a stable content hash for lock inventory
|
|
75
|
+
|
|
76
|
+
Core uses this report only via `@b-apm/integration-api` to write `deployed_file_hashes` and drive orphan cleanup / frozen re-verify. There is no adapter catalog or MCP configure surface in this package.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Shared target contracts for core ↔ host packages.
|
|
4
|
+
*/
|
|
5
|
+
/** Target identifier (built-in like `cursor`, or vendor `x-<vendor>-<name>`). */
|
|
6
|
+
type TargetId = string;
|
|
7
|
+
/** Source attribution for a discovered primitive (OpenAPM pr-001). */
|
|
8
|
+
type PrimitiveSource = "local" | `dependency:${string}`;
|
|
9
|
+
/** Primitive kinds in the M4 discovery floor (skill | agent | instruction | …). */
|
|
10
|
+
type PrimitiveType = string;
|
|
11
|
+
/** Attributed primitive unit passed to materialize. */
|
|
12
|
+
type AttributedPrimitive = {
|
|
13
|
+
name: string;
|
|
14
|
+
type: PrimitiveType;
|
|
15
|
+
source: PrimitiveSource;
|
|
16
|
+
/** Absolute or project-relative path to the source file/dir. */
|
|
17
|
+
path: string;
|
|
18
|
+
/** Package name when source is dependency:<name>. */
|
|
19
|
+
packageName?: string;
|
|
20
|
+
/** Optional raw content hint; hosts may re-read from path. */
|
|
21
|
+
content?: string;
|
|
22
|
+
/** Complete portable Agent Plugin skill directory, when applicable. */
|
|
23
|
+
skillDirectory?: string;
|
|
24
|
+
/** Resolved portable plugin root for containment-aware targets. */
|
|
25
|
+
pluginRoot?: string;
|
|
26
|
+
/** Identifies the portable Agent Plugins skill format. */
|
|
27
|
+
format?: "agent-plugin";
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
};
|
|
30
|
+
/** Conflict-resolved set (or array) accepted by materialize. */
|
|
31
|
+
type AttributedPrimitiveSet = AttributedPrimitive[] | {
|
|
32
|
+
primitives: AttributedPrimitive[];
|
|
33
|
+
diagnostics?: unknown[];
|
|
34
|
+
};
|
|
35
|
+
/** Context supplied when invoking materialize. */
|
|
36
|
+
type MaterializeContext = {
|
|
37
|
+
cwd: string;
|
|
38
|
+
/** Active target id for this invocation. */
|
|
39
|
+
targetId: TargetId;
|
|
40
|
+
/** Deploy roots declared by the target (relative to cwd). */
|
|
41
|
+
deployRoots: string[];
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
};
|
|
44
|
+
/** One harness file written by materialize (cwd-relative path). */
|
|
45
|
+
type DeployedFile = {
|
|
46
|
+
/** Project-/cwd-relative path using `/` separators. */
|
|
47
|
+
path: string;
|
|
48
|
+
/** Optional content hash (host may omit; core can compute). */
|
|
49
|
+
hash?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Target-supplied primitive ownership for this deployment. Core uses this
|
|
52
|
+
* metadata for lock attribution and never infers it from a host file layout.
|
|
53
|
+
*/
|
|
54
|
+
primitive?: {
|
|
55
|
+
name: string;
|
|
56
|
+
packageName?: string;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Report returned by `materialize` so core can record lock inventory
|
|
61
|
+
* (`deployed_file_hashes`) without importing concrete host packages.
|
|
62
|
+
*/
|
|
63
|
+
type MaterializeReport = {
|
|
64
|
+
/** Registered target that owns these deployment entries when any are reported. */
|
|
65
|
+
targetId?: TargetId;
|
|
66
|
+
deployedFiles: DeployedFile[];
|
|
67
|
+
/** Non-fatal adapter notes (frontmatter drops, unsupported primitive skips, …). */
|
|
68
|
+
diagnostics?: Array<{
|
|
69
|
+
code?: string;
|
|
70
|
+
message: string;
|
|
71
|
+
[key: string]: unknown;
|
|
72
|
+
}>;
|
|
73
|
+
};
|
|
74
|
+
/** Detection predicate hook — true when this host should activate for the project. */
|
|
75
|
+
type TargetDetectFn = (ctx: {
|
|
76
|
+
cwd: string;
|
|
77
|
+
}) => boolean | Promise<boolean>;
|
|
78
|
+
/** A non-sensitive reason why a registered target was not detected. */
|
|
79
|
+
type TargetDetectionDiagnostic = {
|
|
80
|
+
targetId: TargetId;
|
|
81
|
+
message: string;
|
|
82
|
+
};
|
|
83
|
+
/** Result of evaluating every registered target exactly once. */
|
|
84
|
+
type DetectedTargetsResult = {
|
|
85
|
+
detectedIds: TargetId[];
|
|
86
|
+
diagnostics: TargetDetectionDiagnostic[];
|
|
87
|
+
};
|
|
88
|
+
/** Context owned by core while a target emits its compile output. */
|
|
89
|
+
type CompileContext = {
|
|
90
|
+
cwd: string;
|
|
91
|
+
outputFile?: string;
|
|
92
|
+
/** Core-owned write intent; false for validation and preview requests. */
|
|
93
|
+
write: boolean;
|
|
94
|
+
};
|
|
95
|
+
/** Target-owned compile emission result. Paths are project-relative. */
|
|
96
|
+
type CompileReport = {
|
|
97
|
+
path: string;
|
|
98
|
+
content: string;
|
|
99
|
+
wrote: boolean;
|
|
100
|
+
};
|
|
101
|
+
type CompileFn = (primitives: AttributedPrimitiveSet, context: CompileContext) => CompileReport | Promise<CompileReport>;
|
|
102
|
+
/**
|
|
103
|
+
* Host-agnostic MCP server definition passed to optional `configureMcp`.
|
|
104
|
+
* Concrete targets map transport, command, or URL into their native config shape.
|
|
105
|
+
*/
|
|
106
|
+
type McpServerConfig = {
|
|
107
|
+
name: string;
|
|
108
|
+
transport?: string;
|
|
109
|
+
type?: string;
|
|
110
|
+
command?: string;
|
|
111
|
+
args?: unknown[];
|
|
112
|
+
url?: string;
|
|
113
|
+
env?: Record<string, string>;
|
|
114
|
+
/** Package provenance (dependency name or local). */
|
|
115
|
+
packageName?: string;
|
|
116
|
+
[key: string]: unknown;
|
|
117
|
+
};
|
|
118
|
+
/** Context for optional MCP configure. */
|
|
119
|
+
type ConfigureMcpContext = {
|
|
120
|
+
cwd: string;
|
|
121
|
+
/** Active target id for this invocation. */
|
|
122
|
+
targetId?: TargetId;
|
|
123
|
+
/** Deploy roots declared by the target (relative to cwd). */
|
|
124
|
+
deployRoots?: string[];
|
|
125
|
+
[key: string]: unknown;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Report returned by `configureMcp` so core can record lock `mcp_*` inventory
|
|
129
|
+
* without importing concrete host packages.
|
|
130
|
+
*/
|
|
131
|
+
type ConfigureMcpReport = {
|
|
132
|
+
/** Registered target that owns the reported MCP deployment. */
|
|
133
|
+
targetId?: TargetId;
|
|
134
|
+
/**
|
|
135
|
+
* Path to the written target MCP config: project-/cwd-relative for project hosts,
|
|
136
|
+
* or absolute / `~/…` for home-scoped hosts.
|
|
137
|
+
*/
|
|
138
|
+
configPath: string;
|
|
139
|
+
/** Server names written/updated. */
|
|
140
|
+
servers?: string[];
|
|
141
|
+
deployedFiles?: DeployedFile[];
|
|
142
|
+
/** Explicit adapter decisions or unsupported-server diagnostics. */
|
|
143
|
+
diagnostics?: Array<{
|
|
144
|
+
code: string;
|
|
145
|
+
message: string;
|
|
146
|
+
server?: string;
|
|
147
|
+
}>;
|
|
148
|
+
};
|
|
149
|
+
/** Optional MCP configure hook — targets that lack it are skipped for MCP. */
|
|
150
|
+
type ConfigureMcpFn = (servers: McpServerConfig[] | Record<string, McpServerConfig>, ctx?: ConfigureMcpContext) => ConfigureMcpReport | Promise<ConfigureMcpReport>;
|
|
151
|
+
/**
|
|
152
|
+
* Install-time MCP env policy for a host.
|
|
153
|
+
* - `bake` (default when omitted): resolve APM `${VAR}` / `${env:VAR}` / `<VAR>` to literals before configure
|
|
154
|
+
* - `translate`: pass APM placeholders through; host writes runtime `${VAR}` forms
|
|
155
|
+
* Bapm `{bake:NAME}` directives are still resolved (fail-closed) for both modes.
|
|
156
|
+
*/
|
|
157
|
+
type McpEnvMode = "bake" | "translate";
|
|
158
|
+
/** Concrete target contract consumed by core only through `@b-apm/integration-api`. */
|
|
159
|
+
type BapmIntegration = {
|
|
160
|
+
id: TargetId;
|
|
161
|
+
/** Registered deploy root(s) relative to project cwd (tg-002). */
|
|
162
|
+
deployRoots: string[];
|
|
163
|
+
detect: TargetDetectFn;
|
|
164
|
+
materialize: (primitives: AttributedPrimitiveSet, ctx?: MaterializeContext) => void | MaterializeReport | Promise<void | MaterializeReport>;
|
|
165
|
+
getDeployRoots?: () => string[];
|
|
166
|
+
/** Optional host-agnostic MCP configure; targets without it are skipped for MCP. */
|
|
167
|
+
configureMcp?: ConfigureMcpFn;
|
|
168
|
+
/** Optional host-owned compile rendering and output placement. */
|
|
169
|
+
compile?: CompileFn;
|
|
170
|
+
/**
|
|
171
|
+
* Optional MCP env handling mode. Omitted ⇒ bake-compatible (Cursor default).
|
|
172
|
+
* Install reads this through the api contract — no host-id allowlists in core.
|
|
173
|
+
*/
|
|
174
|
+
mcpEnvMode?: McpEnvMode;
|
|
175
|
+
[key: string]: unknown;
|
|
176
|
+
};
|
|
177
|
+
type IntegrationRegistry = {
|
|
178
|
+
register(target: BapmIntegration): void;
|
|
179
|
+
list(): BapmIntegration[];
|
|
180
|
+
get(id: TargetId): BapmIntegration | undefined;
|
|
181
|
+
getAll(): BapmIntegration[];
|
|
182
|
+
/** Evaluate each registered detector once, treating failures as non-matches. */
|
|
183
|
+
detect(cwd: string): Promise<DetectedTargetsResult>;
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* Host-owned marketplace output. The neutral payload types deliberately avoid
|
|
187
|
+
* describing any host document schema; integrations own that representation.
|
|
188
|
+
*/
|
|
189
|
+
type MarketplaceOutputCapability = {
|
|
190
|
+
format: string;
|
|
191
|
+
defaultOutput: string;
|
|
192
|
+
map: (config: unknown, resolved: unknown[]) => Record<string, unknown>;
|
|
193
|
+
};
|
|
194
|
+
/** Marketplace-only integrations need no runtime detect or deployment hooks. */
|
|
195
|
+
type MarketplaceOutputIntegration = {
|
|
196
|
+
id: string;
|
|
197
|
+
marketplaceOutput: MarketplaceOutputCapability;
|
|
198
|
+
};
|
|
199
|
+
type MarketplaceOutputRegistry = {
|
|
200
|
+
register(integration: MarketplaceOutputIntegration): void;
|
|
201
|
+
list(): MarketplaceOutputIntegration[];
|
|
202
|
+
get(format: string): MarketplaceOutputIntegration | undefined;
|
|
203
|
+
};
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/registry.d.ts
|
|
206
|
+
/**
|
|
207
|
+
* Create an empty in-memory target registry for core Install and tests.
|
|
208
|
+
* No concrete host imports — callers register doubles or host packages.
|
|
209
|
+
*/
|
|
210
|
+
declare function createIntegrationRegistry(): IntegrationRegistry;
|
|
211
|
+
/** Create a registry for marketplace-output-only or combined integrations. */
|
|
212
|
+
declare function createMarketplaceOutputRegistry(): MarketplaceOutputRegistry;
|
|
213
|
+
//#endregion
|
|
214
|
+
//#region src/helpers.d.ts
|
|
215
|
+
/** Shared command frontmatter allowlist (Cursor / Claude Code / Grok-style prompts). */
|
|
216
|
+
declare const SHARED_COMMAND_FRONTMATTER_KEYS: ReadonlySet<string>;
|
|
217
|
+
/** Normalize `AttributedPrimitiveSet` (array or `{ primitives }`) to a list. */
|
|
218
|
+
declare function primitivesList(set: AttributedPrimitiveSet): AttributedPrimitive[];
|
|
219
|
+
/** Known primitive kinds dispatched by `primitivesMaterialize`. */
|
|
220
|
+
type PrimitiveMaterializeKind = "skill" | "instruction" | "agent" | "command" | "hook";
|
|
221
|
+
/** Context passed to each materialize handler. */
|
|
222
|
+
type PrimitiveMaterializeContext = {
|
|
223
|
+
/** Path-safe single segment from `primitive.name`. */
|
|
224
|
+
name: string;
|
|
225
|
+
/** Lowercased `primitive.type` (default `"skill"`). */
|
|
226
|
+
type: string;
|
|
227
|
+
};
|
|
228
|
+
type PrimitiveMaterializeHandler = (primitive: AttributedPrimitive, ctx: PrimitiveMaterializeContext) => void | Promise<void>;
|
|
229
|
+
/**
|
|
230
|
+
* Host handlers keyed by primitive kind. Missing keys are skipped.
|
|
231
|
+
* `unknown` runs only when the type matches no known kind.
|
|
232
|
+
*/
|
|
233
|
+
type PrimitiveMaterializeHandlers = Partial<Record<PrimitiveMaterializeKind, PrimitiveMaterializeHandler>> & {
|
|
234
|
+
unknown?: PrimitiveMaterializeHandler;
|
|
235
|
+
};
|
|
236
|
+
/**
|
|
237
|
+
* Dispatch each primitive to a typed handler (`skill`, `instruction`, …).
|
|
238
|
+
* Prefer this over a manual `primitivesList` + `if (/skill/)` loop in hosts.
|
|
239
|
+
* Known kinds without a handler are skipped; `unknown` covers unmatched types.
|
|
240
|
+
*/
|
|
241
|
+
declare function primitivesMaterialize(set: AttributedPrimitiveSet, handlers: PrimitiveMaterializeHandlers): Promise<void>;
|
|
242
|
+
/** Replace path separators so a primitive name is safe as a single path segment. */
|
|
243
|
+
declare function sanitizeName(name: string): string;
|
|
244
|
+
/** True when `absPath` is `rootRel` or a descendant under `cwd`. */
|
|
245
|
+
declare function isUnderRoot(cwd: string, absPath: string, rootRel: string): boolean;
|
|
246
|
+
/** Throw if `absPath` is outside every registered deploy root. */
|
|
247
|
+
declare function assertUnderDeployRoots(cwd: string, absPath: string, deployRoots: string[]): void;
|
|
248
|
+
/**
|
|
249
|
+
* Resolve primitive body: inline `content`, preferred nested file, source file,
|
|
250
|
+
* or minimal frontmatter stub.
|
|
251
|
+
*/
|
|
252
|
+
declare function readPrimitiveContent(p: AttributedPrimitive, preferredFile?: string): string;
|
|
253
|
+
/** Absolute path → cwd-relative path with `/` separators. */
|
|
254
|
+
declare function toPosixRel(cwd: string, absPath: string): string;
|
|
255
|
+
/**
|
|
256
|
+
* Walk up from `startPath` (file or directory) to the nearest package root
|
|
257
|
+
* marked by `apm.yml`, `bapm.yml`, or `plugin.json`.
|
|
258
|
+
* Falls back to `dirname(startPath)` when no marker is found.
|
|
259
|
+
*/
|
|
260
|
+
declare function findPackageRoot(startPath: string): string;
|
|
261
|
+
/** True when `candidate` is `root` or a descendant (path containment). */
|
|
262
|
+
declare function isWithin(root: string, candidate: string): boolean;
|
|
263
|
+
/** Recursively list files under `dir` (absolute paths). */
|
|
264
|
+
declare function listFiles(dir: string): string[];
|
|
265
|
+
/**
|
|
266
|
+
* Copy a validated portable Agent Plugin skill directory without retaining symlinks.
|
|
267
|
+
* Throws when the skill escapes `pluginRoot`.
|
|
268
|
+
*/
|
|
269
|
+
declare function copyPortableSkillDirectory(sourceDir: string, pluginRoot: unknown, destDir: string): void;
|
|
270
|
+
/**
|
|
271
|
+
* Host-agnostic skill materialize: Agent Plugin tree / SKILL.md copy / content stub.
|
|
272
|
+
* Hosts only choose `destDir` (cwd-relative or absolute under deploy roots).
|
|
273
|
+
*/
|
|
274
|
+
declare function materializeSkill(args: {
|
|
275
|
+
primitive: AttributedPrimitive;
|
|
276
|
+
cwd: string;
|
|
277
|
+
deployRoots: string[];
|
|
278
|
+
/** Destination skill directory (e.g. `.agents/skills/hello` or absolute). */
|
|
279
|
+
destDir: string;
|
|
280
|
+
}): DeployedFile[];
|
|
281
|
+
/** True when the target exposes an MCP configure hook (any documented alias). */
|
|
282
|
+
declare function hasConfigureMcp(target: BapmIntegration | Record<string, unknown>): boolean;
|
|
283
|
+
/**
|
|
284
|
+
* Resolve optional MCP configure from a registered target.
|
|
285
|
+
* Accepts `configureMcp` and documented aliases without requiring every host to
|
|
286
|
+
* use the same method name.
|
|
287
|
+
*/
|
|
288
|
+
declare function getConfigureMcp(target: BapmIntegration | Record<string, unknown>): ConfigureMcpFn | undefined;
|
|
289
|
+
/**
|
|
290
|
+
* Assert deploy-root containment, mkdir parents, write content, return inventory row.
|
|
291
|
+
*/
|
|
292
|
+
declare function writeDeployedFile(args: {
|
|
293
|
+
cwd: string;
|
|
294
|
+
deployRoots: string[];
|
|
295
|
+
/** Destination path relative to `cwd` (or absolute under cwd). */
|
|
296
|
+
destRel: string;
|
|
297
|
+
content: string | Buffer;
|
|
298
|
+
primitive?: {
|
|
299
|
+
name: string;
|
|
300
|
+
packageName?: string;
|
|
301
|
+
};
|
|
302
|
+
}): DeployedFile;
|
|
303
|
+
/**
|
|
304
|
+
* Deterministic markdown document from attributed primitives (AGENTS.md family).
|
|
305
|
+
*/
|
|
306
|
+
declare function renderPrimitivesMarkdown(args: {
|
|
307
|
+
primitives: AttributedPrimitive[];
|
|
308
|
+
title: string;
|
|
309
|
+
filter?: (p: AttributedPrimitive) => boolean;
|
|
310
|
+
emptyMessage?: string;
|
|
311
|
+
sectionHeading?: (p: AttributedPrimitive) => string;
|
|
312
|
+
preferredFile?: string;
|
|
313
|
+
}): string;
|
|
314
|
+
/**
|
|
315
|
+
* Build a `CompileReport` from markdown content; optionally write under cwd.
|
|
316
|
+
*/
|
|
317
|
+
declare function compileMarkdownReport(args: {
|
|
318
|
+
cwd: string;
|
|
319
|
+
outputFile: string;
|
|
320
|
+
write: boolean;
|
|
321
|
+
content: string;
|
|
322
|
+
requireBasename?: string;
|
|
323
|
+
outsideCwdMessage?: string;
|
|
324
|
+
}): CompileReport;
|
|
325
|
+
/**
|
|
326
|
+
* Drop YAML frontmatter keys not in `preserved`; preserve body and non-key lines.
|
|
327
|
+
*/
|
|
328
|
+
declare function filterFrontmatterKeys(source: string, preserved: ReadonlySet<string>): {
|
|
329
|
+
content: string;
|
|
330
|
+
droppedKeys: string[];
|
|
331
|
+
};
|
|
332
|
+
/** Owned-hook sidecar document (hosts write only the fields they need). */
|
|
333
|
+
type HookOwnershipSidecar = {
|
|
334
|
+
owned: Record<string, {
|
|
335
|
+
packageName?: string;
|
|
336
|
+
entries?: Array<{
|
|
337
|
+
event: string;
|
|
338
|
+
command: string;
|
|
339
|
+
}>;
|
|
340
|
+
scripts?: string[];
|
|
341
|
+
hookFile?: string;
|
|
342
|
+
hookFiles?: string[];
|
|
343
|
+
}>;
|
|
344
|
+
};
|
|
345
|
+
/** Read ownership sidecar; missing/malformed → `{ owned: {} }`. */
|
|
346
|
+
declare function readHookOwnershipSidecar(path: string): HookOwnershipSidecar;
|
|
347
|
+
/** Write `{ owned }` as pretty JSON with trailing newline (caller asserts deploy roots). */
|
|
348
|
+
declare function writeHookOwnershipSidecar(path: string, doc: HookOwnershipSidecar): void;
|
|
349
|
+
/**
|
|
350
|
+
* Remove entries whose `command` appears in any owned `entries`.
|
|
351
|
+
* Mutates `hooks` in place; does not delete files from disk.
|
|
352
|
+
*/
|
|
353
|
+
declare function stripOwnedHookCommands(hooks: Record<string, unknown>, ownership: HookOwnershipSidecar): void;
|
|
354
|
+
/**
|
|
355
|
+
* Best-effort delete of owned `scripts`, `hookFile`, and `hookFiles` under `cwd`.
|
|
356
|
+
* Missing paths are ignored; does not mutate hooks JSON.
|
|
357
|
+
*/
|
|
358
|
+
declare function removeOwnedHookArtifacts(cwd: string, ownership: HookOwnershipSidecar): void;
|
|
359
|
+
type CopyHookScriptArgs = {
|
|
360
|
+
cwd: string;
|
|
361
|
+
deployRoots: string[];
|
|
362
|
+
hookFile: string;
|
|
363
|
+
command: string;
|
|
364
|
+
alreadyDeployedNeedle: string;
|
|
365
|
+
destRel: string;
|
|
366
|
+
commandAsDotSlash?: boolean;
|
|
367
|
+
};
|
|
368
|
+
type CopyHookScriptResult = {
|
|
369
|
+
commandRel: string;
|
|
370
|
+
scriptRel?: string;
|
|
371
|
+
};
|
|
372
|
+
/**
|
|
373
|
+
* Simple hook-script copy for hosts that resolve next to the hook file or under
|
|
374
|
+
* `findPackageRoot`, then write a caller-supplied `destRel` under deploy roots.
|
|
375
|
+
*/
|
|
376
|
+
declare function copyHookScript(args: CopyHookScriptArgs): CopyHookScriptResult;
|
|
377
|
+
//#endregion
|
|
378
|
+
export { type AttributedPrimitive, type AttributedPrimitiveSet, type BapmIntegration, type CompileContext, type CompileFn, type CompileReport, type ConfigureMcpContext, type ConfigureMcpFn, type ConfigureMcpReport, type CopyHookScriptArgs, type CopyHookScriptResult, type DeployedFile, type DetectedTargetsResult, type HookOwnershipSidecar, type IntegrationRegistry, type MarketplaceOutputCapability, type MarketplaceOutputIntegration, type MarketplaceOutputRegistry, type MaterializeContext, type MaterializeReport, type McpEnvMode, type McpServerConfig, type PrimitiveMaterializeContext, type PrimitiveMaterializeHandler, type PrimitiveMaterializeHandlers, type PrimitiveMaterializeKind, type PrimitiveSource, type PrimitiveType, SHARED_COMMAND_FRONTMATTER_KEYS, type TargetDetectFn, type TargetDetectionDiagnostic, type TargetId, assertUnderDeployRoots, compileMarkdownReport, copyHookScript, copyPortableSkillDirectory, createIntegrationRegistry, createMarketplaceOutputRegistry, filterFrontmatterKeys, findPackageRoot, getConfigureMcp, hasConfigureMcp, isUnderRoot, isWithin, listFiles, materializeSkill, primitivesList, primitivesMaterialize, readHookOwnershipSidecar, readPrimitiveContent, removeOwnedHookArtifacts, renderPrimitivesMarkdown, sanitizeName, stripOwnedHookCommands, toPosixRel, writeDeployedFile, writeHookOwnershipSidecar };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
//#region src/registry.ts
|
|
4
|
+
/**
|
|
5
|
+
* Create an empty in-memory target registry for core Install and tests.
|
|
6
|
+
* No concrete host imports — callers register doubles or host packages.
|
|
7
|
+
*/
|
|
8
|
+
function createIntegrationRegistry() {
|
|
9
|
+
const byId = /* @__PURE__ */ new Map();
|
|
10
|
+
return {
|
|
11
|
+
register(target) {
|
|
12
|
+
if (!target || typeof target !== "object") throw new TypeError("register requires a BapmIntegration object");
|
|
13
|
+
const id = String(target.id ?? "").trim();
|
|
14
|
+
if (!id) throw new TypeError("BapmIntegration.id is required");
|
|
15
|
+
if (!Array.isArray(target.deployRoots)) throw new TypeError(`BapmIntegration ${id} requires deployRoots array`);
|
|
16
|
+
if (typeof target.detect !== "function") throw new TypeError(`BapmIntegration ${id} requires detect()`);
|
|
17
|
+
if (typeof target.materialize !== "function") throw new TypeError(`BapmIntegration ${id} requires materialize()`);
|
|
18
|
+
byId.set(id, target.id === id ? target : {
|
|
19
|
+
...target,
|
|
20
|
+
id
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
list() {
|
|
24
|
+
return [...byId.values()];
|
|
25
|
+
},
|
|
26
|
+
get(id) {
|
|
27
|
+
return byId.get(id);
|
|
28
|
+
},
|
|
29
|
+
getAll() {
|
|
30
|
+
return [...byId.values()];
|
|
31
|
+
},
|
|
32
|
+
async detect(cwd) {
|
|
33
|
+
const detectedIds = [];
|
|
34
|
+
const diagnostics = [];
|
|
35
|
+
for (const target of byId.values()) try {
|
|
36
|
+
if (await target.detect({ cwd })) detectedIds.push(target.id);
|
|
37
|
+
} catch {
|
|
38
|
+
diagnostics.push({
|
|
39
|
+
targetId: target.id,
|
|
40
|
+
message: `Target "${target.id}" detection did not match`
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
detectedIds,
|
|
45
|
+
diagnostics
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Create a registry for marketplace-output-only or combined integrations. */
|
|
51
|
+
function createMarketplaceOutputRegistry() {
|
|
52
|
+
const byFormat = /* @__PURE__ */ new Map();
|
|
53
|
+
return {
|
|
54
|
+
register(integration) {
|
|
55
|
+
const format = integration?.marketplaceOutput?.format?.trim();
|
|
56
|
+
if (!integration?.id?.trim() || !format || !integration.marketplaceOutput.defaultOutput) throw new TypeError("Marketplace output integration requires id, format, and defaultOutput");
|
|
57
|
+
if (typeof integration.marketplaceOutput.map !== "function") throw new TypeError(`Marketplace output integration ${integration.id} requires map()`);
|
|
58
|
+
byFormat.set(format, {
|
|
59
|
+
...integration,
|
|
60
|
+
id: integration.id.trim(),
|
|
61
|
+
marketplaceOutput: {
|
|
62
|
+
...integration.marketplaceOutput,
|
|
63
|
+
format
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
list() {
|
|
68
|
+
return [...byFormat.values()];
|
|
69
|
+
},
|
|
70
|
+
get(format) {
|
|
71
|
+
return byFormat.get(format);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/helpers.ts
|
|
77
|
+
/**
|
|
78
|
+
* Shared materialize helpers for host targets (fs/path, primitive content).
|
|
79
|
+
*/
|
|
80
|
+
/** Shared command frontmatter allowlist (Cursor / Claude Code / Grok-style prompts). */
|
|
81
|
+
const SHARED_COMMAND_FRONTMATTER_KEYS = /* @__PURE__ */ new Set([
|
|
82
|
+
"description",
|
|
83
|
+
"allowed-tools",
|
|
84
|
+
"model",
|
|
85
|
+
"argument-hint",
|
|
86
|
+
"input"
|
|
87
|
+
]);
|
|
88
|
+
/** Normalize `AttributedPrimitiveSet` (array or `{ primitives }`) to a list. */
|
|
89
|
+
function primitivesList(set) {
|
|
90
|
+
if (Array.isArray(set)) return set;
|
|
91
|
+
if (set && typeof set === "object" && Array.isArray(set.primitives)) return set.primitives;
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
const KIND_MATCHERS = [
|
|
95
|
+
{
|
|
96
|
+
kind: "skill",
|
|
97
|
+
test: /skill/
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
kind: "instruction",
|
|
101
|
+
test: /instruction/
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
kind: "agent",
|
|
105
|
+
test: /agent/
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
kind: "command",
|
|
109
|
+
test: /command/
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
kind: "hook",
|
|
113
|
+
test: /hook/
|
|
114
|
+
}
|
|
115
|
+
];
|
|
116
|
+
function resolvePrimitiveKind(type) {
|
|
117
|
+
for (const { kind, test } of KIND_MATCHERS) if (test.test(type)) return kind;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Dispatch each primitive to a typed handler (`skill`, `instruction`, …).
|
|
121
|
+
* Prefer this over a manual `primitivesList` + `if (/skill/)` loop in hosts.
|
|
122
|
+
* Known kinds without a handler are skipped; `unknown` covers unmatched types.
|
|
123
|
+
*/
|
|
124
|
+
async function primitivesMaterialize(set, handlers) {
|
|
125
|
+
for (const primitive of primitivesList(set)) {
|
|
126
|
+
const type = String(primitive.type ?? "skill").toLowerCase();
|
|
127
|
+
const ctx = {
|
|
128
|
+
name: sanitizeName(String(primitive.name)),
|
|
129
|
+
type
|
|
130
|
+
};
|
|
131
|
+
const kind = resolvePrimitiveKind(type);
|
|
132
|
+
const handler = kind ? handlers[kind] : handlers.unknown;
|
|
133
|
+
if (handler) await handler(primitive, ctx);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Replace path separators so a primitive name is safe as a single path segment. */
|
|
137
|
+
function sanitizeName(name) {
|
|
138
|
+
return String(name || "unnamed").replace(/[/\\]/g, "-");
|
|
139
|
+
}
|
|
140
|
+
/** True when `absPath` is `rootRel` or a descendant under `cwd`. */
|
|
141
|
+
function isUnderRoot(cwd, absPath, rootRel) {
|
|
142
|
+
const rootAbs = resolve(cwd, rootRel);
|
|
143
|
+
const rel = relative(rootAbs, absPath);
|
|
144
|
+
return rel === "" || !rel.startsWith("..") && !rel.startsWith(`..${sep}`);
|
|
145
|
+
}
|
|
146
|
+
/** Throw if `absPath` is outside every registered deploy root. */
|
|
147
|
+
function assertUnderDeployRoots(cwd, absPath, deployRoots) {
|
|
148
|
+
if (!deployRoots.some((r) => isUnderRoot(cwd, absPath, r))) throw new Error(`materialize refuses path outside deploy roots: ${absPath}`);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Resolve primitive body: inline `content`, preferred nested file, source file,
|
|
152
|
+
* or minimal frontmatter stub.
|
|
153
|
+
*/
|
|
154
|
+
function readPrimitiveContent(p, preferredFile) {
|
|
155
|
+
if (typeof p.content === "string") return p.content;
|
|
156
|
+
const src = p.path ? resolve(p.path) : void 0;
|
|
157
|
+
if (src && existsSync(src)) {
|
|
158
|
+
if (preferredFile) {
|
|
159
|
+
const nested = src.endsWith(preferredFile) ? src : join(src, preferredFile);
|
|
160
|
+
if (existsSync(nested) && statSync(nested).isFile()) return readFileSync(nested, "utf8");
|
|
161
|
+
}
|
|
162
|
+
if (statSync(src).isFile()) return readFileSync(src, "utf8");
|
|
163
|
+
}
|
|
164
|
+
const name = sanitizeName(String(p.name));
|
|
165
|
+
return `---\nname: ${name}\n---\n# ${name}\n`;
|
|
166
|
+
}
|
|
167
|
+
/** Absolute path → cwd-relative path with `/` separators. */
|
|
168
|
+
function toPosixRel(cwd, absPath) {
|
|
169
|
+
return relative(cwd, absPath).split(sep).join("/");
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Walk up from `startPath` (file or directory) to the nearest package root
|
|
173
|
+
* marked by `apm.yml`, `bapm.yml`, or `plugin.json`.
|
|
174
|
+
* Falls back to `dirname(startPath)` when no marker is found.
|
|
175
|
+
*/
|
|
176
|
+
function findPackageRoot(startPath) {
|
|
177
|
+
let dir = existsSync(startPath) && statSync(startPath).isDirectory() ? startPath : dirname(startPath);
|
|
178
|
+
for (;;) {
|
|
179
|
+
if (existsSync(join(dir, "apm.yml")) || existsSync(join(dir, "bapm.yml")) || existsSync(join(dir, "plugin.json"))) return dir;
|
|
180
|
+
const parent = dirname(dir);
|
|
181
|
+
if (parent === dir) break;
|
|
182
|
+
dir = parent;
|
|
183
|
+
}
|
|
184
|
+
return existsSync(startPath) && statSync(startPath).isDirectory() ? startPath : dirname(startPath);
|
|
185
|
+
}
|
|
186
|
+
/** True when `candidate` is `root` or a descendant (path containment). */
|
|
187
|
+
function isWithin(root, candidate) {
|
|
188
|
+
const rel = relative(root, candidate);
|
|
189
|
+
return rel === "" || !rel.startsWith("..") && !rel.startsWith("/") && rel !== "..";
|
|
190
|
+
}
|
|
191
|
+
/** Recursively list files under `dir` (absolute paths). */
|
|
192
|
+
function listFiles(dir) {
|
|
193
|
+
const files = [];
|
|
194
|
+
for (const name of readdirSync(dir)) {
|
|
195
|
+
const path = join(dir, name);
|
|
196
|
+
if (statSync(path).isDirectory()) files.push(...listFiles(path));
|
|
197
|
+
else files.push(path);
|
|
198
|
+
}
|
|
199
|
+
return files;
|
|
200
|
+
}
|
|
201
|
+
function treeIsContained(directory, root, visited) {
|
|
202
|
+
if (visited.has(directory)) return true;
|
|
203
|
+
visited.add(directory);
|
|
204
|
+
for (const entry of readdirSync(directory)) {
|
|
205
|
+
const resolved = realpathSync(join(directory, entry));
|
|
206
|
+
if (!isWithin(root, resolved)) return false;
|
|
207
|
+
if (statSync(resolved).isDirectory() && !treeIsContained(resolved, root, visited)) return false;
|
|
208
|
+
}
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Copy a validated portable Agent Plugin skill directory without retaining symlinks.
|
|
213
|
+
* Throws when the skill escapes `pluginRoot`.
|
|
214
|
+
*/
|
|
215
|
+
function copyPortableSkillDirectory(sourceDir, pluginRoot, destDir) {
|
|
216
|
+
const source = realpathSync(sourceDir);
|
|
217
|
+
const root = typeof pluginRoot === "string" ? realpathSync(pluginRoot) : void 0;
|
|
218
|
+
if (!root || !isWithin(root, source) || !statSync(source).isDirectory()) throw new Error("Agent Plugin skill directory is outside its plugin root");
|
|
219
|
+
const skillFile = realpathSync(join(source, "SKILL.md"));
|
|
220
|
+
if (!isWithin(root, skillFile) || !statSync(skillFile).isFile() || !treeIsContained(source, root, /* @__PURE__ */ new Set())) throw new Error("Agent Plugin skill contains a path outside its plugin root");
|
|
221
|
+
mkdirSync(destDir, { recursive: true });
|
|
222
|
+
cpSync(source, destDir, {
|
|
223
|
+
recursive: true,
|
|
224
|
+
dereference: true,
|
|
225
|
+
force: true
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Host-agnostic skill materialize: Agent Plugin tree / SKILL.md copy / content stub.
|
|
230
|
+
* Hosts only choose `destDir` (cwd-relative or absolute under deploy roots).
|
|
231
|
+
*/
|
|
232
|
+
function materializeSkill(args) {
|
|
233
|
+
const { primitive: p, cwd, deployRoots } = args;
|
|
234
|
+
const destDir = resolve(cwd, args.destDir);
|
|
235
|
+
const destFile = join(destDir, "SKILL.md");
|
|
236
|
+
assertUnderDeployRoots(cwd, destFile, deployRoots);
|
|
237
|
+
mkdirSync(destDir, { recursive: true });
|
|
238
|
+
const src = p.path ? resolve(p.path) : void 0;
|
|
239
|
+
if (p.format === "agent-plugin" && typeof p.skillDirectory === "string") copyPortableSkillDirectory(p.skillDirectory, p.pluginRoot, destDir);
|
|
240
|
+
else if (src && existsSync(src)) {
|
|
241
|
+
const skillMd = src.endsWith("SKILL.md") ? src : join(src, "SKILL.md");
|
|
242
|
+
if (existsSync(skillMd) && statSync(skillMd).isFile()) cpSync(skillMd, destFile);
|
|
243
|
+
else writeFileSync(destFile, readPrimitiveContent(p, "SKILL.md"), "utf8");
|
|
244
|
+
} else writeFileSync(destFile, readPrimitiveContent(p, "SKILL.md"), "utf8");
|
|
245
|
+
return listFiles(destDir).map((path) => ({
|
|
246
|
+
path: toPosixRel(cwd, path),
|
|
247
|
+
primitive: {
|
|
248
|
+
name: String(p.name),
|
|
249
|
+
packageName: p.packageName
|
|
250
|
+
}
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
const MCP_CONFIGURE_ALIASES = [
|
|
254
|
+
"configureMcp",
|
|
255
|
+
"writeMcpConfig",
|
|
256
|
+
"deployMcp",
|
|
257
|
+
"configureMcpServers"
|
|
258
|
+
];
|
|
259
|
+
/** True when the target exposes an MCP configure hook (any documented alias). */
|
|
260
|
+
function hasConfigureMcp(target) {
|
|
261
|
+
return getConfigureMcp(target) !== void 0;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Resolve optional MCP configure from a registered target.
|
|
265
|
+
* Accepts `configureMcp` and documented aliases without requiring every host to
|
|
266
|
+
* use the same method name.
|
|
267
|
+
*/
|
|
268
|
+
function getConfigureMcp(target) {
|
|
269
|
+
const rec = target;
|
|
270
|
+
for (const key of MCP_CONFIGURE_ALIASES) {
|
|
271
|
+
const fn = rec[key];
|
|
272
|
+
if (typeof fn === "function") return fn.bind(target);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Assert deploy-root containment, mkdir parents, write content, return inventory row.
|
|
277
|
+
*/
|
|
278
|
+
function writeDeployedFile(args) {
|
|
279
|
+
const cwd = resolve(args.cwd);
|
|
280
|
+
const destFile = resolve(cwd, args.destRel);
|
|
281
|
+
assertUnderDeployRoots(cwd, destFile, args.deployRoots);
|
|
282
|
+
mkdirSync(dirname(destFile), { recursive: true });
|
|
283
|
+
writeFileSync(destFile, args.content);
|
|
284
|
+
const row = { path: toPosixRel(cwd, destFile) };
|
|
285
|
+
if (args.primitive) row.primitive = args.primitive;
|
|
286
|
+
return row;
|
|
287
|
+
}
|
|
288
|
+
function defaultSectionHeading(p) {
|
|
289
|
+
return `${p.name} (${p.type})`;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Deterministic markdown document from attributed primitives (AGENTS.md family).
|
|
293
|
+
*/
|
|
294
|
+
function renderPrimitivesMarkdown(args) {
|
|
295
|
+
const sorted = [...args.filter ? args.primitives.filter(args.filter) : args.primitives].sort((a, b) => {
|
|
296
|
+
const type = String(a.type ?? "").localeCompare(String(b.type ?? ""));
|
|
297
|
+
if (type !== 0) return type;
|
|
298
|
+
const name = String(a.name ?? "").localeCompare(String(b.name ?? ""));
|
|
299
|
+
return name !== 0 ? name : String(a.path ?? "").localeCompare(String(b.path ?? ""));
|
|
300
|
+
});
|
|
301
|
+
const emptyMessage = args.emptyMessage ?? "_No discoverable primitives._";
|
|
302
|
+
const heading = args.sectionHeading ?? defaultSectionHeading;
|
|
303
|
+
const sections = [
|
|
304
|
+
args.title,
|
|
305
|
+
"",
|
|
306
|
+
"<!-- Generated by bapm compile. Do not edit by hand. -->",
|
|
307
|
+
""
|
|
308
|
+
];
|
|
309
|
+
if (sorted.length === 0) return [
|
|
310
|
+
...sections,
|
|
311
|
+
emptyMessage,
|
|
312
|
+
""
|
|
313
|
+
].join("\n");
|
|
314
|
+
for (const primitive of sorted) {
|
|
315
|
+
sections.push(`## ${heading(primitive)}`, "");
|
|
316
|
+
sections.push(readPrimitiveContent(primitive, args.preferredFile ?? `# ${primitive.name}\n`).trimEnd(), "");
|
|
317
|
+
}
|
|
318
|
+
return sections.join("\n");
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Build a `CompileReport` from markdown content; optionally write under cwd.
|
|
322
|
+
*/
|
|
323
|
+
function compileMarkdownReport(args) {
|
|
324
|
+
const cwd = resolve(args.cwd);
|
|
325
|
+
const outputPath = resolve(cwd, args.outputFile);
|
|
326
|
+
const rel = relative(cwd, outputPath);
|
|
327
|
+
const sepChar = process.platform === "win32" ? "\\" : "/";
|
|
328
|
+
if (!rel || rel === ".." || rel.startsWith(`..${sepChar}`)) throw new Error(args.outsideCwdMessage ?? "Compile output must be a cwd-relative file path");
|
|
329
|
+
if (args.requireBasename && basename(outputPath) !== args.requireBasename) throw new Error(`Compile output basename must be ${args.requireBasename}`);
|
|
330
|
+
const path = rel.split(sep).join("/");
|
|
331
|
+
const wrote = args.write;
|
|
332
|
+
if (wrote) {
|
|
333
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
334
|
+
writeFileSync(outputPath, args.content, "utf8");
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
path,
|
|
338
|
+
content: args.content,
|
|
339
|
+
wrote
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Drop YAML frontmatter keys not in `preserved`; preserve body and non-key lines.
|
|
344
|
+
*/
|
|
345
|
+
function filterFrontmatterKeys(source, preserved) {
|
|
346
|
+
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
347
|
+
if (!match) return {
|
|
348
|
+
content: source,
|
|
349
|
+
droppedKeys: []
|
|
350
|
+
};
|
|
351
|
+
const rawFm = match[1] ?? "";
|
|
352
|
+
const body = match[2] ?? "";
|
|
353
|
+
const kept = [];
|
|
354
|
+
const droppedKeys = [];
|
|
355
|
+
for (const line of rawFm.split(/\r?\n/)) {
|
|
356
|
+
if (!line.trim()) continue;
|
|
357
|
+
const keyMatch = line.match(/^([A-Za-z0-9_-]+)\s*:/);
|
|
358
|
+
if (!keyMatch) {
|
|
359
|
+
kept.push(line);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const key = keyMatch[1];
|
|
363
|
+
if (preserved.has(key)) kept.push(line);
|
|
364
|
+
else droppedKeys.push(key);
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
content: `${kept.length > 0 ? `---\n${kept.join("\n")}\n---\n` : ""}${body}`.replace(/\s+$/, "\n"),
|
|
368
|
+
droppedKeys
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
/** Read ownership sidecar; missing/malformed → `{ owned: {} }`. */
|
|
372
|
+
function readHookOwnershipSidecar(path) {
|
|
373
|
+
if (!existsSync(path)) return { owned: {} };
|
|
374
|
+
try {
|
|
375
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
376
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { owned: {} };
|
|
377
|
+
const owned = raw.owned;
|
|
378
|
+
if (!owned || typeof owned !== "object" || Array.isArray(owned)) return { owned: {} };
|
|
379
|
+
return { owned };
|
|
380
|
+
} catch {
|
|
381
|
+
return { owned: {} };
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/** Write `{ owned }` as pretty JSON with trailing newline (caller asserts deploy roots). */
|
|
385
|
+
function writeHookOwnershipSidecar(path, doc) {
|
|
386
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
387
|
+
writeFileSync(path, `${JSON.stringify({ owned: doc.owned ?? {} }, null, 2)}\n`, "utf8");
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Remove entries whose `command` appears in any owned `entries`.
|
|
391
|
+
* Mutates `hooks` in place; does not delete files from disk.
|
|
392
|
+
*/
|
|
393
|
+
function stripOwnedHookCommands(hooks, ownership) {
|
|
394
|
+
const ownedCommands = /* @__PURE__ */ new Set();
|
|
395
|
+
for (const record of Object.values(ownership.owned ?? {})) for (const entry of record.entries ?? []) if (entry.command) ownedCommands.add(entry.command);
|
|
396
|
+
if (ownedCommands.size === 0) return;
|
|
397
|
+
for (const [event, entries] of Object.entries(hooks)) {
|
|
398
|
+
if (!Array.isArray(entries)) continue;
|
|
399
|
+
hooks[event] = entries.filter((e) => {
|
|
400
|
+
const cmd = e && typeof e === "object" && typeof e.command === "string" ? e.command : "";
|
|
401
|
+
return !ownedCommands.has(cmd);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Best-effort delete of owned `scripts`, `hookFile`, and `hookFiles` under `cwd`.
|
|
407
|
+
* Missing paths are ignored; does not mutate hooks JSON.
|
|
408
|
+
*/
|
|
409
|
+
function removeOwnedHookArtifacts(cwd, ownership) {
|
|
410
|
+
for (const record of Object.values(ownership.owned ?? {})) {
|
|
411
|
+
const rels = [];
|
|
412
|
+
if (typeof record.hookFile === "string" && record.hookFile) rels.push(record.hookFile);
|
|
413
|
+
if (Array.isArray(record.hookFiles)) {
|
|
414
|
+
for (const f of record.hookFiles) if (typeof f === "string" && f) rels.push(f);
|
|
415
|
+
}
|
|
416
|
+
if (Array.isArray(record.scripts)) {
|
|
417
|
+
for (const s of record.scripts) if (typeof s === "string" && s) rels.push(s);
|
|
418
|
+
}
|
|
419
|
+
for (const rel of rels) try {
|
|
420
|
+
const abs = join(cwd, rel);
|
|
421
|
+
if (existsSync(abs)) rmSync(abs, { force: true });
|
|
422
|
+
} catch {}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Simple hook-script copy for hosts that resolve next to the hook file or under
|
|
427
|
+
* `findPackageRoot`, then write a caller-supplied `destRel` under deploy roots.
|
|
428
|
+
*/
|
|
429
|
+
function copyHookScript(args) {
|
|
430
|
+
const { cwd, deployRoots, hookFile, command, alreadyDeployedNeedle, destRel, commandAsDotSlash } = args;
|
|
431
|
+
if (command.includes(alreadyDeployedNeedle)) return { commandRel: commandAsDotSlash ? command.startsWith("./") ? command : `./${command.replace(/^\//, "")}` : command };
|
|
432
|
+
const cleaned = command.replace(/^\.\//, "");
|
|
433
|
+
const packageRoot = findPackageRoot(hookFile);
|
|
434
|
+
const source = [resolve(dirname(hookFile), cleaned), resolve(packageRoot, cleaned)].find((p) => {
|
|
435
|
+
try {
|
|
436
|
+
return existsSync(p) && statSync(p).isFile();
|
|
437
|
+
} catch {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
if (!source) return { commandRel: command };
|
|
442
|
+
const destAbs = join(cwd, destRel);
|
|
443
|
+
assertUnderDeployRoots(cwd, destAbs, deployRoots);
|
|
444
|
+
mkdirSync(dirname(destAbs), { recursive: true });
|
|
445
|
+
cpSync(source, destAbs);
|
|
446
|
+
return {
|
|
447
|
+
commandRel: commandAsDotSlash ? `./${destRel}` : destRel,
|
|
448
|
+
scriptRel: destRel
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
//#endregion
|
|
452
|
+
export { SHARED_COMMAND_FRONTMATTER_KEYS, assertUnderDeployRoots, compileMarkdownReport, copyHookScript, copyPortableSkillDirectory, createIntegrationRegistry, createMarketplaceOutputRegistry, filterFrontmatterKeys, findPackageRoot, getConfigureMcp, hasConfigureMcp, isUnderRoot, isWithin, listFiles, materializeSkill, primitivesList, primitivesMaterialize, readHookOwnershipSidecar, readPrimitiveContent, removeOwnedHookArtifacts, renderPrimitivesMarkdown, sanitizeName, stripOwnedHookCommands, toPosixRel, writeDeployedFile, writeHookOwnershipSidecar };
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@b-apm/integration-api",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared contracts and registry for bapm host integrations",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./dist/index.mjs",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^26.1.2",
|
|
19
|
+
"typescript": "^7.0.2",
|
|
20
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
|
|
21
|
+
"vite-plus": "latest"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22.12.0"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/Sdju/bapm.git",
|
|
29
|
+
"directory": "packages/integration-api"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/Sdju/bapm#readme",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/Sdju/bapm/issues"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"unstable",
|
|
37
|
+
"bapm",
|
|
38
|
+
"b-apm"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "vp pack",
|
|
42
|
+
"dev": "vp pack --watch",
|
|
43
|
+
"test": "vp test",
|
|
44
|
+
"check": "vp check"
|
|
45
|
+
}
|
|
46
|
+
}
|