@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 +24 -0
- package/agent-meta/manifest.json +2 -2
- package/build/llm/index.d.ts +13 -0
- package/build/llm/index.d.ts.map +1 -0
- package/build/llm/index.js +13 -0
- package/build/llm/index.js.map +1 -0
- package/build/llm/manifest.d.ts +29 -0
- package/build/llm/manifest.d.ts.map +1 -0
- package/build/llm/manifest.js +58 -0
- package/build/llm/manifest.js.map +1 -0
- package/build/llm/plugin.d.ts +17 -0
- package/build/llm/plugin.d.ts.map +1 -0
- package/build/llm/plugin.js +115 -0
- package/build/llm/plugin.js.map +1 -0
- package/build/llm/resolve.d.ts +4 -0
- package/build/llm/resolve.d.ts.map +1 -0
- package/build/llm/resolve.js +145 -0
- package/build/llm/resolve.js.map +1 -0
- package/build/llm/types.d.ts +59 -0
- package/build/llm/types.d.ts.map +1 -0
- package/build/llm/types.js +3 -0
- package/build/llm/types.js.map +1 -0
- package/package.json +23 -1
- package/src/llm/index.ts +13 -0
- package/src/llm/manifest.ts +74 -0
- package/src/llm/plugin.ts +133 -0
- package/src/llm/resolve.ts +180 -0
- package/src/llm/types.ts +71 -0
- package/tests/llm-plugin.test.ts +158 -0
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
|
+
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"package": "@owlmeans/agent-skills",
|
|
4
|
-
"version": "0.1.
|
|
5
|
-
"generatedAt": "2026-08-
|
|
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":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAClD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,WAAW,CAAA;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAE1D,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAGzF,MAAM,UAAU,GAAG,YAAY,CAAA;AAC/B,MAAM,QAAQ,GAAG,eAAe,CAAA;AAChC,MAAM,YAAY,GAAG,iBAAiB,CAAA;AACtC,MAAM,WAAW,GAAG,MAAM,CAAA;AAC1B,MAAM,eAAe,GAAG,IAAI,CAAA;AAE5B,oFAAoF;AACpF,MAAM,WAAW,GAAG,CAAC,WAAmB,EAAE,GAAG,KAAe,EAAU,EAAE,CACtE,CAAC,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AAE/D;;;;;GAKG;AACH,MAAM,SAAS,GAAG,KAAK,EACrB,WAAmB,EACnB,QAAyB,EACzB,UAA6B,EACE,EAAE;IACjC,MAAM,IAAI,GAAG,KAAK,EAAE,IAAY,EAAmB,EAAE;QACnD,IAAI,CAAC;YACH,OAAO,MAAM,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAA;QAClD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,CAAA;QACX,CAAC;IACH,CAAC,CAAA;IAED,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,IAAI,CAAC,WAAW,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAA;IAC9E,IAAI,QAAQ,IAAI,IAAI,EAAE,CAAC;QACrB,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,MAAM,GAAsB,EAAE,CAAA;IACpC,KAAK,MAAM,KAAK,IAAI,YAAY,CAAC,QAAQ,EAAE,UAAU,CAAC,EAAE,CAAC;QACvD,MAAM,IAAI,GAAG,SAAS,CAAC,MAAM,IAAI,CAAC,WAAW,CAAC,WAAW,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACxE,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;YAChB,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAA;QAChD,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC;QACtB,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE;QACrE,CAAC,CAAC,IAAI,CAAA;AACV,CAAC,CAAA;AAED;;;;;GAKG;AACH,MAAM,YAAY,GAAG,CAAC,WAAmB,EAAE,IAAY,EAAiB,EAAE;IACxE,IAAI,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,CAAA;IAC/B,SAAU,CAAC;QACT,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,CAAC,CAAA;QACxE,IAAI,UAAU,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC;YAC1C,OAAO,SAAS,CAAA;QAClB,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;QAC/B,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;YACvB,OAAO,IAAI,CAAA;QACb,CAAC;QACD,OAAO,GAAG,MAAM,CAAA;IAClB,CAAC;AACH,CAAC,CAAA;AAED,MAAM,SAAS,GAAG,CAChB,WAAmB,EACnB,IAAY,EACZ,UAA6B,EACP,EAAE;IACxB,MAAM,GAAG,GAAG,YAAY,CAAC,WAAW,EAAE,IAAI,CAAC,CAAA;IAC3C,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC;QAChB,OAAO,IAAI,CAAA;IACb,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,IAAY,EAAU,EAAE;QACpC,IAAI,CAAC;YACH,OAAO,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QACnC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,CAAA;QACX,CAAC;IACH,CAAC,CAAA;IAED,MAAM,QAAQ,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAA;IACzD,IAAI,QAAQ,IAAI,IAAI,EAAE,CAAC;QACrB,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,MAAM,GAAsB,EAAE,CAAA;IACpC,KAAK,MAAM,KAAK,IAAI,YAAY,CAAC,QAAQ,EAAE,UAAU,CAAC,EAAE,CAAC;QACvD,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;QACjE,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;YAChB,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAA;QAChD,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC;QACtB,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE;QACrE,CAAC,CAAC,IAAI,CAAA;AACV,CAAC,CAAA;AAED;;;;;;GAMG;AACH,MAAM,UAAU,GAAG,KAAK,EACtB,WAAmB,EACnB,OAA6B,EAC7B,UAA6B,EACE,EAAE;IACjC,IAAI,OAAO,CAAC,KAAK,KAAK,KAAK,IAAI,OAAO,UAAU,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;QACtE,OAAO,IAAI,CAAA;IACb,CAAC;IACD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,YAAY,CAAA;IACzC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,WAAW,CAAA;IACtC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,eAAe,CAAA;IAClD,MAAM,GAAG,GAAG,CAAC,IAAY,EAAU,EAAE,CACnC,qCAAqC,IAAI,IAAI,GAAG,aAAa,QAAQ,CAAC,WAAW,CAAC,IAAI,UAAU,IAAI,IAAI,EAAE,CAAA;IAE5G,oFAAoF;IACpF,kFAAkF;IAClF,MAAM,GAAG,GAAG,KAAK,EAAE,IAAY,EAAmB,EAAE;QAClD,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;YACjF,OAAO,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;QACjD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,CAAA;QACX,CAAC;IACH,CAAC,CAAA;IAED,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAA;IACnD,IAAI,QAAQ,IAAI,IAAI,EAAE,CAAC;QACrB,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,OAAO,GAAG,YAAY,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAA;IAClD,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAC,KAAK,EAAC,EAAE,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;IAC9F,MAAM,MAAM,GAAG,OAAO;SACnB,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAE,EAAE,CAAC,CAAC;SAChD,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC;SACjC,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAA;IAE9D,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC;QACtB,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE;QACtE,CAAC,CAAC,IAAI,CAAA;AACV,CAAC,CAAA;AAED,4DAA4D;AAC5D,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,EACpC,WAAmB,EACnB,OAA6B,EAC7B,UAA6B,EACE,EAAE;IACjC,MAAM,QAAQ,GAAG,mBAAmB,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;IACnD,IAAI,QAAQ,IAAI,IAAI,EAAE,CAAC;QACrB,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,WAAW,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAA;QAChE,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;YAClB,OAAO,KAAK,CAAA;QACd,CAAC;IACH,CAAC;IAED,MAAM,KAAK,GAAG,SAAS,CAAC,WAAW,EAAE,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,EAAE,UAAU,CAAC,CAAA;IAC9E,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;QAClB,OAAO,KAAK,CAAA;IACd,CAAC;IAED,OAAO,UAAU,CAAC,WAAW,EAAE,OAAO,EAAE,UAAU,CAAC,CAAA;AACrD,CAAC,CAAA"}
|
|
@@ -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 @@
|
|
|
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.
|
|
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
|
}
|
package/src/llm/index.ts
ADDED
|
@@ -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
|
+
}
|
package/src/llm/types.ts
ADDED
|
@@ -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
|
+
})
|