nymor 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +237 -0
- package/dist/agents/targets.js +111 -0
- package/dist/commands/add.js +121 -0
- package/dist/commands/compile.js +159 -0
- package/dist/commands/doctor.js +205 -0
- package/dist/commands/init.js +98 -0
- package/dist/commands/inject.js +24 -0
- package/dist/commands/learn.js +145 -0
- package/dist/commands/list.js +55 -0
- package/dist/commands/remove.js +38 -0
- package/dist/commands/update.js +80 -0
- package/dist/commands/validate.js +82 -0
- package/dist/compiler/agentsmd.js +17 -0
- package/dist/compiler/block.js +25 -0
- package/dist/compiler/claude.js +16 -0
- package/dist/compiler/copilot.js +29 -0
- package/dist/compiler/cursor.js +38 -0
- package/dist/compiler/kiro.js +24 -0
- package/dist/detector/agents.js +24 -0
- package/dist/detector/stack.js +113 -0
- package/dist/index.js +52 -0
- package/dist/registry/cache.js +60 -0
- package/dist/registry/client.js +135 -0
- package/dist/registry/resolver.js +29 -0
- package/dist/registry/types.js +2 -0
- package/dist/templates/bootstrap.js +97 -0
- package/dist/templates/cicada-json.js +11 -0
- package/dist/templates/nymor-json.js +11 -0
- package/dist/utils/manifest.js +32 -0
- package/dist/utils/paths.js +30 -0
- package/dist/utils/skills.js +114 -0
- package/package.json +32 -0
- package/src/agents/targets.ts +141 -0
- package/src/commands/compile.ts +202 -0
- package/src/commands/doctor.ts +253 -0
- package/src/commands/init.ts +113 -0
- package/src/commands/learn.ts +175 -0
- package/src/commands/list.ts +57 -0
- package/src/commands/validate.ts +89 -0
- package/src/compiler/block.ts +26 -0
- package/src/compiler/claude.ts +13 -0
- package/src/compiler/copilot.ts +28 -0
- package/src/compiler/cursor.ts +38 -0
- package/src/compiler/kiro.ts +22 -0
- package/src/detector/agents.ts +26 -0
- package/src/detector/stack.ts +135 -0
- package/src/index.ts +59 -0
- package/src/templates/bootstrap.ts +109 -0
- package/src/templates/nymor-json.ts +15 -0
- package/src/utils/manifest.ts +38 -0
- package/src/utils/paths.ts +25 -0
- package/src/utils/skills.ts +152 -0
- package/tests/compiler/__snapshots__/claude.test.ts.snap +65 -0
- package/tests/compiler/__snapshots__/copilot.test.ts.snap +54 -0
- package/tests/compiler/__snapshots__/cursor.test.ts.snap +62 -0
- package/tests/compiler/__snapshots__/kiro.test.ts.snap +54 -0
- package/tests/compiler/block.test.ts +24 -0
- package/tests/compiler/claude.test.ts +46 -0
- package/tests/compiler/copilot.test.ts +15 -0
- package/tests/compiler/cursor.test.ts +15 -0
- package/tests/compiler/kiro.test.ts +15 -0
- package/tests/detector/agents.test.ts +48 -0
- package/tests/detector/stack.test.ts +29 -0
- package/tests/e2e/init-and-compile.test.ts +227 -0
- package/tests/fixtures/skills/scoped/SKILL.md +18 -0
- package/tests/fixtures/skills/simple/SKILL.md +16 -0
- package/tests/fixtures/skills/with-examples/SKILL.md +18 -0
- package/tests/fixtures/skills/with-examples/examples/example.md +3 -0
- package/tests/fixtures/stacks/django/manage.py +2 -0
- package/tests/fixtures/stacks/django/requirements.txt +1 -0
- package/tests/fixtures/stacks/fastapi/requirements.txt +1 -0
- package/tests/fixtures/stacks/go/go.mod +3 -0
- package/tests/fixtures/stacks/nodejs/package.json +5 -0
- package/tests/fixtures/stacks/react/package.json +5 -0
- package/tests/fixtures/stacks/rust/Cargo.toml +4 -0
- package/tests/fixtures/stacks/vue/package.json +8 -0
- package/tests/fixtures/stacks/vue/vite.config.ts +5 -0
- package/tests/utils/manifest.test.ts +31 -0
- package/tests/utils/paths.test.ts +23 -0
- package/tests/utils/skills.test.ts +49 -0
- package/tsconfig.json +14 -0
- package/vitest.config.ts +8 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mustafakmelli
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
```
|
|
2
|
+
███╗ ██╗██╗ ██╗███╗ ███╗ ██████╗ ██████╗
|
|
3
|
+
████╗ ██║╚██╗ ██╔╝████╗ ████║██╔═══██╗██╔══██╗
|
|
4
|
+
██╔██╗ ██║ ╚████╔╝ ██╔████╔██║██║ ██║██████╔╝
|
|
5
|
+
██║╚██╗██║ ╚██╔╝ ██║╚██╔╝██║██║ ██║██╔══██╗
|
|
6
|
+
██║ ╚████║ ██║ ██║ ╚═╝ ██║╚██████╔╝██║ ██║
|
|
7
|
+
╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
# Nymor
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<strong>Teach your repo what your agents keep forgetting.</strong>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<a href="https://www.npmjs.com/package/nymor"><img src="https://img.shields.io/npm/v/nymor?color=blue&label=npm" alt="npm version" /></a>
|
|
18
|
+
<a href="https://github.com/mustafakmelli/nymor/blob/main/LICENSE"><img src="https://img.shields.io/github/license/mustafakmelli/nymor?color=black" alt="license" /></a>
|
|
19
|
+
<img src="https://img.shields.io/badge/zero-network_calls-green" alt="zero network" />
|
|
20
|
+
<img src="https://img.shields.io/badge/agents-10_supported-orange" alt="agents" />
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
You correct your agent. It forgets.
|
|
26
|
+
You correct it again. It forgets again.
|
|
27
|
+
You write the rule in a comment. The next chat starts fresh.
|
|
28
|
+
|
|
29
|
+
**Nymor stops that loop.**
|
|
30
|
+
|
|
31
|
+
When a rule is worth keeping, `/nymor-learn` captures it as a skill — committed to your repo, compiled into every agent surface, loaded in every future chat. The agent stops forgetting because the memory lives in the code.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## How it feels
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
You: "We don't use API routes here. Always use Server Actions."
|
|
39
|
+
Agent: "Got it. This looks like a durable repo rule —
|
|
40
|
+
want me to capture it with /nymor-learn?"
|
|
41
|
+
You: /nymor-learn "Use Server Actions for mutations, not API routes"
|
|
42
|
+
Agent: Writing .nymor/skills/server-actions-only/SKILL.md ...
|
|
43
|
+
Updating nymor.json ...
|
|
44
|
+
Running nymor compile ...
|
|
45
|
+
✓ Skill saved and compiled to Claude, Cursor, Copilot, Kiro
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Next chat. Same rule. Already known.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
npm install -g nymor
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Or run without installing:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
npx nymor init
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Quick start
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
# Initialize repo memory and agent guidance
|
|
70
|
+
nymor init
|
|
71
|
+
|
|
72
|
+
# Then inside your agent (Cursor, Claude, Copilot, Kiro):
|
|
73
|
+
/nymor-learn "Use Server Actions for mutations, not API routes"
|
|
74
|
+
|
|
75
|
+
# See what your repo has learned
|
|
76
|
+
nymor list
|
|
77
|
+
|
|
78
|
+
# Recompile after manual edits
|
|
79
|
+
nymor compile
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## The only workflow that matters
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
Correct agent ──→ /nymor-learn ──→ .nymor/skills/<id>/SKILL.md
|
|
88
|
+
│
|
|
89
|
+
┌───────────────────┼───────────────────┐
|
|
90
|
+
↓ ↓ ↓
|
|
91
|
+
.claude/skills/ .cursor/rules/ .github/instructions/
|
|
92
|
+
CLAUDE.md nymor.mdc nymor-bootstrap.md
|
|
93
|
+
↓ ↓ ↓
|
|
94
|
+
git commit ──────── git diff ──────── open PR
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The agent writes the skill. Nymor compiles it. Git owns the history. Your team reviews it like any other change.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Supported agents
|
|
102
|
+
|
|
103
|
+
| Agent | Output |
|
|
104
|
+
| ------------------------------------------------------ | ---------------------------------------------- |
|
|
105
|
+
| Claude Code | `.claude/skills/`, `CLAUDE.md` |
|
|
106
|
+
| Cursor | `.cursor/rules/nymor-*.mdc` |
|
|
107
|
+
| GitHub Copilot | `.github/instructions/nymor-*.instructions.md`, `.github/prompts/nymor-learn.prompt.md` |
|
|
108
|
+
| Kiro | `.kiro/steering/nymor-*.md` |
|
|
109
|
+
| Codex, OpenCode, Aider, Goose, Zed, Warp, Devin, Junie | `AGENTS.md` |
|
|
110
|
+
| Gemini CLI | `GEMINI.md` |
|
|
111
|
+
| Windsurf | `.windsurf/rules/nymor.md` |
|
|
112
|
+
| Goose (native) | `.goose/skills/` |
|
|
113
|
+
| OpenCode (native) | `.opencode/skill/` |
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Skill format
|
|
118
|
+
|
|
119
|
+
Skills are plain Markdown. Human-readable, diffable, reviewable in a PR.
|
|
120
|
+
|
|
121
|
+
```md
|
|
122
|
+
---
|
|
123
|
+
name: Server Actions Only
|
|
124
|
+
description: Use this when changing application mutations.
|
|
125
|
+
globs:
|
|
126
|
+
- "app/**/*.ts"
|
|
127
|
+
- "app/**/*.tsx"
|
|
128
|
+
alwaysApply: false
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
# Skill: Server Actions Only
|
|
132
|
+
|
|
133
|
+
## Rule
|
|
134
|
+
|
|
135
|
+
Use Server Actions for mutations. Do not create API routes for app mutations.
|
|
136
|
+
|
|
137
|
+
## Why
|
|
138
|
+
|
|
139
|
+
Keeps mutation logic close to the UI and preserves the repo's auth pattern.
|
|
140
|
+
|
|
141
|
+
## Example
|
|
142
|
+
|
|
143
|
+
// WRONG
|
|
144
|
+
export async function POST(req: Request) { ... } // app/api/users/route.ts
|
|
145
|
+
|
|
146
|
+
// CORRECT
|
|
147
|
+
'use server'
|
|
148
|
+
export async function updateUser(data: UserData) { ... } // app/users/actions.ts
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Commands
|
|
154
|
+
|
|
155
|
+
```sh
|
|
156
|
+
nymor init # initialize repo memory, write /nymor-learn to all agents
|
|
157
|
+
nymor compile # regenerate indexes and all agent outputs
|
|
158
|
+
nymor list # list active repo skills
|
|
159
|
+
nymor doctor # check manifest, globs, and generated outputs
|
|
160
|
+
nymor validate # validate skill file structure
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## What Nymor does not do
|
|
166
|
+
|
|
167
|
+
- **No central catalog.** Skills are yours. They live in your repo. There is nothing to install from the internet.
|
|
168
|
+
- **No LLM calls.** Nymor compiles and validates. The agent writes the skill. You review it.
|
|
169
|
+
- **No magic.** Every output is a plain file you can read, edit, delete, and commit.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## `nymor.json`
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"version": "1",
|
|
178
|
+
"agents": ["claude", "cursor", "copilot", "kiro", "agents-md"],
|
|
179
|
+
"local": ["server-actions-only", "commit-conventions"]
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Generated files
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
.nymor/
|
|
189
|
+
skills/
|
|
190
|
+
server-actions-only/
|
|
191
|
+
SKILL.md
|
|
192
|
+
index.md
|
|
193
|
+
index.json
|
|
194
|
+
nymor.json
|
|
195
|
+
|
|
196
|
+
CLAUDE.md ← Claude bootstrap
|
|
197
|
+
.claude/commands/nymor-learn.md ← Claude slash command
|
|
198
|
+
.claude/skills/<id>/SKILL.md ← Claude native skills
|
|
199
|
+
|
|
200
|
+
.cursor/rules/nymor.mdc ← Cursor bootstrap
|
|
201
|
+
.cursor/rules/nymor-<id>.mdc ← Cursor per-skill
|
|
202
|
+
.cursor/commands/nymor-learn.md ← Cursor slash command
|
|
203
|
+
|
|
204
|
+
.github/instructions/nymor-bootstrap.instructions.md
|
|
205
|
+
.github/instructions/nymor-<id>.instructions.md
|
|
206
|
+
.github/prompts/nymor-learn.prompt.md ← Copilot slash command
|
|
207
|
+
|
|
208
|
+
.kiro/steering/nymor.md
|
|
209
|
+
.kiro/steering/nymor-<id>.md
|
|
210
|
+
|
|
211
|
+
AGENTS.md ← Codex, Aider, Goose, etc.
|
|
212
|
+
GEMINI.md
|
|
213
|
+
.windsurf/rules/nymor.md
|
|
214
|
+
.goose/skills/<id>/SKILL.md
|
|
215
|
+
.opencode/skill/<id>/SKILL.md
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Development
|
|
221
|
+
|
|
222
|
+
```sh
|
|
223
|
+
npm run build
|
|
224
|
+
npm test
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Philosophy
|
|
230
|
+
|
|
231
|
+
> The agent is smart. Your repo is the memory.
|
|
232
|
+
|
|
233
|
+
Skills are not configuration. They are the accumulated knowledge of how your team works — decisions made, mistakes corrected, patterns established. Nymor makes that knowledge durable, reviewable, and portable across every agent your team uses.
|
|
234
|
+
|
|
235
|
+
Every `/nymor-learn` is a conversation turned into institutional memory.
|
|
236
|
+
|
|
237
|
+
Made for teams who want their agents to actually know how they work.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DEFAULT_AGENT_TARGETS = exports.AGENT_TARGETS = void 0;
|
|
7
|
+
exports.getAgentTargetDefinition = getAgentTargetDefinition;
|
|
8
|
+
exports.isAgentTarget = isAgentTarget;
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
exports.AGENT_TARGETS = [
|
|
11
|
+
{
|
|
12
|
+
id: "claude",
|
|
13
|
+
label: "Claude Code",
|
|
14
|
+
short: "Claude",
|
|
15
|
+
description: "Claude skills, CLAUDE.md bootstrap, and /nymor-learn command",
|
|
16
|
+
detectPaths: [".claude", "CLAUDE.md"],
|
|
17
|
+
kind: "claude",
|
|
18
|
+
bootstrapFile: "CLAUDE.md",
|
|
19
|
+
commandFile: path_1.default.join(".claude", "commands", "nymor-learn.md")
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "cursor",
|
|
23
|
+
label: "Cursor",
|
|
24
|
+
short: "Cursor",
|
|
25
|
+
description: "Cursor rules and /nymor-learn command",
|
|
26
|
+
detectPaths: [".cursor"],
|
|
27
|
+
kind: "cursor",
|
|
28
|
+
bootstrapFile: path_1.default.join(".cursor", "rules", "nymor.mdc"),
|
|
29
|
+
commandFile: path_1.default.join(".cursor", "commands", "nymor-learn.md")
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "copilot",
|
|
33
|
+
label: "GitHub Copilot",
|
|
34
|
+
short: "Copilot",
|
|
35
|
+
description: "GitHub Copilot instructions and /nymor-learn prompt",
|
|
36
|
+
detectPaths: [
|
|
37
|
+
path_1.default.join(".github", "copilot-instructions.md"),
|
|
38
|
+
path_1.default.join(".github", "instructions"),
|
|
39
|
+
path_1.default.join(".github", "prompts")
|
|
40
|
+
],
|
|
41
|
+
kind: "copilot",
|
|
42
|
+
bootstrapFile: path_1.default.join(".github", "instructions", "nymor-bootstrap.instructions.md"),
|
|
43
|
+
commandFile: path_1.default.join(".github", "prompts", "nymor-learn.prompt.md")
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "kiro",
|
|
47
|
+
label: "Kiro",
|
|
48
|
+
short: "Kiro",
|
|
49
|
+
description: "Kiro steering files",
|
|
50
|
+
detectPaths: [".kiro"],
|
|
51
|
+
kind: "kiro",
|
|
52
|
+
bootstrapFile: path_1.default.join(".kiro", "steering", "nymor.md")
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "agents-md",
|
|
56
|
+
label: "AGENTS.md",
|
|
57
|
+
short: "AGENTS.md",
|
|
58
|
+
description: "Shared AGENTS.md for Codex, OpenCode, Aider, Goose, Zed, Warp, Devin, and Junie",
|
|
59
|
+
detectPaths: ["AGENTS.md", ".codex", ".aider.conf.yml", ".zed", ".warp", ".junie", ".goose", ".opencode"],
|
|
60
|
+
kind: "shared-md",
|
|
61
|
+
bootstrapFile: "AGENTS.md",
|
|
62
|
+
sharedConsumers: ["Codex", "OpenCode", "Aider", "Goose", "Zed", "Warp", "Devin", "Junie"]
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "gemini",
|
|
66
|
+
label: "Gemini CLI",
|
|
67
|
+
short: "Gemini",
|
|
68
|
+
description: "GEMINI.md managed block",
|
|
69
|
+
detectPaths: ["GEMINI.md", ".gemini"],
|
|
70
|
+
kind: "gemini",
|
|
71
|
+
bootstrapFile: "GEMINI.md"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "windsurf",
|
|
75
|
+
label: "Windsurf",
|
|
76
|
+
short: "Windsurf",
|
|
77
|
+
description: "Windsurf project rule file",
|
|
78
|
+
detectPaths: [".windsurf", ".windsurfrules"],
|
|
79
|
+
kind: "windsurf",
|
|
80
|
+
bootstrapFile: path_1.default.join(".windsurf", "rules", "nymor.md")
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: "goose",
|
|
84
|
+
label: "Goose",
|
|
85
|
+
short: "Goose",
|
|
86
|
+
description: "Goose native skills",
|
|
87
|
+
detectPaths: [".goose"],
|
|
88
|
+
kind: "native-skills",
|
|
89
|
+
nativeSkillDir: path_1.default.join(".goose", "skills")
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "opencode",
|
|
93
|
+
label: "OpenCode",
|
|
94
|
+
short: "OpenCode",
|
|
95
|
+
description: "OpenCode native skills",
|
|
96
|
+
detectPaths: [".opencode", "opencode.json"],
|
|
97
|
+
kind: "native-skills",
|
|
98
|
+
nativeSkillDir: path_1.default.join(".opencode", "skill")
|
|
99
|
+
}
|
|
100
|
+
];
|
|
101
|
+
exports.DEFAULT_AGENT_TARGETS = exports.AGENT_TARGETS.map((target) => target.id);
|
|
102
|
+
function getAgentTargetDefinition(id) {
|
|
103
|
+
const target = exports.AGENT_TARGETS.find((item) => item.id === id);
|
|
104
|
+
if (!target) {
|
|
105
|
+
throw new Error(`Unknown agent target: ${id}`);
|
|
106
|
+
}
|
|
107
|
+
return target;
|
|
108
|
+
}
|
|
109
|
+
function isAgentTarget(value) {
|
|
110
|
+
return exports.AGENT_TARGETS.some((target) => target.id === value);
|
|
111
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.addCommand = addCommand;
|
|
7
|
+
exports.installRegistrySkill = installRegistrySkill;
|
|
8
|
+
exports.parseRegistrySkill = parseRegistrySkill;
|
|
9
|
+
exports.createEmptyLockfile = createEmptyLockfile;
|
|
10
|
+
exports.computeTarballIntegrity = computeTarballIntegrity;
|
|
11
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
12
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
15
|
+
const compile_1 = require("./compile");
|
|
16
|
+
const client_1 = require("../registry/client");
|
|
17
|
+
const cache_1 = require("../registry/cache");
|
|
18
|
+
const resolver_1 = require("../registry/resolver");
|
|
19
|
+
const manifest_1 = require("../utils/manifest");
|
|
20
|
+
const paths_1 = require("../utils/paths");
|
|
21
|
+
const INDEX_TTL_MS = 60 * 60 * 1000;
|
|
22
|
+
async function addCommand(skill, options = {}) {
|
|
23
|
+
const projectRoot = process.cwd();
|
|
24
|
+
const parsed = parseRegistrySkill(skill);
|
|
25
|
+
const manifest = await (0, manifest_1.readManifest)(projectRoot);
|
|
26
|
+
if (manifest.skills[parsed.packageName]) {
|
|
27
|
+
console.log(picocolors_1.default.yellow(`Skill ${parsed.packageName} is already in cicada.json; reinstalling.`));
|
|
28
|
+
}
|
|
29
|
+
const requestedRange = options.version ?? "latest";
|
|
30
|
+
const result = await installRegistrySkill(projectRoot, parsed, requestedRange, Boolean(options.offline));
|
|
31
|
+
manifest.skills[parsed.packageName] = requestedRange;
|
|
32
|
+
await (0, manifest_1.writeManifest)(projectRoot, manifest);
|
|
33
|
+
const lockfile = (await (0, manifest_1.readLockfile)(projectRoot)) ?? createEmptyLockfile();
|
|
34
|
+
lockfile.skills[parsed.packageName] = {
|
|
35
|
+
version: result.version,
|
|
36
|
+
integrity: result.integrity,
|
|
37
|
+
resolved: result.resolved
|
|
38
|
+
};
|
|
39
|
+
await (0, manifest_1.writeLockfile)(projectRoot, lockfile);
|
|
40
|
+
await (0, compile_1.compileCommand)();
|
|
41
|
+
console.log("");
|
|
42
|
+
console.log(`${picocolors_1.default.green("✓")} Installed ${result.packageName}@${result.version}`);
|
|
43
|
+
console.log(` ${result.destinationDir}`);
|
|
44
|
+
}
|
|
45
|
+
async function installRegistrySkill(projectRoot, parsed, range, offline) {
|
|
46
|
+
const index = await getSkillIndex(parsed, offline);
|
|
47
|
+
const version = (0, resolver_1.resolveVersion)(range, Object.keys(index.versions));
|
|
48
|
+
const registryVersion = index.versions[version];
|
|
49
|
+
if (!registryVersion) {
|
|
50
|
+
throw new Error(`Registry index for ${parsed.packageName} does not include ${version}`);
|
|
51
|
+
}
|
|
52
|
+
const { tarball, integrity } = await getTarball(parsed, version, registryVersion.integrity, offline);
|
|
53
|
+
const destinationDir = path_1.default.join((0, paths_1.getSkillsDir)(projectRoot), parsed.folderName);
|
|
54
|
+
await fs_extra_1.default.emptyDir(destinationDir);
|
|
55
|
+
await (0, client_1.extractSkillTarball)(tarball, destinationDir);
|
|
56
|
+
const skillPath = path_1.default.join(destinationDir, "SKILL.md");
|
|
57
|
+
if (!(await fs_extra_1.default.pathExists(skillPath))) {
|
|
58
|
+
throw new Error(`Installed tarball for ${parsed.packageName}@${version} did not contain SKILL.md`);
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
packageName: parsed.packageName,
|
|
62
|
+
version,
|
|
63
|
+
integrity,
|
|
64
|
+
resolved: (0, client_1.getSkillTarballUrl)(parsed.scope, parsed.name, version),
|
|
65
|
+
destinationDir
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function parseRegistrySkill(value) {
|
|
69
|
+
const match = /^(@[^/\s]+)\/([^/\s]+)$/.exec(value);
|
|
70
|
+
if (!match) {
|
|
71
|
+
throw new Error(`Invalid skill "${value}". Expected a scoped package like @cicada/commit-conventions.`);
|
|
72
|
+
}
|
|
73
|
+
const [, scope, name] = match;
|
|
74
|
+
return {
|
|
75
|
+
packageName: `${scope}/${name}`,
|
|
76
|
+
scope,
|
|
77
|
+
name,
|
|
78
|
+
folderName: `${scope}__${name}`
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function createEmptyLockfile() {
|
|
82
|
+
return {
|
|
83
|
+
lockfileVersion: 1,
|
|
84
|
+
skills: {}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function computeTarballIntegrity(tarball) {
|
|
88
|
+
return `sha256-${crypto_1.default.createHash("sha256").update(tarball).digest("base64")}`;
|
|
89
|
+
}
|
|
90
|
+
async function getSkillIndex(parsed, offline) {
|
|
91
|
+
const cacheKey = parsed.folderName;
|
|
92
|
+
const cached = await (0, cache_1.getCachedIndex)(cacheKey, offline ? Number.POSITIVE_INFINITY : INDEX_TTL_MS);
|
|
93
|
+
if (cached) {
|
|
94
|
+
return cached;
|
|
95
|
+
}
|
|
96
|
+
if (offline) {
|
|
97
|
+
throw new Error(`No cached registry index for ${parsed.packageName}; cannot add in offline mode.`);
|
|
98
|
+
}
|
|
99
|
+
const index = await (0, client_1.fetchSkillIndex)(parsed.scope, parsed.name);
|
|
100
|
+
await (0, cache_1.putCachedIndex)(cacheKey, index);
|
|
101
|
+
return index;
|
|
102
|
+
}
|
|
103
|
+
async function getTarball(parsed, version, expectedIntegrity, offline) {
|
|
104
|
+
const cached = await (0, cache_1.getCachedTarball)(parsed.scope, parsed.name, version);
|
|
105
|
+
if (cached) {
|
|
106
|
+
const cachedIntegrity = computeTarballIntegrity(cached);
|
|
107
|
+
if (cachedIntegrity !== expectedIntegrity) {
|
|
108
|
+
throw new Error(`Cached tarball integrity mismatch for ${parsed.packageName}@${version}: expected ${expectedIntegrity}, got ${cachedIntegrity}`);
|
|
109
|
+
}
|
|
110
|
+
return { tarball: cached, integrity: cachedIntegrity };
|
|
111
|
+
}
|
|
112
|
+
if (offline) {
|
|
113
|
+
throw new Error(`No cached tarball for ${parsed.packageName}@${version}; cannot add in offline mode.`);
|
|
114
|
+
}
|
|
115
|
+
const fetched = await (0, client_1.fetchSkillTarball)(parsed.scope, parsed.name, version);
|
|
116
|
+
if (fetched.integrity !== expectedIntegrity) {
|
|
117
|
+
throw new Error(`Registry integrity mismatch for ${parsed.packageName}@${version}: index has ${expectedIntegrity}, integrity.txt has ${fetched.integrity}`);
|
|
118
|
+
}
|
|
119
|
+
await (0, cache_1.putCachedTarball)(parsed.scope, parsed.name, version, fetched.tarball, fetched.integrity);
|
|
120
|
+
return fetched;
|
|
121
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.compileCommand = compileCommand;
|
|
7
|
+
exports.planCompileOutputs = planCompileOutputs;
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const targets_1 = require("../agents/targets");
|
|
11
|
+
const copilot_1 = require("../compiler/copilot");
|
|
12
|
+
const cursor_1 = require("../compiler/cursor");
|
|
13
|
+
const kiro_1 = require("../compiler/kiro");
|
|
14
|
+
const block_1 = require("../compiler/block");
|
|
15
|
+
const bootstrap_1 = require("../templates/bootstrap");
|
|
16
|
+
const skills_1 = require("../utils/skills");
|
|
17
|
+
const manifest_1 = require("../utils/manifest");
|
|
18
|
+
const paths_1 = require("../utils/paths");
|
|
19
|
+
async function compileCommand() {
|
|
20
|
+
const projectRoot = process.cwd();
|
|
21
|
+
const skillsDir = (0, paths_1.getSkillsDir)(projectRoot);
|
|
22
|
+
if (!(await fs_extra_1.default.pathExists(skillsDir))) {
|
|
23
|
+
console.log("No skills found. Run nymor init first.");
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const skills = await (0, skills_1.loadSkills)(skillsDir);
|
|
28
|
+
const { markdown, json } = (0, skills_1.buildSkillIndex)(skills);
|
|
29
|
+
await fs_extra_1.default.ensureDir((0, paths_1.getNymorDir)(projectRoot));
|
|
30
|
+
await fs_extra_1.default.writeFile((0, paths_1.getIndexMarkdownPath)(projectRoot), markdown, "utf8");
|
|
31
|
+
await fs_extra_1.default.writeFile((0, paths_1.getIndexJsonPath)(projectRoot), json, "utf8");
|
|
32
|
+
const manifest = await (0, manifest_1.readManifest)(projectRoot);
|
|
33
|
+
const agentSet = new Set(manifest.agents);
|
|
34
|
+
for (const target of targets_1.AGENT_TARGETS) {
|
|
35
|
+
if (agentSet.has(target.id)) {
|
|
36
|
+
await writeTargetOutputs(projectRoot, target, skills);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
console.log(`Compiled ${skills.length} skills.`);
|
|
40
|
+
}
|
|
41
|
+
async function planCompileOutputs(projectRoot) {
|
|
42
|
+
const skillsDir = (0, paths_1.getSkillsDir)(projectRoot);
|
|
43
|
+
const skills = await (0, skills_1.loadSkills)(skillsDir);
|
|
44
|
+
const { markdown, json } = (0, skills_1.buildSkillIndex)(skills);
|
|
45
|
+
const manifest = await (0, manifest_1.readManifest)(projectRoot);
|
|
46
|
+
const agentSet = new Set(manifest.agents);
|
|
47
|
+
const files = [
|
|
48
|
+
textFile((0, paths_1.getIndexMarkdownPath)(projectRoot), markdown),
|
|
49
|
+
textFile((0, paths_1.getIndexJsonPath)(projectRoot), json)
|
|
50
|
+
];
|
|
51
|
+
for (const target of targets_1.AGENT_TARGETS) {
|
|
52
|
+
if (agentSet.has(target.id)) {
|
|
53
|
+
files.push(...(await planTargetOutputs(projectRoot, target, skills)));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return files;
|
|
57
|
+
}
|
|
58
|
+
async function writeTargetOutputs(projectRoot, target, skills) {
|
|
59
|
+
for (const file of await planTargetOutputs(projectRoot, target, skills)) {
|
|
60
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(file.path));
|
|
61
|
+
await fs_extra_1.default.writeFile(file.path, file.content);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function planTargetOutputs(projectRoot, target, skills) {
|
|
65
|
+
const files = [];
|
|
66
|
+
if (target.bootstrapFile) {
|
|
67
|
+
files.push(await planBootstrap(projectRoot, target, skills));
|
|
68
|
+
}
|
|
69
|
+
if (target.commandFile) {
|
|
70
|
+
files.push(textFile(path_1.default.join(projectRoot, target.commandFile), `${(0, bootstrap_1.renderLearnCommand)(target)}\n`));
|
|
71
|
+
}
|
|
72
|
+
switch (target.kind) {
|
|
73
|
+
case "claude":
|
|
74
|
+
files.push(...(await planClaudeOutputs(skills, projectRoot)));
|
|
75
|
+
break;
|
|
76
|
+
case "cursor":
|
|
77
|
+
for (const skill of skills) {
|
|
78
|
+
files.push(textFile(path_1.default.join(projectRoot, ".cursor", "rules", `nymor-${skill.id}.mdc`), (0, cursor_1.renderCursorRule)(skill)));
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
case "copilot":
|
|
82
|
+
for (const skill of skills) {
|
|
83
|
+
files.push(textFile(path_1.default.join(projectRoot, ".github", "instructions", `nymor-${skill.id}.instructions.md`), (0, copilot_1.renderCopilotInstructions)(skill)));
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
case "kiro":
|
|
87
|
+
for (const skill of skills) {
|
|
88
|
+
files.push(textFile(path_1.default.join(projectRoot, ".kiro", "steering", `nymor-${skill.id}.md`), (0, kiro_1.renderKiroSteering)(skill)));
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
case "native-skills":
|
|
92
|
+
if (target.nativeSkillDir) {
|
|
93
|
+
files.push(...(await planNativeSkillOutputs(projectRoot, target, skills)));
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
case "shared-md":
|
|
97
|
+
case "gemini":
|
|
98
|
+
case "windsurf":
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
return files;
|
|
102
|
+
}
|
|
103
|
+
async function planBootstrap(projectRoot, target, skills) {
|
|
104
|
+
const targetPath = path_1.default.join(projectRoot, target.bootstrapFile);
|
|
105
|
+
const content = (0, bootstrap_1.renderBootstrap)(target, skills);
|
|
106
|
+
if (target.id === "claude" || target.id === "agents-md" || target.id === "gemini") {
|
|
107
|
+
const existing = (await fs_extra_1.default.pathExists(targetPath)) ? await fs_extra_1.default.readFile(targetPath, "utf8") : null;
|
|
108
|
+
return textFile(targetPath, (0, block_1.upsertManagedBlock)(existing, content));
|
|
109
|
+
}
|
|
110
|
+
return textFile(targetPath, `${content.trimEnd()}\n`);
|
|
111
|
+
}
|
|
112
|
+
async function planClaudeOutputs(skills, projectRoot) {
|
|
113
|
+
const outputRoot = path_1.default.join(projectRoot, ".claude", "skills");
|
|
114
|
+
const files = [];
|
|
115
|
+
for (const skill of skills) {
|
|
116
|
+
const sourceFiles = await listFilesRecursive(skill.dirPath);
|
|
117
|
+
for (const sourcePath of sourceFiles) {
|
|
118
|
+
const relative = path_1.default.relative(skill.dirPath, sourcePath);
|
|
119
|
+
files.push({
|
|
120
|
+
path: path_1.default.join(outputRoot, skill.id, relative),
|
|
121
|
+
content: await fs_extra_1.default.readFile(sourcePath)
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return files;
|
|
126
|
+
}
|
|
127
|
+
async function planNativeSkillOutputs(projectRoot, target, skills) {
|
|
128
|
+
const files = [
|
|
129
|
+
textFile(path_1.default.join(projectRoot, target.nativeSkillDir, "nymor-learn", "SKILL.md"), `${(0, bootstrap_1.renderLearnSkill)(target)}\n`)
|
|
130
|
+
];
|
|
131
|
+
for (const skill of skills) {
|
|
132
|
+
const sourceFiles = await listFilesRecursive(skill.dirPath);
|
|
133
|
+
for (const sourcePath of sourceFiles) {
|
|
134
|
+
const relative = path_1.default.relative(skill.dirPath, sourcePath);
|
|
135
|
+
files.push({
|
|
136
|
+
path: path_1.default.join(projectRoot, target.nativeSkillDir, skill.id, relative),
|
|
137
|
+
content: await fs_extra_1.default.readFile(sourcePath)
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return files;
|
|
142
|
+
}
|
|
143
|
+
async function listFilesRecursive(root) {
|
|
144
|
+
const entries = await fs_extra_1.default.readdir(root, { withFileTypes: true });
|
|
145
|
+
const files = [];
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
const entryPath = path_1.default.join(root, entry.name);
|
|
148
|
+
if (entry.isDirectory()) {
|
|
149
|
+
files.push(...(await listFilesRecursive(entryPath)));
|
|
150
|
+
}
|
|
151
|
+
else if (entry.isFile()) {
|
|
152
|
+
files.push(entryPath);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return files.sort();
|
|
156
|
+
}
|
|
157
|
+
function textFile(filePath, content) {
|
|
158
|
+
return { path: filePath, content: Buffer.from(content, "utf8") };
|
|
159
|
+
}
|