@eric0117/agentforge 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +275 -0
- package/dist/add-agent.js +145 -0
- package/dist/add-skill.js +185 -0
- package/dist/agent-prompt.js +211 -0
- package/dist/agentforge-config.js +106 -0
- package/dist/agents/claude.js +46 -0
- package/dist/agents/codex.js +67 -0
- package/dist/agents/cursor.js +54 -0
- package/dist/agents/index.js +15 -0
- package/dist/agents/io.js +252 -0
- package/dist/agents/types.js +1 -0
- package/dist/cli.js +374 -0
- package/dist/confirm.js +20 -0
- package/dist/doctor.js +223 -0
- package/dist/enter.js +85 -0
- package/dist/init.js +272 -0
- package/dist/lang-prompt.js +88 -0
- package/dist/list-skills.js +120 -0
- package/dist/logo.js +181 -0
- package/dist/path-prompt.js +148 -0
- package/dist/remove-agent.js +63 -0
- package/dist/remove-skill.js +88 -0
- package/dist/rename.js +222 -0
- package/dist/skill-prompt.js +199 -0
- package/dist/skills-data.js +727 -0
- package/dist/sync-skills.js +59 -0
- package/dist/templates/CLAUDE.md.tpl +141 -0
- package/dist/templates/context-handoff.SKILL.md.tpl +222 -0
- package/dist/templates/cross-repo-impact.SKILL.md.tpl +241 -0
- package/dist/templates/feature-retro.SKILL.md.tpl +312 -0
- package/dist/templates/feature-start.SKILL.md.tpl +631 -0
- package/dist/templates/history.SKILL.md.tpl +165 -0
- package/dist/templates/incident-context.SKILL.md.tpl +260 -0
- package/dist/templates/pr-create.SKILL.md.tpl +403 -0
- package/dist/templates/pr-review-analyze.SKILL.md.tpl +303 -0
- package/dist/templates/pre-deploy-check.SKILL.md.tpl +350 -0
- package/dist/templates/project-router.SKILL.md.tpl +55 -0
- package/dist/templates/release-coordinate.SKILL.md.tpl +209 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eric0117
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# agentforge
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="https://github.com/user-attachments/assets/68277906-3c7f-442e-a68d-2ab2631698ab" width="720" alt="agentforge" />
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@eric0117/agentforge"><img src="https://img.shields.io/npm/v/@eric0117/agentforge.svg?style=flat-square" alt="npm version" /></a>
|
|
9
|
+
<a href="https://www.npmjs.com/package/@eric0117/agentforge"><img src="https://img.shields.io/npm/dm/@eric0117/agentforge.svg?style=flat-square" alt="npm downloads" /></a>
|
|
10
|
+
<a href="./LICENSE"><img src="https://img.shields.io/npm/l/@eric0117/agentforge.svg?style=flat-square" alt="license" /></a>
|
|
11
|
+
<img src="https://img.shields.io/node/v/@eric0117/agentforge.svg?style=flat-square" alt="node" />
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
> Multi-repo workspace bootstrapper for **Claude Code**, **Cursor**, and **OpenAI Codex CLI**.
|
|
15
|
+
|
|
16
|
+
**You're a developer. You don't work on one thing at a time. And when you do, it never lives in one repo.**
|
|
17
|
+
|
|
18
|
+
A bug fix touches the API and the admin panel. A feature ships PRs across three services. Tomorrow you're back on yesterday's work — and a teammate just edited the same files.
|
|
19
|
+
|
|
20
|
+
`agentforge` makes that the default workflow — not the chaos. One feature = one slug = a git worktree per repo, side by side on disk. Multiple features in flight, each with its own AI session. Skills triggered by natural language — no commands to memorize. Finished work archived with enough context that next month's "how did we handle X?" returns a real answer.
|
|
21
|
+
|
|
22
|
+
It does **not** ship its own AI runtime — bring your own Claude Code / Cursor / Codex CLI.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Why
|
|
27
|
+
|
|
28
|
+
Working across several repos at once is painful with a single AI agent:
|
|
29
|
+
|
|
30
|
+
- Your session is rooted in one repo, but the change touches three.
|
|
31
|
+
- You lose context when you switch terminals to a different repo.
|
|
32
|
+
- Parallel features step on each other's branches.
|
|
33
|
+
- "How did we solve this last time?" disappears the moment a PR is merged.
|
|
34
|
+
|
|
35
|
+
agentforge gives you a flat directory layout where every feature has its own per-repo git worktrees, every AI agent gets the same set of skills (in the same language), and finished work is archived with enough metadata to be queried later.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Install (scoped package — command on PATH is still `agentforge`)
|
|
43
|
+
npm install -g @eric0117/agentforge
|
|
44
|
+
|
|
45
|
+
# Bootstrap a workspace — interactive prompts walk you through
|
|
46
|
+
# language (en / ko / ja) and which agents to install (Claude / Cursor / Codex)
|
|
47
|
+
mkdir my-workspace && cd my-workspace
|
|
48
|
+
agentforge init
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
<p align="center">
|
|
52
|
+
<img src="https://github.com/user-attachments/assets/0eb690b2-afaf-475e-85f2-5ef33a99b118" width="720" alt="agentforge init — interactive prompts" />
|
|
53
|
+
</p>
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Clone your repos into repos/
|
|
57
|
+
git clone https://github.com/your-org/backend-api.git repos/backend-api
|
|
58
|
+
git clone https://github.com/your-org/admin-web.git repos/admin-web
|
|
59
|
+
|
|
60
|
+
# Start working — open the AI CLI of your choice from the workspace root
|
|
61
|
+
claude # or: cursor . / codex
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
> Prefer non-interactive? `agentforge init . --agent all --lang en --yes` skips every prompt.
|
|
65
|
+
|
|
66
|
+
From inside the session, describe what you want in natural language. agentforge skills pick the right action — no command memorization needed:
|
|
67
|
+
|
|
68
|
+
> "Let's start a new feature: tighten the rate limit"
|
|
69
|
+
|
|
70
|
+
→ `agentforge-feature-start` proposes a slug, asks which repos are in scope, creates worktrees under `anvil/<slug>/<repo>/`, and (in Claude Code) dispatches a background session you can switch to with `←`.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Directory layout
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
my-workspace/
|
|
78
|
+
├── repos/ # main branch of each repo (read-only / explore)
|
|
79
|
+
│ ├── backend-api/
|
|
80
|
+
│ └── admin-web/
|
|
81
|
+
├── anvil/ # IN-PROGRESS features only
|
|
82
|
+
│ └── <slug>/ # e.g. 260524-feat-rate-limit
|
|
83
|
+
│ ├── backend-api/ # git worktree on a feature branch
|
|
84
|
+
│ ├── admin-web/ # git worktree on a feature branch
|
|
85
|
+
│ └── CLAUDE.md # feature description + context + repo list
|
|
86
|
+
├── artifacts/ # closed features, by completion date
|
|
87
|
+
│ └── 20260524/
|
|
88
|
+
│ └── <slug>/
|
|
89
|
+
│ ├── CLAUDE.md # moved here at retro time
|
|
90
|
+
│ ├── RETRO.md # retrospective
|
|
91
|
+
│ ├── refs.json # per-repo branch / HEAD / PR pointers
|
|
92
|
+
│ ├── plans/ # plan files
|
|
93
|
+
│ └── sessions/ # AI session transcripts
|
|
94
|
+
├── agentforge/ # workspace metadata
|
|
95
|
+
│ ├── config.json # which agents, which language
|
|
96
|
+
│ ├── skills/ # master skill files (single source of truth)
|
|
97
|
+
│ └── log.jsonl # append-only activity log
|
|
98
|
+
└── .claude/skills/ # per-agent skill copies (auto-generated)
|
|
99
|
+
.cursor/rules/
|
|
100
|
+
.agents/skills/ # codex
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The filesystem is the source of truth — there is no separate metadata file to drift. `ls anvil/` shows what's in flight; `ls artifacts/` shows what's done.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## How a feature flows
|
|
108
|
+
|
|
109
|
+
| Step | What you say | Skill that fires |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| 1. Question / explore | "Where is the auth handler in the backend API?" | `agentforge-project-router` |
|
|
112
|
+
| 2. Discover something to change | "Let's fix this — start a feature" | `agentforge-feature-start` |
|
|
113
|
+
| 3. (Optional) plan the work | "How should we split this?" | (any agent — plan it together) |
|
|
114
|
+
| 4. Implement | (regular coding in the dispatched session) | — |
|
|
115
|
+
| 5. Check blast radius before merging | "Where else is `X` used?" | `agentforge-cross-repo-impact` |
|
|
116
|
+
| 6. Pre-merge ops sanity check | "Anything ops needs before I ship?" | `agentforge-pre-deploy-check` |
|
|
117
|
+
| 7. Open PRs for the feature | "Open PRs for this feature" | `agentforge-pr-create` |
|
|
118
|
+
| 8. Plan the merge / deploy order | "Which PR first?" | `agentforge-release-coordinate` |
|
|
119
|
+
| 9. Audit review comments | "What do we need to fix from the review?" | `agentforge-pr-review-analyze` |
|
|
120
|
+
| 10. Hand off mid-flight (optional) | "I'm going on vacation — package this up" | `agentforge-context-handoff` |
|
|
121
|
+
| 11. Close the feature | "We're done — write a retro" | `agentforge-feature-retro` |
|
|
122
|
+
|
|
123
|
+
You don't have to remember the skill names — they're triggered by natural language, in English / 한국어 / 日本語.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Skills
|
|
128
|
+
|
|
129
|
+
All skills live in `agentforge/skills/` (master) and are auto-propagated to every installed agent (`.claude/skills/`, `.cursor/rules/`, `.agents/skills/`) by `agentforge sync-skills`.
|
|
130
|
+
|
|
131
|
+
| Skill | What it does |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `agentforge-project-router` | Routes a natural-language question to the right `repos/<name>/`. |
|
|
134
|
+
| `agentforge-feature-start` | Creates per-repo git worktrees for a new feature; re-runnable to add repos to an existing one. Detects per-repo branch-naming conventions from history. |
|
|
135
|
+
| `agentforge-cross-repo-impact` | Traces the blast radius of a change across every repo in the workspace. |
|
|
136
|
+
| `agentforge-pre-deploy-check` | Surfaces non-code changes (migrations, env vars, cache keys, queue contracts, infra files) that ops needs to handle before merge. Read-only. |
|
|
137
|
+
| `agentforge-pr-create` | Opens one PR per repo for a feature; cross-links the PRs. Never force-pushes, never merges. |
|
|
138
|
+
| `agentforge-pr-review-analyze` | Pulls every review thread, verifies each against the live code, returns a prioritized action list. |
|
|
139
|
+
| `agentforge-release-coordinate` | Plans the multi-repo merge / deploy order with preconditions, wait conditions, and a reverse-order rollback playbook. Read-only. |
|
|
140
|
+
| `agentforge-context-handoff` | Packages a feature's current state into `HANDOFF.md` so another developer (or future-you) can pick up without context loss. |
|
|
141
|
+
| `agentforge-feature-retro` | Closes a feature: writes the retrospective, archives session logs and PR refs into `artifacts/`, removes worktrees, cleans up. |
|
|
142
|
+
| `agentforge-incident-context` | First-responder context for a production page: searches every repo for the alert clue, names recent committers, traces the call path. Read-only. |
|
|
143
|
+
| `agentforge-history` | Queries past features — "how did we handle X last time?", "which feature added Y?", with grounded file / commit / PR references. Read-only. |
|
|
144
|
+
|
|
145
|
+
Every skill that modifies state asks before destructive operations and writes activity to `agentforge/log.jsonl`.
|
|
146
|
+
|
|
147
|
+
You can also add **your own** skills (`agentforge add-skill`) — they get propagated to every agent the same way.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## CLI reference
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
agentforge init [path] # bootstrap a workspace
|
|
155
|
+
agentforge add-agent [agents] [path] # add Claude / Cursor / Codex to an existing workspace
|
|
156
|
+
agentforge remove-agent <agent> [path]
|
|
157
|
+
agentforge list-skills [path] # show all installed skills
|
|
158
|
+
agentforge add-skill [path] # author a new skill
|
|
159
|
+
agentforge remove-skill <name> [path]
|
|
160
|
+
agentforge sync-skills [path] # propagate master skill edits to every agent
|
|
161
|
+
agentforge enter [slug] # cd into a feature worktree + launch claude
|
|
162
|
+
agentforge rename <old-slug> <new-slug> # rename a feature (worktrees, branch, CLAUDE.md)
|
|
163
|
+
agentforge doctor [path] # diagnose a workspace
|
|
164
|
+
agentforge help
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Flags:
|
|
168
|
+
- `--force` — overwrite per-agent files (always backs up to `.bak` first).
|
|
169
|
+
- `--yes` — non-interactive; assume yes on confirmation prompts.
|
|
170
|
+
- `--lang en|ko|ja` — language for skill bodies.
|
|
171
|
+
- `--agent claude,cursor,codex` or `--agent all` — which agents to scaffold for.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Multi-agent support
|
|
176
|
+
|
|
177
|
+
agentforge writes the same skill set into the file layout each AI CLI expects:
|
|
178
|
+
|
|
179
|
+
| Agent | Skill location | Workspace guide |
|
|
180
|
+
|---|---|---|
|
|
181
|
+
| **Claude Code** | `.claude/skills/<id>/SKILL.md` | `CLAUDE.md` |
|
|
182
|
+
| **Cursor** | `.cursor/rules/<id>.mdc` | `.cursor/rules/CLAUDE.mdc` |
|
|
183
|
+
| **OpenAI Codex CLI** | `.agents/skills/<id>.md` | `AGENTS.md` |
|
|
184
|
+
|
|
185
|
+
Edit a file in `agentforge/skills/` and run `agentforge sync-skills` — every agent picks up the change with the previous version backed up to `.bak`.
|
|
186
|
+
|
|
187
|
+
### How skills flow
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
User runs: Source of truth:
|
|
191
|
+
───────────── ─────────────────
|
|
192
|
+
agentforge add-skill ─────→ agentforge/skills/<id>.md
|
|
193
|
+
│
|
|
194
|
+
agentforge sync-skills ─────→ │ (renders the template into
|
|
195
|
+
│ each agent's expected layout)
|
|
196
|
+
▼
|
|
197
|
+
┌─────────────────┼─────────────────┐
|
|
198
|
+
▼ ▼ ▼
|
|
199
|
+
.claude/skills/ .cursor/rules/ .agents/skills/
|
|
200
|
+
(Claude Code) (Cursor) (Codex CLI)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The master copy in `agentforge/skills/` is the **single source of truth**. The per-agent
|
|
204
|
+
files are regenerated from it — never edit them directly; your changes will be
|
|
205
|
+
overwritten on the next `sync-skills`.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Internationalization
|
|
210
|
+
|
|
211
|
+
Skills are stored as templates with a `{{OUTPUT_LANGUAGE_INSTRUCTION}}` placeholder. The workspace's `agentforge/config.json` `lang` field decides which language is baked in at install time (`en` / `ko` / `ja`). Switch languages by re-running `agentforge init --force-skills --lang <code>` — your master files in `agentforge/skills/` are preserved.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Requirements
|
|
216
|
+
|
|
217
|
+
- Node.js ≥ 18
|
|
218
|
+
- `git` ≥ 2.20 (for `git worktree`)
|
|
219
|
+
- `gh` (GitHub CLI) — only for PR-related skills (`pr-create`, `pr-review-analyze`, `release-coordinate`)
|
|
220
|
+
- The AI CLI of your choice (Claude Code, Cursor, or Codex CLI) — for skill invocation
|
|
221
|
+
- Optional: `jq` — speeds up the activity log writer; the skills fall back to hand-built JSON if missing
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Conventions
|
|
226
|
+
|
|
227
|
+
- `repos/<name>/` is **read-only** — no code edits there. Use `agentforge-feature-start` to spawn a worktree.
|
|
228
|
+
- One feature = one slug = one directory under `anvil/` (and later `artifacts/<date>/`).
|
|
229
|
+
- Slug format: `<YYMMDD>-<kind>-<core>` where `<kind>` is `feat` / `fix` / `refactor` / `chore`.
|
|
230
|
+
- Branch names per repo follow each repo's own convention, detected from recent branches at feature-start time. They may differ from the slug.
|
|
231
|
+
- `anvil/` only contains in-progress work. Completed features move to `artifacts/<YYYYMMDD>/<slug>/`.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## FAQ
|
|
236
|
+
|
|
237
|
+
**Do I need Claude Code?**
|
|
238
|
+
No. agentforge ships skills for Claude Code, Cursor, and OpenAI Codex CLI — pick one or install all three with `--agent all`. Skills are plain markdown rendered into each agent's expected layout.
|
|
239
|
+
|
|
240
|
+
**I edited a skill, but my AI session doesn't seem to use the new version.**
|
|
241
|
+
Skill files load at AI session startup. After `agentforge sync-skills` (or any edit to a master file), restart your AI CLI session — the in-memory copy in the running session won't pick up file changes.
|
|
242
|
+
|
|
243
|
+
**My `.claude/skills/<id>/` is filling up with `SKILL.md.bak.N` files. Is that normal?**
|
|
244
|
+
Yes — `sync-skills` always backs up the previous version before overwriting. Safe to clean up periodically:
|
|
245
|
+
```bash
|
|
246
|
+
find .claude/skills .cursor/rules .agents/skills -name "*.bak*" -delete
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**Can I use this alongside Nx / Turborepo / Lerna / Bazel?**
|
|
250
|
+
Yes — they're orthogonal. agentforge structures the **dev workflow** (per-feature worktrees, skill triggers, archives). Monorepo build tools structure the **build graph**. Use both.
|
|
251
|
+
|
|
252
|
+
**Why git worktrees instead of just branches?**
|
|
253
|
+
With worktrees, every feature is a separate working directory on disk. You can have one AI session open per feature, working in parallel without `git stash` / branch-switching dances. Each worktree has its own `node_modules` / build output if needed.
|
|
254
|
+
|
|
255
|
+
**Branch name per repo isn't the slug — why?**
|
|
256
|
+
Each repo can follow its own convention (`feature/<TICKET>-<topic>`, `feat/<slug>`, `<user>/<topic>`). `agentforge-feature-start` samples recent branches per repo to detect the pattern, then proposes a branch name that fits — the slug just names the worktree directory.
|
|
257
|
+
|
|
258
|
+
**How do I write my own skill?**
|
|
259
|
+
Run `agentforge add-skill` from inside the workspace — it scaffolds a master file under `agentforge/skills/<id>.md`, optionally seeded from `--from <file>`. Edit the markdown, then `agentforge sync-skills`. Existing skills in `agentforge/skills/` are good references for structure (frontmatter + body + `## Output language` block).
|
|
260
|
+
|
|
261
|
+
**Windows support?**
|
|
262
|
+
agentforge itself is plain Node.js and works on Windows. `git worktree` works on Windows too, but long path edge cases and shell-script-style command examples in some skills may surface issues — file an issue if you hit one.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Contributing
|
|
267
|
+
|
|
268
|
+
Architecture, build flow, and skill-author conventions live in [`CLAUDE.md`](./CLAUDE.md) — read that before opening a PR. Issues and pull requests welcome.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## License
|
|
273
|
+
|
|
274
|
+
MIT.
|
|
275
|
+
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { pickAgents } from "./agent-prompt.js";
|
|
4
|
+
import { readMasterDir } from "./agents/io.js";
|
|
5
|
+
import { AGENTS, getAgent } from "./agents/index.js";
|
|
6
|
+
import { masterDir, readConfig, upsertConfig, } from "./agentforge-config.js";
|
|
7
|
+
import { pickLanguage } from "./lang-prompt.js";
|
|
8
|
+
import { SKILLS } from "./skills-data.js";
|
|
9
|
+
const DIM = "\x1b[2m";
|
|
10
|
+
const GREEN = "\x1b[32m";
|
|
11
|
+
const YELLOW = "\x1b[33m";
|
|
12
|
+
const CYAN = "\x1b[36m";
|
|
13
|
+
const BOLD = "\x1b[1m";
|
|
14
|
+
const RED = "\x1b[31m";
|
|
15
|
+
const RESET = "\x1b[0m";
|
|
16
|
+
export async function runAddAgent(opts) {
|
|
17
|
+
const root = resolve(opts.pathArg ?? process.cwd());
|
|
18
|
+
if (!isAgentforgeWorkspace(root)) {
|
|
19
|
+
process.stderr.write(`\n${YELLOW}⚠${RESET} Not an agentforge workspace yet.\n` +
|
|
20
|
+
` ${DIM}${root}${RESET}\n\n` +
|
|
21
|
+
` Run ${CYAN}agentforge init${RESET} here first to set one up.\n\n`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
// master must exist before we can install adapters from it
|
|
25
|
+
if (!existsSync(masterDir(root))) {
|
|
26
|
+
process.stderr.write(`\n${YELLOW}⚠${RESET} No master skills directory at ${DIM}${masterDir(root)}${RESET}\n\n` +
|
|
27
|
+
` Run ${CYAN}agentforge init${RESET} here first to set up the workspace.\n\n`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
const cfg = readConfig(root);
|
|
31
|
+
// If user passed --lang and it conflicts with what's already written into
|
|
32
|
+
// master files, we'd silently produce a mixed-lang workspace (new agent's
|
|
33
|
+
// CLAUDE.md/.cursorrules/AGENTS.md in --lang, but the actual skill files
|
|
34
|
+
// copied from master in the original lang). Refuse and point at the
|
|
35
|
+
// re-init path.
|
|
36
|
+
if (opts.lang && cfg?.lang && opts.lang !== cfg.lang) {
|
|
37
|
+
process.stderr.write(`\n${YELLOW}⚠${RESET} workspace was initialized with ${BOLD}lang=${cfg.lang}${RESET}, ` +
|
|
38
|
+
`you passed ${BOLD}--lang ${opts.lang}${RESET}.\n\n` +
|
|
39
|
+
` Master skills are already in ${cfg.lang} — mixing langs would produce inconsistent output.\n` +
|
|
40
|
+
` To switch the whole workspace to ${opts.lang}: ${CYAN}agentforge init ${root} --lang ${opts.lang} --force-skills${RESET}\n` +
|
|
41
|
+
` To keep ${cfg.lang}: drop the ${CYAN}--lang${RESET} flag.\n\n`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const installed = new Set(cfg?.agents ?? Array.from(detectInstalledAgentsFallback(root)));
|
|
45
|
+
const lang = await resolveLanguage(opts, cfg?.lang);
|
|
46
|
+
const agentsToAdd = await resolveAgentList(opts, installed, lang, root);
|
|
47
|
+
if (agentsToAdd.length === 0) {
|
|
48
|
+
console.log(`${YELLOW}no agents selected — nothing to do.${RESET}`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const { skills: masterSkills, skipped } = readMasterDir(masterDir(root));
|
|
52
|
+
if (skipped.length > 0) {
|
|
53
|
+
for (const sk of skipped) {
|
|
54
|
+
console.log(` ${DIM}skipped master file ${sk.file} — ${sk.reason}${RESET}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
console.log("");
|
|
58
|
+
console.log(`${BOLD}${GREEN}+${RESET} adding agents to ${CYAN}${root}${RESET}`);
|
|
59
|
+
console.log("");
|
|
60
|
+
for (const id of agentsToAdd) {
|
|
61
|
+
const adapter = getAgent(id);
|
|
62
|
+
console.log(`${BOLD}${CYAN}▸ ${adapter.label}${RESET}`);
|
|
63
|
+
adapter.install({
|
|
64
|
+
root,
|
|
65
|
+
masterSkills,
|
|
66
|
+
skillCatalog: SKILLS.slice(),
|
|
67
|
+
lang,
|
|
68
|
+
forceSkills: opts.forceSkills,
|
|
69
|
+
forceClaude: opts.forceClaude,
|
|
70
|
+
});
|
|
71
|
+
console.log("");
|
|
72
|
+
}
|
|
73
|
+
// update config.agents (union)
|
|
74
|
+
upsertConfig(root, { agents: [...installed, ...agentsToAdd] });
|
|
75
|
+
printNextSteps(root, agentsToAdd);
|
|
76
|
+
}
|
|
77
|
+
async function resolveLanguage(opts, configLang) {
|
|
78
|
+
if (opts.lang)
|
|
79
|
+
return opts.lang;
|
|
80
|
+
if (configLang)
|
|
81
|
+
return configLang; // honor what's already in config
|
|
82
|
+
if (opts.yes)
|
|
83
|
+
return "en";
|
|
84
|
+
return pickLanguage();
|
|
85
|
+
}
|
|
86
|
+
async function resolveAgentList(opts, installed, lang, root) {
|
|
87
|
+
if (opts.agents && opts.agents.length > 0) {
|
|
88
|
+
const dup = opts.agents.filter((id) => installed.has(id));
|
|
89
|
+
if (dup.length > 0) {
|
|
90
|
+
console.log(`${DIM} note: already installed → skipping: ${dup.join(", ")}${RESET}`);
|
|
91
|
+
}
|
|
92
|
+
return opts.agents.filter((id) => !installed.has(id));
|
|
93
|
+
}
|
|
94
|
+
if (opts.yes) {
|
|
95
|
+
const remaining = AGENTS.map((a) => a.id).filter((id) => !installed.has(id));
|
|
96
|
+
if (remaining.length === 0) {
|
|
97
|
+
console.log(`${YELLOW}all known agents are already installed in ${root}.${RESET}`);
|
|
98
|
+
}
|
|
99
|
+
return remaining;
|
|
100
|
+
}
|
|
101
|
+
if (installed.size === AGENTS.length) {
|
|
102
|
+
console.log(`${YELLOW}all known agents are already installed in ${root}.${RESET}`);
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
return pickAgents(lang, {
|
|
106
|
+
disabled: installed,
|
|
107
|
+
headerLabel: "Agents to add",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/** fallback when config.json doesn't exist yet — same logic as before */
|
|
111
|
+
function detectInstalledAgentsFallback(root) {
|
|
112
|
+
const installed = new Set();
|
|
113
|
+
if (existsSync(join(root, ".claude/skills")) ||
|
|
114
|
+
existsSync(join(root, "CLAUDE.md")))
|
|
115
|
+
installed.add("claude");
|
|
116
|
+
if (existsSync(join(root, ".cursor/rules")) ||
|
|
117
|
+
existsSync(join(root, ".cursorrules")))
|
|
118
|
+
installed.add("cursor");
|
|
119
|
+
if (existsSync(join(root, ".agents/skills")) ||
|
|
120
|
+
existsSync(join(root, "AGENTS.md")))
|
|
121
|
+
installed.add("codex");
|
|
122
|
+
return installed;
|
|
123
|
+
}
|
|
124
|
+
function isAgentforgeWorkspace(root) {
|
|
125
|
+
const markers = [
|
|
126
|
+
"agentforge",
|
|
127
|
+
"repos",
|
|
128
|
+
"anvil",
|
|
129
|
+
"artifacts",
|
|
130
|
+
".claude",
|
|
131
|
+
".cursor",
|
|
132
|
+
".agents",
|
|
133
|
+
"CLAUDE.md",
|
|
134
|
+
"AGENTS.md",
|
|
135
|
+
".cursorrules",
|
|
136
|
+
];
|
|
137
|
+
return markers.some((m) => existsSync(join(root, m)));
|
|
138
|
+
}
|
|
139
|
+
function printNextSteps(root, agentIds) {
|
|
140
|
+
const labels = agentIds
|
|
141
|
+
.map((id) => AGENTS.find((a) => a.id === id)?.label ?? id)
|
|
142
|
+
.join(", ");
|
|
143
|
+
console.log(`${BOLD}${GREEN}✓${RESET} added: ${labels} ${DIM}(in ${root})${RESET}`);
|
|
144
|
+
console.log("");
|
|
145
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import * as readline from "node:readline/promises";
|
|
5
|
+
import { splitFrontmatter } from "./agents/io.js";
|
|
6
|
+
import { confirm } from "./confirm.js";
|
|
7
|
+
import { masterDir, requireWorkspace } from "./agentforge-config.js";
|
|
8
|
+
import { LANG_INSTRUCTIONS } from "./skills-data.js";
|
|
9
|
+
import { runSyncSkills } from "./sync-skills.js";
|
|
10
|
+
const DIM = "\x1b[2m";
|
|
11
|
+
const GREEN = "\x1b[32m";
|
|
12
|
+
const YELLOW = "\x1b[33m";
|
|
13
|
+
const CYAN = "\x1b[36m";
|
|
14
|
+
const BOLD = "\x1b[1m";
|
|
15
|
+
const RED = "\x1b[31m";
|
|
16
|
+
const RESET = "\x1b[0m";
|
|
17
|
+
export async function runAddSkill(opts) {
|
|
18
|
+
const root = resolve(opts.pathArg ?? process.cwd());
|
|
19
|
+
const cfg = requireWorkspace(root);
|
|
20
|
+
if (opts.fromFile) {
|
|
21
|
+
await addFromFile(root, opts.fromFile);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
await addInteractive(root, cfg.lang, opts.noEdit, opts.yes);
|
|
25
|
+
}
|
|
26
|
+
if (!opts.noEdit) {
|
|
27
|
+
console.log("");
|
|
28
|
+
console.log(`${BOLD}${CYAN}↻${RESET} propagating to agents...`);
|
|
29
|
+
// adding a skill changes root-level skill indexes (e.g. AGENTS.md Skills
|
|
30
|
+
// section), so we ask for a root-guide refresh as well.
|
|
31
|
+
await runSyncSkills({
|
|
32
|
+
pathArg: root,
|
|
33
|
+
forceSkills: false,
|
|
34
|
+
forceClaude: true,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
console.log("");
|
|
39
|
+
console.log(`${DIM}master file ready. Edit it, then run \`agentforge sync-skills\` to propagate.${RESET}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function addFromFile(root, fromFile) {
|
|
43
|
+
const src = resolve(fromFile);
|
|
44
|
+
if (!existsSync(src)) {
|
|
45
|
+
process.stderr.write(`\n${RED}✗${RESET} Source file not found.\n ${DIM}${src}${RESET}\n\n`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
if (!statSync(src).isFile()) {
|
|
49
|
+
process.stderr.write(`\n${RED}✗${RESET} ${DIM}${src}${RESET} is not a regular file.\n ${DIM}--from expects a .md file path${RESET}\n\n`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
const content = readFileSync(src, "utf8");
|
|
53
|
+
const { frontmatter } = splitFrontmatter(content);
|
|
54
|
+
const name = frontmatter["name"];
|
|
55
|
+
if (!name || !frontmatter["description"]) {
|
|
56
|
+
process.stderr.write(`\n${RED}✗${RESET} Missing required frontmatter in ${DIM}${src}${RESET}\n\n` +
|
|
57
|
+
` The file needs a frontmatter block at the top:\n` +
|
|
58
|
+
` ${DIM}---${RESET}\n` +
|
|
59
|
+
` ${CYAN}name:${RESET} my-skill\n` +
|
|
60
|
+
` ${CYAN}description:${RESET} One-line summary of what this skill does.\n` +
|
|
61
|
+
` ${DIM}---${RESET}\n\n`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
if (!isValidSkillName(name)) {
|
|
65
|
+
process.stderr.write(`\n${RED}✗${RESET} Invalid skill name: "${name}"\n\n` +
|
|
66
|
+
` Use kebab-case: letters, digits, hyphens. Must start with a letter.\n` +
|
|
67
|
+
` ${DIM}Example: my-debug-helper${RESET}\n\n`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
const dst = join(masterDir(root), `${name}.md`);
|
|
71
|
+
if (existsSync(dst)) {
|
|
72
|
+
process.stderr.write(`\n${RED}✗${RESET} Skill "${name}" already exists.\n ${DIM}${dst}${RESET}\n\n` +
|
|
73
|
+
` Run ${CYAN}agentforge remove-skill ${name}${RESET} to remove it, or choose a different name.\n\n`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
mkdirSync(masterDir(root), { recursive: true });
|
|
77
|
+
copyFileSync(src, dst);
|
|
78
|
+
console.log(`${GREEN}+${RESET} added master skill: ${name} ${DIM}(from ${src})${RESET}`);
|
|
79
|
+
}
|
|
80
|
+
async function addInteractive(root, lang, noEdit, yes) {
|
|
81
|
+
const rl = readline.createInterface({
|
|
82
|
+
input: process.stdin,
|
|
83
|
+
output: process.stdout,
|
|
84
|
+
});
|
|
85
|
+
let name = "";
|
|
86
|
+
while (true) {
|
|
87
|
+
name = (await rl.question(`${CYAN}?${RESET} ${BOLD}Skill name${RESET} ${DIM}(kebab-case, e.g. my-debug-helper)${RESET} `)).trim();
|
|
88
|
+
if (!isValidSkillName(name)) {
|
|
89
|
+
console.log(` ${RED}invalid name.${RESET} ${DIM}letters/digits/hyphens, must start with a letter.${RESET}`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (existsSync(join(masterDir(root), `${name}.md`))) {
|
|
93
|
+
console.log(` ${RED}already exists:${RESET} ${name}`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
const description = (await rl.question(`${CYAN}?${RESET} ${BOLD}Description${RESET} ${DIM}(one line — what this skill does)${RESET}\n${DIM}›${RESET} `)).trim();
|
|
99
|
+
if (description === "") {
|
|
100
|
+
rl.close();
|
|
101
|
+
process.stderr.write(`\n${RED}✗${RESET} Description is required — it's how agents decide when to use this skill.\n\n`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
rl.close();
|
|
105
|
+
// build placeholder body
|
|
106
|
+
const body = buildPlaceholderBody(name, description, lang);
|
|
107
|
+
const dst = join(masterDir(root), `${name}.md`);
|
|
108
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
109
|
+
writeFileSync(dst, body);
|
|
110
|
+
console.log(`${GREEN}+${RESET} wrote master file: agentforge/skills/${name}.md`);
|
|
111
|
+
if (noEdit)
|
|
112
|
+
return;
|
|
113
|
+
const openEditor = yes || (await confirm("Open this skill in $EDITOR now?", true));
|
|
114
|
+
if (!openEditor)
|
|
115
|
+
return;
|
|
116
|
+
await openInEditor(dst);
|
|
117
|
+
}
|
|
118
|
+
function isValidSkillName(name) {
|
|
119
|
+
return /^[a-z][a-z0-9-]*$/.test(name);
|
|
120
|
+
}
|
|
121
|
+
function buildPlaceholderBody(name, description, lang) {
|
|
122
|
+
return `---
|
|
123
|
+
name: ${name}
|
|
124
|
+
description: ${description}
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
# ${name}
|
|
128
|
+
|
|
129
|
+
<!-- Write your skill instructions here. -->
|
|
130
|
+
<!-- When the user's request matches the description above, this content -->
|
|
131
|
+
<!-- becomes the agent's playbook for the response. -->
|
|
132
|
+
|
|
133
|
+
## When to apply
|
|
134
|
+
|
|
135
|
+
<!-- describe trigger conditions -->
|
|
136
|
+
|
|
137
|
+
## How to do it
|
|
138
|
+
|
|
139
|
+
<!-- step-by-step procedure -->
|
|
140
|
+
|
|
141
|
+
## Rules
|
|
142
|
+
|
|
143
|
+
<!-- constraints, safety, do/don't -->
|
|
144
|
+
|
|
145
|
+
## Output language
|
|
146
|
+
|
|
147
|
+
${LANG_INSTRUCTIONS[lang]}
|
|
148
|
+
`;
|
|
149
|
+
}
|
|
150
|
+
async function openInEditor(path) {
|
|
151
|
+
const editor = process.env.VISUAL ||
|
|
152
|
+
process.env.EDITOR ||
|
|
153
|
+
(await firstAvailable(["nano", "vim", "vi"]));
|
|
154
|
+
if (!editor) {
|
|
155
|
+
console.log(`${YELLOW}no $VISUAL/$EDITOR set and no fallback editor (nano/vim/vi) found.${RESET}`);
|
|
156
|
+
console.log(` ${DIM}Edit the file manually: ${path}${RESET}`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// split editor string in case it has args (e.g. "code -w")
|
|
160
|
+
const parts = editor.split(/\s+/).filter(Boolean);
|
|
161
|
+
const cmd = parts[0];
|
|
162
|
+
const args = [...parts.slice(1), path];
|
|
163
|
+
await new Promise((resolveProm, rejectProm) => {
|
|
164
|
+
const child = spawn(cmd, args, { stdio: "inherit" });
|
|
165
|
+
child.on("error", rejectProm);
|
|
166
|
+
child.on("exit", (code) => {
|
|
167
|
+
if (code === 0 || code === null)
|
|
168
|
+
resolveProm();
|
|
169
|
+
else
|
|
170
|
+
rejectProm(new Error(`editor exited with code ${code}`));
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async function firstAvailable(cmds) {
|
|
175
|
+
for (const c of cmds) {
|
|
176
|
+
const ok = await new Promise((res) => {
|
|
177
|
+
const child = spawn("which", [c], { stdio: "ignore" });
|
|
178
|
+
child.on("exit", (code) => res(code === 0));
|
|
179
|
+
child.on("error", () => res(false));
|
|
180
|
+
});
|
|
181
|
+
if (ok)
|
|
182
|
+
return c;
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|