@owlmeans/agent-skills 0.1.15 → 0.1.16
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 +46 -18
- package/agent-meta/manifest.json +15 -99
- package/agent-meta/skills/agent-memory/SKILL.md +8 -7
- package/agent-meta/skills/getting-started/SKILL.md +2 -1
- package/agent-meta/skills/git/SKILL.md +2 -1
- package/agent-meta/skills/memory-promotion/SKILL.md +5 -4
- package/agent-meta/skills/memory-recompact/SKILL.md +3 -3
- package/agent-meta/skills/reuse-code/SKILL.md +4 -3
- package/agent-meta/skills/router-plugins/SKILL.md +2 -1
- package/agent-meta/skills/scaffolding/SKILL.md +15 -13
- package/agent-meta/skills/self-education/SKILL.md +10 -10
- package/agent-meta/skills/shadcn-versions/SKILL.md +3 -2
- package/agent-meta/skills/shadcn-web/SKILL.md +2 -1
- package/agent-meta/skills/skill-authoring/SKILL.md +44 -39
- package/build/apply.d.ts +2 -1
- package/build/apply.d.ts.map +1 -1
- package/build/apply.js +40 -4
- package/build/apply.js.map +1 -1
- package/build/args.d.ts +4 -2
- package/build/args.d.ts.map +1 -1
- package/build/args.js +6 -7
- package/build/args.js.map +1 -1
- package/build/discover.d.ts +8 -5
- package/build/discover.d.ts.map +1 -1
- package/build/discover.js +13 -11
- package/build/discover.js.map +1 -1
- 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 +30 -0
- package/build/llm/manifest.d.ts.map +1 -0
- package/build/llm/manifest.js +59 -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/build/plan.d.ts.map +1 -1
- package/build/plan.js +7 -6
- package/build/plan.js.map +1 -1
- package/build/run.d.ts.map +1 -1
- package/build/run.js +9 -12
- package/build/run.js.map +1 -1
- package/package.json +23 -1
- package/src/apply.ts +41 -4
- package/src/args.ts +12 -9
- package/src/discover.ts +13 -14
- package/src/llm/index.ts +13 -0
- package/src/llm/manifest.ts +75 -0
- package/src/llm/plugin.ts +133 -0
- package/src/llm/resolve.ts +180 -0
- package/src/llm/types.ts +71 -0
- package/src/plan.ts +8 -6
- package/src/run.ts +8 -11
- package/tests/installer.test.ts +61 -45
- package/tests/llm-plugin.test.ts +158 -0
- package/agent-meta/instructions/agent-memory.instructions.md +0 -143
- package/agent-meta/instructions/getting-started.instructions.md +0 -62
- package/agent-meta/instructions/git.instructions.md +0 -52
- package/agent-meta/instructions/memory-promotion.instructions.md +0 -111
- package/agent-meta/instructions/memory-recompact.instructions.md +0 -73
- package/agent-meta/instructions/reuse-code.instructions.md +0 -50
- package/agent-meta/instructions/router-plugins.instructions.md +0 -46
- package/agent-meta/instructions/scaffolding.instructions.md +0 -68
- package/agent-meta/instructions/self-education.instructions.md +0 -85
- package/agent-meta/instructions/shadcn-versions.instructions.md +0 -47
- package/agent-meta/instructions/shadcn-web.instructions.md +0 -95
- package/agent-meta/instructions/skill-authoring.instructions.md +0 -66
package/src/args.ts
CHANGED
|
@@ -2,8 +2,10 @@ export interface CliArgs {
|
|
|
2
2
|
dir: string
|
|
3
3
|
yes: boolean
|
|
4
4
|
only: string[]
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/** @deprecated no-op — skills install to `.agents/skills/` for every agent. */
|
|
6
|
+
claudeOnly?: boolean
|
|
7
|
+
/** @deprecated no-op — skills install to `.agents/skills/` for every agent. */
|
|
8
|
+
copilotOnly?: boolean
|
|
7
9
|
extras: boolean
|
|
8
10
|
force: boolean
|
|
9
11
|
dryRun: boolean
|
|
@@ -12,14 +14,16 @@ export interface CliArgs {
|
|
|
12
14
|
|
|
13
15
|
const HELP = `owlmeans-agent-skills — install embedded @owlmeans/* agent guidance
|
|
14
16
|
|
|
17
|
+
Skills are written to .agents/skills/<name>/SKILL.md — the Agent Skills standard
|
|
18
|
+
location read by Copilot, Codex and other agents. Projects with a .claude/
|
|
19
|
+
directory also get the per-skill symlinks Claude Code needs.
|
|
20
|
+
|
|
15
21
|
Usage: npx @owlmeans/agent-skills [options]
|
|
16
22
|
|
|
17
23
|
Options:
|
|
18
24
|
--dir <path> target project directory (default: cwd)
|
|
19
25
|
--yes, -y skip interactive confirmation
|
|
20
26
|
--only <pkg,...> comma-separated @owlmeans/* package names to install from
|
|
21
|
-
--claude-only install only Claude Code skills
|
|
22
|
-
--copilot-only install only Copilot instructions
|
|
23
27
|
--extras include extras bundled with the installer (default: on)
|
|
24
28
|
--no-extras skip installer-bundled extras
|
|
25
29
|
--force overwrite locally-edited files (no AUTO-GENERATED banner)
|
|
@@ -27,14 +31,15 @@ Options:
|
|
|
27
31
|
--help, -h show this help
|
|
28
32
|
`
|
|
29
33
|
|
|
34
|
+
const OBSOLETE_TOOL_FLAG =
|
|
35
|
+
'note: %s is obsolete — skills install to .agents/skills/ for every agent.\n'
|
|
36
|
+
|
|
30
37
|
export const parseArgs = (argv: string[]): CliArgs | null => {
|
|
31
38
|
const args = argv.slice(2) // strip node + script
|
|
32
39
|
const result: CliArgs = {
|
|
33
40
|
dir: process.cwd(),
|
|
34
41
|
yes: false,
|
|
35
42
|
only: [],
|
|
36
|
-
claudeOnly: false,
|
|
37
|
-
copilotOnly: false,
|
|
38
43
|
extras: true,
|
|
39
44
|
force: false,
|
|
40
45
|
dryRun: false,
|
|
@@ -60,10 +65,8 @@ export const parseArgs = (argv: string[]): CliArgs | null => {
|
|
|
60
65
|
result.dryRun = true
|
|
61
66
|
break
|
|
62
67
|
case '--claude-only':
|
|
63
|
-
result.claudeOnly = true
|
|
64
|
-
break
|
|
65
68
|
case '--copilot-only':
|
|
66
|
-
|
|
69
|
+
process.stderr.write(OBSOLETE_TOOL_FLAG.replace('%s', a))
|
|
67
70
|
break
|
|
68
71
|
case '--extras':
|
|
69
72
|
result.extras = true
|
package/src/discover.ts
CHANGED
|
@@ -211,17 +211,20 @@ export interface DiscoverOptions {
|
|
|
211
211
|
extras?: boolean
|
|
212
212
|
/** Restrict to entries from these package names. */
|
|
213
213
|
only?: string[]
|
|
214
|
-
/** Install only skills. */
|
|
215
|
-
claudeOnly?: boolean
|
|
216
|
-
/** Install only instructions. */
|
|
217
|
-
copilotOnly?: boolean
|
|
218
214
|
}
|
|
219
215
|
|
|
220
|
-
/**
|
|
216
|
+
/**
|
|
217
|
+
* Collect all DiscoveredEntries, deduplicated by name.
|
|
218
|
+
*
|
|
219
|
+
* Only `kind: 'skill'` entries are installed. Packages published before schema v2
|
|
220
|
+
* also carry `kind: 'instruction'` twins of the same knowledge (the Copilot format
|
|
221
|
+
* that predates the Agent Skills standard); they are dropped here so a mixed
|
|
222
|
+
* node_modules never installs both halves.
|
|
223
|
+
*/
|
|
221
224
|
export const discover = (targetDir: string, opts: DiscoverOptions = {}): DiscoveredEntry[] => {
|
|
222
|
-
const { extras = true, only
|
|
225
|
+
const { extras = true, only } = opts
|
|
223
226
|
|
|
224
|
-
const allEntries = scanNodeModules(targetDir)
|
|
227
|
+
const allEntries = scanNodeModules(targetDir).filter(e => e.kind === 'skill')
|
|
225
228
|
|
|
226
229
|
if (extras) {
|
|
227
230
|
// Get installer's own package name+version from its package.json
|
|
@@ -235,13 +238,13 @@ export const discover = (targetDir: string, opts: DiscoverOptions = {}): Discove
|
|
|
235
238
|
if (pkg.version) selfVersion = pkg.version
|
|
236
239
|
}
|
|
237
240
|
} catch { /* use defaults */ }
|
|
238
|
-
allEntries.push(...scanSelfExtras(selfName, selfVersion))
|
|
241
|
+
allEntries.push(...scanSelfExtras(selfName, selfVersion).filter(e => e.kind === 'skill'))
|
|
239
242
|
}
|
|
240
243
|
|
|
241
|
-
// Deduplicate by
|
|
244
|
+
// Deduplicate by name: prefer highest semver version
|
|
242
245
|
const byKey = new Map<string, DiscoveredEntry>()
|
|
243
246
|
for (const e of allEntries) {
|
|
244
|
-
const key =
|
|
247
|
+
const key = e.name
|
|
245
248
|
const existing = byKey.get(key)
|
|
246
249
|
if (existing == null) {
|
|
247
250
|
byKey.set(key, e)
|
|
@@ -257,10 +260,6 @@ export const discover = (targetDir: string, opts: DiscoverOptions = {}): Discove
|
|
|
257
260
|
result = result.filter(e => only.some(n => matchPkgName(n, e.packageName)))
|
|
258
261
|
}
|
|
259
262
|
|
|
260
|
-
// Apply tool filters
|
|
261
|
-
if (claudeOnly) result = result.filter(e => e.kind === 'skill')
|
|
262
|
-
if (copilotOnly) result = result.filter(e => e.kind === 'instruction')
|
|
263
|
-
|
|
264
263
|
return result
|
|
265
264
|
}
|
|
266
265
|
|
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,75 @@
|
|
|
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 appear only in manifests published before schema v2 and are
|
|
47
|
+
* dropped: they are the Copilot twin of the same knowledge, so including both would
|
|
48
|
+
* double the token cost of every package for no added information.
|
|
49
|
+
*/
|
|
50
|
+
export const skillEntries = (
|
|
51
|
+
manifest: Manifest,
|
|
52
|
+
categories: readonly string[],
|
|
53
|
+
): ManifestEntry[] =>
|
|
54
|
+
manifest.entries
|
|
55
|
+
.filter(entry => entry.kind === 'skill' && categories.includes(entry.category))
|
|
56
|
+
.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Turn one embedded skill file into a definition bound to the `Packages` block.
|
|
60
|
+
*
|
|
61
|
+
* The alias is namespaced by package so two packages documenting the same concept cannot
|
|
62
|
+
* collide in the registry, and the order weight is pushed past the default so package
|
|
63
|
+
* knowledge always renders after whatever the host declared itself.
|
|
64
|
+
*/
|
|
65
|
+
export const toSkill = (
|
|
66
|
+
packageName: string,
|
|
67
|
+
entry: ManifestEntry,
|
|
68
|
+
body: string,
|
|
69
|
+
): SkillDefinition => ({
|
|
70
|
+
alias: `${packageName}#${entry.name}`,
|
|
71
|
+
title: `${packageName} — ${entry.name}`,
|
|
72
|
+
body,
|
|
73
|
+
block: PromptBlock.Packages,
|
|
74
|
+
order: DEFAULT_SKILL_ORDER + 1,
|
|
75
|
+
})
|
|
@@ -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
|
+
}
|
package/src/plan.ts
CHANGED
|
@@ -17,12 +17,14 @@ export interface InstallItem {
|
|
|
17
17
|
/** The banner text injected by sync-agent-meta into every generated file. */
|
|
18
18
|
export const AUTO_GENERATED_BANNER = '<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->'
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Skills install into `.agents/skills/<name>/SKILL.md` — the canonical store of the
|
|
22
|
+
* Agent Skills standard, discovered natively by Copilot and Codex. Claude Code reads
|
|
23
|
+
* them through the per-skill symlinks in `.claude/skills/` that {@link applyInstall}
|
|
24
|
+
* maintains.
|
|
25
|
+
*/
|
|
26
|
+
const targetPath = (targetDir: string, entry: DiscoveredEntry): string =>
|
|
27
|
+
join(targetDir, '.agents', 'skills', entry.name, 'SKILL.md')
|
|
26
28
|
|
|
27
29
|
export interface PlanOptions {
|
|
28
30
|
/** Treat conflicts as 'overwrite' (--force). */
|
package/src/run.ts
CHANGED
|
@@ -18,25 +18,21 @@ const padEnd = (s: string, n: number): string => s + ' '.repeat(Math.max(0, n -
|
|
|
18
18
|
|
|
19
19
|
const printTable = (items: InstallItem[]): void => {
|
|
20
20
|
const colWidths = {
|
|
21
|
-
|
|
22
|
-
name: Math.max(...items.map(i => i.entry.name.length), 4) + 2,
|
|
21
|
+
name: Math.max(...items.map(i => i.entry.name.length), 5) + 2,
|
|
23
22
|
pkg: Math.max(...items.map(i => i.entry.packageName.length), 7) + 2,
|
|
24
23
|
action: 10,
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
const header =
|
|
28
|
-
padEnd('
|
|
29
|
-
padEnd('name', colWidths.name) +
|
|
27
|
+
padEnd('skill', colWidths.name) +
|
|
30
28
|
padEnd('package', colWidths.pkg) +
|
|
31
29
|
'action'
|
|
32
30
|
process.stdout.write(header + '\n')
|
|
33
31
|
process.stdout.write('-'.repeat(header.length) + '\n')
|
|
34
32
|
|
|
35
33
|
for (const item of items) {
|
|
36
|
-
const kindLabel = item.entry.kind === 'skill' ? 'skill' : 'instruction'
|
|
37
34
|
const actionLabel = ACTION_LABEL[item.action]
|
|
38
35
|
process.stdout.write(
|
|
39
|
-
padEnd(kindLabel, colWidths.kind) +
|
|
40
36
|
padEnd(item.entry.name, colWidths.name) +
|
|
41
37
|
padEnd(item.entry.packageName, colWidths.pkg) +
|
|
42
38
|
actionLabel +
|
|
@@ -65,8 +61,8 @@ export const run = async (args: CliArgs): Promise<RunResult> => {
|
|
|
65
61
|
process.stderr.write(
|
|
66
62
|
`\nerror: linked monorepo detected — ${pkgList}${more} are symlinked.\n\n` +
|
|
67
63
|
`In this setup, agents already load guidance from the linked monorepo's\n` +
|
|
68
|
-
`root .
|
|
69
|
-
`
|
|
64
|
+
`root AGENTS.md and .agents/skills/. Installing embedded copies would place\n` +
|
|
65
|
+
`stale duplicates that conflict with the canonical live files.\n\n` +
|
|
70
66
|
`Pass --force only if you understand this and want to proceed anyway.\n\n`,
|
|
71
67
|
)
|
|
72
68
|
return { code: 4, message: 'linked monorepo' }
|
|
@@ -77,8 +73,6 @@ export const run = async (args: CliArgs): Promise<RunResult> => {
|
|
|
77
73
|
const entries = discover(targetDir, {
|
|
78
74
|
extras: args.extras,
|
|
79
75
|
only: args.only.length > 0 ? args.only : undefined,
|
|
80
|
-
claudeOnly: args.claudeOnly,
|
|
81
|
-
copilotOnly: args.copilotOnly,
|
|
82
76
|
})
|
|
83
77
|
|
|
84
78
|
if (entries.length === 0) {
|
|
@@ -156,12 +150,15 @@ export const run = async (args: CliArgs): Promise<RunResult> => {
|
|
|
156
150
|
}
|
|
157
151
|
|
|
158
152
|
// 7. Apply
|
|
159
|
-
const result = applyInstall(resolvedItems)
|
|
153
|
+
const result = applyInstall(resolvedItems, targetDir)
|
|
160
154
|
|
|
161
155
|
process.stdout.write(
|
|
162
156
|
`Done: ${result.installed} installed, ${result.updated} updated, ` +
|
|
163
157
|
`${result.skipped} up-to-date, ${result.conflicts} skipped (conflicts).\n`,
|
|
164
158
|
)
|
|
159
|
+
if (result.linked > 0) {
|
|
160
|
+
process.stdout.write(`Linked ${result.linked} skill(s) into .claude/skills/ for Claude Code.\n`)
|
|
161
|
+
}
|
|
165
162
|
|
|
166
163
|
return { code: 0 }
|
|
167
164
|
}
|