@projitect/blueprint-skills 0.0.0-stage → 1.0.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 Kapil Easwar
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.
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @projitect/blueprint-skills
3
+ *
4
+ * Installs agent skills — a `SKILL.md` plus any referenced scripts and assets — into a repo-local
5
+ * skills directory. Each file is written in owned mode, so projitect rewrites it on `remodel`,
6
+ * flags edits as drift in `inspect`, and removes files a newer package version stops shipping.
7
+ *
8
+ * Layout: `<target>/<skill-name>/<relative path>`, with `target` defaulting to `.claude/skills`.
9
+ * Writes are permission-scoped to `<target>/**`; sibling skills and user files are never touched.
10
+ * Removing a skill prunes only that skill's own directory tree, never `<target>` or its ancestors.
11
+ */
12
+ import type { Blueprint } from "@projitect/core";
13
+ /** One skill: a single-segment `name` and its files, keyed by `/`-separated relative path. */
14
+ export interface Skill {
15
+ readonly name: string;
16
+ /** Must contain `SKILL.md`. Other keys may be nested, e.g. `scripts/run.sh`. */
17
+ readonly files: Readonly<Record<string, string>>;
18
+ }
19
+ export interface SkillsOptions {
20
+ readonly skills: readonly Skill[];
21
+ /** Skills root, relative to the project. Defaults to `".claude/skills"`. */
22
+ readonly target?: string;
23
+ /** Stable, namespaced owner id such as `"acme:skills"`. Every skill package needs its own. */
24
+ readonly id: string;
25
+ /** The skill package's own version; recorded in the lockfile so upgrades are tracked. */
26
+ readonly version: string;
27
+ }
28
+ /**
29
+ * Build the skills blueprint. Emits one owned-file operation per skill file, all attributed to
30
+ * the blueprint `id`.
31
+ */
32
+ export declare const skills: (options: SkillsOptions) => Blueprint.Blueprint;
33
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAMhD,8FAA8F;AAC9F,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CACjD;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAA;IACjC,4EAA4E;IAC5E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,yFAAyF;IACzF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAgFD;;;GAGG;AACH,eAAO,MAAM,MAAM,GAAI,SAAS,aAAa,KAAG,SAAS,CAAC,SA6BzD,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @projitect/blueprint-skills
3
+ *
4
+ * Installs agent skills — a `SKILL.md` plus any referenced scripts and assets — into a repo-local
5
+ * skills directory. Each file is written in owned mode, so projitect rewrites it on `remodel`,
6
+ * flags edits as drift in `inspect`, and removes files a newer package version stops shipping.
7
+ *
8
+ * Layout: `<target>/<skill-name>/<relative path>`, with `target` defaulting to `.claude/skills`.
9
+ * Writes are permission-scoped to `<target>/**`; sibling skills and user files are never touched.
10
+ * Removing a skill prunes only that skill's own directory tree, never `<target>` or its ancestors.
11
+ */
12
+ import { Array, Effect, Predicate, String } from "effect";
13
+ import { ownFile } from "@projitect/blueprint";
14
+ import { ChangeSet as CS, Errors } from "@projitect/core";
15
+ const DEFAULT_TARGET = ".claude/skills";
16
+ const SKILL_FILE = "SKILL.md";
17
+ /** Why a single path segment is unusable, or `undefined` when it is fine. */
18
+ const segmentProblem = (segment) => {
19
+ if (!String.isNonEmpty(segment)) {
20
+ return "must not be empty";
21
+ }
22
+ if (segment === "." || segment === "..") {
23
+ return "must not be a `.` or `..` segment";
24
+ }
25
+ if (segment.includes("\\")) {
26
+ return "must not contain a backslash";
27
+ }
28
+ if (segment.includes("\0")) {
29
+ return "must not contain a NUL character";
30
+ }
31
+ if (/^[A-Za-z]:/u.test(segment)) {
32
+ return "must not start with a drive letter";
33
+ }
34
+ return undefined;
35
+ };
36
+ /** A skill name is exactly one segment, so it additionally must not contain a separator. */
37
+ const nameProblem = (name) => name.includes("/") ? "must not contain a path separator" : segmentProblem(name);
38
+ const GLOB_METACHARACTERS = /[*?[\]{}()!]/u;
39
+ /** A target is a path whose segments are literal: glob metacharacters would widen `<target>/**`. */
40
+ const targetProblem = (target) => GLOB_METACHARACTERS.test(target)
41
+ ? "must not contain glob metacharacters (* ? [ ] { } ( ) !)"
42
+ : pathProblem(target);
43
+ /** First problem across the `/`-separated segments of a relative path, or `undefined`. */
44
+ const pathProblem = (value) => Array.findFirst(String.split(value, "/"), (segment) => Predicate.isNotUndefined(segmentProblem(segment))).pipe((found) => (found._tag === "Some" ? segmentProblem(found.value) : undefined));
45
+ const invalid = (field, value, problem) => new Errors.SkillInvalid({
46
+ id: "pjt.skills.invalid-path",
47
+ field,
48
+ value,
49
+ message: `Invalid skill ${field} ${JSON.stringify(value)}: ${problem}.`,
50
+ });
51
+ const validate = (options, target) => {
52
+ const names = Array.map(options.skills, (skill) => skill.name);
53
+ const failures = [
54
+ ...(options.id.includes(":") && String.isNonEmpty(options.id.replaceAll(":", ""))
55
+ ? []
56
+ : [invalid("id", options.id, "must be a namespaced id such as `acme:skills`")]),
57
+ ...(String.isNonEmpty(options.version)
58
+ ? []
59
+ : [invalid("version", options.version, "must be the skill package's version")]),
60
+ ...Array.fromNullishOr(targetProblem(target)).map((p) => invalid("target", target, p)),
61
+ ...Array.flatMap(options.skills, (skill) => [
62
+ ...Array.fromNullishOr(nameProblem(skill.name)).map((p) => invalid("name", skill.name, p)),
63
+ ...(Object.hasOwn(skill.files, SKILL_FILE)
64
+ ? []
65
+ : [invalid("skill", skill.name, `must include a ${SKILL_FILE} file`)]),
66
+ ...Array.flatMap(Object.keys(skill.files), (path) => Array.fromNullishOr(pathProblem(path)).map((p) => invalid("path", path, p))),
67
+ ]),
68
+ ...Array.flatMap(Array.dedupe(names), (name) => Array.filter(names, (n) => n === name).length > 1
69
+ ? [invalid("skill", name, "is declared more than once in this package")]
70
+ : []),
71
+ ];
72
+ return Array.head(failures);
73
+ };
74
+ /**
75
+ * Build the skills blueprint. Emits one owned-file operation per skill file, all attributed to
76
+ * the blueprint `id`.
77
+ */
78
+ export const skills = (options) => {
79
+ const { id, version } = options;
80
+ const target = options.target ?? DEFAULT_TARGET;
81
+ return {
82
+ id,
83
+ version,
84
+ description: `agent skills in ${target}`,
85
+ permissions: [{ kind: "write", glob: `${target}/**` }],
86
+ plan: Effect.gen(function* () {
87
+ const failure = validate(options, target);
88
+ if (failure._tag === "Some") {
89
+ return yield* failure.value;
90
+ }
91
+ const files = Array.flatMap(options.skills, (skill) => Array.map(Object.entries(skill.files), ([path, content]) => ownFile({
92
+ id,
93
+ version,
94
+ path: `${target}/${skill.name}/${path}`,
95
+ content,
96
+ ownedDir: `${target}/${skill.name}`,
97
+ })));
98
+ const changeSets = yield* Effect.forEach(files, (b) => b.plan);
99
+ return CS.Reducer.combineAll(changeSets);
100
+ }),
101
+ };
102
+ };
103
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
2
  "name": "@projitect/blueprint-skills",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0",
