@owlmeans/agent-skills 0.1.15 → 0.1.16-rc.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/README.md CHANGED
@@ -90,3 +90,27 @@ The embedded files are version-matched to this package release. Do not edit them
90
90
  directly — they are regenerated on each publish. To contribute guidance edits,
91
91
  open a PR against the source monorepo.
92
92
  <!-- owlmeans:agent-guidance:end -->
93
+
94
+ ## Package skills in a prompt (`@owlmeans/agent-skills/llm`)
95
+
96
+ `owlmeansPackagesPlugin(options)` notices which `@owlmeans/*` packages a request mentions
97
+ and loads their published skills into the `Packages` block. Everything a package documents
98
+ is loaded — there is no relevance filtering, by design.
99
+
100
+ Resolution order per package: the host's `LlmFileProvider` (the only path that sees a
101
+ sandbox or remote workspace) → an installed copy under `node_modules` → the canonical
102
+ repository over HTTPS. Every failure is a miss, never a throw. Results, including misses,
103
+ are cached per plugin instance.
104
+
105
+ ```typescript
106
+ ctx.prompts().use(owlmeansPackagesPlugin({
107
+ files: () => ctx.files(), // tried first
108
+ exclude: ['@owlmeans/llm'], // already covered by the static Skills block
109
+ fetch: false, // air-gapped: skip the repository fallback
110
+ }))
111
+ ```
112
+
113
+ The manifest deliberately carries no git ref — version-matching comes from shipping the
114
+ copy inside the tarball, so for a package that is NOT installed the ref is a plugin option
115
+ (`ref`, default `main`).
116
+
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "package": "@owlmeans/agent-skills",
4
- "version": "0.1.15",
5
- "generatedAt": "2026-08-07T17:06:32.899Z",
4
+ "version": "0.1.16-rc.0",
5
+ "generatedAt": "2026-08-11T14:02:34.977Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The LLM-facing half of this package: a prompt plugin that turns a mention of an
3
+ * `@owlmeans/*` package in a prompt into that package's published skills.
4
+ *
5
+ * Deliberately NOT re-exported from the package index. The installer CLI has no runtime
6
+ * dependencies and must keep it that way; this entry point resolves `@owlmeans/llm` and
7
+ * `@owlmeans/llm-common`, which only a consumer already using the LLM layer will have.
8
+ */
9
+ export * from './types.js';
10
+ export * from './manifest.js';
11
+ export * from './resolve.js';
12
+ export * from './plugin.js';
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/llm/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,cAAc,YAAY,CAAA;AAC1B,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The LLM-facing half of this package: a prompt plugin that turns a mention of an
3
+ * `@owlmeans/*` package in a prompt into that package's published skills.
4
+ *
5
+ * Deliberately NOT re-exported from the package index. The installer CLI has no runtime
6
+ * dependencies and must keep it that way; this entry point resolves `@owlmeans/llm` and
7
+ * `@owlmeans/llm-common`, which only a consumer already using the LLM layer will have.
8
+ */
9
+ export * from './types.js';
10
+ export * from './manifest.js';
11
+ export * from './resolve.js';
12
+ export * from './plugin.js';
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/llm/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,cAAc,YAAY,CAAA;AAC1B,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA"}
@@ -0,0 +1,29 @@
1
+ import type { SkillDefinition } from '@owlmeans/llm-common';
2
+ import type { Manifest, ManifestEntry } from '../discover.js';
3
+ /** `@owlmeans/foo` → `foo`; the repo lays packages out under their unscoped name. */
4
+ export declare const unscoped: (packageName: string) => string;
5
+ export declare const parseManifest: (raw: string) => Manifest | null;
6
+ /**
7
+ * Strip the YAML frontmatter and the generated-file banner from an embedded SKILL.md.
8
+ *
9
+ * Both are installer bookkeeping — `name`, `description`, `applyTo`, "do not edit". Sent
10
+ * to a model they are pure noise in the most expensive part of the prompt, and the
11
+ * "do not edit" line actively misleads it about what it is being shown.
12
+ */
13
+ export declare const stripMeta: (content: string) => string;
14
+ /**
15
+ * Skill entries worth loading, in a deterministic order.
16
+ *
17
+ * Instruction entries are dropped: they are the Copilot twin of the same knowledge, so
18
+ * including both would double the token cost of every package for no added information.
19
+ */
20
+ export declare const skillEntries: (manifest: Manifest, categories: readonly string[]) => ManifestEntry[];
21
+ /**
22
+ * Turn one embedded skill file into a definition bound to the `Packages` block.
23
+ *
24
+ * The alias is namespaced by package so two packages documenting the same concept cannot
25
+ * collide in the registry, and the order weight is pushed past the default so package
26
+ * knowledge always renders after whatever the host declared itself.
27
+ */
28
+ export declare const toSkill: (packageName: string, entry: ManifestEntry, body: string) => SkillDefinition;
29
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/llm/manifest.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC3D,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAE7D,qFAAqF;AACrF,eAAO,MAAM,QAAQ,GAAI,aAAa,MAAM,KAAG,MACE,CAAA;AAEjD,eAAO,MAAM,aAAa,GAAI,KAAK,MAAM,KAAG,QAAQ,GAAG,IAUtD,CAAA;AAID;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,GAAI,SAAS,MAAM,KAAG,MAW3C,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GACvB,UAAU,QAAQ,EAClB,YAAY,SAAS,MAAM,EAAE,KAC5B,aAAa,EAGmD,CAAA;AAEnE;;;;;;GAMG;AACH,eAAO,MAAM,OAAO,GAClB,aAAa,MAAM,EACnB,OAAO,aAAa,EACpB,MAAM,MAAM,KACX,eAMD,CAAA"}
@@ -0,0 +1,58 @@
1
+ import { DEFAULT_SKILL_ORDER, PromptBlock } from '@owlmeans/llm-common';
2
+ /** `@owlmeans/foo` → `foo`; the repo lays packages out under their unscoped name. */
3
+ export const unscoped = (packageName) => packageName.slice(packageName.indexOf('/') + 1);
4
+ export const parseManifest = (raw) => {
5
+ try {
6
+ const manifest = JSON.parse(raw);
7
+ if (typeof manifest.schemaVersion !== 'number' || !Array.isArray(manifest.entries)) {
8
+ return null;
9
+ }
10
+ return manifest;
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ };
16
+ const BANNER = /^<!--\s*AUTO-GENERATED[^>]*-->\s*/;
17
+ /**
18
+ * Strip the YAML frontmatter and the generated-file banner from an embedded SKILL.md.
19
+ *
20
+ * Both are installer bookkeeping — `name`, `description`, `applyTo`, "do not edit". Sent
21
+ * to a model they are pure noise in the most expensive part of the prompt, and the
22
+ * "do not edit" line actively misleads it about what it is being shown.
23
+ */
24
+ export const stripMeta = (content) => {
25
+ let body = content.replace(/^/, '');
26
+ if (body.startsWith('---')) {
27
+ const end = body.indexOf('\n---', 3);
28
+ if (end >= 0) {
29
+ const after = body.indexOf('\n', end + 1);
30
+ body = after >= 0 ? body.slice(after + 1) : '';
31
+ }
32
+ }
33
+ return body.replace(BANNER, '').trim();
34
+ };
35
+ /**
36
+ * Skill entries worth loading, in a deterministic order.
37
+ *
38
+ * Instruction entries are dropped: they are the Copilot twin of the same knowledge, so
39
+ * including both would double the token cost of every package for no added information.
40
+ */
41
+ export const skillEntries = (manifest, categories) => manifest.entries
42
+ .filter(entry => entry.kind === 'skill' && categories.includes(entry.category))
43
+ .sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
44
+ /**
45
+ * Turn one embedded skill file into a definition bound to the `Packages` block.
46
+ *
47
+ * The alias is namespaced by package so two packages documenting the same concept cannot
48
+ * collide in the registry, and the order weight is pushed past the default so package
49
+ * knowledge always renders after whatever the host declared itself.
50
+ */
51
+ export const toSkill = (packageName, entry, body) => ({
52
+ alias: `${packageName}#${entry.name}`,
53
+ title: `${packageName} — ${entry.name}`,
54
+ body,
55
+ block: PromptBlock.Packages,
56
+ order: DEFAULT_SKILL_ORDER + 1,
57
+ });
58
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/llm/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAIvE,qFAAqF;AACrF,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,WAAmB,EAAU,EAAE,CACtD,WAAW,CAAC,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAA;AAEjD,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,GAAW,EAAmB,EAAE;IAC5D,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAa,CAAA;QAC5C,IAAI,OAAO,QAAQ,CAAC,aAAa,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YACnF,OAAO,IAAI,CAAA;QACb,CAAC;QACD,OAAO,QAAQ,CAAA;IACjB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC,CAAA;AAED,MAAM,MAAM,GAAG,mCAAmC,CAAA;AAElD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,OAAe,EAAU,EAAE;IACnD,IAAI,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;IACpC,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3B,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;QACpC,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;YACb,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,GAAG,CAAC,CAAC,CAAA;YACzC,IAAI,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;QAChD,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;AACxC,CAAC,CAAA;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAC1B,QAAkB,EAClB,UAA6B,EACZ,EAAE,CACnB,QAAQ,CAAC,OAAO;KACb,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,OAAO,IAAI,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;KAC9E,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAEnE;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CACrB,WAAmB,EACnB,KAAoB,EACpB,IAAY,EACK,EAAE,CAAC,CAAC;IACrB,KAAK,EAAE,GAAG,WAAW,IAAI,KAAK,CAAC,IAAI,EAAE;IACrC,KAAK,EAAE,GAAG,WAAW,MAAM,KAAK,CAAC,IAAI,EAAE;IACvC,IAAI;IACJ,KAAK,EAAE,WAAW,CAAC,QAAQ;IAC3B,KAAK,EAAE,mBAAmB,GAAG,CAAC;CAC/B,CAAC,CAAA"}
@@ -0,0 +1,17 @@
1
+ import type { LlmPromptPlugin } from '@owlmeans/llm';
2
+ import type { PackageSkillsOptions } from './types.js';
3
+ export declare const PACKAGE_SKILLS_PLUGIN = "owlmeans-packages";
4
+ /**
5
+ * A prompt plugin that notices which `@owlmeans/*` packages a request talks about and
6
+ * loads their published skills into the system prompt.
7
+ *
8
+ * Everything the package documents is loaded — there is no relevance filtering, by
9
+ * design: choosing a subset would need a judgement the model is better placed to make
10
+ * than a regex, and a partial answer about an API is worse than none.
11
+ *
12
+ * The result lands in the `Packages` block, which sits behind its own cache breakpoint.
13
+ * That is the whole reason the block exists: what a request mentions changes call to
14
+ * call, and it must not shift a single byte of the role and skills every call shares.
15
+ */
16
+ export declare const owlmeansPackagesPlugin: (options?: PackageSkillsOptions) => LlmPromptPlugin;
17
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/llm/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAiB,MAAM,eAAe,CAAA;AAGnE,OAAO,KAAK,EAAiB,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAErE,eAAO,MAAM,qBAAqB,sBAAsB,CAAA;AAuBxD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,GAAI,UAAS,oBAAyB,KAAG,eA0F3E,CAAA"}
@@ -0,0 +1,115 @@
1
+ import { PromptBlock, resolveFileProvider } from '@owlmeans/llm-common';
2
+ import { renderSkill } from '@owlmeans/llm/prompt';
3
+ import { loadPackageSkills } from './resolve.js';
4
+ import { OWLMEANS_SCOPE } from './types.js';
5
+ export const PACKAGE_SKILLS_PLUGIN = 'owlmeans-packages';
6
+ const DEFAULT_MAX_PACKAGES = 5;
7
+ const DEFAULT_CATEGORIES = ['package-specific', 'multi-package'];
8
+ const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
9
+ const textOf = (content) => {
10
+ if (typeof content === 'string') {
11
+ return content;
12
+ }
13
+ if (Array.isArray(content)) {
14
+ return content
15
+ .map(part => {
16
+ const text = part.text;
17
+ return typeof text === 'string' ? text : '';
18
+ })
19
+ .join('\n');
20
+ }
21
+ return '';
22
+ };
23
+ /**
24
+ * A prompt plugin that notices which `@owlmeans/*` packages a request talks about and
25
+ * loads their published skills into the system prompt.
26
+ *
27
+ * Everything the package documents is loaded — there is no relevance filtering, by
28
+ * design: choosing a subset would need a judgement the model is better placed to make
29
+ * than a regex, and a partial answer about an API is worse than none.
30
+ *
31
+ * The result lands in the `Packages` block, which sits behind its own cache breakpoint.
32
+ * That is the whole reason the block exists: what a request mentions changes call to
33
+ * call, and it must not shift a single byte of the role and skills every call shares.
34
+ */
35
+ export const owlmeansPackagesPlugin = (options = {}) => {
36
+ const scopes = options.scopes ?? [OWLMEANS_SCOPE];
37
+ // No dots and no trailing hyphen, so `@owlmeans/llm.` yields `@owlmeans/llm` — prose
38
+ // and code both end package names with punctuation.
39
+ const pattern = new RegExp(`(?:${scopes.map(escape).join('|')})\\/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?`, 'g');
40
+ const exclude = options.exclude ?? [];
41
+ const maxPackages = options.maxPackages ?? DEFAULT_MAX_PACKAGES;
42
+ const categories = options.categories ?? [...DEFAULT_CATEGORIES];
43
+ /**
44
+ * Resolved packages, negatives included. Without caching the misses, a prompt that
45
+ * mentions a package which does not publish skills would hit GitHub on every call.
46
+ */
47
+ const cache = new Map();
48
+ /**
49
+ * Per-provider caches, keyed by the provider INSTANCE.
50
+ *
51
+ * A host's file provider is usually per-project (a sandbox, a slot, one checkout), so
52
+ * two executions can resolve the same package name to different content. Sharing one
53
+ * cache across them would serve project A's skills to project B. A `WeakMap` keeps the
54
+ * entries alive exactly as long as the provider is.
55
+ */
56
+ const perProvider = new WeakMap();
57
+ const load = (packageName, ctx) => {
58
+ // The context's provider is the specific one — it sees the project this call is
59
+ // actually about. The constructor option is the fallback for a host that wires one
60
+ // globally. Without this, a per-execution provider was never consulted at all.
61
+ const provider = resolveFileProvider(ctx.files) ?? resolveFileProvider(options.files);
62
+ const store = provider != null
63
+ ? perProvider.get(provider) ?? (() => {
64
+ const fresh = new Map();
65
+ perProvider.set(provider, fresh);
66
+ return fresh;
67
+ })()
68
+ : cache;
69
+ const pending = store.get(packageName);
70
+ if (pending != null) {
71
+ return pending;
72
+ }
73
+ const promise = loadPackageSkills(packageName, { ...options, files: provider }, categories);
74
+ store.set(packageName, promise);
75
+ return promise;
76
+ };
77
+ const detect = (ctx) => {
78
+ const found = new Set();
79
+ for (const message of ctx.messages) {
80
+ const text = textOf(message.content);
81
+ if (text === '') {
82
+ continue;
83
+ }
84
+ for (const match of text.matchAll(pattern)) {
85
+ found.add(match[0]);
86
+ }
87
+ }
88
+ // Alphabetical, not first-seen: two prompts that mention the same packages in a
89
+ // different order must still produce the same bytes.
90
+ return [...found]
91
+ .filter(name => !exclude.includes(name))
92
+ .sort()
93
+ .slice(0, maxPackages);
94
+ };
95
+ return {
96
+ alias: PACKAGE_SKILLS_PLUGIN,
97
+ order: 50,
98
+ inspect: async (ctx) => {
99
+ const names = detect(ctx);
100
+ if (names.length === 0) {
101
+ return;
102
+ }
103
+ const resolved = await Promise.all(names.map(name => load(name, ctx)));
104
+ for (const found of resolved) {
105
+ if (found == null) {
106
+ continue;
107
+ }
108
+ for (const skill of found.skills) {
109
+ ctx.add(PromptBlock.Packages, renderSkill(skill));
110
+ }
111
+ }
112
+ },
113
+ };
114
+ };
115
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/llm/plugin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { PackageSkills, PackageSkillsOptions } from './types.js';
2
+ /** Try each source in order of trustworthiness and cost. */
3
+ export declare const loadPackageSkills: (packageName: string, options: PackageSkillsOptions, categories: readonly string[]) => Promise<PackageSkills | null>;
4
+ //# sourceMappingURL=resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/llm/resolve.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AA0JrE,4DAA4D;AAC5D,eAAO,MAAM,iBAAiB,GAC5B,aAAa,MAAM,EACnB,SAAS,oBAAoB,EAC7B,YAAY,SAAS,MAAM,EAAE,KAC5B,OAAO,CAAC,aAAa,GAAG,IAAI,CAe9B,CAAA"}
@@ -0,0 +1,145 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, join, resolve as resolvePath } from 'node:path';
3
+ import { resolveFileProvider } from '@owlmeans/llm-common';
4
+ import { parseManifest, skillEntries, stripMeta, toSkill, unscoped } from './manifest.js';
5
+ const AGENT_META = 'agent-meta';
6
+ const MANIFEST = 'manifest.json';
7
+ const DEFAULT_REPO = 'owlmeans/common';
8
+ const DEFAULT_REF = 'main';
9
+ const DEFAULT_TIMEOUT = 5000;
10
+ /** Path of an embedded file relative to a project root, in the host's own terms. */
11
+ const projectPath = (packageName, ...parts) => ['node_modules', packageName, AGENT_META, ...parts].join('/');
12
+ /**
13
+ * Source 1 — the host's own file access.
14
+ *
15
+ * Tried first because it is the only one that sees the project the model is actually
16
+ * working on: a sandbox, a remote workspace, a container this process cannot reach.
17
+ */
18
+ const fromFiles = async (packageName, provider, categories) => {
19
+ const read = async (path) => {
20
+ try {
21
+ return await provider.readFile(path, true) ?? '';
22
+ }
23
+ catch {
24
+ return '';
25
+ }
26
+ };
27
+ const manifest = parseManifest(await read(projectPath(packageName, MANIFEST)));
28
+ if (manifest == null) {
29
+ return null;
30
+ }
31
+ const skills = [];
32
+ for (const entry of skillEntries(manifest, categories)) {
33
+ const body = stripMeta(await read(projectPath(packageName, entry.file)));
34
+ if (body !== '') {
35
+ skills.push(toSkill(packageName, entry, body));
36
+ }
37
+ }
38
+ return skills.length > 0
39
+ ? { packageName, version: manifest.version, source: 'files', skills }
40
+ : null;
41
+ };
42
+ /**
43
+ * Source 2 — an installed copy on this filesystem.
44
+ *
45
+ * Walks upward rather than assuming a layout: a hoisted install puts the package at the
46
+ * workspace root, a nested one puts it beside the consumer.
47
+ */
48
+ const findLocalDir = (packageName, from) => {
49
+ let current = resolvePath(from);
50
+ for (;;) {
51
+ const candidate = join(current, 'node_modules', packageName, AGENT_META);
52
+ if (existsSync(join(candidate, MANIFEST))) {
53
+ return candidate;
54
+ }
55
+ const parent = dirname(current);
56
+ if (parent === current) {
57
+ return null;
58
+ }
59
+ current = parent;
60
+ }
61
+ };
62
+ const fromLocal = (packageName, from, categories) => {
63
+ const dir = findLocalDir(packageName, from);
64
+ if (dir == null) {
65
+ return null;
66
+ }
67
+ const read = (path) => {
68
+ try {
69
+ return readFileSync(path, 'utf8');
70
+ }
71
+ catch {
72
+ return '';
73
+ }
74
+ };
75
+ const manifest = parseManifest(read(join(dir, MANIFEST)));
76
+ if (manifest == null) {
77
+ return null;
78
+ }
79
+ const skills = [];
80
+ for (const entry of skillEntries(manifest, categories)) {
81
+ const body = stripMeta(read(join(dir, ...entry.file.split('/'))));
82
+ if (body !== '') {
83
+ skills.push(toSkill(packageName, entry, body));
84
+ }
85
+ }
86
+ return skills.length > 0
87
+ ? { packageName, version: manifest.version, source: 'local', skills }
88
+ : null;
89
+ };
90
+ /**
91
+ * Source 3 — the canonical repository, for a package that is mentioned but not installed.
92
+ *
93
+ * It reads the EMBEDDED copies (`packages/<name>/agent-meta/`) rather than the canonical
94
+ * root ones: the embedded layout is uniform across packages and is exactly what the
95
+ * manifest describes, so one shape of URL works for every package.
96
+ */
97
+ const fromRemote = async (packageName, options, categories) => {
98
+ if (options.fetch === false || typeof globalThis.fetch !== 'function') {
99
+ return null;
100
+ }
101
+ const repo = options.repo ?? DEFAULT_REPO;
102
+ const ref = options.ref ?? DEFAULT_REF;
103
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT;
104
+ const url = (file) => `https://raw.githubusercontent.com/${repo}/${ref}/packages/${unscoped(packageName)}/${AGENT_META}/${file}`;
105
+ // Every failure here is a miss, never a throw: a prompt plugin that breaks the call
106
+ // because GitHub was slow is worse than a prompt without one package's knowledge.
107
+ const get = async (file) => {
108
+ try {
109
+ const response = await fetch(url(file), { signal: AbortSignal.timeout(timeout) });
110
+ return response.ok ? await response.text() : '';
111
+ }
112
+ catch {
113
+ return '';
114
+ }
115
+ };
116
+ const manifest = parseManifest(await get(MANIFEST));
117
+ if (manifest == null) {
118
+ return null;
119
+ }
120
+ const entries = skillEntries(manifest, categories);
121
+ const bodies = await Promise.all(entries.map(async (entry) => stripMeta(await get(entry.file))));
122
+ const skills = entries
123
+ .map((entry, i) => ({ entry, body: bodies[i] }))
124
+ .filter(({ body }) => body !== '')
125
+ .map(({ entry, body }) => toSkill(packageName, entry, body));
126
+ return skills.length > 0
127
+ ? { packageName, version: manifest.version, source: 'remote', skills }
128
+ : null;
129
+ };
130
+ /** Try each source in order of trustworthiness and cost. */
131
+ export const loadPackageSkills = async (packageName, options, categories) => {
132
+ const provider = resolveFileProvider(options.files);
133
+ if (provider != null) {
134
+ const found = await fromFiles(packageName, provider, categories);
135
+ if (found != null) {
136
+ return found;
137
+ }
138
+ }
139
+ const local = fromLocal(packageName, options.dir ?? process.cwd(), categories);
140
+ if (local != null) {
141
+ return local;
142
+ }
143
+ return fromRemote(packageName, options, categories);
144
+ };
145
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/llm/resolve.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ import type { FileProviderRef, SkillDefinition } from '@owlmeans/llm-common';
2
+ /** The default scope the plugin looks for in a prompt. */
3
+ export declare const OWLMEANS_SCOPE = "@owlmeans";
4
+ /** Where a package's skills came from — surfaced in logs, useful when a lookup surprises you. */
5
+ export type SkillSource = 'files' | 'local' | 'remote';
6
+ export interface PackageSkills {
7
+ packageName: string;
8
+ version: string;
9
+ source: SkillSource;
10
+ skills: SkillDefinition[];
11
+ }
12
+ export interface PackageSkillsOptions {
13
+ /**
14
+ * Host file access, tried FIRST. It reads the consuming project's own tree, so it is
15
+ * the only path that works when the project lives somewhere this process cannot reach
16
+ * directly — a sandbox, a remote workspace, a container.
17
+ */
18
+ files?: FileProviderRef;
19
+ /** Package scopes to detect. Defaults to `['@owlmeans']`. */
20
+ scopes?: string[];
21
+ /**
22
+ * Directory the local `node_modules` lookup starts from, walking upward.
23
+ * Defaults to `process.cwd()`.
24
+ */
25
+ dir?: string;
26
+ /** GitHub `owner/repo` holding the canonical packages. Defaults to `owlmeans/common`. */
27
+ repo?: string;
28
+ /**
29
+ * Git ref to read the embedded copies from. Defaults to `main`.
30
+ *
31
+ * The manifest deliberately carries no ref of its own — version-matching is guaranteed
32
+ * by shipping the copy inside the tarball, so there is nothing to pin against for a
33
+ * package that is NOT installed. Set this to a tag when reproducibility matters more
34
+ * than freshness.
35
+ */
36
+ ref?: string;
37
+ /** Set to `false` to skip the GitHub fallback entirely (air-gapped runs). */
38
+ fetch?: boolean;
39
+ /** Milliseconds before a remote lookup is abandoned. Defaults to 5000. */
40
+ timeout?: number;
41
+ /**
42
+ * Packages to ignore — typically the ones whose skills the static `Skills` block
43
+ * already carries, so they are not paid for twice.
44
+ */
45
+ exclude?: string[];
46
+ /**
47
+ * Ceiling on how many mentioned packages are loaded into one prompt. Defaults to 5.
48
+ * A prompt that name-drops a dozen packages is usually not asking about all of them,
49
+ * and every one of them costs context.
50
+ */
51
+ maxPackages?: number;
52
+ /**
53
+ * Manifest categories to include. Defaults to package knowledge only — `general`
54
+ * entries describe how an agent should behave, not what a package does, and belong in
55
+ * the host's own skill catalogue rather than in a package lookup.
56
+ */
57
+ categories?: Array<'package-specific' | 'multi-package' | 'general'>;
58
+ }
59
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/llm/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAE5E,0DAA0D;AAC1D,eAAO,MAAM,cAAc,cAAc,CAAA;AAEzC,iGAAiG;AACjG,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAA;AAEtD,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,WAAW,CAAA;IACnB,MAAM,EAAE,eAAe,EAAE,CAAA;CAC1B;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,KAAK,CAAC,EAAE,eAAe,CAAA;IAEvB,6DAA6D;IAC7D,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IAEjB;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IAEZ,yFAAyF;IACzF,IAAI,CAAC,EAAE,MAAM,CAAA;IAEb;;;;;;;OAOG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IAEZ,6EAA6E;IAC7E,KAAK,CAAC,EAAE,OAAO,CAAA;IAEf,0EAA0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAElB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IAEpB;;;;OAIG;IACH,UAAU,CAAC,EAAE,KAAK,CAAC,kBAAkB,GAAG,eAAe,GAAG,SAAS,CAAC,CAAA;CACrE"}
@@ -0,0 +1,3 @@
1
+ /** The default scope the plugin looks for in a prompt. */
2
+ export const OWLMEANS_SCOPE = '@owlmeans';
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/llm/types.ts"],"names":[],"mappings":"AAEA,0DAA0D;AAC1D,MAAM,CAAC,MAAM,cAAc,GAAG,WAAW,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/agent-skills",
3
- "version": "0.1.15",
3
+ "version": "0.1.16-rc.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -22,15 +22,37 @@
22
22
  "default": "./build/index.js",
