claudeup 4.30.0 → 4.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,128 +1,134 @@
1
1
  # claudeup
2
2
 
3
- TUI tool for managing Claude Code plugins, MCPs, and configuration.
3
+ Manage a Claude Code setup — plugins, MCP servers, skills, CLI tools and the
4
+ binaries plugins depend on — from a TUI, a CLI, or a committed team manifest.
4
5
 
5
- ## Installation
6
+ ## Install
6
7
 
7
8
  ```bash
8
- # Using pnpm
9
- pnpm add -g claudeup
10
-
11
- # Using pnpx (no install)
12
- pnpx claudeup
13
-
14
- # Using npm
15
- npm install -g claudeup
16
-
17
- # Using Bun
18
- bun add -g claudeup
9
+ bun add -g claudeup # recommended
10
+ npm install -g claudeup # also works
19
11
  ```
20
12
 
21
- ## Features
22
-
23
- ### 1. MCP Server Setup
24
- Configure Model Context Protocol servers with a curated list:
25
- - **File System** - Read/write files, search directories
26
- - **Database** - SQLite, PostgreSQL connections
27
- - **Developer Tools** - GitHub, GitLab, Chrome DevTools, Puppeteer
28
- - **API & Web** - HTTP requests, Brave Search
29
- - **Productivity** - Google Drive, Slack, Memory
30
- - **AI & Intelligence** - Claude Context (semantic search), Sequential Thinking
31
-
32
- ### 2. Plugin Marketplaces
33
- Add official and community plugin marketplaces:
34
- - **Anthropic Official** - Official Claude Code plugins
35
- - **MadAppGang** - Frontend, Backend, Code Analysis plugins
36
-
37
- ### 3. Manage Plugins
38
- Install and configure plugins from added marketplaces:
39
- - Frontend Development (React/TypeScript)
40
- - Code Analysis (Deep investigation)
41
- - Bun Backend (TypeScript backend)
42
- - Orchestration (Multi-agent patterns)
43
- - Agent Development (Create agents)
44
-
45
- ### 4. Status Line Configuration
46
- Configure the Claude Code status line with presets:
47
- - Minimal, Standard, Detailed
48
- - Git-aware, Token-focused
49
- - Time-tracking, Developer
50
-
51
- ## Usage
13
+ Updates go through claudeup itself, not the package manager:
52
14
 
53
15
  ```bash
54
- # Launch TUI
55
- claudeup
56
-
57
- # Or with npx
58
- npx claudeup
16
+ claudeup update
59
17
  ```
60
18
 
61
- ### Navigation
62
- - `↑/↓` or `j/k` - Move selection
63
- - `Enter` - Select item
64
- - `Escape/q` - Go back / Exit
65
- - `?` - Show help
66
- - `1-4` - Quick jump to screens
19
+ You get a self-contained binary for your platform (an `optionalDependencies`
20
+ package gated on `os`/`cpu`). On a platform without a prebuilt binary the
21
+ launcher falls back to running from source under Bun, so the install still works.
67
22
 
68
- ## Development
23
+ ## Two ways to use it
69
24
 
70
- ```bash
71
- # Install dependencies
72
- pnpm install
25
+ **Interactive** — run `claudeup` with no arguments for the TUI. Good for
26
+ exploring what is available and toggling things on one machine.
73
27
 
74
- # Build
75
- pnpm build
28
+ **Declarative** — commit a `.claude/profiles.json` and run `claudeup install`.
29
+ Good for making a teammate's machine match yours. This is the
30
+ [team configuration guide](docs/team-configuration.md).
76
31
 
77
- # Run in development
78
- pnpm start:bun # With Bun
79
- pnpm start # With Node
80
- ```
32
+ ## Commands
81
33
 
82
- ## Configuration Files
34
+ ```
35
+ claudeup Open the interactive TUI
36
+
37
+ install [profile] Install a profile's plugins, binaries, skills and
38
+ env, then activate it. No argument = every profile.
39
+ --check Report drift only, write nothing (CI gate)
40
+ --yes, -y Skip the confirmation prompt
41
+ --force Discard unsynced local edits instead of refusing
42
+ profile list Show every profile; ● marks the active one
43
+ profile show <name> Print a profile's fully resolved closure
44
+ profile switch <name> Repoint the active profile — offline, no reinstall
45
+ profile sync Promote local edits back into .claude/profiles.json
46
+ doctor [--fix] Check binary deps, profile symlinks, conventions
47
+
48
+ claude [args...] Check for plugin updates (1h cache), then run claude
49
+ --force, -f Force the update check
50
+ update Update claudeup itself
51
+ --version, -v Version + update check
52
+ --help, -h This list
53
+ ```
83
54
 
