@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.
Files changed (77) hide show
  1. package/README.md +46 -18
  2. package/agent-meta/manifest.json +15 -99
  3. package/agent-meta/skills/agent-memory/SKILL.md +8 -7
  4. package/agent-meta/skills/getting-started/SKILL.md +2 -1
  5. package/agent-meta/skills/git/SKILL.md +2 -1
  6. package/agent-meta/skills/memory-promotion/SKILL.md +5 -4
  7. package/agent-meta/skills/memory-recompact/SKILL.md +3 -3
  8. package/agent-meta/skills/reuse-code/SKILL.md +4 -3
  9. package/agent-meta/skills/router-plugins/SKILL.md +2 -1
  10. package/agent-meta/skills/scaffolding/SKILL.md +15 -13
  11. package/agent-meta/skills/self-education/SKILL.md +10 -10
  12. package/agent-meta/skills/shadcn-versions/SKILL.md +3 -2
  13. package/agent-meta/skills/shadcn-web/SKILL.md +2 -1
  14. package/agent-meta/skills/skill-authoring/SKILL.md +44 -39
  15. package/build/apply.d.ts +2 -1
  16. package/build/apply.d.ts.map +1 -1
  17. package/build/apply.js +40 -4
  18. package/build/apply.js.map +1 -1
  19. package/build/args.d.ts +4 -2
  20. package/build/args.d.ts.map +1 -1
  21. package/build/args.js +6 -7
  22. package/build/args.js.map +1 -1
  23. package/build/discover.d.ts +8 -5
  24. package/build/discover.d.ts.map +1 -1
  25. package/build/discover.js +13 -11
  26. package/build/discover.js.map +1 -1
  27. package/build/llm/index.d.ts +13 -0
  28. package/build/llm/index.d.ts.map +1 -0
  29. package/build/llm/index.js +13 -0
  30. package/build/llm/index.js.map +1 -0
  31. package/build/llm/manifest.d.ts +30 -0
  32. package/build/llm/manifest.d.ts.map +1 -0
  33. package/build/llm/manifest.js +59 -0
  34. package/build/llm/manifest.js.map +1 -0
  35. package/build/llm/plugin.d.ts +17 -0
  36. package/build/llm/plugin.d.ts.map +1 -0
  37. package/build/llm/plugin.js +115 -0
  38. package/build/llm/plugin.js.map +1 -0
  39. package/build/llm/resolve.d.ts +4 -0
  40. package/build/llm/resolve.d.ts.map +1 -0
  41. package/build/llm/resolve.js +145 -0
  42. package/build/llm/resolve.js.map +1 -0
  43. package/build/llm/types.d.ts +59 -0
  44. package/build/llm/types.d.ts.map +1 -0
  45. package/build/llm/types.js +3 -0
  46. package/build/llm/types.js.map +1 -0
  47. package/build/plan.d.ts.map +1 -1
  48. package/build/plan.js +7 -6
  49. package/build/plan.js.map +1 -1
  50. package/build/run.d.ts.map +1 -1
  51. package/build/run.js +9 -12
  52. package/build/run.js.map +1 -1
  53. package/package.json +23 -1
  54. package/src/apply.ts +41 -4
  55. package/src/args.ts +12 -9
  56. package/src/discover.ts +13 -14
  57. package/src/llm/index.ts +13 -0
  58. package/src/llm/manifest.ts +75 -0
  59. package/src/llm/plugin.ts +133 -0
  60. package/src/llm/resolve.ts +180 -0
  61. package/src/llm/types.ts +71 -0
  62. package/src/plan.ts +8 -6
  63. package/src/run.ts +8 -11
  64. package/tests/installer.test.ts +61 -45
  65. package/tests/llm-plugin.test.ts +158 -0
  66. package/agent-meta/instructions/agent-memory.instructions.md +0 -143
  67. package/agent-meta/instructions/getting-started.instructions.md +0 -62
  68. package/agent-meta/instructions/git.instructions.md +0 -52
  69. package/agent-meta/instructions/memory-promotion.instructions.md +0 -111
  70. package/agent-meta/instructions/memory-recompact.instructions.md +0 -73
  71. package/agent-meta/instructions/reuse-code.instructions.md +0 -50
  72. package/agent-meta/instructions/router-plugins.instructions.md +0 -46
  73. package/agent-meta/instructions/scaffolding.instructions.md +0 -68
  74. package/agent-meta/instructions/self-education.instructions.md +0 -85
  75. package/agent-meta/instructions/shadcn-versions.instructions.md +0 -47
  76. package/agent-meta/instructions/shadcn-web.instructions.md +0 -95
  77. 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
- claudeOnly: boolean
6
- copilotOnly: boolean
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
- result.copilotOnly = true
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
- /** Collect all DiscoveredEntries, deduplicated by (kind, name). */
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, claudeOnly = false, copilotOnly = false } = opts
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 (kind, name): prefer highest semver version
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 = `${e.kind}:${e.name}`
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
 
@@ -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
+ }
@@ -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
- const targetPath = (targetDir: string, entry: DiscoveredEntry): string => {
21
- if (entry.kind === 'skill') {
22
- return join(targetDir, '.claude', 'skills', entry.name, 'SKILL.md')
23
- }
24
- return join(targetDir, '.github', 'instructions', `${entry.name}.instructions.md`)
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
- kind: 11,
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('kind', colWidths.kind) +
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 .claude/skills/ and .github/instructions/. Installing embedded copies\n` +
69
- `would place stale duplicates that conflict with the canonical live files.\n\n` +
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
  }