23
23
  "module": "./build/index.js",
24
24
  "types": "./build/index.d.ts"
25
+ },
26
+ "./llm": {
27
+ "import": "./build/llm/index.js",
28
+ "require": "./build/llm/index.js",
29
+ "default": "./build/llm/index.js",
30
+ "module": "./build/llm/index.js",
31
+ "types": "./build/llm/index.d.ts"
25
32
  }
26
33
  },
27
34
  "devDependencies": {
35
+ "@langchain/core": "^1.1.39",
28
36
  "@owlmeans/dep-config": "workspace:*",
37
+ "@owlmeans/llm": "^0.1.16-rc.0",
38
+ "@owlmeans/llm-common": "^0.1.16-rc.0",
29
39
  "@types/bun": "^1.3.14",
30
40
  "@types/node": "^26.1.0",
31
41
  "nodemon": "^3.1.14",
32
42
  "typescript": "^6.0.3"
33
43
  },
44
+ "peerDependencies": {
45
+ "@owlmeans/llm": "^0.1.16-rc.0",
46
+ "@owlmeans/llm-common": "^0.1.16-rc.0"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "@owlmeans/llm": {
50
+ "optional": true
51
+ },
52
+ "@owlmeans/llm-common": {
53
+ "optional": true
54
+ }
55
+ },
34
56
  "publishConfig": {
35
57
  "access": "public"
36
58
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The LLM-facing half of this package: a prompt plugin that turns a mention of an
3
+ * `@owlmeans/*` package in a prompt into that package's published skills.
4
+ *
5
+ * Deliberately NOT re-exported from the package index. The installer CLI has no runtime
6
+ * dependencies and must keep it that way; this entry point resolves `@owlmeans/llm` and
7
+ * `@owlmeans/llm-common`, which only a consumer already using the LLM layer will have.
8
+ */
9
+
10
+ export * from './types.js'
11
+ export * from './manifest.js'
12
+ export * from './resolve.js'
13
+ export * from './plugin.js'
@@ -0,0 +1,74 @@
1
+ import { DEFAULT_SKILL_ORDER, PromptBlock } from '@owlmeans/llm-common'
2
+ import type { SkillDefinition } from '@owlmeans/llm-common'
3
+ import type { Manifest, ManifestEntry } from '../discover.js'
4
+
5
+ /** `@owlmeans/foo` → `foo`; the repo lays packages out under their unscoped name. */
6
+ export const unscoped = (packageName: string): string =>
7
+ packageName.slice(packageName.indexOf('/') + 1)
8
+
9
+ export const parseManifest = (raw: string): Manifest | null => {
10
+ try {
11
+ const manifest = JSON.parse(raw) as Manifest
12
+ if (typeof manifest.schemaVersion !== 'number' || !Array.isArray(manifest.entries)) {
13
+ return null
14
+ }
15
+ return manifest
16
+ } catch {
17
+ return null
18
+ }
19
+ }
20
+
21
+ const BANNER = /^<!--\s*AUTO-GENERATED[^>]*-->\s*/
22
+
23
+ /**
24
+ * Strip the YAML frontmatter and the generated-file banner from an embedded SKILL.md.
25
+ *
26
+ * Both are installer bookkeeping — `name`, `description`, `applyTo`, "do not edit". Sent
27
+ * to a model they are pure noise in the most expensive part of the prompt, and the
28
+ * "do not edit" line actively misleads it about what it is being shown.
29
+ */
30
+ export const stripMeta = (content: string): string => {
31
+ let body = content.replace(/^/, '')
32
+ if (body.startsWith('---')) {
33
+ const end = body.indexOf('\n---', 3)
34
+ if (end >= 0) {
35
+ const after = body.indexOf('\n', end + 1)
36
+ body = after >= 0 ? body.slice(after + 1) : ''
37
+ }
38
+ }
39
+
40
+ return body.replace(BANNER, '').trim()
41
+ }
42
+
43
+ /**
44
+ * Skill entries worth loading, in a deterministic order.
45
+ *
46
+ * Instruction entries are dropped: they are the Copilot twin of the same knowledge, so
47
+ * including both would double the token cost of every package for no added information.
48
+ */
49
+ export const skillEntries = (
50
+ manifest: Manifest,
51
+ categories: readonly string[],
52
+ ): ManifestEntry[] =>
53
+ manifest.entries
54
+ .filter(entry => entry.kind === 'skill' && categories.includes(entry.category))
55
+ .sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
56
+
57
+ /**
58
+ * Turn one embedded skill file into a definition bound to the `Packages` block.
59
+ *
60
+ * The alias is namespaced by package so two packages documenting the same concept cannot
61
+ * collide in the registry, and the order weight is pushed past the default so package
62
+ * knowledge always renders after whatever the host declared itself.
63
+ */
64
+ export const toSkill = (
65
+ packageName: string,
66
+ entry: ManifestEntry,
67
+ body: string,
68
+ ): SkillDefinition => ({
69
+ alias: `${packageName}#${entry.name}`,
70
+ title: `${packageName} — ${entry.name}`,
71
+ body,
72
+ block: PromptBlock.Packages,
73
+ order: DEFAULT_SKILL_ORDER + 1,
74
+ })
@@ -0,0 +1,133 @@
1
+ import { PromptBlock, resolveFileProvider } from '@owlmeans/llm-common'
2
+ import { renderSkill } from '@owlmeans/llm/prompt'
3
+ import type { LlmPromptPlugin, PromptContext } from '@owlmeans/llm'
4
+ import { loadPackageSkills } from './resolve.js'
5
+ import { OWLMEANS_SCOPE } from './types.js'
6
+ import type { PackageSkills, PackageSkillsOptions } from './types.js'
7
+
8
+ export const PACKAGE_SKILLS_PLUGIN = 'owlmeans-packages'
9
+
10
+ const DEFAULT_MAX_PACKAGES = 5
11
+ const DEFAULT_CATEGORIES = ['package-specific', 'multi-package'] as const
12
+
13
+ const escape = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
14
+
15
+ const textOf = (content: unknown): string => {
16
+ if (typeof content === 'string') {
17
+ return content
18
+ }
19
+ if (Array.isArray(content)) {
20
+ return content
21
+ .map(part => {
22
+ const text = (part as { text?: unknown }).text
23
+ return typeof text === 'string' ? text : ''
24
+ })
25
+ .join('\n')
26
+ }
27
+
28
+ return ''
29
+ }
30
+
31
+ /**
32
+ * A prompt plugin that notices which `@owlmeans/*` packages a request talks about and
33
+ * loads their published skills into the system prompt.
34
+ *
35
+ * Everything the package documents is loaded — there is no relevance filtering, by
36
+ * design: choosing a subset would need a judgement the model is better placed to make
37
+ * than a regex, and a partial answer about an API is worse than none.
38
+ *
39
+ * The result lands in the `Packages` block, which sits behind its own cache breakpoint.
40
+ * That is the whole reason the block exists: what a request mentions changes call to
41
+ * call, and it must not shift a single byte of the role and skills every call shares.
42
+ */
43
+ export const owlmeansPackagesPlugin = (options: PackageSkillsOptions = {}): LlmPromptPlugin => {
44
+ const scopes = options.scopes ?? [OWLMEANS_SCOPE]
45
+ // No dots and no trailing hyphen, so `@owlmeans/llm.` yields `@owlmeans/llm` — prose
46
+ // and code both end package names with punctuation.
47
+ const pattern = new RegExp(
48
+ `(?:${scopes.map(escape).join('|')})\\/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?`, 'g'
49
+ )
50
+ const exclude = options.exclude ?? []
51
+ const maxPackages = options.maxPackages ?? DEFAULT_MAX_PACKAGES
52
+ const categories = options.categories ?? [...DEFAULT_CATEGORIES]
53
+
54
+ /**
55
+ * Resolved packages, negatives included. Without caching the misses, a prompt that
56
+ * mentions a package which does not publish skills would hit GitHub on every call.
57
+ */
58
+ const cache = new Map<string, Promise<PackageSkills | null>>()
59
+
60
+ /**
61
+ * Per-provider caches, keyed by the provider INSTANCE.
62
+ *
63
+ * A host's file provider is usually per-project (a sandbox, a slot, one checkout), so
64
+ * two executions can resolve the same package name to different content. Sharing one
65
+ * cache across them would serve project A's skills to project B. A `WeakMap` keeps the
66
+ * entries alive exactly as long as the provider is.
67
+ */
68
+ const perProvider = new WeakMap<object, Map<string, Promise<PackageSkills | null>>>()
69
+
70
+ const load = (packageName: string, ctx: PromptContext): Promise<PackageSkills | null> => {
71
+ // The context's provider is the specific one — it sees the project this call is
72
+ // actually about. The constructor option is the fallback for a host that wires one
73
+ // globally. Without this, a per-execution provider was never consulted at all.
74
+ const provider = resolveFileProvider(ctx.files) ?? resolveFileProvider(options.files)
75
+ const store = provider != null
76
+ ? perProvider.get(provider) ?? (() => {
77
+ const fresh = new Map<string, Promise<PackageSkills | null>>()
78
+ perProvider.set(provider, fresh)
79
+ return fresh
80
+ })()
81
+ : cache
82
+
83
+ const pending = store.get(packageName)
84
+ if (pending != null) {
85
+ return pending
86
+ }
87
+ const promise = loadPackageSkills(packageName, { ...options, files: provider }, categories)
88
+ store.set(packageName, promise)
89
+
90
+ return promise
91
+ }
92
+
93
+ const detect = (ctx: PromptContext): string[] => {
94
+ const found = new Set<string>()
95
+ for (const message of ctx.messages) {
96
+ const text = textOf((message as { content?: unknown }).content)
97
+ if (text === '') {
98
+ continue
99
+ }
100
+ for (const match of text.matchAll(pattern)) {
101
+ found.add(match[0])
102
+ }
103
+ }
104
+
105
+ // Alphabetical, not first-seen: two prompts that mention the same packages in a
106
+ // different order must still produce the same bytes.
107
+ return [...found]
108
+ .filter(name => !exclude.includes(name))
109
+ .sort()
110
+ .slice(0, maxPackages)
111
+ }
112
+
113
+ return {
114
+ alias: PACKAGE_SKILLS_PLUGIN,
115
+ order: 50,
116
+
117
+ inspect: async ctx => {
118
+ const names = detect(ctx)
119
+ if (names.length === 0) {
120
+ return
121
+ }
122
+ const resolved = await Promise.all(names.map(name => load(name, ctx)))
123
+ for (const found of resolved) {
124
+ if (found == null) {
125
+ continue
126
+ }
127
+ for (const skill of found.skills) {
128
+ ctx.add(PromptBlock.Packages, renderSkill(skill))
129
+ }
130
+ }
131
+ },
132
+ }
133
+ }
@@ -0,0 +1,180 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { dirname, join, resolve as resolvePath } from 'node:path'
3
+ import { resolveFileProvider } from '@owlmeans/llm-common'
4
+ import type { LlmFileProvider, SkillDefinition } from '@owlmeans/llm-common'
5
+ import { parseManifest, skillEntries, stripMeta, toSkill, unscoped } from './manifest.js'
6
+ import type { PackageSkills, PackageSkillsOptions } from './types.js'
7
+
8
+ const AGENT_META = 'agent-meta'
9
+ const MANIFEST = 'manifest.json'
10
+ const DEFAULT_REPO = 'owlmeans/common'
11
+ const DEFAULT_REF = 'main'
12
+ const DEFAULT_TIMEOUT = 5000
13
+
14
+ /** Path of an embedded file relative to a project root, in the host's own terms. */
15
+ const projectPath = (packageName: string, ...parts: string[]): string =>
16
+ ['node_modules', packageName, AGENT_META, ...parts].join('/')
17
+
18
+ /**
19
+ * Source 1 — the host's own file access.
20
+ *
21
+ * Tried first because it is the only one that sees the project the model is actually
22
+ * working on: a sandbox, a remote workspace, a container this process cannot reach.
23
+ */
24
+ const fromFiles = async (
25
+ packageName: string,
26
+ provider: LlmFileProvider,
27
+ categories: readonly string[],
28
+ ): Promise<PackageSkills | null> => {
29
+ const read = async (path: string): Promise<string> => {
30
+ try {
31
+ return await provider.readFile(path, true) ?? ''
32
+ } catch {
33
+ return ''
34
+ }
35
+ }
36
+
37
+ const manifest = parseManifest(await read(projectPath(packageName, MANIFEST)))
38
+ if (manifest == null) {
39
+ return null
40
+ }
41
+
42
+ const skills: SkillDefinition[] = []
43
+ for (const entry of skillEntries(manifest, categories)) {
44
+ const body = stripMeta(await read(projectPath(packageName, entry.file)))
45
+ if (body !== '') {
46
+ skills.push(toSkill(packageName, entry, body))
47
+ }
48
+ }
49
+
50
+ return skills.length > 0
51
+ ? { packageName, version: manifest.version, source: 'files', skills }
52
+ : null
53
+ }
54
+
55
+ /**
56
+ * Source 2 — an installed copy on this filesystem.
57
+ *
58
+ * Walks upward rather than assuming a layout: a hoisted install puts the package at the
59
+ * workspace root, a nested one puts it beside the consumer.
60
+ */
61
+ const findLocalDir = (packageName: string, from: string): string | null => {
62
+ let current = resolvePath(from)
63
+ for (; ;) {
64
+ const candidate = join(current, 'node_modules', packageName, AGENT_META)
65
+ if (existsSync(join(candidate, MANIFEST))) {
66
+ return candidate
67
+ }
68
+ const parent = dirname(current)
69
+ if (parent === current) {
70
+ return null
71
+ }
72
+ current = parent
73
+ }
74
+ }
75
+
76
+ const fromLocal = (
77
+ packageName: string,
78
+ from: string,
79
+ categories: readonly string[],
80
+ ): PackageSkills | null => {
81
+ const dir = findLocalDir(packageName, from)
82
+ if (dir == null) {
83
+ return null
84
+ }
85
+ const read = (path: string): string => {
86
+ try {
87
+ return readFileSync(path, 'utf8')
88
+ } catch {
89
+ return ''
90
+ }
91
+ }
92
+
93
+ const manifest = parseManifest(read(join(dir, MANIFEST)))
94
+ if (manifest == null) {
95
+ return null
96
+ }
97
+
98
+ const skills: SkillDefinition[] = []
99
+ for (const entry of skillEntries(manifest, categories)) {
100
+ const body = stripMeta(read(join(dir, ...entry.file.split('/'))))
101
+ if (body !== '') {
102
+ skills.push(toSkill(packageName, entry, body))
103
+ }
104
+ }
105
+
106
+ return skills.length > 0
107
+ ? { packageName, version: manifest.version, source: 'local', skills }
108
+ : null
109
+ }
110
+
111
+ /**
112
+ * Source 3 — the canonical repository, for a package that is mentioned but not installed.
113
+ *
114
+ * It reads the EMBEDDED copies (`packages/<name>/agent-meta/`) rather than the canonical
115
+ * root ones: the embedded layout is uniform across packages and is exactly what the
116
+ * manifest describes, so one shape of URL works for every package.
117
+ */
118
+ const fromRemote = async (
119
+ packageName: string,
120
+ options: PackageSkillsOptions,
121
+ categories: readonly string[],
122
+ ): Promise<PackageSkills | null> => {
123
+ if (options.fetch === false || typeof globalThis.fetch !== 'function') {
124
+ return null
125
+ }
126
+ const repo = options.repo ?? DEFAULT_REPO
127
+ const ref = options.ref ?? DEFAULT_REF
128
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT
129
+ const url = (file: string): string =>
130
+ `https://raw.githubusercontent.com/${repo}/${ref}/packages/${unscoped(packageName)}/${AGENT_META}/${file}`
131
+
132
+ // Every failure here is a miss, never a throw: a prompt plugin that breaks the call
133
+ // because GitHub was slow is worse than a prompt without one package's knowledge.
134
+ const get = async (file: string): Promise<string> => {
135
+ try {
136
+ const response = await fetch(url(file), { signal: AbortSignal.timeout(timeout) })
137
+ return response.ok ? await response.text() : ''
138
+ } catch {
139
+ return ''
140
+ }
141
+ }
142
+
143
+ const manifest = parseManifest(await get(MANIFEST))
144
+ if (manifest == null) {
145
+ return null
146
+ }
147
+
148
+ const entries = skillEntries(manifest, categories)
149
+ const bodies = await Promise.all(entries.map(async entry => stripMeta(await get(entry.file))))
150
+ const skills = entries
151
+ .map((entry, i) => ({ entry, body: bodies[i]! }))
152
+ .filter(({ body }) => body !== '')
153
+ .map(({ entry, body }) => toSkill(packageName, entry, body))
154
+
155
+ return skills.length > 0
156
+ ? { packageName, version: manifest.version, source: 'remote', skills }
157
+ : null
158
+ }
159
+
160
+ /** Try each source in order of trustworthiness and cost. */
161
+ export const loadPackageSkills = async (
162
+ packageName: string,
163
+ options: PackageSkillsOptions,
164
+ categories: readonly string[],
165
+ ): Promise<PackageSkills | null> => {
166
+ const provider = resolveFileProvider(options.files)
167
+ if (provider != null) {
168
+ const found = await fromFiles(packageName, provider, categories)
169
+ if (found != null) {
170
+ return found
171
+ }
172
+ }
173
+
174
+ const local = fromLocal(packageName, options.dir ?? process.cwd(), categories)
175
+ if (local != null) {
176
+ return local
177
+ }
178
+
179
+ return fromRemote(packageName, options, categories)
180
+ }
@@ -0,0 +1,71 @@
1
+ import type { FileProviderRef, SkillDefinition } from '@owlmeans/llm-common'
2
+
3
+ /** The default scope the plugin looks for in a prompt. */
4
+ export const OWLMEANS_SCOPE = '@owlmeans'
5
+
6
+ /** Where a package's skills came from — surfaced in logs, useful when a lookup surprises you. */
7
+ export type SkillSource = 'files' | 'local' | 'remote'
8
+
9
+ export interface PackageSkills {
10
+ packageName: string
11
+ version: string
12
+ source: SkillSource
13
+ skills: SkillDefinition[]
14
+ }
15
+
16
+ export interface PackageSkillsOptions {
17
+ /**
18
+ * Host file access, tried FIRST. It reads the consuming project's own tree, so it is
19
+ * the only path that works when the project lives somewhere this process cannot reach
20
+ * directly — a sandbox, a remote workspace, a container.
21
+ */
22
+ files?: FileProviderRef
23
+
24
+ /** Package scopes to detect. Defaults to `['@owlmeans']`. */
25
+ scopes?: string[]
26
+
27
+ /**
28
+ * Directory the local `node_modules` lookup starts from, walking upward.
29
+ * Defaults to `process.cwd()`.
30
+ */
31
+ dir?: string
32
+
33
+ /** GitHub `owner/repo` holding the canonical packages. Defaults to `owlmeans/common`. */
34
+ repo?: string
35
+
36
+ /**
37
+ * Git ref to read the embedded copies from. Defaults to `main`.
38
+ *
39
+ * The manifest deliberately carries no ref of its own — version-matching is guaranteed
40
+ * by shipping the copy inside the tarball, so there is nothing to pin against for a
41
+ * package that is NOT installed. Set this to a tag when reproducibility matters more
42
+ * than freshness.
43
+ */
44
+ ref?: string
45
+
46
+ /** Set to `false` to skip the GitHub fallback entirely (air-gapped runs). */
47
+ fetch?: boolean
48
+
49
+ /** Milliseconds before a remote lookup is abandoned. Defaults to 5000. */
50
+ timeout?: number
51
+
52
+ /**
53
+ * Packages to ignore — typically the ones whose skills the static `Skills` block
54
+ * already carries, so they are not paid for twice.
55
+ */
56
+ exclude?: string[]
57
+
58
+ /**
59
+ * Ceiling on how many mentioned packages are loaded into one prompt. Defaults to 5.
60
+ * A prompt that name-drops a dozen packages is usually not asking about all of them,
61
+ * and every one of them costs context.
62
+ */
63
+ maxPackages?: number
64
+
65
+ /**
66
+ * Manifest categories to include. Defaults to package knowledge only — `general`
67
+ * entries describe how an agent should behave, not what a package does, and belong in
68
+ * the host's own skill catalogue rather than in a package lookup.
69
+ */
70
+ categories?: Array<'package-specific' | 'multi-package' | 'general'>
71
+ }
@@ -0,0 +1,158 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { PromptBlock } from '@owlmeans/llm-common'
3
+ import type { LlmFileProvider } from '@owlmeans/llm-common'
4
+ import { anthropicPlugin, makePromptService } from '@owlmeans/llm'
5
+ import type { ModelConfig, PromptService } from '@owlmeans/llm'
6
+ import { owlmeansPackagesPlugin, stripMeta, unscoped } from '@owlmeans/agent-skills/llm'
7
+
8
+ const model = anthropicPlugin.build({
9
+ alias: 'spec',
10
+ secret: 'sk-test',
11
+ callbacks: [],
12
+ config: { alias: 'spec', model: 'claude-haiku-4-5-20251001' } as ModelConfig,
13
+ })
14
+
15
+ let seq = 0
16
+ const compose = (svc: PromptService, text: string) =>
17
+ svc.compose({}, [{ role: 'user', content: text }], { model, provider: anthropicPlugin })
18
+
19
+ const withPlugin = (options: Parameters<typeof owlmeansPackagesPlugin>[0] = {}): PromptService =>
20
+ makePromptService(
21
+ { plugins: [owlmeansPackagesPlugin({ fetch: false, ...options })] },
22
+ `spec-pkg-${seq++}`,
23
+ )
24
+
25
+ /** A host file provider that serves one package, recording every path it is asked for. */
26
+ const fakeProvider = (asked: string[], body: string): LlmFileProvider => ({
27
+ getSourceList: async () => [],
28
+ writeFile: async () => { },
29
+ deleteFile: async () => { },
30
+ readFile: async path => {
31
+ asked.push(path)
32
+ if (path.endsWith('manifest.json')) {
33
+ return JSON.stringify({
34
+ schemaVersion: 1,
35
+ package: '@owlmeans/auth',
36
+ version: '9.9.9',
37
+ generatedAt: '',
38
+ canonicalRepo: '',
39
+ entries: [{
40
+ kind: 'skill',
41
+ name: 'auth',
42
+ category: 'package-specific',
43
+ file: 'skills/auth/SKILL.md',
44
+ canonicalPath: '.claude/skills/auth/SKILL.md',
45
+ }],
46
+ })
47
+ }
48
+ return `---\nname: auth\n---\n${body}`
49
+ },
50
+ })
51
+
52
+ describe('@owlmeans/agent-skills — embedded metadata', () => {
53
+ test('frontmatter and the generated banner are stripped from a skill body', () => {
54
+ const body = stripMeta([
55
+ '---',
56
+ 'name: auth',
57
+ 'description: something',
58
+ '---',
59
+ '<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->',
60
+ '',
61
+ '# @owlmeans/auth',
62
+ ].join('\n'))
63
+ expect(body).toBe('# @owlmeans/auth')
64
+ })
65
+
66
+ test('content without frontmatter survives untouched', () => {
67
+ expect(stripMeta('# Plain\n\nbody')).toBe('# Plain\n\nbody')
68
+ })
69
+
70
+ test('the repo lays packages out under their unscoped name', () => {
71
+ expect(unscoped('@owlmeans/llm-common')).toBe('llm-common')
72
+ })
73
+ })
74
+
75
+ describe('@owlmeans/agent-skills — package skills plugin', () => {
76
+ test('a mentioned package contributes its skills to the packages block', async () => {
77
+ const result = await compose(withPlugin(), 'How do I use @owlmeans/auth in a handler?')
78
+ const packages = result.blocks.find(block => block.block === PromptBlock.Packages)
79
+ expect(packages?.text).toContain('@owlmeans/auth — auth')
80
+ })
81
+
82
+ test('a package nobody mentioned is not loaded', async () => {
83
+ const result = await compose(withPlugin(), 'How do I write a handler?')
84
+ expect(result.blocks.find(block => block.block === PromptBlock.Packages)).toBeUndefined()
85
+ })
86
+
87
+ test('an excluded package is skipped even when mentioned', async () => {
88
+ const result = await compose(
89
+ withPlugin({ exclude: ['@owlmeans/auth'] }), 'How do I use @owlmeans/auth?'
90
+ )
91
+ expect(result.blocks.find(block => block.block === PromptBlock.Packages)).toBeUndefined()
92
+ })
93
+
94
+ // Two prompts that name the same packages in a different order must produce the same
95
+ // bytes, or they can never share a cache entry.
96
+ test('mention order does not change the composed bytes', async () => {
97
+ const first = await compose(withPlugin(), 'compare @owlmeans/auth with @owlmeans/context')
98
+ const second = await compose(withPlugin(), 'compare @owlmeans/context with @owlmeans/auth')
99
+ const packages = (result: typeof first) =>
100
+ result.blocks.find(block => block.block === PromptBlock.Packages)?.text
101
+ expect(packages(second)).toBe(packages(first))
102
+ })
103
+
104
+ test('trailing punctuation is not part of the package name', async () => {
105
+ const result = await compose(withPlugin(), 'What about @owlmeans/auth. And @owlmeans/auth,')
106
+ expect(result.blocks.find(block => block.block === PromptBlock.Packages)?.text)
107
+ .toContain('@owlmeans/auth — auth')
108
+ })
109
+
110
+ test('a provider wired at construction is preferred over the local filesystem', async () => {
111
+ const asked: string[] = []
112
+ const files = fakeProvider(asked, 'HOSTED BODY')
113
+ const result = await compose(withPlugin({ files: () => files }), 'about @owlmeans/auth')
114
+ expect(asked[0]).toBe('node_modules/@owlmeans/auth/agent-meta/manifest.json')
115
+ expect(result.blocks.find(block => block.block === PromptBlock.Packages)?.text)
116
+ .toContain('HOSTED BODY')
117
+ })
118
+
119
+ // The host's provider is per-execution (a sandbox, a slot, one checkout), so it can only
120
+ // arrive on the compose context — a plugin built once at context composition cannot know
121
+ // it. Reading only the constructor option left this path dead.
122
+ test('a provider supplied on the compose context is used and preferred', async () => {
123
+ const asked: string[] = []
124
+ const files = fakeProvider(asked, 'CONTEXT BODY')
125
+ const svc = withPlugin()
126
+ const result = await svc.compose(
127
+ {},
128
+ [{ role: 'user', content: 'about @owlmeans/auth' }],
129
+ { model, provider: anthropicPlugin, files: () => files },
130
+ )
131
+ expect(asked[0]).toBe('node_modules/@owlmeans/auth/agent-meta/manifest.json')
132
+ expect(result.blocks.find(block => block.block === PromptBlock.Packages)?.text)
133
+ .toContain('CONTEXT BODY')
134
+ })
135
+
136
+ test('two providers do not share each other\'s cached packages', async () => {
137
+ const svc = withPlugin()
138
+ const one = fakeProvider([], 'FIRST PROJECT')
139
+ const two = fakeProvider([], 'SECOND PROJECT')
140
+ const run = (files: LlmFileProvider) => svc.compose(
141
+ {},
142
+ [{ role: 'user', content: 'about @owlmeans/auth' }],
143
+ { model, provider: anthropicPlugin, files: () => files },
144
+ )
145
+ const first = await run(one)
146
+ const second = await run(two)
147
+ const packages = (r: typeof first) =>
148
+ r.blocks.find(block => block.block === PromptBlock.Packages)?.text
149
+ expect(packages(first)).toContain('FIRST PROJECT')
150
+ expect(packages(second)).toContain('SECOND PROJECT')
151
+ })
152
+
153
+ // A prompt plugin that throws takes the whole model call with it.
154
+ test('an unreachable package degrades the prompt instead of failing the call', async () => {
155
+ const result = await compose(withPlugin(), 'about @owlmeans/does-not-exist-anywhere')
156
+ expect(result.blocks.find(block => block.block === PromptBlock.Packages)).toBeUndefined()
157
+ })
158
+ })