4
+ "description": "Agent skills blueprint for projitect — installs versioned skill packages (SKILL.md plus referenced files) into a repo-local skills directory, with drift detection.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/projitect/projitect.git",
8
+ "directory": "packages/blueprint-skills"
9
+ },
10
+ "license": "MIT",
11
+ "type": "module",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src",
21
+ "!**/*.tsbuildinfo"
22
+ ],
23
+ "devDependencies": {
24
+ "effect": "4.0.0-rc.111",
25
+ "typescript": "5.9.3",
26
+ "@projitect/blueprint": "1.0.0",
27
+ "@projitect/core": "1.0.0",
28
+ "@projitect/test-kit": "1.0.0",
29
+ "@projitect/cli-internals": "1.0.0"
30
+ },
31
+ "peerDependencies": {
32
+ "effect": "4.0.0-rc.111",
33
+ "@projitect/blueprint": "1.0.0",
34
+ "@projitect/core": "1.0.0"
35
+ },
36
+ "engines": {
37
+ "node": ">=22.12.0"
38
+ },
39
+ "projitect": {
40
+ "type": "blueprint",
41
+ "import": "import { skills } from \"@projitect/blueprint-skills\"",
42
+ "call": "skills({ skills: [] })"
43
+ },
44
+ "scripts": {
45
+ "build": "tsc --build",
46
+ "build:clean": "tsc --build --clean",
47
+ "tc": "tsc --build"
48
+ }
6
49
  }