84
- claudeup modifies these Claude Code configuration files:
55
+ `claudeup doctor` exits non-zero when it finds a problem, so it works as a CI
56
+ check. So does `claudeup install --check`.
85
57
 
86
- - `.claude/settings.json` - Shared settings (plugins, marketplaces)
87
- - `.claude/settings.local.json` - Local settings (MCP servers, allowMcp)
58
+ ## What the TUI covers
88
59
 
89
- ## Release Process
60
+ | Screen | What it manages |
61
+ |---|---|
62
+ | Plugins | Install, enable, disable; shows version changes and newly installed plugins |
63
+ | Skills | Browse and install skills from configured skill repos |
64
+ | MCP | Add MCP servers from a curated catalog |
65
+ | Settings | Claude Code settings, from a catalog of known keys |
66
+ | Profiles | Saved plugin sets (the TUI's own, older profile concept) |
67
+ | CLI Tools | Install claudish, mnemex and friends via the right package manager |
68
+ | Git State | Gitignore conventions and repo hygiene |
69
+ | Alias | Shell alias + flag management for launching `claude` |
90
70
 
91
- Releases are automated via GitHub Actions using [npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers/) with OIDC - no tokens required.
71
+ Navigate with `↑/↓` or `j/k`, `Enter` to select, `r` to refresh, `?` for help,
72
+ `q`/`Escape` to go back. Number keys `1`–`8` jump straight to a screen.
92
73
 
93
- ### One-Time Setup
74
+ ## Files claudeup touches
94
75
 
95
- 1. **Publish first version manually** (required for new packages):
96
- ```bash
97
- npm run build
98
- npm publish --access public
99
- ```
76
+ | Path | Owner | What claudeup does |
77
+ |---|---|---|
78
+ | `.claude/profiles.json` | you, committed | reads it; `profile sync` writes to it |
79
+ | `.claude/_profiles/<name>/` | claudeup, gitignored | generated build output |
80
+ | `.claude/settings.json` | Claude Code | in profile mode, a symlink into `_profiles/` |
81
+ | `.claude/settings.local.json` | you, gitignored | env values collected during install |
82
+ | `.mcp.json` | Claude Code | in profile mode, a symlink into `_profiles/` |
83
+ | `~/.claude/plugins/*` | Claude Code | never written directly — always via the `claude` CLI |
100
84
 
101
- 2. **Configure Trusted Publisher on npmjs.com** (already done):
102
- - Repository: `MadAppGang/magus`
103
- - Workflow: `claudeup-release.yml`
85
+ claudeup never hand-edits Claude Code's own registries
86
+ (`installed_plugins.json`, `known_marketplaces.json`, the plugin cache). Those go
87
+ through `claude plugin ...` so Claude Code stays the single writer.
104
88
 
105
- ### Releasing New Versions
89
+ ## Development
106
90
 
107
91
  ```bash
108
- # 1. Update version in package.json
109
- npm version patch # or minor, major
110
-
111
- # 2. Push the tag
112
- git push origin main --tags
92
+ bun install
93
+ bun test # full suite
94
+ bun run typecheck
95
+ bun run lint
96
+ bun run src/main.tsx # run from source
97
+ bun run build:binaries # cross-compile all platform binaries + their packages
113
98
  ```
114
99
 
115
- This triggers:
116
- 1. **claudeup-release.yml** - Builds and publishes to npm via OIDC (no tokens needed)
117
- 2. **create-release.yml** - Creates a GitHub release with changelog
100
+ Tests run per-file isolated in CI (`bun run test:ci`) because `mock.module`
101
+ leaks across files in a shared process.
102
+
103
+ **Do not bump `@opentui` past 0.1.x.** 0.4.x breaks `bun build --compile`: it
104
+ resolves tree-sitter workers eagerly, bypassing the `OTUI_ASSET_ROOT` override,
105
+ so a bare `import "@opentui/core"` crashes at import time in the compiled
106
+ binary. Re-run the compile spike before changing that pin.
118
107
 
119
- ### Manual Release
108
+ ## Releasing
109
+
110
+ Tag-driven. `.github/workflows/claudeup-release.yml` fires on `tools/claudeup/v*`:
111
+
112
+ 1. **verify** — install, typecheck, `test:ci`, version and optional-deps gates
113
+ 2. **binaries** — a matrix building each target on its **native** runner
114
+ (`bun --compile` cannot cross-compile in CI because opentui ships a
115
+ per-platform native library)
116
+ 3. **publish** — platform packages first, main package last, so its
117
+ `optionalDependencies` already resolve
120
118
 
121
119
  ```bash
122
- npm run build
123
- npm publish --access public
120
+ # bump "version" AND the three optionalDependencies to match
121
+ bun run check:optional-deps
122
+ git commit -am "feat(claudeup): vX.Y.Z - description"
123
+ git tag -a tools/claudeup/vX.Y.Z -m "Release message"
124
+ git push origin main --tags
124
125
  ```
125
126
 
127
+ Auth is npm OIDC trusted publishing — the workflow references no secrets. Adding
128
+ a **new** platform target needs a one-time manual publish to create the package
129
+ name (OIDC can only authenticate against a package that already exists); see
130
+ [`docs/npm-trusted-publishing-setup.md`](../../docs/npm-trusted-publishing-setup.md).
131
+
126
132
  ## License
127
133
 
128
134
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeup",
3
- "version": "4.30.0",
3
+ "version": "4.31.0",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -64,8 +64,8 @@
64
64
  "typescript": "^5.6.3"
65
65
  },
