@skillsmanager/cli 0.0.4 → 0.0.5

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 CHANGED
@@ -1,21 +1,13 @@
1
- MIT License
1
+ Copyright 2026 Ajay Prakash
2
2
 
3
- Copyright (c) 2026 Ajay Prakash
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
4
6
 
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:
7
+ http://www.apache.org/licenses/LICENSE-2.0
11
8
 
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.
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
package/README.md CHANGED
@@ -1,58 +1,107 @@
1
+ [![npm](https://img.shields.io/npm/v/%40skillsmanager%2Fcli)](https://www.npmjs.com/package/@skillsmanager/cli)
2
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
3
+ [![Node >=18](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
4
+ [![CI](https://github.com/talktoajayprakash/skillsmanager/actions/workflows/release.yml/badge.svg)](https://github.com/talktoajayprakash/skillsmanager/actions)
5
+ [![Docs](https://img.shields.io/badge/docs-skillsmanager-blue)](https://talktoajayprakash.github.io/skillsmanager)
6
+
1
7
  # Skills Manager
2
8
 
3
- A CLI for AI agents to discover, fetch, and share agent skills stored in Google Drive.
9
+ **One place to manage, sync, and share all your AI agent skills across every agent you use.**
10
+
11
+ You build skills for your AI agents, but keeping track of them is a mess. They're scattered across GitHub repos, local folders, and machines. Each agent has its own directory. Nothing is searchable. Nothing is shared.
12
+
13
+ Skills Manager fixes this. It gives every skill a home — in Google Drive, GitHub, or any storage backend you choose — and makes them instantly available to any agent via a single CLI command. Your agents can search, fetch, and use any skill regardless of where it lives.
14
+
15
+ Build skills confidently, store them where you want, and sync them across every device and agent you work with — Claude, Cursor, OpenAI Codex, OpenClaw, and beyond.
16
+
17
+ ## Why Skills Manager?
18
+
19
+ - **Unified skill library** — one searchable index across all your skills, wherever they're stored
20
+ - **Cross-agent** — install any skill into Claude, Cursor, Windsurf, Copilot, Gemini, and more
21
+ - **Backend-agnostic** — store in Google Drive, GitHub, Dropbox, AWS S3, or local filesystem
22
+ - **Sync across devices** — skills follow you, not your machine
23
+ - **No duplication** — cached once locally, symlinked into each agent's directory
24
+ - **Git-friendly** — plain Markdown files, easy to version-control and review
4
25
 
5
- ## What is it?
26
+ ## Supported Agents
6
27
 
7
- Skills Manager lets you maintain a personal library of agent skills in Google Drive and install them into any supported AI agent (Claude, Cursor, Windsurf, Copilot, etc.) with a single command.
28
+ `claude` · `codex` · `cursor` · `windsurf` · `copilot` · `gemini` · `roo` · `openclaw` · `agents`
8
29
 
9
- Skills are downloaded to a local cache (`~/.skillsmanager/cache/`) and symlinked into the agent's skills directory. No duplicationone copy, many agents.
30
+ > **OpenClaw users:** OpenClaw's skill system uses the same `SKILL.md` format and directory-based loading that Skills Manager is built around. Your OpenClaw skills are first-class citizens store them in any backend, search them, and sync them across devices just like any other skill.
31
+ >
32
+ > Any agent that reads from a skills directory works with Skills Manager. If your agent can read a file, it can use your skills.
10
33
 
11
- ## Supported agents
34
+ ## Quick Start
12
35
 
13
- `claude`, `codex`, `cursor`, `windsurf`, `copilot`, `gemini`, `roo`, `agents`
36
+ ### 1. Install
14
37
 
15
- ## Installation
38
+ ```bash
39
+ npm install -g @skillsmanager/cli
40
+ ```
41
+
42
+ ### 2. Install the skillsmanager skill (lets your agent drive Skills Manager)
16
43
 
17
44
  ```bash
18
- npm install -g skillsmanager
45
+ skillsmanager install
19
46
  ```
20
47
 
21
- ## Google Drive setup
48
+ This installs the bundled `skillsmanager` skill into all detected agents so your AI assistant can manage skills on your behalf.
22
49
 
23
- 1. Go to [Google Cloud Console](https://console.cloud.google.com/)
24
- 2. Create a project → enable the **Google Drive API**
25
- 3. Create OAuth 2.0 credentials (Desktop app)
50
+ ### 3. One-time Google Drive setup
51
+
52
+ Skills Manager uses Google Drive as a remote registry. To connect it:
53
+
54
+ 1. Go to [Google Cloud Console](https://console.cloud.google.com/) and create a project
55
+ 2. Enable the **Google Drive API** for that project
56
+ 3. Create **OAuth 2.0 credentials** (Desktop app type)
26
57
  4. Download `credentials.json` and save it to `~/.skillsmanager/credentials.json`
27
58
 
28
- ## Usage
59
+ Then authenticate and discover your registries:
29
60
 
30
61
  ```bash
31
- # Authenticate and discover registries
32
- skillsmanager init
62
+ skillsmanager setup google # walks you through OAuth
63
+ skillsmanager refresh # discovers collections in your Drive
64
+ ```
33
65
 
34
- # List all available skills
35
- skillsmanager list
66
+ ## Commands
36
67
 
37
- # Search skills by name or description
38
- skillsmanager search <query>
68
+ | Command | Description |
69
+ |---|---|
70
+ | `skillsmanager install` | Install the skillsmanager skill to all agents |
71
+ | `skillsmanager list` | List all available skills |
72
+ | `skillsmanager search <query>` | Search skills by name or description |
73
+ | `skillsmanager fetch <name> --agent <agent>` | Download and install a skill for an agent |
74
+ | `skillsmanager add <path>` | Upload a local skill to a collection |
75
+ | `skillsmanager update <path>` | Push local changes back to remote storage |
76
+ | `skillsmanager refresh` | Re-discover collections from remote |
77
+ | `skillsmanager collection create` | Create a new skill collection |
78
+ | `skillsmanager registry push --backend gdrive` | Push local registry to Google Drive |
39
79
 
40
- # Download a skill and install it for an agent
41
- skillsmanager fetch <skill-name> --agent claude
80
+ ## Local Development
42
81
 
43
- # Add a local skill to your registry
44
- skillsmanager add ./my-skill
82
+ ```bash
83
+ git clone https://github.com/talktoajayprakash/skillsmanager.git
84
+ cd skillsmanager
85
+ npm install
86
+ npm run build # compiles TypeScript to dist/
87
+ npm link # makes `skillsmanager` available globally from source
88
+ ```
45
89
 
46
- # Push local changes to an existing skill back to Drive
47
- skillsmanager update <skill-name>
90
+ Run tests:
48
91
 
49
- # Re-scan Drive for new registries
50
- skillsmanager refresh
92
+ ```bash
93
+ npm test
94
+ ```
95
+
96
+ To run without installing globally:
97
+
98
+ ```bash
99
+ node dist/index.js <command>
51
100
  ```
52
101
 
53
102
  ## Registry format
54
103
 
55
- Skills are indexed by a `SKILLS_SYNC.yaml` file inside any Google Drive folder you own:
104
+ Skills are indexed by a `SKILLS_REGISTRY.yaml` file inside any Google Drive folder you own:
56
105
 
57
106
  ```yaml
58
107
  name: my-skills
@@ -61,12 +110,9 @@ skills:
61
110
  - name: code-review
62
111
  path: code-review/
63
112
  description: Reviews code for bugs, style, and security issues
64
- - name: write-tests
65
- path: write-tests/
66
- description: Generates unit tests for a given function or module
67
113
  ```
68
114
 
69
- Each skill is a directory containing a `SKILL.md` file with YAML frontmatter:
115
+ Each skill is a directory with a `SKILL.md` file:
70
116
 
71
117
  ```markdown
72
118
  ---
@@ -77,8 +123,16 @@ description: Reviews code for bugs, style, and security issues
77
123
  ... skill instructions ...
78
124
  ```
79
125
 
80
- Skills Manager auto-discovers any `SKILLS_SYNC.yaml` file owned by your Google account, so registries are found automatically on `skillsmanager init` or `skillsmanager refresh`.
126
+ Skills Manager auto-discovers any `SKILLS_REGISTRY.yaml` in your Google account on `refresh`.
127
+
128
+ ## Contributing
129
+
130
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) — PRs welcome.
131
+
132
+ ## Security
133
+
134
+ See [SECURITY.md](./SECURITY.md) for how to report vulnerabilities.
81
135
 
82
- ## Design doc
136
+ ## License
83
137
 
84
- See [WRITEUP.md](./WRITEUP.md) for the full design.
138
+ [Apache 2.0](LICENSE)
package/dist/auth.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { OAuth2Client } from "google-auth-library";
2
2
  export declare function runAuthFlow(): Promise<OAuth2Client>;
3
+ export declare function getAuthedEmail(client: OAuth2Client): Promise<string | null>;
3
4
  export declare function getAuthClient(): OAuth2Client;
4
5
  export declare function hasToken(): boolean;
5
6
  export declare function ensureAuth(): Promise<OAuth2Client>;
package/dist/auth.js CHANGED
@@ -2,7 +2,11 @@ import fs from "fs";
2
2
  import http from "http";
3
3
  import { google } from "googleapis";
4
4
  import { TOKEN_PATH, ensureConfigDir, readCredentials, credentialsExist } from "./config.js";
5
- const SCOPES = ["https://www.googleapis.com/auth/drive"];
5
+ const SCOPES = [
6
+ "https://www.googleapis.com/auth/drive",
7
+ "openid",
8
+ "https://www.googleapis.com/auth/userinfo.email",
9
+ ];
6
10
  const LOOPBACK_PORT = 3847;
7
11
  const REDIRECT_URI = `http://localhost:${LOOPBACK_PORT}`;
8
12
  function createOAuth2Client() {
@@ -26,6 +30,8 @@ function waitForAuthCode() {
26
30
  if (error) {
27
31
  res.writeHead(200, { "Content-Type": "text/html" });
28
32
  res.end("<h2>Authorization failed.</h2><p>You can close this tab.</p>");
33
+ res.socket?.destroy();
34
+ server.closeAllConnections?.();
29
35
  server.close();
30
36
  reject(new Error(`OAuth error: ${error}`));
31
37
  return;
@@ -33,6 +39,8 @@ function waitForAuthCode() {
33
39
  if (code) {
34
40
  res.writeHead(200, { "Content-Type": "text/html" });
35
41
  res.end("<h2>Authorization successful!</h2><p>You can close this tab and return to the terminal.</p>");
42
+ res.socket?.destroy();
43
+ server.closeAllConnections?.();
36
44
  server.close();
37
45
  resolve(code);
38
46
  }
@@ -59,9 +67,18 @@ export async function runAuthFlow() {
59
67
  const { tokens } = await client.getToken(code);
60
68
  client.setCredentials(tokens);
61
69
  saveToken(client);
62
- console.log("Authorization successful.");
63
70
  return client;
64
71
  }
72
+ export async function getAuthedEmail(client) {
73
+ try {
74
+ const oauth2 = google.oauth2({ version: "v2", auth: client });
75
+ const res = await oauth2.userinfo.get();
76
+ return res.data.email ?? null;
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
65
82
  export function getAuthClient() {
66
83
  if (!fs.existsSync(TOKEN_PATH)) {
67
84
  throw new Error(`Not authenticated. Run "skillsmanager init" first.`);
@@ -13,6 +13,7 @@ export declare class GDriveBackend implements StorageBackend {
13
13
  private downloadFolder;
14
14
  createCollection(folderName: string): Promise<CollectionInfo>;
15
15
  deleteCollection(collection: CollectionInfo): Promise<void>;
16
+ deleteSkill(collection: CollectionInfo, skillName: string): Promise<void>;
16
17
  uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<void>;
17
18
  discoverRegistries(): Promise<Omit<RegistryInfo, "id">[]>;
18
19
  readRegistry(registry: RegistryInfo): Promise<RegistryFile>;
@@ -185,6 +185,12 @@ export class GDriveBackend {
185
185
  requestBody: { trashed: true },
186
186
  });
187
187
  }
188
+ async deleteSkill(collection, skillName) {
189
+ const folderId = await this.findFolder(skillName, collection.folderId);
190
+ if (folderId) {
191
+ await this.drive.files.update({ fileId: folderId, requestBody: { trashed: true } });
192
+ }
193
+ }
188
194
  async uploadSkill(collection, localPath, skillName) {
189
195
  let folderId = await this.findFolder(skillName, collection.folderId);
190
196
  if (!folderId) {
@@ -12,12 +12,13 @@ export declare class GithubBackend implements StorageBackend {
12
12
  downloadSkill(collection: CollectionInfo, skillName: string, destDir: string): Promise<void>;
13
13
  uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<void>;
14
14
  deleteCollection(collection: CollectionInfo): Promise<void>;
15
+ deleteSkill(collection: CollectionInfo, skillName: string): Promise<void>;
15
16
  discoverRegistries(): Promise<Omit<RegistryInfo, "id">[]>;
16
17
  readRegistry(registry: RegistryInfo): Promise<RegistryFile>;
17
18
  writeRegistry(registry: RegistryInfo, data: RegistryFile): Promise<void>;
18
19
  resolveCollectionRef(ref: RegistryCollectionRef): Promise<Omit<CollectionInfo, "id"> | null>;
19
20
  createRegistry(name?: string, repoRef?: string): Promise<RegistryInfo>;
20
- createCollection(collectionName: string, repoRef?: string): Promise<CollectionInfo>;
21
+ createCollection(collectionName: string, repoRef?: string, skillsRepoRef?: string): Promise<CollectionInfo>;
21
22
  static detectRepoContext(absPath: string): {
22
23
  repo: string;
23
24
  repoRoot: string;
@@ -20,6 +20,10 @@ function parseRef(folderId) {
20
20
  function workdirFor(repo) {
21
21
  return path.join(GITHUB_WORKDIR, repo.replace("/", "_"));
22
22
  }
23
+ /** Returns the repo where skill files live — defaults to the collection host repo. */
24
+ function skillsRepo(col, hostRepo) {
25
+ return col.metadata?.repo ?? hostRepo;
26
+ }
23
27
  // ── CLI helpers ───────────────────────────────────────────────────────────────
24
28
  function ghExec(args, opts) {
25
29
  const r = spawnSync("gh", args, {
@@ -206,26 +210,33 @@ export class GithubBackend {
206
210
  await this.commitAndPush(workdir, `chore: update ${COLLECTION_FILENAME} for ${collection.name}`);
207
211
  }
208
212
  async downloadSkill(collection, skillName, destDir) {
209
- const { repo } = parseRef(collection.folderId);
210
- const workdir = this.ensureWorkdir(repo);
211
- // Refresh to get latest
212
- gitExec(["pull", "--ff-only"], workdir);
213
+ const { repo: hostRepo } = parseRef(collection.folderId);
213
214
  const col = await this.readCollection(collection);
214
215
  const entry = col.skills.find((s) => s.name === skillName);
215
216
  if (!entry) {
216
217
  throw new Error(`Skill "${skillName}" not found in collection "${collection.name}"`);
217
218
  }
219
+ const srcRepo = skillsRepo(col, hostRepo);
220
+ const workdir = this.ensureWorkdir(srcRepo);
221
+ gitExec(["pull", "--ff-only"], workdir);
218
222
  const skillPath = path.join(workdir, entry.path);
219
223
  if (!fs.existsSync(skillPath)) {
220
- throw new Error(`Skill directory not found at "${entry.path}" in repo "${repo}"`);
224
+ throw new Error(`Skill directory not found at "${entry.path}" in repo "${srcRepo}"`);
221
225
  }
222
226
  if (path.resolve(skillPath) !== path.resolve(destDir)) {
223
227
  copyDirSync(skillPath, destDir);
224
228
  }
225
229
  }
226
230
  async uploadSkill(collection, localPath, skillName) {
227
- const { repo } = parseRef(collection.folderId);
228
- const workdir = this.ensureWorkdir(repo);
231
+ const { repo: hostRepo } = parseRef(collection.folderId);
232
+ // If collection points to a foreign skills repo, we can't upload there
233
+ const col = await this.readCollection(collection);
234
+ const foreign = col.metadata?.repo;
235
+ if (foreign && foreign !== hostRepo) {
236
+ throw new Error(`Cannot upload skill to collection "${collection.name}": its skills source is "${foreign}" (a repo you may not own). ` +
237
+ `Use --remote-path to register a skill path from that repo instead.`);
238
+ }
239
+ const workdir = this.ensureWorkdir(hostRepo);
229
240
  const resolvedLocal = path.resolve(localPath);
230
241
  const resolvedWorkdir = path.resolve(workdir);
231
242
  if (resolvedLocal.startsWith(resolvedWorkdir + path.sep) ||
@@ -249,6 +260,21 @@ export class GithubBackend {
249
260
  gitExec(["add", "-A"], workdir);
250
261
  await this.commitAndPush(workdir, `chore: remove collection ${collection.name}`);
251
262
  }
263
+ async deleteSkill(collection, skillName) {
264
+ const { repo: hostRepo } = parseRef(collection.folderId);
265
+ const col = await this.readCollection(collection);
266
+ const entry = col.skills.find((s) => s.name === skillName);
267
+ if (!entry)
268
+ return;
269
+ const srcRepo = skillsRepo(col, hostRepo);
270
+ const workdir = this.ensureWorkdir(srcRepo);
271
+ const skillPath = path.join(workdir, entry.path);
272
+ if (!fs.existsSync(skillPath))
273
+ return;
274
+ fs.rmSync(skillPath, { recursive: true, force: true });
275
+ gitExec(["add", "-A"], workdir);
276
+ await this.commitAndPush(workdir, `chore: remove skill ${skillName}`);
277
+ }
252
278
  // ── Registry operations ───────────────────────────────────────────────────────
253
279
  async discoverRegistries() {
254
280
  const r = ghExec(["repo", "list", "--json", "nameWithOwner", "--limit", "100"]);
@@ -332,7 +358,7 @@ export class GithubBackend {
332
358
  };
333
359
  }
334
360
  // ── createCollection ─────────────────────────────────────────────────────────
335
- async createCollection(collectionName, repoRef) {
361
+ async createCollection(collectionName, repoRef, skillsRepoRef) {
336
362
  if (!repoRef)
337
363
  throw new Error("GitHub backend requires --repo <owner/repo>");
338
364
  const repo = repoRef;
@@ -343,6 +369,9 @@ export class GithubBackend {
343
369
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
344
370
  const owner = await this.getOwner();
345
371
  const colData = { name: collectionName, owner, skills: [] };
372
+ if (skillsRepoRef && skillsRepoRef !== repo) {
373
+ colData.metadata = { repo: skillsRepoRef };
374
+ }
346
375
  fs.writeFileSync(filePath, serializeCollection(colData));
347
376
  gitExec(["add", path.join(metaDir, COLLECTION_FILENAME)], workdir);
348
377
  await this.commitAndPush(workdir, `chore: init collection ${collectionName}`);
@@ -7,6 +7,7 @@ export interface StorageBackend {
7
7
  downloadSkill(collection: CollectionInfo, skillName: string, destDir: string): Promise<void>;
8
8
  uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<void>;
9
9
  deleteCollection(collection: CollectionInfo): Promise<void>;
10
+ deleteSkill(collection: CollectionInfo, skillName: string): Promise<void>;
10
11
  discoverRegistries(): Promise<Omit<RegistryInfo, "id">[]>;
11
12
  readRegistry(registry: RegistryInfo): Promise<RegistryFile>;
12
13
  writeRegistry(registry: RegistryInfo, data: RegistryFile): Promise<void>;
@@ -12,6 +12,7 @@ export declare class LocalBackend implements StorageBackend {
12
12
  downloadSkill(collection: CollectionInfo, skillName: string, destDir: string): Promise<void>;
13
13
  uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<void>;
14
14
  deleteCollection(collection: CollectionInfo): Promise<void>;
15
+ deleteSkill(collection: CollectionInfo, skillName: string): Promise<void>;
15
16
  discoverRegistries(): Promise<Omit<RegistryInfo, "id">[]>;
16
17
  readRegistry(registry: RegistryInfo): Promise<RegistryFile>;
17
18
  writeRegistry(registry: RegistryInfo, data: RegistryFile): Promise<void>;
@@ -78,6 +78,12 @@ export class LocalBackend {
78
78
  fs.rmSync(collection.folderId, { recursive: true, force: true });
79
79
  }
80
80
  }
81
+ async deleteSkill(collection, skillName) {
82
+ const skillPath = path.join(collection.folderId, skillName);
83
+ if (fs.existsSync(skillPath)) {
84
+ fs.rmSync(skillPath, { recursive: true, force: true });
85
+ }
86
+ }
81
87
  // ── Registry operations ──────────────────────────────────────────────────
82
88
  async discoverRegistries() {
83
89
  if (!fs.existsSync(LOCAL_REGISTRY_PATH))
@@ -1,3 +1,6 @@
1
1
  export declare function addCommand(skillPath: string, options: {
2
2
  collection?: string;
3
+ remotePath?: string;
4
+ name?: string;
5
+ description?: string;
3
6
  }): Promise<void>;
@@ -7,6 +7,11 @@ import { readConfig, trackSkill } from "../config.js";
7
7
  import { GithubBackend } from "../backends/github.js";
8
8
  import { resolveBackend } from "../backends/resolve.js";
9
9
  export async function addCommand(skillPath, options) {
10
+ // ── Remote-path mode: register a skill from a foreign repo without local files ─
11
+ if (options.remotePath) {
12
+ await addRemotePath(options);
13
+ return;
14
+ }
10
15
  const absPath = path.resolve(skillPath);
11
16
  if (!fs.existsSync(absPath) || !fs.statSync(absPath).isDirectory()) {
12
17
  console.log(chalk.red(`"${skillPath}" is not a valid directory.`));
@@ -75,21 +80,136 @@ export async function addCommand(skillPath, options) {
75
80
  console.log(chalk.red("No collections configured. Run: skillsmanager collection create"));
76
81
  return;
77
82
  }
83
+ // If the collection has metadata.repo (foreign skills repo), handle specially
84
+ if (collection.backend === "github") {
85
+ const github = new GithubBackend();
86
+ const col = await github.readCollection(collection);
87
+ const foreignRepo = col.metadata?.repo;
88
+ if (foreignRepo) {
89
+ const ctx = GithubBackend.detectRepoContext(absPath);
90
+ if (!ctx || ctx.repo !== foreignRepo) {
91
+ console.log(chalk.red(`This collection's skills source is "${foreignRepo}". ` +
92
+ `The provided path does not belong to that repo.\n` +
93
+ chalk.dim(` To register a skill by path without a local clone, use:\n`) +
94
+ chalk.dim(` skillsmanager add --collection ${collection.name} --remote-path <rel/path> --name <name> --description <desc>`)));
95
+ return;
96
+ }
97
+ // Path is from the foreign repo (cloned locally) — register relative path only, no upload
98
+ const spinner = ora(`Adding ${chalk.bold(skillName)} to ${collection.name}...`).start();
99
+ try {
100
+ const existing = col.skills.findIndex((s) => s.name === skillName);
101
+ const entry = { name: skillName, path: ctx.relPath, description };
102
+ if (existing >= 0) {
103
+ col.skills[existing] = entry;
104
+ }
105
+ else {
106
+ col.skills.push(entry);
107
+ }
108
+ await github.writeCollection(collection, col);
109
+ trackSkill(skillName, collection.id, absPath);
110
+ spinner.succeed(`${chalk.bold(skillName)} registered in ${collection.name} at ${chalk.dim(ctx.relPath)}`);
111
+ }
112
+ catch (err) {
113
+ spinner.fail(`Failed: ${err.message}`);
114
+ }
115
+ return;
116
+ }
117
+ }
78
118
  const backend = await resolveBackend(collection.backend);
79
119
  await uploadToCollection(backend, collection, absPath, skillName, description);
80
120
  }
121
+ // ── Remote-path mode: register a skill entry without local files ─────────────
122
+ async function addRemotePath(options) {
123
+ const { remotePath, name: skillName, description = "", collection: collectionName } = options;
124
+ if (!remotePath) {
125
+ console.log(chalk.red("--remote-path requires a relative path (e.g. tools/my-skill/)"));
126
+ return;
127
+ }
128
+ if (!skillName) {
129
+ console.log(chalk.red("--remote-path requires --name <skill-name>"));
130
+ return;
131
+ }
132
+ let config;
133
+ try {
134
+ config = readConfig();
135
+ }
136
+ catch {
137
+ console.log(chalk.red("No config found. Run: skillsmanager collection create"));
138
+ return;
139
+ }
140
+ let collection = config.collections[0];
141
+ if (collectionName) {
142
+ const found = config.collections.find((c) => c.name === collectionName);
143
+ if (!found) {
144
+ console.log(chalk.red(`Collection "${collectionName}" not found.`));
145
+ console.log(chalk.dim(` Available: ${config.collections.map((c) => c.name).join(", ")}`));
146
+ return;
147
+ }
148
+ collection = found;
149
+ }
150
+ if (!collection) {
151
+ console.log(chalk.red("No collections configured. Run: skillsmanager collection create"));
152
+ return;
153
+ }
154
+ if (collection.backend !== "github") {
155
+ console.log(chalk.red("--remote-path is only supported for GitHub collections."));
156
+ return;
157
+ }
158
+ const github = new GithubBackend();
159
+ const spinner = ora(`Registering ${chalk.bold(skillName)} in ${collection.name} at ${chalk.dim(remotePath)}...`).start();
160
+ try {
161
+ const col = await github.readCollection(collection);
162
+ const existing = col.skills.findIndex((s) => s.name === skillName);
163
+ const entry = { name: skillName, path: remotePath, description };
164
+ if (existing >= 0) {
165
+ col.skills[existing] = entry;
166
+ }
167
+ else {
168
+ col.skills.push(entry);
169
+ }
170
+ await github.writeCollection(collection, col);
171
+ spinner.succeed(`${chalk.bold(skillName)} registered in ${collection.name} at ${chalk.dim(remotePath)}`);
172
+ }
173
+ catch (err) {
174
+ spinner.fail(`Failed: ${err.message}`);
175
+ }
176
+ }
81
177
  // ── GitHub path: register in-repo skill or copy external skill ────────────────
82
178
  async function addToGithub(absPath, ctx, skillName, description, collection) {
83
179
  const github = new GithubBackend();
84
180
  const spinner = ora(`Adding ${chalk.bold(skillName)} to github:${collection.folderId}...`).start();
85
181
  try {
182
+ const col = await github.readCollection(collection);
183
+ const foreignRepo = col.metadata?.repo;
184
+ const hostRepo = collection.folderId.split(":")[0];
185
+ // If collection has metadata.repo pointing to a foreign repo, validate that
186
+ // the local skill belongs to that foreign repo (not the collection host repo).
187
+ if (foreignRepo && foreignRepo !== hostRepo) {
188
+ if (ctx.repo !== foreignRepo) {
189
+ spinner.fail(`This collection's skills source is "${foreignRepo}" but the provided path belongs to "${ctx.repo}".`);
190
+ return;
191
+ }
192
+ // Skill is in the foreign repo (cloned locally) — register path only, no upload
193
+ const entry = { name: skillName, path: ctx.relPath, description };
194
+ const existing = col.skills.findIndex((s) => s.name === skillName);
195
+ if (existing >= 0) {
196
+ col.skills[existing] = entry;
197
+ }
198
+ else {
199
+ col.skills.push(entry);
200
+ }
201
+ await github.writeCollection(collection, col);
202
+ trackSkill(skillName, collection.id, absPath);
203
+ spinner.succeed(`${chalk.bold(skillName)} registered in github:${collection.folderId} at ${chalk.dim(ctx.relPath)}`);
204
+ return;
205
+ }
206
+ // Standard case: skill is in (or being added to) the collection's host repo
86
207
  // uploadSkill is a no-op for in-repo skills; copies if external
87
208
  await github.uploadSkill(collection, absPath, skillName);
88
209
  // Determine effective skill path in the repo
89
210
  const skillEntry = absPath.startsWith(ctx.repoRoot)
90
211
  ? ctx.relPath // in-repo: use relative path
91
212
  : `.agentskills/${skillName}`; // external: was copied here by uploadSkill
92
- const col = await github.readCollection(collection);
93
213
  const existing = col.skills.findIndex((s) => s.name === skillName);
94
214
  if (existing >= 0) {
95
215
  col.skills[existing] = { name: skillName, path: skillEntry, description };
@@ -1,4 +1,5 @@
1
1
  export declare function collectionCreateCommand(name?: string, options?: {
2
2
  backend?: string;
3
3
  repo?: string;
4
+ skillsRepo?: string;
4
5
  }): Promise<void>;