package/src/index.ts ADDED
@@ -0,0 +1,149 @@
1
+ /**
2
+ * @projitect/blueprint-skills
3
+ *
4
+ * Installs agent skills — a `SKILL.md` plus any referenced scripts and assets — into a repo-local
5
+ * skills directory. Each file is written in owned mode, so projitect rewrites it on `remodel`,
6
+ * flags edits as drift in `inspect`, and removes files a newer package version stops shipping.
7
+ *
8
+ * Layout: `<target>/<skill-name>/<relative path>`, with `target` defaulting to `.claude/skills`.
9
+ * Writes are permission-scoped to `<target>/**`; sibling skills and user files are never touched.
10
+ * Removing a skill prunes only that skill's own directory tree, never `<target>` or its ancestors.
11
+ */
12
+
13
+ import { Array, Effect, Predicate, String } from "effect"
14
+ import { ownFile } from "@projitect/blueprint"
15
+ import type { Blueprint } from "@projitect/core"
16
+ import { ChangeSet as CS, Errors } from "@projitect/core"
17
+
18
+ const DEFAULT_TARGET = ".claude/skills"
19
+ const SKILL_FILE = "SKILL.md"
20
+
21
+ /** One skill: a single-segment `name` and its files, keyed by `/`-separated relative path. */
22
+ export interface Skill {
23
+ readonly name: string
24
+ /** Must contain `SKILL.md`. Other keys may be nested, e.g. `scripts/run.sh`. */
25
+ readonly files: Readonly<Record<string, string>>
26
+ }
27
+
28
+ export interface SkillsOptions {
29
+ readonly skills: readonly Skill[]
30
+ /** Skills root, relative to the project. Defaults to `".claude/skills"`. */
31
+ readonly target?: string
32
+ /** Stable, namespaced owner id such as `"acme:skills"`. Every skill package needs its own. */
33
+ readonly id: string
34
+ /** The skill package's own version; recorded in the lockfile so upgrades are tracked. */
35
+ readonly version: string
36
+ }
37
+
38
+ /** Why a single path segment is unusable, or `undefined` when it is fine. */
39
+ const segmentProblem = (segment: string): string | undefined => {
40
+ if (!String.isNonEmpty(segment)) {
41
+ return "must not be empty"
42
+ }
43
+ if (segment === "." || segment === "..") {
44
+ return "must not be a `.` or `..` segment"
45
+ }
46
+ if (segment.includes("\\")) {
47
+ return "must not contain a backslash"
48
+ }
49
+ if (segment.includes("\0")) {
50
+ return "must not contain a NUL character"
51
+ }
52
+ if (/^[A-Za-z]:/u.test(segment)) {
53
+ return "must not start with a drive letter"
54
+ }
55
+ return undefined
56
+ }
57
+
58
+ /** A skill name is exactly one segment, so it additionally must not contain a separator. */
59
+ const nameProblem = (name: string): string | undefined =>
60
+ name.includes("/") ? "must not contain a path separator" : segmentProblem(name)
61
+
62
+ const GLOB_METACHARACTERS = /[*?[\]{}()!]/u
63
+
64
+ /** A target is a path whose segments are literal: glob metacharacters would widen `<target>/**`. */
65
+ const targetProblem = (target: string): string | undefined =>
66
+ GLOB_METACHARACTERS.test(target)
67
+ ? "must not contain glob metacharacters (* ? [ ] { } ( ) !)"
68
+ : pathProblem(target)
69
+
70
+ /** First problem across the `/`-separated segments of a relative path, or `undefined`. */
71
+ const pathProblem = (value: string): string | undefined =>
72
+ Array.findFirst(String.split(value, "/"), (segment) =>
73
+ Predicate.isNotUndefined(segmentProblem(segment)),
74
+ ).pipe((found) => (found._tag === "Some" ? segmentProblem(found.value) : undefined))
75
+
76
+ const invalid = (
77
+ field: "target" | "name" | "path" | "skill" | "id" | "version",
78
+ value: string,
79
+ problem: string,
80
+ ) =>
81
+ new Errors.SkillInvalid({
82
+ id: "pjt.skills.invalid-path",
83
+ field,
84
+ value,
85
+ message: `Invalid skill ${field} ${JSON.stringify(value)}: ${problem}.`,
86
+ })
87
+
88
+ const validate = (options: SkillsOptions, target: string) => {
89
+ const names = Array.map(options.skills, (skill) => skill.name)
90
+ const failures = [
91
+ ...(options.id.includes(":") && String.isNonEmpty(options.id.replaceAll(":", ""))
92
+ ? []
93
+ : [invalid("id", options.id, "must be a namespaced id such as `acme:skills`")]),
94
+ ...(String.isNonEmpty(options.version)
95
+ ? []
96
+ : [invalid("version", options.version, "must be the skill package's version")]),
97
+ ...Array.fromNullishOr(targetProblem(target)).map((p) => invalid("target", target, p)),
98
+ ...Array.flatMap(options.skills, (skill) => [
99
+ ...Array.fromNullishOr(nameProblem(skill.name)).map((p) => invalid("name", skill.name, p)),
100
+ ...(Object.hasOwn(skill.files, SKILL_FILE)
101
+ ? []
102
+ : [invalid("skill", skill.name, `must include a ${SKILL_FILE} file`)]),
103
+ ...Array.flatMap(Object.keys(skill.files), (path) =>
104
+ Array.fromNullishOr(pathProblem(path)).map((p) => invalid("path", path, p)),
105
+ ),
106
+ ]),
107
+ ...Array.flatMap(Array.dedupe(names), (name) =>
108
+ Array.filter(names, (n) => n === name).length > 1
109
+ ? [invalid("skill", name, "is declared more than once in this package")]
110
+ : [],
111
+ ),
112
+ ]
113
+ return Array.head(failures)
114
+ }
115
+
116
+ /**
117
+ * Build the skills blueprint. Emits one owned-file operation per skill file, all attributed to
118
+ * the blueprint `id`.
119
+ */
120
+ export const skills = (options: SkillsOptions): Blueprint.Blueprint => {
121
+ const { id, version } = options
122
+ const target = options.target ?? DEFAULT_TARGET
123
+
124
+ return {
125
+ id,
126
+ version,
127
+ description: `agent skills in ${target}`,
128
+ permissions: [{ kind: "write", glob: `${target}/**` }],
129
+ plan: Effect.gen(function* () {
130
+ const failure = validate(options, target)
131
+ if (failure._tag === "Some") {
132
+ return yield* failure.value
133
+ }
134
+ const files = Array.flatMap(options.skills, (skill) =>
135
+ Array.map(Object.entries(skill.files), ([path, content]) =>
136
+ ownFile({
137
+ id,
138
+ version,
139
+ path: `${target}/${skill.name}/${path}`,
140
+ content,
141
+ ownedDir: `${target}/${skill.name}`,
142
+ }),
143
+ ),
144
+ )
145
+ const changeSets = yield* Effect.forEach(files, (b) => b.plan)
146
+ return CS.Reducer.combineAll(changeSets)
147
+ }),
148
+ }
149
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.