66
66
  "optionalDependencies": {
67
- "claudeup-darwin-arm64": "4.30.0",
68
- "claudeup-darwin-x64": "4.30.0",
69
- "claudeup-linux-x64": "4.30.0"
67
+ "claudeup-darwin-arm64": "4.31.0",
68
+ "claudeup-darwin-x64": "4.31.0",
69
+ "claudeup-linux-x64": "4.31.0"
70
70
  }
71
71
  }
@@ -38,6 +38,36 @@ describe("buildProfileSettings", () => {
38
38
  });
39
39
  expect(s.enableAllProjectMcpServers).toBe(true);
40
40
  });
41
+
42
+ // Regression: `install` enables the manifest-wide union at USER scope, and
43
+ // Claude Code resolves enabledPlugins per plugin id — an id the project
44
+ // scope omits falls through to the user scope's `true`. A profile that
45
+ // merely omits another profile's plugins therefore never disables them, so
46
+ // `profile switch` silently becomes additive instead of exclusive.
47
+ test("explicitly disables union plugins the profile excludes", () => {
48
+ const s = buildProfileSettings(
49
+ closure({ plugins: { "designer@magus": "latest" } }),
50
+ ["designer@magus", "terminal@magus", "gtd@magus"],
51
+ );
52
+ expect(s.enabledPlugins).toEqual({
53
+ "designer@magus": true,
54
+ "terminal@magus": false,
55
+ "gtd@magus": false,
56
+ });
57
+ });
58
+
59
+ test("a profile's own plugin wins over its union entry", () => {
60
+ const s = buildProfileSettings(
61
+ closure({ plugins: { "terminal@magus": "4.1.4" } }),
62
+ ["terminal@magus"],
63
+ );
64
+ expect(s.enabledPlugins).toEqual({ "terminal@magus": true });
65
+ });
66
+
67
+ test("no union given -> no false entries (unchanged legacy shape)", () => {
68
+ const s = buildProfileSettings(closure({ plugins: { "dev@magus": "latest" } }));
69
+ expect(s.enabledPlugins).toEqual({ "dev@magus": true });
70
+ });
41
71
  });
42
72
 
