@michaeltroya/pouch 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,237 @@
1
+ # Pouch
2
+
3
+ Create one agent skill and share it across Codex, Cursor, and Claude.
4
+
5
+ Pouch is a small CLI for keeping agent skills in one canonical place. Instead of copying the same `SKILL.md` into every agent's folder, Pouch creates the real skill once and links each agent to it.
6
+
7
+ ```txt
8
+ .agents/skills/review-helper/SKILL.md
9
+ .codex/skills/review-helper -> ../../.agents/skills/review-helper
10
+ .cursor/skills/review-helper -> ../../.agents/skills/review-helper
11
+ .claude/skills/review-helper -> ../../.agents/skills/review-helper
12
+ ```
13
+
14
+ ## Why Pouch?
15
+
16
+ Agent skills are useful, but they tend to drift when each tool keeps its own copy.
17
+
18
+ Pouch gives you one source of truth:
19
+
20
+ - Create a skill once.
21
+ - Use it from multiple agents.
22
+ - Keep project-local skills inside the repo.
23
+ - Keep personal/global skills in your home directory.
24
+ - Avoid overwriting existing skills by accident.
25
+
26
+ Pouch is CLI-only. It does not expose a JavaScript API.
27
+
28
+ ## Quick Start
29
+
30
+ Requires Node.js 22.13 or newer.
31
+
32
+ Run without installing:
33
+
34
+ ```sh
35
+ pnpm dlx @michaeltroya/pouch create
36
+ ```
37
+
38
+ With npm:
39
+
40
+ ```sh
41
+ npx @michaeltroya/pouch create
42
+ ```
43
+
44
+ With Yarn:
45
+
46
+ ```sh
47
+ yarn dlx @michaeltroya/pouch create
48
+ ```
49
+
50
+ Or install globally:
51
+
52
+ ```sh
53
+ npm install --global @michaeltroya/pouch
54
+ pouch create
55
+ ```
56
+
57
+ For local development in this repo:
58
+
59
+ ```sh
60
+ pnpm install
61
+ pnpm build
62
+ ```
63
+
64
+ During local development, run the CLI directly from TypeScript:
65
+
66
+ ```sh
67
+ pnpm dev -- create
68
+ ```
69
+
70
+ After building, the package exposes a `pouch` binary:
71
+
72
+ ```sh
73
+ pouch create
74
+ ```
75
+
76
+ ## Usage
77
+
78
+ Create a shared skill:
79
+
80
+ ```sh
81
+ pouch create
82
+ ```
83
+
84
+ Pouch will ask:
85
+
86
+ 1. Skill name
87
+ 2. Skill description
88
+ 3. Where the canonical skill should live
89
+ 4. Which agents should use it
90
+
91
+ You can choose a project-local skill:
92
+
93
+ ```txt
94
+ ./.agents/skills/<skill-name>
95
+ ```
96
+
97
+ Or a global skill:
98
+
99
+ ```txt
100
+ ~/.agents/skills/<skill-name>
101
+ ```
102
+
103
+ Example output:
104
+
105
+ ```txt
106
+ review-helper
107
+ Canonical: /path/to/project/.agents/skills/review-helper
108
+ Codex: /path/to/project/.codex/skills/review-helper (created)
109
+ Cursor: /path/to/project/.cursor/skills/review-helper (created)
110
+ Claude: /path/to/project/.claude/skills/review-helper (created)
111
+ ```
112
+
113
+ ## What Pouch Creates
114
+
115
+ Creating a project-local skill named `Review Helper` produces:
116
+
117
+ ```txt
118
+ .agents/
119
+ skills/
120
+ review-helper/
121
+ SKILL.md
122
+ .codex/
123
+ skills/
124
+ review-helper -> ../../.agents/skills/review-helper
125
+ .cursor/
126
+ skills/
127
+ review-helper -> ../../.agents/skills/review-helper
128
+ .claude/
129
+ skills/
130
+ review-helper -> ../../.agents/skills/review-helper
131
+ ```
132
+
133
+ The generated `SKILL.md` starts like this:
134
+
135
+ ```md
136
+ ---
137
+ name: review-helper
138
+ description: 'Helps agents review code consistently.'
139
+ ---
140
+
141
+ # Review Helper
142
+
143
+ Helps agents review code consistently.
144
+ ```
145
+
146
+ ## Supported Agents
147
+
148
+ | Agent | Project-local skill directory | Global skill directory |
149
+ | ------ | ----------------------------- | ---------------------- |
150
+ | Codex | `./.codex/skills` | `~/.codex/skills` |
151
+ | Cursor | `./.cursor/skills` | `~/.cursor/skills` |
152
+ | Claude | `./.claude/skills` | `~/.claude/skills` |
153
+
154
+ ## Safety
155
+
156
+ Pouch is conservative with your files:
157
+
158
+ - It will not overwrite an existing canonical `SKILL.md`.
159
+ - It will not replace an existing non-symlink in an agent skill directory.
160
+ - It will not repoint an existing symlink that targets a different skill.
161
+ - If symlink creation fails partway through, it rolls back the skill and links it created during that run.
162
+
163
+ For project-local skills, Pouch creates relative symlinks so the links keep working if the project folder is moved.
164
+
165
+ On Windows, symlink creation may require Developer Mode or an elevated shell.
166
+
167
+ ## Local Development
168
+
169
+ This repo uses pnpm.
170
+
171
+ Install dependencies:
172
+
173
+ ```sh
174
+ pnpm install
175
+ ```
176
+
177
+ Run the CLI from TypeScript:
178
+
179
+ ```sh
180
+ pnpm dev -- create
181
+ ```
182
+
183
+ Build the package:
184
+
185
+ ```sh
186
+ pnpm build
187
+ ```
188
+
189
+ Run checks and formatting:
190
+
191
+ ```sh
192
+ pnpm check
193
+ pnpm lint
194
+ pnpm format
195
+ ```
196
+
197
+ Tests are available through:
198
+
199
+ ```sh
200
+ pnpm test
201
+ ```
202
+
203
+ ## Project Structure
204
+
205
+ ```txt
206
+ src/
207
+ index.ts CLI entrypoint
208
+ commands/
209
+ create.ts Prompt flow for `pouch create`
210
+ lib/
211
+ agents.ts Supported agents and skill directory resolution
212
+ fs.ts Shared filesystem helpers
213
+ paths.ts Canonical skill paths and location schema
214
+ skill.ts Skill creation, symlink planning, and rollback
215
+ ```
216
+
217
+ ## Contributing
218
+
219
+ Issues and pull requests are welcome.
220
+
221
+ Before opening a PR, please run:
222
+
223
+ ```sh
224
+ pnpm check
225
+ pnpm lint
226
+ pnpm format
227
+ ```
228
+
229
+ If your change touches behavior, also run:
230
+
231
+ ```sh
232
+ pnpm test
233
+ ```
234
+
235
+ ## License
236
+
237
+ MIT
@@ -0,0 +1 @@
1
+ export { };
package/dist/index.mjs ADDED
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { intro, isCancel, multiselect, note, outro, select, spinner, text } from "@clack/prompts";
4
+ import { z } from "zod";
5
+ import path from "node:path";
6
+ import os from "node:os";
7
+ import fs from "fs-extra";
8
+ //#region src/lib/paths.ts
9
+ const canonicalSkillLocationSchema = z.enum(["home", "project"]);
10
+ function getLocationRoot(location) {
11
+ if (location === "project") return process.cwd();
12
+ return os.homedir();
13
+ }
14
+ function getCanonicalSkillsDirectory(location) {
15
+ return path.join(getLocationRoot(location), ".agents", "skills");
16
+ }
17
+ function getCanonicalSkillPath(location, skillName) {
18
+ return path.join(getCanonicalSkillsDirectory(location), skillName);
19
+ }
20
+ function toComparablePath(value) {
21
+ const resolved = path.resolve(value);
22
+ if (resolved.startsWith("/private/var/")) return resolved.replace("/private/var/", "/var/");
23
+ return resolved;
24
+ }
25
+ //#endregion
26
+ //#region src/lib/agents.ts
27
+ const agentIdSchema = z.enum([
28
+ "codex",
29
+ "cursor",
30
+ "claude"
31
+ ]);
32
+ const supportedAgents = [
33
+ {
34
+ id: "codex",
35
+ label: "Codex",
36
+ directoryName: ".codex"
37
+ },
38
+ {
39
+ id: "cursor",
40
+ label: "Cursor",
41
+ directoryName: ".cursor"
42
+ },
43
+ {
44
+ id: "claude",
45
+ label: "Claude",
46
+ directoryName: ".claude"
47
+ }
48
+ ];
49
+ function getAgentsById(agentIds, location) {
50
+ const selected = new Set(agentIds);
51
+ return supportedAgents.filter((agent) => selected.has(agent.id)).map((agent) => ({
52
+ id: agent.id,
53
+ label: agent.label,
54
+ skillDirectory: getAgentSkillDirectory(agent.directoryName, location)
55
+ }));
56
+ }
57
+ function getAgentSkillDirectory(directoryName, location) {
58
+ return path.join(getLocationRoot(location), directoryName, "skills");
59
+ }
60
+ //#endregion
61
+ //#region src/lib/fs.ts
62
+ function isNodeError(error) {
63
+ return error instanceof Error && "code" in error;
64
+ }
65
+ async function lstatOrNull(filePath) {
66
+ return fs.lstat(filePath).catch((error) => {
67
+ if (isNodeError(error) && error.code === "ENOENT") return null;
68
+ throw error;
69
+ });
70
+ }
71
+ //#endregion
72
+ //#region src/lib/skill.ts
73
+ const skillInputSchema = z.object({
74
+ name: z.string().trim().min(1, "Skill name is required"),
75
+ description: z.string().trim().min(1, "Skill description is required")
76
+ });
77
+ function toSkillSlug(value) {
78
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
79
+ }
80
+ async function createSkill(input) {
81
+ const parsed = skillInputSchema.parse(input);
82
+ const skillName = toSkillSlug(parsed.name);
83
+ if (!skillName) throw new Error("Skill name must include at least one letter or number.");
84
+ const agents = dedupeAgents(input.agents);
85
+ const canonicalPath = getCanonicalSkillPath(input.location, skillName);
86
+ const skillFilePath = path.join(canonicalPath, "SKILL.md");
87
+ const canonicalExisted = await fs.pathExists(canonicalPath);
88
+ await ensureCanonicalSkillCanBeCreated(canonicalPath);
89
+ const linkPlans = await planSkillSymlinks(agents, canonicalPath, skillName);
90
+ await fs.ensureDir(canonicalPath);
91
+ await writeSkillFile(skillFilePath, parsed.name, parsed.description);
92
+ const links = [];
93
+ const createdLinkPaths = [];
94
+ try {
95
+ for (const linkPlan of linkPlans) {
96
+ const result = await applySkillSymlink(linkPlan, canonicalPath);
97
+ if (result.status === "created") createdLinkPaths.push(result.linkPath);
98
+ links.push(result);
99
+ }
100
+ } catch (error) {
101
+ await rollbackCreatedPaths({
102
+ canonicalExisted,
103
+ canonicalPath,
104
+ createdLinkPaths,
105
+ skillFilePath
106
+ });
107
+ throw error;
108
+ }
109
+ return {
110
+ skillName,
111
+ canonicalPath,
112
+ skillFilePath,
113
+ links
114
+ };
115
+ }
116
+ function dedupeAgents(agents) {
117
+ const seen = /* @__PURE__ */ new Set();
118
+ return agents.filter((agent) => {
119
+ if (seen.has(agent.id)) return false;
120
+ seen.add(agent.id);
121
+ return true;
122
+ });
123
+ }
124
+ async function writeSkillFile(skillFilePath, name, description) {
125
+ await fs.writeFile(skillFilePath, renderSkillMarkdown(name, description), { flag: "wx" }).catch((error) => {
126
+ if (isNodeError(error) && error.code === "EEXIST") throw new Error(`A canonical skill already exists at ${skillFilePath}`);
127
+ throw error;
128
+ });
129
+ }
130
+ async function ensureCanonicalSkillCanBeCreated(canonicalPath) {
131
+ await ensureDirectoryPathCanBeCreated(path.dirname(canonicalPath));
132
+ const existingCanonicalPath = await lstatOrNull(canonicalPath);
133
+ if (existingCanonicalPath && !existingCanonicalPath.isDirectory()) throw new Error(`Cannot create skill because ${canonicalPath} already exists.`);
134
+ if (existingCanonicalPath) {
135
+ const contents = await fs.readdir(canonicalPath);
136
+ if (contents.length > 0 && !contents.includes("SKILL.md")) throw new Error(`Cannot create skill because ${canonicalPath} already exists and is not empty.`);
137
+ }
138
+ }
139
+ function renderSkillMarkdown(name, description) {
140
+ return `---
141
+ name: ${toSkillSlug(name)}
142
+ description: ${JSON.stringify(description.trim())}
143
+ ---
144
+
145
+ # ${name.trim()}
146
+
147
+ ${description.trim()}
148
+ `;
149
+ }
150
+ async function planSkillSymlinks(agents, canonicalPath, skillName) {
151
+ return Promise.all(agents.map(async (agent) => {
152
+ const linkPath = path.join(agent.skillDirectory, skillName);
153
+ await ensureDirectoryPathCanBeCreated(path.dirname(linkPath));
154
+ const existing = await lstatOrNull(linkPath);
155
+ if (!existing) return {
156
+ agent,
157
+ linkPath,
158
+ action: "link"
159
+ };
160
+ if (!existing.isSymbolicLink()) throw new Error(`Cannot link skill because ${linkPath} already exists and is not a symlink.`);
161
+ const target = await fs.readlink(linkPath);
162
+ const resolvedTarget = path.resolve(path.dirname(linkPath), target);
163
+ if (toComparablePath(resolvedTarget) !== toComparablePath(canonicalPath)) throw new Error(`Cannot link skill because ${linkPath} points to ${resolvedTarget}.`);
164
+ return {
165
+ agent,
166
+ linkPath,
167
+ action: "skip"
168
+ };
169
+ }));
170
+ }
171
+ async function ensureDirectoryPathCanBeCreated(directoryPath) {
172
+ const existing = await lstatOrNull(directoryPath);
173
+ if (existing) {
174
+ if (!existing.isDirectory()) throw new Error(`Cannot create directory because ${directoryPath} already exists and is not a directory.`);
175
+ return;
176
+ }
177
+ const parentPath = path.dirname(directoryPath);
178
+ if (parentPath === directoryPath) return;
179
+ await ensureDirectoryPathCanBeCreated(parentPath);
180
+ }
181
+ async function applySkillSymlink(plan, canonicalPath) {
182
+ if (plan.action === "skip") return {
183
+ agent: plan.agent,
184
+ linkPath: plan.linkPath,
185
+ status: "existing"
186
+ };
187
+ await fs.ensureDir(path.dirname(plan.linkPath));
188
+ const linkTarget = path.relative(path.dirname(plan.linkPath), canonicalPath);
189
+ await fs.symlink(linkTarget, plan.linkPath, "dir");
190
+ return {
191
+ agent: plan.agent,
192
+ linkPath: plan.linkPath,
193
+ status: "created"
194
+ };
195
+ }
196
+ async function rollbackCreatedPaths(options) {
197
+ await Promise.all(options.createdLinkPaths.map((linkPath) => fs.remove(linkPath)));
198
+ if (options.canonicalExisted) {
199
+ await fs.remove(options.skillFilePath);
200
+ return;
201
+ }
202
+ await fs.remove(options.canonicalPath);
203
+ }
204
+ //#endregion
205
+ //#region src/commands/create.ts
206
+ function promptOrExit(value) {
207
+ if (isCancel(value)) {
208
+ outro("Cancelled");
209
+ process.exit(0);
210
+ }
211
+ return value;
212
+ }
213
+ function requireNonEmpty(message) {
214
+ return (value) => {
215
+ if (!(value ?? "").trim()) return message;
216
+ };
217
+ }
218
+ function createCommand() {
219
+ const command = new Command("create");
220
+ command.description("Create a shared skill and link it into agent skill directories");
221
+ command.action(async () => {
222
+ intro("Create a shared agent skill");
223
+ const name = promptOrExit(await text({
224
+ message: "Skill name",
225
+ placeholder: "review-helper",
226
+ validate: requireNonEmpty("Enter a skill name.")
227
+ }));
228
+ const description = promptOrExit(await text({
229
+ message: "Skill description",
230
+ placeholder: "Helps agents review code consistently.",
231
+ validate: requireNonEmpty("Enter a skill description.")
232
+ }));
233
+ const location = promptOrExit(await select({
234
+ message: "Where should the canonical skill live?",
235
+ options: [{
236
+ value: "project",
237
+ label: "Current project",
238
+ hint: "./.agents/skills/<skill-name>"
239
+ }, {
240
+ value: "home",
241
+ label: "Home directory",
242
+ hint: "~/.agents/skills/<skill-name>"
243
+ }]
244
+ }));
245
+ const selectedAgentIds = promptOrExit(await multiselect({
246
+ message: "Which agents should use this skill?",
247
+ options: supportedAgents.map((agent) => ({
248
+ value: agent.id,
249
+ label: agent.label
250
+ })),
251
+ required: true
252
+ }));
253
+ const canonicalLocation = canonicalSkillLocationSchema.parse(location);
254
+ const agents = getAgentsById(agentIdSchema.array().parse(selectedAgentIds), canonicalLocation);
255
+ const activity = spinner();
256
+ activity.start("Creating canonical skill and symlinks");
257
+ try {
258
+ const result = await createSkill({
259
+ name,
260
+ description,
261
+ location: canonicalLocation,
262
+ agents
263
+ });
264
+ activity.stop("Skill created");
265
+ note([`Canonical: ${result.canonicalPath}`, ...result.links.map((link) => `${link.agent.label}: ${link.linkPath} (${link.status})`)].join("\n"), result.skillName);
266
+ outro("Done");
267
+ } catch (error) {
268
+ activity.stop("Could not create skill");
269
+ outro(error instanceof Error ? error.message : "Unknown error");
270
+ process.exit(1);
271
+ }
272
+ });
273
+ return command;
274
+ }
275
+ //#endregion
276
+ //#region src/index.ts
277
+ const program = new Command();
278
+ program.name("pouch").description("Create and link shared agent skills").version("0.1.0");
279
+ program.addCommand(createCommand());
280
+ await program.parseAsync();
281
+ //#endregion
282
+ export {};
283
+
284
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/lib/paths.ts","../src/lib/agents.ts","../src/lib/fs.ts","../src/lib/skill.ts","../src/commands/create.ts","../src/index.ts"],"sourcesContent":["import os from 'node:os'\nimport path from 'node:path'\nimport { z } from 'zod'\n\nexport const canonicalSkillLocationSchema = z.enum(['home', 'project'])\n\nexport type CanonicalSkillLocation = z.infer<typeof canonicalSkillLocationSchema>\n\nexport function resolveHomePath(value: string): string {\n if (value === '~') {\n return os.homedir()\n }\n\n if (value.startsWith('~/')) {\n return path.join(os.homedir(), value.slice(2))\n }\n\n return value\n}\n\nexport function getLocationRoot(location: CanonicalSkillLocation): string {\n if (location === 'project') {\n return process.cwd()\n }\n\n return os.homedir()\n}\n\nexport function getCanonicalSkillsDirectory(location: CanonicalSkillLocation): string {\n return path.join(getLocationRoot(location), '.agents', 'skills')\n}\n\nexport function getCanonicalSkillPath(location: CanonicalSkillLocation, skillName: string): string {\n return path.join(getCanonicalSkillsDirectory(location), skillName)\n}\n\nexport function toComparablePath(value: string): string {\n const resolved = path.resolve(value)\n\n if (resolved.startsWith('/private/var/')) {\n return resolved.replace('/private/var/', '/var/')\n }\n\n return resolved\n}\n","import { z } from 'zod'\nimport path from 'node:path'\nimport { getLocationRoot } from '@/lib/paths.js'\nimport type { CanonicalSkillLocation } from '@/lib/paths.js'\n\nexport const agentIdSchema = z.enum(['codex', 'cursor', 'claude'])\n\nexport type AgentId = z.infer<typeof agentIdSchema>\n\nexport type AgentDefinition = {\n id: AgentId\n label: string\n skillDirectory: string\n}\n\ntype SupportedAgent = {\n id: AgentId\n label: string\n directoryName: string\n}\n\nexport const supportedAgents = [\n {\n id: 'codex',\n label: 'Codex',\n directoryName: '.codex',\n },\n {\n id: 'cursor',\n label: 'Cursor',\n directoryName: '.cursor',\n },\n {\n id: 'claude',\n label: 'Claude',\n directoryName: '.claude',\n },\n] satisfies SupportedAgent[]\n\nexport function getAgentsById(\n agentIds: AgentId[],\n location: CanonicalSkillLocation,\n): AgentDefinition[] {\n const selected = new Set(agentIds)\n return supportedAgents\n .filter((agent) => selected.has(agent.id))\n .map((agent) => ({\n id: agent.id,\n label: agent.label,\n skillDirectory: getAgentSkillDirectory(agent.directoryName, location),\n }))\n}\n\nfunction getAgentSkillDirectory(directoryName: string, location: CanonicalSkillLocation): string {\n return path.join(getLocationRoot(location), directoryName, 'skills')\n}\n","import fs from 'fs-extra'\n\nexport function isNodeError(error: unknown): error is NodeJS.ErrnoException {\n return error instanceof Error && 'code' in error\n}\n\nexport async function lstatOrNull(filePath: string): Promise<fs.Stats | null> {\n return fs.lstat(filePath).catch((error: unknown) => {\n if (isNodeError(error) && error.code === 'ENOENT') {\n return null\n }\n\n throw error\n })\n}\n","import path from 'node:path'\nimport fs from 'fs-extra'\nimport { z } from 'zod'\nimport type { AgentDefinition } from '@/lib/agents.js'\nimport { isNodeError, lstatOrNull } from '@/lib/fs.js'\nimport {\n getCanonicalSkillPath,\n toComparablePath,\n type CanonicalSkillLocation,\n} from '@/lib/paths.js'\n\nconst skillInputSchema = z.object({\n name: z.string().trim().min(1, 'Skill name is required'),\n description: z.string().trim().min(1, 'Skill description is required'),\n})\n\nexport type CreateSkillInput = z.input<typeof skillInputSchema> & {\n agents: AgentDefinition[]\n location: CanonicalSkillLocation\n}\n\nexport type SkillLinkPlan =\n | { action: 'link'; agent: AgentDefinition; linkPath: string }\n | { action: 'skip'; agent: AgentDefinition; linkPath: string }\n\nexport type SkillLinkResult = {\n agent: AgentDefinition\n linkPath: string\n status: 'created' | 'existing'\n}\n\nexport type CreateSkillResult = {\n skillName: string\n canonicalPath: string\n skillFilePath: string\n links: SkillLinkResult[]\n}\n\nexport function toSkillSlug(value: string): string {\n return value\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n}\n\nexport async function createSkill(input: CreateSkillInput): Promise<CreateSkillResult> {\n const parsed = skillInputSchema.parse(input)\n const skillName = toSkillSlug(parsed.name)\n\n if (!skillName) {\n throw new Error('Skill name must include at least one letter or number.')\n }\n\n const agents = dedupeAgents(input.agents)\n const canonicalPath = getCanonicalSkillPath(input.location, skillName)\n const skillFilePath = path.join(canonicalPath, 'SKILL.md')\n const canonicalExisted = await fs.pathExists(canonicalPath)\n\n await ensureCanonicalSkillCanBeCreated(canonicalPath)\n const linkPlans = await planSkillSymlinks(agents, canonicalPath, skillName)\n\n await fs.ensureDir(canonicalPath)\n await writeSkillFile(skillFilePath, parsed.name, parsed.description)\n\n const links: SkillLinkResult[] = []\n const createdLinkPaths: string[] = []\n\n try {\n for (const linkPlan of linkPlans) {\n const result = await applySkillSymlink(linkPlan, canonicalPath)\n\n if (result.status === 'created') {\n createdLinkPaths.push(result.linkPath)\n }\n\n links.push(result)\n }\n } catch (error) {\n await rollbackCreatedPaths({\n canonicalExisted,\n canonicalPath,\n createdLinkPaths,\n skillFilePath,\n })\n\n throw error\n }\n\n return {\n skillName,\n canonicalPath,\n skillFilePath,\n links,\n }\n}\n\nfunction dedupeAgents(agents: AgentDefinition[]): AgentDefinition[] {\n const seen = new Set<AgentDefinition['id']>()\n\n return agents.filter((agent) => {\n if (seen.has(agent.id)) {\n return false\n }\n\n seen.add(agent.id)\n return true\n })\n}\n\nasync function writeSkillFile(\n skillFilePath: string,\n name: string,\n description: string,\n): Promise<void> {\n await fs\n .writeFile(skillFilePath, renderSkillMarkdown(name, description), {\n flag: 'wx',\n })\n .catch((error: unknown) => {\n if (isNodeError(error) && error.code === 'EEXIST') {\n throw new Error(`A canonical skill already exists at ${skillFilePath}`)\n }\n\n throw error\n })\n}\n\nasync function ensureCanonicalSkillCanBeCreated(canonicalPath: string): Promise<void> {\n await ensureDirectoryPathCanBeCreated(path.dirname(canonicalPath))\n\n const existingCanonicalPath = await lstatOrNull(canonicalPath)\n\n if (existingCanonicalPath && !existingCanonicalPath.isDirectory()) {\n throw new Error(`Cannot create skill because ${canonicalPath} already exists.`)\n }\n\n if (existingCanonicalPath) {\n const contents = await fs.readdir(canonicalPath)\n\n if (contents.length > 0 && !contents.includes('SKILL.md')) {\n throw new Error(\n `Cannot create skill because ${canonicalPath} already exists and is not empty.`,\n )\n }\n }\n}\n\nfunction renderSkillMarkdown(name: string, description: string): string {\n return `---\nname: ${toSkillSlug(name)}\ndescription: ${JSON.stringify(description.trim())}\n---\n\n# ${name.trim()}\n\n${description.trim()}\n`\n}\n\nasync function planSkillSymlinks(\n agents: AgentDefinition[],\n canonicalPath: string,\n skillName: string,\n): Promise<SkillLinkPlan[]> {\n return Promise.all(\n agents.map(async (agent) => {\n const linkPath = path.join(agent.skillDirectory, skillName)\n await ensureDirectoryPathCanBeCreated(path.dirname(linkPath))\n\n const existing = await lstatOrNull(linkPath)\n\n if (!existing) {\n return {\n agent,\n linkPath,\n action: 'link' as const,\n }\n }\n\n if (!existing.isSymbolicLink()) {\n throw new Error(\n `Cannot link skill because ${linkPath} already exists and is not a symlink.`,\n )\n }\n\n const target = await fs.readlink(linkPath)\n const resolvedTarget = path.resolve(path.dirname(linkPath), target)\n\n if (toComparablePath(resolvedTarget) !== toComparablePath(canonicalPath)) {\n throw new Error(`Cannot link skill because ${linkPath} points to ${resolvedTarget}.`)\n }\n\n return {\n agent,\n linkPath,\n action: 'skip' as const,\n }\n }),\n )\n}\n\nasync function ensureDirectoryPathCanBeCreated(directoryPath: string): Promise<void> {\n const existing = await lstatOrNull(directoryPath)\n\n if (existing) {\n if (!existing.isDirectory()) {\n throw new Error(\n `Cannot create directory because ${directoryPath} already exists and is not a directory.`,\n )\n }\n\n return\n }\n\n const parentPath = path.dirname(directoryPath)\n\n if (parentPath === directoryPath) {\n return\n }\n\n await ensureDirectoryPathCanBeCreated(parentPath)\n}\n\nasync function applySkillSymlink(\n plan: SkillLinkPlan,\n canonicalPath: string,\n): Promise<SkillLinkResult> {\n if (plan.action === 'skip') {\n return {\n agent: plan.agent,\n linkPath: plan.linkPath,\n status: 'existing',\n }\n }\n\n await fs.ensureDir(path.dirname(plan.linkPath))\n const linkTarget = path.relative(path.dirname(plan.linkPath), canonicalPath)\n await fs.symlink(linkTarget, plan.linkPath, 'dir')\n\n return {\n agent: plan.agent,\n linkPath: plan.linkPath,\n status: 'created',\n }\n}\n\nasync function rollbackCreatedPaths(options: {\n canonicalExisted: boolean\n canonicalPath: string\n createdLinkPaths: string[]\n skillFilePath: string\n}): Promise<void> {\n await Promise.all(options.createdLinkPaths.map((linkPath) => fs.remove(linkPath)))\n\n if (options.canonicalExisted) {\n await fs.remove(options.skillFilePath)\n return\n }\n\n await fs.remove(options.canonicalPath)\n}\n","import { intro, isCancel, multiselect, note, outro, select, spinner, text } from '@clack/prompts'\nimport { Command } from 'commander'\nimport { agentIdSchema, getAgentsById, supportedAgents } from '@/lib/agents.js'\nimport { createSkill } from '@/lib/skill.js'\nimport { canonicalSkillLocationSchema } from '@/lib/paths.js'\n\nfunction promptOrExit<T>(value: T | symbol): T {\n if (isCancel(value)) {\n outro('Cancelled')\n process.exit(0)\n }\n\n return value\n}\n\nfunction requireNonEmpty(message: string) {\n return (value?: string) => {\n if (!(value ?? '').trim()) {\n return message\n }\n return undefined\n }\n}\n\nexport function createCommand(): Command {\n const command = new Command('create')\n\n command.description('Create a shared skill and link it into agent skill directories')\n\n command.action(async () => {\n intro('Create a shared agent skill')\n\n const name = promptOrExit(\n await text({\n message: 'Skill name',\n placeholder: 'review-helper',\n validate: requireNonEmpty('Enter a skill name.'),\n }),\n )\n\n const description = promptOrExit(\n await text({\n message: 'Skill description',\n placeholder: 'Helps agents review code consistently.',\n validate: requireNonEmpty('Enter a skill description.'),\n }),\n )\n\n const location = promptOrExit(\n await select({\n message: 'Where should the canonical skill live?',\n options: [\n {\n value: 'project',\n label: 'Current project',\n hint: './.agents/skills/<skill-name>',\n },\n {\n value: 'home',\n label: 'Home directory',\n hint: '~/.agents/skills/<skill-name>',\n },\n ],\n }),\n )\n\n const selectedAgentIds = promptOrExit(\n await multiselect({\n message: 'Which agents should use this skill?',\n options: supportedAgents.map((agent) => ({\n value: agent.id,\n label: agent.label,\n })),\n required: true,\n }),\n )\n\n const canonicalLocation = canonicalSkillLocationSchema.parse(location)\n const parsedAgentIds = agentIdSchema.array().parse(selectedAgentIds)\n const agents = getAgentsById(parsedAgentIds, canonicalLocation)\n const activity = spinner()\n\n activity.start('Creating canonical skill and symlinks')\n\n try {\n const result = await createSkill({\n name,\n description,\n location: canonicalLocation,\n agents,\n })\n\n activity.stop('Skill created')\n\n note(\n [\n `Canonical: ${result.canonicalPath}`,\n ...result.links.map((link) => `${link.agent.label}: ${link.linkPath} (${link.status})`),\n ].join('\\n'),\n result.skillName,\n )\n\n outro('Done')\n } catch (error) {\n activity.stop('Could not create skill')\n outro(error instanceof Error ? error.message : 'Unknown error')\n process.exit(1)\n }\n })\n\n return command\n}\n","#!/usr/bin/env node\n\nimport { Command } from 'commander'\nimport { createCommand } from '@/commands/create.js'\n\nconst program = new Command()\n\nprogram.name('pouch').description('Create and link shared agent skills').version('0.1.0')\n\nprogram.addCommand(createCommand())\n\nawait program.parseAsync()\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@michaeltroya/pouch",
3
+ "version": "0.1.0",
4
+ "description": "Create and link shared agent skills",
5
+ "keywords": [
6
+ "agents",
7
+ "ai",
8
+ "claude",
9
+ "cli",
10
+ "codex",
11
+ "cursor",
12
+ "skills"
13
+ ],
14
+ "homepage": "https://github.com/michaeltroya/pouch#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/michaeltroya/pouch/issues"
17
+ },
18
+ "license": "MIT",
19
+ "author": "Michael Troya",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/michaeltroya/pouch.git"
23
+ },
24
+ "bin": {
25
+ "pouch": "./dist/index.mjs"
26
+ },
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "type": "module",
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "scripts": {
35
+ "dev": "tsx src/index.ts",
36
+ "build": "vp pack",
37
+ "check": "vp check",
38
+ "check:fix": "vp check --fix",
39
+ "format": "vp fmt",
40
+ "lint": "vp lint",
41
+ "prepack": "vp pack",
42
+ "test": "vp test",
43
+ "prepare": "vp config"
44
+ },
45
+ "dependencies": {
46
+ "@clack/prompts": "^1.5.1",
47
+ "commander": "^15.0.0",
48
+ "fs-extra": "^11.3.5",
49
+ "zod": "^4.4.3"
50
+ },
51
+ "devDependencies": {
52
+ "@types/fs-extra": "^11.0.4",
53
+ "@types/node": "^25.9.3",
54
+ "oxfmt": "^0.54.0",
55
+ "oxlint": "^1.69.0",
56
+ "tsx": "^4.22.4",
57
+ "typescript": "^6.0.3",
58
+ "vite-plus": "catalog:",
59
+ "vitest": "catalog:"
60
+ },
61
+ "engines": {
62
+ "node": ">=22.13.0"
63
+ },
64
+ "packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620"
65
+ }