@skillsmanager/cli 0.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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -0
  3. package/dist/auth.d.ts +5 -0
  4. package/dist/auth.js +68 -0
  5. package/dist/backends/gdrive.d.ts +24 -0
  6. package/dist/backends/gdrive.js +371 -0
  7. package/dist/backends/interface.d.ts +14 -0
  8. package/dist/backends/interface.js +1 -0
  9. package/dist/backends/local.d.ts +20 -0
  10. package/dist/backends/local.js +159 -0
  11. package/dist/bm25.d.ts +20 -0
  12. package/dist/bm25.js +65 -0
  13. package/dist/cache.d.ts +21 -0
  14. package/dist/cache.js +59 -0
  15. package/dist/commands/add.d.ts +3 -0
  16. package/dist/commands/add.js +62 -0
  17. package/dist/commands/collection.d.ts +1 -0
  18. package/dist/commands/collection.js +42 -0
  19. package/dist/commands/fetch.d.ts +5 -0
  20. package/dist/commands/fetch.js +46 -0
  21. package/dist/commands/init.d.ts +1 -0
  22. package/dist/commands/init.js +45 -0
  23. package/dist/commands/install.d.ts +8 -0
  24. package/dist/commands/install.js +89 -0
  25. package/dist/commands/list.d.ts +3 -0
  26. package/dist/commands/list.js +38 -0
  27. package/dist/commands/refresh.d.ts +1 -0
  28. package/dist/commands/refresh.js +46 -0
  29. package/dist/commands/registry.d.ts +14 -0
  30. package/dist/commands/registry.js +258 -0
  31. package/dist/commands/search.d.ts +1 -0
  32. package/dist/commands/search.js +37 -0
  33. package/dist/commands/setup/google.d.ts +1 -0
  34. package/dist/commands/setup/google.js +281 -0
  35. package/dist/commands/update.d.ts +3 -0
  36. package/dist/commands/update.js +83 -0
  37. package/dist/config.d.ts +28 -0
  38. package/dist/config.js +136 -0
  39. package/dist/index.d.ts +2 -0
  40. package/dist/index.js +128 -0
  41. package/dist/ready.d.ts +15 -0
  42. package/dist/ready.js +39 -0
  43. package/dist/registry.d.ts +10 -0
  44. package/dist/registry.js +58 -0
  45. package/dist/types.d.ts +54 -0
  46. package/dist/types.js +13 -0
  47. package/package.json +56 -0
  48. package/skills/skillsmanager/SKILL.md +139 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ajay Prakash
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,84 @@
1
+ # Skills Manager
2
+
3
+ A CLI for AI agents to discover, fetch, and share agent skills stored in Google Drive.
4
+
5
+ ## What is it?
6
+
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.
8
+
9
+ Skills are downloaded to a local cache (`~/.skillsmanager/cache/`) and symlinked into the agent's skills directory. No duplication — one copy, many agents.
10
+
11
+ ## Supported agents
12
+
13
+ `claude`, `codex`, `cursor`, `windsurf`, `copilot`, `gemini`, `roo`, `agents`
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install -g skillsmanager
19
+ ```
20
+
21
+ ## Google Drive setup
22
+
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)
26
+ 4. Download `credentials.json` and save it to `~/.skillsmanager/credentials.json`
27
+
28
+ ## Usage
29
+
30
+ ```bash
31
+ # Authenticate and discover registries
32
+ skillsmanager init
33
+
34
+ # List all available skills
35
+ skillsmanager list
36
+
37
+ # Search skills by name or description
38
+ skillsmanager search <query>
39
+
40
+ # Download a skill and install it for an agent
41
+ skillsmanager fetch <skill-name> --agent claude
42
+
43
+ # Add a local skill to your registry
44
+ skillsmanager add ./my-skill
45
+
46
+ # Push local changes to an existing skill back to Drive
47
+ skillsmanager update <skill-name>
48
+
49
+ # Re-scan Drive for new registries
50
+ skillsmanager refresh
51
+ ```
52
+
53
+ ## Registry format
54
+
55
+ Skills are indexed by a `SKILLS_SYNC.yaml` file inside any Google Drive folder you own:
56
+
57
+ ```yaml
58
+ name: my-skills
59
+ owner: you@example.com
60
+ skills:
61
+ - name: code-review
62
+ path: code-review/
63
+ 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
+ ```
68
+
69
+ Each skill is a directory containing a `SKILL.md` file with YAML frontmatter:
70
+
71
+ ```markdown
72
+ ---
73
+ name: code-review
74
+ description: Reviews code for bugs, style, and security issues
75
+ ---
76
+
77
+ ... skill instructions ...
78
+ ```
79
+
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`.
81
+
82
+ ## Design doc
83
+
84
+ See [WRITEUP.md](./WRITEUP.md) for the full design.
package/dist/auth.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { OAuth2Client } from "google-auth-library";
2
+ export declare function runAuthFlow(): Promise<OAuth2Client>;
3
+ export declare function getAuthClient(): OAuth2Client;
4
+ export declare function hasToken(): boolean;
5
+ export declare function ensureAuth(): Promise<OAuth2Client>;
package/dist/auth.js ADDED
@@ -0,0 +1,68 @@
1
+ import fs from "fs";
2
+ import readline from "readline";
3
+ import { google } from "googleapis";
4
+ import { TOKEN_PATH, ensureConfigDir, readCredentials, credentialsExist } from "./config.js";
5
+ const SCOPES = ["https://www.googleapis.com/auth/drive"];
6
+ function createOAuth2Client() {
7
+ const { client_id, client_secret } = readCredentials();
8
+ return new google.auth.OAuth2(client_id, client_secret, "urn:ietf:wg:oauth:2.0:oob");
9
+ }
10
+ function saveToken(client) {
11
+ ensureConfigDir();
12
+ fs.writeFileSync(TOKEN_PATH, JSON.stringify(client.credentials, null, 2));
13
+ }
14
+ function prompt(question) {
15
+ const rl = readline.createInterface({
16
+ input: process.stdin,
17
+ output: process.stdout,
18
+ });
19
+ return new Promise((resolve) => {
20
+ rl.question(question, (answer) => {
21
+ rl.close();
22
+ resolve(answer.trim());
23
+ });
24
+ });
25
+ }
26
+ export async function runAuthFlow() {
27
+ const client = createOAuth2Client();
28
+ const authUrl = client.generateAuthUrl({
29
+ access_type: "offline",
30
+ scope: SCOPES,
31
+ prompt: "consent",
32
+ });
33
+ console.log("\nOpen this URL in your browser to authorize Skills Manager:\n");
34
+ console.log(authUrl);
35
+ console.log();
36
+ const code = await prompt("Paste the authorization code here: ");
37
+ const { tokens } = await client.getToken(code);
38
+ client.setCredentials(tokens);
39
+ saveToken(client);
40
+ return client;
41
+ }
42
+ export function getAuthClient() {
43
+ if (!fs.existsSync(TOKEN_PATH)) {
44
+ throw new Error(`Not authenticated. Run "skillsmanager init" first.`);
45
+ }
46
+ const client = createOAuth2Client();
47
+ const token = JSON.parse(fs.readFileSync(TOKEN_PATH, "utf-8"));
48
+ client.setCredentials(token);
49
+ client.on("tokens", (newTokens) => {
50
+ const merged = { ...client.credentials, ...newTokens };
51
+ client.setCredentials(merged);
52
+ saveToken(client);
53
+ });
54
+ return client;
55
+ }
56
+ export function hasToken() {
57
+ return fs.existsSync(TOKEN_PATH);
58
+ }
59
+ export async function ensureAuth() {
60
+ if (!credentialsExist()) {
61
+ throw new Error("No credentials found. Run: skillsmanager setup google");
62
+ }
63
+ if (!hasToken()) {
64
+ console.log("Not authenticated — launching login...\n");
65
+ return runAuthFlow();
66
+ }
67
+ return getAuthClient();
68
+ }
@@ -0,0 +1,24 @@
1
+ import type { OAuth2Client } from "google-auth-library";
2
+ import type { StorageBackend } from "./interface.js";
3
+ import type { CollectionFile, CollectionInfo, RegistryCollectionRef, RegistryFile, RegistryInfo } from "../types.js";
4
+ export declare class GDriveBackend implements StorageBackend {
5
+ private drive;
6
+ private oauth2;
7
+ constructor(auth: OAuth2Client);
8
+ getOwner(): Promise<string>;
9
+ getOwnerEmail(): Promise<string>;
10
+ discoverCollections(): Promise<Omit<CollectionInfo, "id">[]>;
11
+ readCollection(collection: CollectionInfo): Promise<CollectionFile>;
12
+ writeCollection(collection: CollectionInfo, data: CollectionFile): Promise<void>;
13
+ downloadSkill(collection: CollectionInfo, skillName: string, destDir: string): Promise<void>;
14
+ private downloadFolder;
15
+ createCollection(folderName: string): Promise<CollectionInfo>;
16
+ uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<void>;
17
+ discoverRegistries(): Promise<Omit<RegistryInfo, "id">[]>;
18
+ readRegistry(registry: RegistryInfo): Promise<RegistryFile>;
19
+ writeRegistry(registry: RegistryInfo, data: RegistryFile): Promise<void>;
20
+ resolveCollectionRef(ref: RegistryCollectionRef): Promise<Omit<CollectionInfo, "id"> | null>;
21
+ createRegistry(name?: string): Promise<RegistryInfo>;
22
+ private findFolder;
23
+ private uploadFolder;
24
+ }
@@ -0,0 +1,371 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { google } from "googleapis";
4
+ import { parseCollection, serializeCollection, parseRegistryFile, serializeRegistryFile, COLLECTION_FILENAME, LEGACY_COLLECTION_FILENAME, REGISTRY_FILENAME, } from "../registry.js";
5
+ import { Readable } from "stream";
6
+ import { randomUUID } from "crypto";
7
+ const FOLDER_MIME = "application/vnd.google-apps.folder";
8
+ export class GDriveBackend {
9
+ drive;
10
+ oauth2;
11
+ constructor(auth) {
12
+ this.drive = google.drive({ version: "v3", auth });
13
+ this.oauth2 = google.oauth2({ version: "v2", auth });
14
+ }
15
+ async getOwner() {
16
+ const res = await this.oauth2.userinfo.get();
17
+ return res.data.email ?? "";
18
+ }
19
+ // Alias for backwards compat
20
+ async getOwnerEmail() {
21
+ return this.getOwner();
22
+ }
23
+ // ── Collection operations ────────────────────────────────────────────────
24
+ async discoverCollections() {
25
+ const collections = [];
26
+ let pageToken;
27
+ do {
28
+ const res = await this.drive.files.list({
29
+ q: `(name='${COLLECTION_FILENAME}' or name='${LEGACY_COLLECTION_FILENAME}') and 'me' in owners and trashed=false`,
30
+ fields: "nextPageToken, files(id, name, parents)",
31
+ pageSize: 100,
32
+ ...(pageToken ? { pageToken } : {}),
33
+ });
34
+ pageToken = res.data.nextPageToken ?? undefined;
35
+ for (const file of res.data.files ?? []) {
36
+ const parentId = file.parents?.[0];
37
+ if (!parentId)
38
+ continue;
39
+ const parent = await this.drive.files.get({
40
+ fileId: parentId,
41
+ fields: "id, name",
42
+ });
43
+ const rawName = parent.data.name ?? "unknown";
44
+ collections.push({
45
+ name: rawName.replace(/^SKILLS_/i, ""),
46
+ backend: "gdrive",
47
+ folderId: parentId,
48
+ registryFileId: file.id ?? undefined,
49
+ });
50
+ }
51
+ } while (pageToken);
52
+ return collections;
53
+ }
54
+ async readCollection(collection) {
55
+ let fileId = collection.registryFileId;
56
+ if (!fileId) {
57
+ // Try new filename first, fall back to legacy
58
+ for (const filename of [COLLECTION_FILENAME, LEGACY_COLLECTION_FILENAME]) {
59
+ const res = await this.drive.files.list({
60
+ q: `name='${filename}' and '${collection.folderId}' in parents and trashed=false`,
61
+ fields: "files(id)",
62
+ pageSize: 1,
63
+ });
64
+ fileId = res.data.files?.[0]?.id ?? undefined;
65
+ if (fileId)
66
+ break;
67
+ }
68
+ if (!fileId) {
69
+ throw new Error(`Collection file not found in "${collection.name}"`);
70
+ }
71
+ }
72
+ const res = await this.drive.files.get({ fileId, alt: "media" }, { responseType: "text" });
73
+ return parseCollection(res.data);
74
+ }
75
+ async writeCollection(collection, data) {
76
+ const content = serializeCollection(data);
77
+ const media = {
78
+ mimeType: "text/yaml",
79
+ body: Readable.from(content),
80
+ };
81
+ if (collection.registryFileId) {
82
+ await this.drive.files.update({
83
+ fileId: collection.registryFileId,
84
+ media,
85
+ });
86
+ }
87
+ else {
88
+ // Try to find existing file (new or legacy name)
89
+ let existingId;
90
+ for (const filename of [COLLECTION_FILENAME, LEGACY_COLLECTION_FILENAME]) {
91
+ const res = await this.drive.files.list({
92
+ q: `name='${filename}' and '${collection.folderId}' in parents and trashed=false`,
93
+ fields: "files(id)",
94
+ pageSize: 1,
95
+ });
96
+ existingId = res.data.files?.[0]?.id ?? undefined;
97
+ if (existingId)
98
+ break;
99
+ }
100
+ if (existingId) {
101
+ await this.drive.files.update({ fileId: existingId, media });
102
+ }
103
+ else {
104
+ await this.drive.files.create({
105
+ requestBody: {
106
+ name: COLLECTION_FILENAME,
107
+ parents: [collection.folderId],
108
+ },
109
+ media,
110
+ });
111
+ }
112
+ }
113
+ }
114
+ async downloadSkill(collection, skillName, destDir) {
115
+ const res = await this.drive.files.list({
116
+ q: `name='${skillName}' and '${collection.folderId}' in parents and mimeType='${FOLDER_MIME}' and trashed=false`,
117
+ fields: "files(id, name)",
118
+ pageSize: 1,
119
+ });
120
+ const skillFolder = res.data.files?.[0];
121
+ if (!skillFolder?.id) {
122
+ throw new Error(`Skill folder "${skillName}" not found in collection "${collection.name}"`);
123
+ }
124
+ await this.downloadFolder(skillFolder.id, destDir);
125
+ }
126
+ async downloadFolder(folderId, destDir) {
127
+ fs.mkdirSync(destDir, { recursive: true });
128
+ let pageToken;
129
+ do {
130
+ const res = await this.drive.files.list({
131
+ q: `'${folderId}' in parents and trashed=false`,
132
+ fields: "nextPageToken, files(id, name, mimeType)",
133
+ pageSize: 100,
134
+ ...(pageToken ? { pageToken } : {}),
135
+ });
136
+ pageToken = res.data.nextPageToken ?? undefined;
137
+ for (const file of res.data.files ?? []) {
138
+ if (file.mimeType === FOLDER_MIME) {
139
+ await this.downloadFolder(file.id, path.join(destDir, file.name));
140
+ }
141
+ else {
142
+ const filePath = path.join(destDir, file.name);
143
+ const fileRes = await this.drive.files.get({ fileId: file.id, alt: "media" }, { responseType: "stream" });
144
+ await new Promise((resolve, reject) => {
145
+ const dest = fs.createWriteStream(filePath);
146
+ fileRes.data.pipe(dest);
147
+ dest.on("finish", resolve);
148
+ dest.on("error", reject);
149
+ });
150
+ }
151
+ }
152
+ } while (pageToken);
153
+ }
154
+ async createCollection(folderName) {
155
+ const folderRes = await this.drive.files.create({
156
+ requestBody: {
157
+ name: folderName,
158
+ mimeType: FOLDER_MIME,
159
+ },
160
+ fields: "id, name",
161
+ });
162
+ const folderId = folderRes.data.id;
163
+ const owner = await this.getOwnerEmail();
164
+ const logicalName = folderName.replace(/^SKILLS_/i, "");
165
+ const emptyCollection = { name: logicalName, owner, skills: [] };
166
+ const content = serializeCollection(emptyCollection);
167
+ const fileRes = await this.drive.files.create({
168
+ requestBody: {
169
+ name: COLLECTION_FILENAME,
170
+ parents: [folderId],
171
+ },
172
+ media: { mimeType: "text/yaml", body: Readable.from(content) },
173
+ fields: "id",
174
+ });
175
+ return {
176
+ id: randomUUID(),
177
+ name: logicalName,
178
+ backend: "gdrive",
179
+ folderId,
180
+ registryFileId: fileRes.data.id ?? undefined,
181
+ };
182
+ }
183
+ async uploadSkill(collection, localPath, skillName) {
184
+ let folderId = await this.findFolder(skillName, collection.folderId);
185
+ if (!folderId) {
186
+ const res = await this.drive.files.create({
187
+ requestBody: {
188
+ name: skillName,
189
+ mimeType: FOLDER_MIME,
190
+ parents: [collection.folderId],
191
+ },
192
+ fields: "id",
193
+ });
194
+ folderId = res.data.id;
195
+ }
196
+ await this.uploadFolder(localPath, folderId);
197
+ }
198
+ // ── Registry operations ──────────────────────────────────────────────────
199
+ async discoverRegistries() {
200
+ const registries = [];
201
+ let pageToken;
202
+ do {
203
+ const res = await this.drive.files.list({
204
+ q: `name='${REGISTRY_FILENAME}' and 'me' in owners and trashed=false`,
205
+ fields: "nextPageToken, files(id, name, parents)",
206
+ pageSize: 100,
207
+ ...(pageToken ? { pageToken } : {}),
208
+ });
209
+ pageToken = res.data.nextPageToken ?? undefined;
210
+ for (const file of res.data.files ?? []) {
211
+ const parentId = file.parents?.[0];
212
+ if (!parentId)
213
+ continue;
214
+ const parent = await this.drive.files.get({
215
+ fileId: parentId,
216
+ fields: "id, name",
217
+ });
218
+ registries.push({
219
+ name: (parent.data.name ?? "unknown").replace(/^SKILLS_/i, ""),
220
+ backend: "gdrive",
221
+ folderId: parentId,
222
+ fileId: file.id ?? undefined,
223
+ });
224
+ }
225
+ } while (pageToken);
226
+ return registries;
227
+ }
228
+ async readRegistry(registry) {
229
+ let fileId = registry.fileId;
230
+ if (!fileId) {
231
+ const res = await this.drive.files.list({
232
+ q: `name='${REGISTRY_FILENAME}' and '${registry.folderId}' in parents and trashed=false`,
233
+ fields: "files(id)",
234
+ pageSize: 1,
235
+ });
236
+ fileId = res.data.files?.[0]?.id ?? undefined;
237
+ if (!fileId) {
238
+ throw new Error(`Registry file not found for "${registry.name}"`);
239
+ }
240
+ }
241
+ const res = await this.drive.files.get({ fileId, alt: "media" }, { responseType: "text" });
242
+ return parseRegistryFile(res.data);
243
+ }
244
+ async writeRegistry(registry, data) {
245
+ const content = serializeRegistryFile(data);
246
+ const media = { mimeType: "text/yaml", body: Readable.from(content) };
247
+ if (registry.fileId) {
248
+ await this.drive.files.update({ fileId: registry.fileId, media });
249
+ }
250
+ else {
251
+ const res = await this.drive.files.list({
252
+ q: `name='${REGISTRY_FILENAME}' and '${registry.folderId}' in parents and trashed=false`,
253
+ fields: "files(id)",
254
+ pageSize: 1,
255
+ });
256
+ const existingId = res.data.files?.[0]?.id;
257
+ if (existingId) {
258
+ await this.drive.files.update({ fileId: existingId, media });
259
+ }
260
+ else {
261
+ await this.drive.files.create({
262
+ requestBody: { name: REGISTRY_FILENAME, parents: [registry.folderId] },
263
+ media,
264
+ });
265
+ }
266
+ }
267
+ }
268
+ async resolveCollectionRef(ref) {
269
+ if (ref.backend !== "gdrive")
270
+ return null;
271
+ // Search for folder by name (try with SKILLS_ prefix and without)
272
+ const names = ref.ref.startsWith("SKILLS_") ? [ref.ref] : [`SKILLS_${ref.ref}`, ref.ref];
273
+ for (const name of names) {
274
+ const res = await this.drive.files.list({
275
+ q: `name='${name}' and mimeType='${FOLDER_MIME}' and 'me' in owners and trashed=false`,
276
+ fields: "files(id, name)",
277
+ pageSize: 1,
278
+ });
279
+ const folder = res.data.files?.[0];
280
+ if (folder?.id) {
281
+ return {
282
+ name: (folder.name ?? name).replace(/^SKILLS_/i, ""),
283
+ backend: "gdrive",
284
+ folderId: folder.id,
285
+ };
286
+ }
287
+ }
288
+ return null;
289
+ }
290
+ async createRegistry(name) {
291
+ const folderName = name ? `SKILLS_REGISTRY_${name}` : "SKILLS_REGISTRY";
292
+ const folderRes = await this.drive.files.create({
293
+ requestBody: { name: folderName, mimeType: FOLDER_MIME },
294
+ fields: "id, name",
295
+ });
296
+ const folderId = folderRes.data.id;
297
+ const owner = await this.getOwnerEmail();
298
+ const registryData = {
299
+ name: name ?? "default",
300
+ owner,
301
+ source: "gdrive",
302
+ collections: [],
303
+ };
304
+ const fileRes = await this.drive.files.create({
305
+ requestBody: { name: REGISTRY_FILENAME, parents: [folderId] },
306
+ media: { mimeType: "text/yaml", body: Readable.from(serializeRegistryFile(registryData)) },
307
+ fields: "id",
308
+ });
309
+ return {
310
+ id: randomUUID(),
311
+ name: name ?? "default",
312
+ backend: "gdrive",
313
+ folderId,
314
+ fileId: fileRes.data.id ?? undefined,
315
+ };
316
+ }
317
+ // ── Private helpers ──────────────────────────────────────────────────────
318
+ async findFolder(name, parentId) {
319
+ const res = await this.drive.files.list({
320
+ q: `name='${name}' and '${parentId}' in parents and mimeType='${FOLDER_MIME}' and trashed=false`,
321
+ fields: "files(id)",
322
+ pageSize: 1,
323
+ });
324
+ return res.data.files?.[0]?.id ?? null;
325
+ }
326
+ async uploadFolder(localDir, parentId) {
327
+ for (const entry of fs.readdirSync(localDir, { withFileTypes: true })) {
328
+ if (entry.name.startsWith("."))
329
+ continue;
330
+ const fullPath = path.join(localDir, entry.name);
331
+ if (entry.isDirectory()) {
332
+ let subFolderId = await this.findFolder(entry.name, parentId);
333
+ if (!subFolderId) {
334
+ const res = await this.drive.files.create({
335
+ requestBody: {
336
+ name: entry.name,
337
+ mimeType: FOLDER_MIME,
338
+ parents: [parentId],
339
+ },
340
+ fields: "id",
341
+ });
342
+ subFolderId = res.data.id;
343
+ }
344
+ await this.uploadFolder(fullPath, subFolderId);
345
+ }
346
+ else {
347
+ const existing = await this.drive.files.list({
348
+ q: `name='${entry.name}' and '${parentId}' in parents and trashed=false`,
349
+ fields: "files(id)",
350
+ pageSize: 1,
351
+ });
352
+ const media = {
353
+ mimeType: "application/octet-stream",
354
+ body: fs.createReadStream(fullPath),
355
+ };
356
+ if (existing.data.files?.[0]?.id) {
357
+ await this.drive.files.update({
358
+ fileId: existing.data.files[0].id,
359
+ media,
360
+ });
361
+ }
362
+ else {
363
+ await this.drive.files.create({
364
+ requestBody: { name: entry.name, parents: [parentId] },
365
+ media,
366
+ });
367
+ }
368
+ }
369
+ }
370
+ }
371
+ }
@@ -0,0 +1,14 @@
1
+ import type { CollectionFile, CollectionInfo, RegistryCollectionRef, RegistryFile, RegistryInfo } from "../types.js";
2
+ export interface StorageBackend {
3
+ getOwner(): Promise<string>;
4
+ discoverCollections(): Promise<Omit<CollectionInfo, "id">[]>;
5
+ readCollection(collection: CollectionInfo): Promise<CollectionFile>;
6
+ writeCollection(collection: CollectionInfo, data: CollectionFile): Promise<void>;
7
+ downloadSkill(collection: CollectionInfo, skillName: string, destDir: string): Promise<void>;
8
+ uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<void>;
9
+ discoverRegistries(): Promise<Omit<RegistryInfo, "id">[]>;
10
+ readRegistry(registry: RegistryInfo): Promise<RegistryFile>;
11
+ writeRegistry(registry: RegistryInfo, data: RegistryFile): Promise<void>;
12
+ resolveCollectionRef(ref: RegistryCollectionRef): Promise<Omit<CollectionInfo, "id"> | null>;
13
+ createRegistry(name?: string): Promise<RegistryInfo>;
14
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ import type { StorageBackend } from "./interface.js";
2
+ import type { CollectionFile, CollectionInfo, RegistryCollectionRef, RegistryFile, RegistryInfo } from "../types.js";
3
+ /**
4
+ * Local filesystem backend — stores collections and the registry under ~/.skillsmanager/.
5
+ * Works with zero setup, no auth, no internet. This is the default backend.
6
+ */
7
+ export declare class LocalBackend implements StorageBackend {
8
+ getOwner(): Promise<string>;
9
+ discoverCollections(): Promise<Omit<CollectionInfo, "id">[]>;
10
+ readCollection(collection: CollectionInfo): Promise<CollectionFile>;
11
+ writeCollection(collection: CollectionInfo, data: CollectionFile): Promise<void>;
12
+ downloadSkill(collection: CollectionInfo, skillName: string, destDir: string): Promise<void>;
13
+ uploadSkill(collection: CollectionInfo, localPath: string, skillName: string): Promise<void>;
14
+ discoverRegistries(): Promise<Omit<RegistryInfo, "id">[]>;
15
+ readRegistry(registry: RegistryInfo): Promise<RegistryFile>;
16
+ writeRegistry(registry: RegistryInfo, data: RegistryFile): Promise<void>;
17
+ resolveCollectionRef(ref: RegistryCollectionRef): Promise<Omit<CollectionInfo, "id"> | null>;
18
+ createRegistry(name?: string): Promise<RegistryInfo>;
19
+ createCollection(name: string): Promise<CollectionInfo>;
20
+ }