43
73
  describe("buildProfileMcp", () => {
@@ -79,4 +109,61 @@ describe("materializeProfile", () => {
79
109
  const settings = await fs.readJson(join(dir, "settings.json"));
80
110
  expect(settings.enabledPlugins).toEqual({ "b@m": true });
81
111
  });
112
+
113
+ // Regression: activating a profile replaces .claude/skills with a symlink,
114
+ // and replacing means fs.remove first. A repo that committed its own project
115
+ // skills before adopting profiles would lose them at that moment.
116
+ test("seeds skills/ from the project's pre-existing .claude/skills", async () => {
117
+ const src = join(project, ".claude", "skills", "systematic-debugging");
118
+ await fs.outputFile(join(src, "SKILL.md"), "# committed project skill\n");
119
+
120
+ const dir = await materializeProfile("p", closure(), project);
121
+
122
+ expect(
123
+ await fs.readFile(join(dir, "skills", "systematic-debugging", "SKILL.md"), "utf8"),
124
+ ).toContain("committed project skill");
125
+ });
126
+
127
+ test("does not re-seed skills/ once the profile dir exists", async () => {
128
+ await fs.outputFile(
129
+ join(project, ".claude", "skills", "gone", "SKILL.md"),
130
+ "# removed later\n",
131
+ );
132
+ const dir = await materializeProfile("p", closure(), project);
133
+ await fs.remove(join(dir, "skills", "gone"));
134
+
135
+ await materializeProfile("p", closure(), project);
136
+
137
+ expect(await fs.pathExists(join(dir, "skills", "gone"))).toBe(false);
138
+ });
139
+
140
+ test("still creates an empty skills/ when the project has none", async () => {
141
+ const dir = await materializeProfile("p", closure(), project);
142
+ expect(await fs.pathExists(join(dir, "skills"))).toBe(true);
143
+ expect(await fs.readdir(join(dir, "skills"))).toEqual([]);
144
+ });
145
+
146
+ test("two profiles from one union each disable the other's plugins", async () => {
147
+ const union = ["designer@magus", "terminal@magus"];
148
+ const fe = await materializeProfile(
149
+ "frontend",
150
+ closure({ plugins: { "designer@magus": "latest" } }),
151
+ project,
152
+ union,
153
+ );
154
+ const be = await materializeProfile(
155
+ "backend",
156
+ closure({ plugins: { "terminal@magus": "latest" } }),
157
+ project,
158
+ union,
159
+ );
160
+ expect((await fs.readJson(join(fe, "settings.json"))).enabledPlugins).toEqual({
161
+ "designer@magus": true,
162
+ "terminal@magus": false,
163
+ });
164
+ expect((await fs.readJson(join(be, "settings.json"))).enabledPlugins).toEqual({
165
+ "designer@magus": false,
166
+ "terminal@magus": true,
167
+ });
168
+ });
82
169
  });
@@ -126,7 +126,16 @@ async function runCheck(
126
126
  // ── install steps ───────────────────────────────────────────────────────────
127
127
 
128
128
  async function ensureToolchains(closure: ResolvedClosure, yes: boolean): Promise<void> {
129
- const toolchains = await detectToolchains(closure.bins);
129
+ // Only the binaries actually missing can need a toolchain. Checking all of
130
+ // them warns "pip isn't installed" on a machine whose pip-provided binaries
131
+ // are already on PATH — an alarming message about work that will not happen.
132
+ const missing: ResolvedBin[] = [];
133
+ for (const bin of closure.bins) {
134
+ if (!(await resolveExecutable(bin.name))) missing.push(bin);
135
+ }
136
+ if (missing.length === 0) return;
137
+
138
+ const toolchains = await detectToolchains(missing);
130
139
  for (const tc of toolchains) {
131
140
  if (tc.present) continue;
132
141
  const bootstrap = TOOLCHAIN_BOOTSTRAP[tc.name];
@@ -160,11 +169,25 @@ async function registerMarketplaces(closure: ResolvedClosure): Promise<void> {
160
169
  }
161
170
  }
162
171
 
172
+ /**
173
+ * Install the closure's plugins at PROJECT scope.
174
+ *
175
+ * A profile is a property of one repo, so its plugins must be too. At user
176
+ * scope, adopting profiles in a single repo would enable that repo's plugins in
177
+ * every other project on the machine — and since Claude Code resolves
178
+ * enabledPlugins per id with fall-through, those would stay on everywhere.
179
+ * Project scope keeps the blast radius to this repo. The plugin *cache* is
180
+ * global either way, so `profile switch` stays offline.
181
+ *
182
+ * Whatever the CLI writes into .claude/settings.json here is transient:
183
+ * materialization rewrites that file from the manifest, and activation replaces
184
+ * it with a symlink. The manifest stays authoritative.
185
+ */
163
186
  async function installPlugins(closure: ResolvedClosure): Promise<void> {
164
187
  for (const pluginId of Object.keys(closure.plugins)) {
165
188
  try {
166
189
  console.log(`+ plugin ${pluginId}`);
167
- await installPlugin(pluginId, "user");
190
+ await installPlugin(pluginId, "project");
168
191
  } catch (e) {
169
192
  console.warn(`⚠ plugin ${pluginId}: ${(e as Error).message}`);
170
193
  }
@@ -317,10 +340,18 @@ export async function runInstallCommand(
317
340
 
318
341
  // Materialize every profile (so `profile switch` needs no reinstall), then
319
342
  // activate the target (or the sole/first profile).
320
- const toMaterialize = flags.profile ? [flags.profile] : profileIds;
321
- for (const id of toMaterialize) {
343
+ //
344
+ // Every profile is materialized even when one was named: a profile arg scopes
345
+ // what gets *installed* and which becomes active, not which settings files
346
+ // exist. It also keeps the union below consistent across all of them — a
347
+ // profile left holding an older union would stop disabling plugins added
348
+ // since, silently making `switch` additive again.
349
+ const unionPluginIds = Object.keys(
350
+ (flags.profile ? await resolveAllProfiles(manifest) : closure).plugins,
351
+ );
352
+ for (const id of profileIds) {
322
353
  const profileClosure = await resolveProfile(manifest, id);
323
- await materializeProfile(id, profileClosure, projectPath);
354
+ await materializeProfile(id, profileClosure, projectPath, unionPluginIds);
324
355
  }
325
356
 
326
357
  // Keep the generated dir + active-profile symlinks out of git before
package/src/cli/router.ts CHANGED
@@ -73,25 +73,33 @@ function printHelp(version: string): void {
73
73
 
74
74
  TUI tool for managing Claude Code plugins, MCPs, and configuration.
75
75
 
76
- Usage: claudeup [options]
77
- claudeup claude [args...] Run claude with auto-update prerunner
78
- claudeup update Update claudeup to latest version
76
+ Usage: claudeup Open the interactive TUI
77
+ claudeup <command> [args]
79
78
 
80
79
  Options:
81
80
  -v, --version Show version and check for updates
82
81
  -h, --help Show this help message
83
82
  --no-refresh Skip auto-refresh of marketplaces on startup
84
83
 
85
- Commands:
84
+ Team profiles — reproduce this repo's Claude Code setup on any machine.
85
+ The manifest is .claude/profiles.json, committed. See docs/team-configuration.md.
86
+ install [profile] Install the profile's plugins, binaries, skills and env,
87
+ then activate it. No argument installs every profile.
88
+ --check Report drift only, write nothing (exits 1 in strict mode)
89
+ --yes, -y Skip the confirmation prompt
90
+ --force Discard unsynced local edits instead of refusing
91
+ profile list Show every profile; ● marks the active one
92
+ profile show <n> Print a profile's fully resolved closure
93
+ profile switch <n> Repoint the active profile — offline, no reinstall
94
+ profile sync Promote local edits back into .claude/profiles.json
95
+ doctor Check binary deps, profile symlinks, and conventions
96
+ --fix Apply the repairs it can make itself
97
+
98
+ Other commands:
86
99
  claude [args...] Check for plugin updates (1h cache), then run claude
87
100
  -f, --force Force update check (bypass 1h cache)
88
101
  update Update claudeup itself to latest version
89
102
 
90
- Experimental (in development):
91
- install [profile] Install a team profile from .claude/profiles.json
92
- profile <cmd> list | show | switch | sync team profiles
93
- doctor Diagnose & repair plugin/marketplace/profile state
94
-
95
103
  Navigation (TUI):
96
104
  [1] Plugins [4] Settings [7] Git State
97
105
  [2] Skills [5] Profiles [8] Alias
@@ -21,11 +21,26 @@ import { profileDir } from "./symlink-manager.js";
21
21
  * enabled, merged with the profile's declared settings. The profile's settings
22
22
  * win over the derived enabledPlugins only if it explicitly sets that key
23
23
  * (it normally won't).
24
+ *
25
+ * `allPluginIds` is the manifest-wide union of plugins across every profile.
26
+ * Plugins in the union that this profile does NOT want are written as an
27
+ * explicit `false`, which is what makes `profile switch` exclusive rather than
28
+ * additive. Claude Code resolves enabledPlugins **per plugin id**, walking from
29
+ * the highest-precedence scope down and taking the first scope that mentions
30
+ * the id — an id a scope omits falls through to the next one. Since
31
+ * `claudeup install` installs the union at user scope, every non-member would
32
+ * otherwise inherit that user-scope `true` and stay enabled while a profile
33
+ * that excludes it is active. Omitting the union (the default) preserves the
34
+ * old additive behavior for callers that have no cross-profile view.
24
35
  */
25
36
  export function buildProfileSettings(
26
37
  closure: ResolvedClosure,
38
+ allPluginIds: readonly string[] = [],
27
39
  ): Record<string, unknown> {
28
40
  const enabledPlugins: Record<string, boolean> = {};
41
+ for (const pluginId of allPluginIds) {
42
+ enabledPlugins[pluginId] = false;
43
+ }
29
44
  for (const pluginId of Object.keys(closure.plugins)) {
30
45
  enabledPlugins[pluginId] = true;
31
46
  }
@@ -39,6 +54,37 @@ export function buildProfileMcp(
39
54
  return { mcpServers: { ...closure.mcpServers } };
40
55
  }
41
56
 
57
+ /**
58
+ * Seed a profile's skills/ dir from the project's pre-existing `.claude/skills/`.
59
+ *
60
+ * Activating a profile replaces `.claude/skills` with a symlink into the profile
61
+ * dir, and replacing means `fs.remove` first. A project that committed its own
62
+ * skills before adopting profiles would lose them at that moment. So the first
63
+ * time a profile's skills/ dir is created, any real (non-symlink) project skills
64
+ * are copied in — every profile inherits what the project already had, and the
65
+ * link swap destroys nothing.
66
+ *
67
+ * Only runs when the profile's skills/ dir does not exist yet, so it never
68
+ * fights a later `skills-manager` install or re-adds a skill the user removed.
69
+ */
70
+ async function seedSkillsFromProject(
71
+ skillsDir: string,
72
+ projectPath?: string,
73
+ ): Promise<void> {
74
+ if (await fs.pathExists(skillsDir)) return;
75
+
76
+ const projectSkills = path.join(projectPath ?? process.cwd(), ".claude", "skills");
77
+ try {
78
+ // lstat, not pathExists: an existing profile symlink is not a source.
79
+ const stat = await fs.lstat(projectSkills);
80
+ if (!stat.isDirectory() || stat.isSymbolicLink()) return;
81
+ if ((await fs.readdir(projectSkills)).length === 0) return;
82
+ await fs.copy(projectSkills, skillsDir, { dereference: true });
83
+ } catch {
84
+ // No project skills dir (or unreadable) — nothing to carry over.
85
+ }
86
+ }
87
+
42
88
  /**
43
89
  * Write `_profiles/<name>/` from a resolved closure. Idempotent: overwrites
44
90
  * settings.json / mcp.json and ensures skills/ exists. Returns the dir path.
@@ -47,15 +93,20 @@ export async function materializeProfile(
47
93
  name: string,
48
94
  closure: ResolvedClosure,
49
95
  projectPath?: string,
96
+ allPluginIds: readonly string[] = [],
50
97
  ): Promise<string> {
51
98
  const dir = profileDir(name, projectPath);
52
99
  await fs.ensureDir(dir);
53
- await fs.writeJson(path.join(dir, "settings.json"), buildProfileSettings(closure), {
54
- spaces: 2,
55
- });
100
+ await fs.writeJson(
101
+ path.join(dir, "settings.json"),
102
+ buildProfileSettings(closure, allPluginIds),
103
+ { spaces: 2 },
104
+ );
56
105
  await fs.writeJson(path.join(dir, "mcp.json"), buildProfileMcp(closure), {
57
106
  spaces: 2,
58
107
  });
59
- await fs.ensureDir(path.join(dir, "skills"));
108
+ const skillsDir = path.join(dir, "skills");
109
+ await seedSkillsFromProject(skillsDir, projectPath);
110
+ await fs.ensureDir(skillsDir);
60
111
  return dir;
61
112
  }