@prave/shared 1.2.2 → 1.4.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.
@@ -0,0 +1,54 @@
1
+ import type { AgentType } from '../types/intelligence.js';
2
+ /**
3
+ * compileSkill — convert a single canonical `SKILL.md` body into the
4
+ * exact file shape each supported agent expects.
5
+ *
6
+ * Why this lives in `@prave/shared`:
7
+ * - The CLI's `prave deploy` writes the converted file straight to
8
+ * the agent's local directory.
9
+ * - The SaaS `/dashboard/compile` page bundles converted files into
10
+ * a zip download.
11
+ * - Both need byte-identical output, otherwise users see drift
12
+ * between "what `prave deploy` puts on disk" and "what the
13
+ * compile page generates". Putting the converters in a shared
14
+ * package is the only way to guarantee that.
15
+ *
16
+ * Per-agent conversion rules (current 6 supported, others stubbed):
17
+ *
18
+ * claude / cline / amp / gemini / codex
19
+ * Identity transform. The canonical SKILL.md format IS the
20
+ * Anthropic skill format; these agents either use it natively or
21
+ * accept it via a `~/.<agent>/skills/<slug>/SKILL.md` directory
22
+ * convention. Returned `relPath`s mirror that layout.
23
+ *
24
+ * cursor
25
+ * Cursor's rules format is `.cursor/rules/<slug>.mdc` with
26
+ * frontmatter `globs:` instead of `triggers:`. We rename the key
27
+ * when frontmatter is present; when absent, we synthesize a
28
+ * minimal frontmatter block so the .mdc file parses cleanly. The
29
+ * body is left untouched.
30
+ *
31
+ * The `path` returned is a **POSIX-style relative path** rooted at the
32
+ * agent's install dir. The CLI joins it onto the expanded `basePath`
33
+ * (e.g. `~/.claude/skills/`); the web zip uses it as the in-archive
34
+ * path. Never absolute, never platform-specific separators — that's
35
+ * the caller's job.
36
+ */
37
+ export interface CompiledArtifact {
38
+ agent: AgentType;
39
+ /** Relative path within the agent's install root. POSIX separators. */
40
+ path: string;
41
+ /** File body to write at `path`. UTF-8 text. */
42
+ content: string;
43
+ /**
44
+ * `true` when the body was rewritten (frontmatter rename, key
45
+ * synthesis, format mutation). UI surfaces this as a small
46
+ * "converted" badge so users know the agent gets a non-canonical
47
+ * shape. `false` for identity-transformed agents.
48
+ */
49
+ converted: boolean;
50
+ }
51
+ export declare function compileSkill(content: string, slug: string, agent: AgentType): CompiledArtifact;
52
+ /** Bundle helper — compile a skill for many agents in one pass. */
53
+ export declare function compileSkillBundle(content: string, slug: string, agents: ReadonlyArray<AgentType>): CompiledArtifact[];
54
+ //# sourceMappingURL=compile-skill.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compile-skill.d.ts","sourceRoot":"","sources":["../../src/lib/compile-skill.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AAEzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,SAAS,CAAA;IAChB,uEAAuE;IACvE,IAAI,EAAE,MAAM,CAAA;IACZ,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAA;IACf;;;;;OAKG;IACH,SAAS,EAAE,OAAO,CAAA;CACnB;AAED,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,GACf,gBAAgB,CAWlB;AAED,mEAAmE;AACnE,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,CAAC,SAAS,CAAC,GAC/B,gBAAgB,EAAE,CAEpB"}
@@ -0,0 +1,52 @@
1
+ export function compileSkill(content, slug, agent) {
2
+ switch (agent) {
3
+ case 'cursor':
4
+ return compileCursor(content, slug);
5
+ case 'claude':
6
+ case 'codex':
7
+ case 'gemini':
8
+ case 'cline':
9
+ case 'amp':
10
+ return compileIdentity(content, slug, agent);
11
+ }
12
+ }
13
+ /** Bundle helper — compile a skill for many agents in one pass. */
14
+ export function compileSkillBundle(content, slug, agents) {
15
+ return agents.map((a) => compileSkill(content, slug, a));
16
+ }
17
+ /* ─── per-agent converters ─────────────────────────────────────── */
18
+ function compileIdentity(content, slug, agent) {
19
+ return {
20
+ agent,
21
+ path: `${slug}/SKILL.md`,
22
+ content,
23
+ converted: false,
24
+ };
25
+ }
26
+ function compileCursor(content, slug) {
27
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?/);
28
+ let out;
29
+ if (fmMatch && fmMatch[1] !== undefined) {
30
+ const body = content.slice(fmMatch[0].length);
31
+ // Cursor's spec uses `globs:` not `triggers:`. Other fields
32
+ // (name, description, tags) pass through unchanged. We rename
33
+ // line-anchored so a `triggers:` substring inside an example
34
+ // code block in the body isn't accidentally rewritten.
35
+ const fmInner = fmMatch[1].replace(/(^|\n)triggers:/g, '$1globs:');
36
+ out = `---\n${fmInner}\n---\n${body}`;
37
+ }
38
+ else {
39
+ // No frontmatter at all — synthesize a minimal one from the slug
40
+ // and the first non-blank line of the body. Cursor refuses .mdc
41
+ // files without frontmatter, so this is the difference between
42
+ // a working rule and a broken one.
43
+ const firstLine = content.trim().replace(/\s+/g, ' ').slice(0, 200);
44
+ out = `---\nname: ${slug}\ndescription: ${firstLine}\n---\n${content}`;
45
+ }
46
+ return {
47
+ agent: 'cursor',
48
+ path: `.cursor/rules/${slug}.mdc`,
49
+ content: out,
50
+ converted: true,
51
+ };
52
+ }
@@ -1,2 +1,4 @@
1
1
  export * from './classify.js';
2
+ export * from './compile-skill.js';
3
+ export * from './secret-scanner.js';
2
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAA;AAC7B,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA"}
package/dist/lib/index.js CHANGED
@@ -1 +1,3 @@
1
1
  export * from './classify.js';
2
+ export * from './compile-skill.js';
3
+ export * from './secret-scanner.js';
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Secret-scanner — shared pre-insert gate for skill bundles.
3
+ *
4
+ * Two layers:
5
+ *
6
+ * 1. **Path patterns** — file names / paths that are categorically
7
+ * forbidden in a public-facing skill bundle. Hits are an immediate
8
+ * reject, no content peek needed.
9
+ *
10
+ * 2. **Content regexes** — high-confidence credential shapes that
11
+ * stand out even inside a text body. We only run these against
12
+ * files that look like text (we don't want to false-match a key
13
+ * shape inside a compressed binary blob).
14
+ *
15
+ * The scanner is called from two places:
16
+ *
17
+ * • `apps/worker/src/jobs/scan-github-repo.job.ts` — silent reject,
18
+ * bundle is marked 'rejected' and never surfaces in the catalogue.
19
+ * • `apps/api/src/services/bundle-storage.service.ts` — surfaces the
20
+ * finding back to the user so they can scrub the file and re-deploy.
21
+ *
22
+ * Same code, two failure modes. Keep this file dependency-free so the
23
+ * worker (Node) and the API (Node) and a future web-side validator
24
+ * (browser) can all run it without bundling friction.
25
+ */
26
+ export interface SecretFinding {
27
+ /** Path of the offending file within the bundle. */
28
+ path: string;
29
+ /** Short identifier of the rule that fired (`env-file`, `aws-key`, …). */
30
+ rule: string;
31
+ /**
32
+ * 1-indexed line where the match was found, when a content regex
33
+ * fired. Omitted for path-based rejects.
34
+ */
35
+ line?: number;
36
+ }
37
+ export interface SecretScanInput {
38
+ /** File path relative to the bundle root, e.g. `scripts/post.ts`. */
39
+ path: string;
40
+ /** File content as a UTF-8 string. NULL/undefined for binary files. */
41
+ content?: string | null;
42
+ }
43
+ export interface SecretScanResult {
44
+ /** Final verdict. `clean` means the bundle is safe to expose / run. */
45
+ status: 'clean' | 'rejected';
46
+ /** Every triggered rule. Empty when `status === 'clean'`. */
47
+ findings: SecretFinding[];
48
+ }
49
+ /**
50
+ * Run both layers across a list of files. Returns a verdict + every
51
+ * matching rule across every file. Empty findings = clean bundle.
52
+ *
53
+ * Files where `content` is missing skip the content-regex stage but
54
+ * still trip path rules.
55
+ */
56
+ export declare function scanForSecrets(files: SecretScanInput[]): SecretScanResult;
57
+ /**
58
+ * Whether a path likely points at a text-readable file (worth running
59
+ * content regexes against). Used by callers to filter the input list
60
+ * before they bother loading binary blobs.
61
+ */
62
+ export declare function isLikelyTextPath(path: string): boolean;
63
+ //# sourceMappingURL=secret-scanner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret-scanner.d.ts","sourceRoot":"","sources":["../../src/lib/secret-scanner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAA;IACZ,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAA;IACZ,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,uEAAuE;IACvE,MAAM,EAAE,OAAO,GAAG,UAAU,CAAA;IAC5B,6DAA6D;IAC7D,QAAQ,EAAE,aAAa,EAAE,CAAA;CAC1B;AAwGD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,gBAAgB,CAmCzE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAStD"}
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Secret-scanner — shared pre-insert gate for skill bundles.
3
+ *
4
+ * Two layers:
5
+ *
6
+ * 1. **Path patterns** — file names / paths that are categorically
7
+ * forbidden in a public-facing skill bundle. Hits are an immediate
8
+ * reject, no content peek needed.
9
+ *
10
+ * 2. **Content regexes** — high-confidence credential shapes that
11
+ * stand out even inside a text body. We only run these against
12
+ * files that look like text (we don't want to false-match a key
13
+ * shape inside a compressed binary blob).
14
+ *
15
+ * The scanner is called from two places:
16
+ *
17
+ * • `apps/worker/src/jobs/scan-github-repo.job.ts` — silent reject,
18
+ * bundle is marked 'rejected' and never surfaces in the catalogue.
19
+ * • `apps/api/src/services/bundle-storage.service.ts` — surfaces the
20
+ * finding back to the user so they can scrub the file and re-deploy.
21
+ *
22
+ * Same code, two failure modes. Keep this file dependency-free so the
23
+ * worker (Node) and the API (Node) and a future web-side validator
24
+ * (browser) can all run it without bundling friction.
25
+ */
26
+ // ── Path patterns ────────────────────────────────────────────────────
27
+ // Matched against the lowercased file path. Anchored at the basename
28
+ // where it makes sense (`.env`) and as substring elsewhere (`secrets/`).
29
+ const PATH_RULES = [
30
+ {
31
+ rule: 'env-file',
32
+ test: (p) => {
33
+ const base = basename(p);
34
+ // .env, .env.local, .env.production, .envrc — but NOT .env.example
35
+ // or .env.sample (templates are encouraged).
36
+ if (base === '.env' || base.startsWith('.env.')) {
37
+ return !/\.(example|sample|template)$/.test(base);
38
+ }
39
+ if (base === '.envrc')
40
+ return true;
41
+ return false;
42
+ },
43
+ },
44
+ {
45
+ rule: 'private-key',
46
+ test: (p) => /\.(pem|p12|pfx|key|jks)$/i.test(p),
47
+ },
48
+ {
49
+ rule: 'ssh-private-key',
50
+ test: (p) => {
51
+ const base = basename(p);
52
+ return /^id_(rsa|ecdsa|ed25519|dsa)$/.test(base);
53
+ },
54
+ },
55
+ {
56
+ rule: 'gcp-service-account',
57
+ test: (p) => {
58
+ const base = basename(p).toLowerCase();
59
+ return (base === 'service-account.json' ||
60
+ /^service-account[-_].*\.json$/.test(base) ||
61
+ base === 'gcp-key.json' ||
62
+ base === 'firebase-adminsdk.json');
63
+ },
64
+ },
65
+ {
66
+ rule: 'aws-credentials',
67
+ test: (p) => {
68
+ const lower = p.toLowerCase();
69
+ return (lower.endsWith('/.aws/credentials') ||
70
+ lower === '.aws/credentials' ||
71
+ lower.endsWith('aws-credentials.json'));
72
+ },
73
+ },
74
+ {
75
+ rule: 'credentials-file',
76
+ test: (p) => {
77
+ const base = basename(p).toLowerCase();
78
+ return (base === 'credentials.json' ||
79
+ base === 'secrets.json' ||
80
+ base === 'secret.json');
81
+ },
82
+ },
83
+ {
84
+ rule: 'secrets-dir',
85
+ test: (p) => /(^|\/)secrets\//i.test(p),
86
+ },
87
+ ];
88
+ // ── Content regexes ──────────────────────────────────────────────────
89
+ // High-confidence shapes only — false positives mean a frustrated user
90
+ // re-uploading after a scrub they couldn't validate. Keep the bar high.
91
+ const CONTENT_RULES = [
92
+ // Anthropic
93
+ { rule: 'anthropic-api-key', regex: /\bsk-ant-[a-zA-Z0-9-_]{20,}\b/ },
94
+ // OpenAI
95
+ { rule: 'openai-api-key', regex: /\bsk-(?:proj-)?[a-zA-Z0-9-_]{32,}\b/ },
96
+ // AWS
97
+ { rule: 'aws-access-key-id', regex: /\bAKIA[0-9A-Z]{16}\b/ },
98
+ { rule: 'aws-secret-access-key', regex: /aws_secret_access_key\s*=\s*['"]?[A-Za-z0-9/+=]{40}\b/i },
99
+ // GitHub
100
+ { rule: 'github-personal-token', regex: /\bghp_[A-Za-z0-9]{36}\b/ },
101
+ { rule: 'github-oauth-token', regex: /\bgho_[A-Za-z0-9]{36}\b/ },
102
+ { rule: 'github-fine-grained-token', regex: /\bgithub_pat_[A-Za-z0-9_]{82}\b/ },
103
+ // Stripe
104
+ { rule: 'stripe-live-key', regex: /\bsk_live_[A-Za-z0-9]{24,}\b/ },
105
+ { rule: 'stripe-restricted-key', regex: /\brk_live_[A-Za-z0-9]{24,}\b/ },
106
+ // Google API key
107
+ { rule: 'google-api-key', regex: /\bAIza[0-9A-Za-z\-_]{35}\b/ },
108
+ // Slack
109
+ { rule: 'slack-bot-token', regex: /\bxoxb-[A-Za-z0-9-]{20,}\b/ },
110
+ { rule: 'slack-user-token', regex: /\bxoxp-[A-Za-z0-9-]{20,}\b/ },
111
+ // GitLab
112
+ { rule: 'gitlab-personal-token', regex: /\bglpat-[A-Za-z0-9_-]{20}\b/ },
113
+ // Supabase / JWT-shaped service-role key (the shape, not the host).
114
+ // Has 3 base64url segments separated by '.'. We require an `eyJ`
115
+ // header (RFC 7519 typ:"JWT") to dodge random base64 strings.
116
+ {
117
+ rule: 'jwt-token',
118
+ regex: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/,
119
+ },
120
+ ];
121
+ /**
122
+ * Run both layers across a list of files. Returns a verdict + every
123
+ * matching rule across every file. Empty findings = clean bundle.
124
+ *
125
+ * Files where `content` is missing skip the content-regex stage but
126
+ * still trip path rules.
127
+ */
128
+ export function scanForSecrets(files) {
129
+ const findings = [];
130
+ for (const file of files) {
131
+ // Path-based rules
132
+ for (const { rule, test } of PATH_RULES) {
133
+ if (test(file.path)) {
134
+ findings.push({ path: file.path, rule });
135
+ }
136
+ }
137
+ // Content-based rules — only on text. We could be smarter (e.g.
138
+ // skip files >1MB) but the worker already caps bundle size and
139
+ // skips known-binary extensions before getting here.
140
+ if (typeof file.content === 'string' && file.content.length > 0) {
141
+ // Split once for line numbers; the regex tests run against the
142
+ // full body so multiline shapes still match.
143
+ const lines = file.content.split('\n');
144
+ for (const { rule, regex } of CONTENT_RULES) {
145
+ // Reset stateful regexes — we don't use /g but be defensive.
146
+ const localRegex = new RegExp(regex.source, regex.flags.replace('g', ''));
147
+ for (let i = 0; i < lines.length; i++) {
148
+ if (localRegex.test(lines[i] ?? '')) {
149
+ findings.push({ path: file.path, rule, line: i + 1 });
150
+ break;
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+ return {
157
+ status: findings.length > 0 ? 'rejected' : 'clean',
158
+ findings,
159
+ };
160
+ }
161
+ /**
162
+ * Whether a path likely points at a text-readable file (worth running
163
+ * content regexes against). Used by callers to filter the input list
164
+ * before they bother loading binary blobs.
165
+ */
166
+ export function isLikelyTextPath(path) {
167
+ const ext = path.toLowerCase().split('.').pop() ?? '';
168
+ return (TEXT_EXTENSIONS.has(ext) ||
169
+ // Files without an extension that we'd still want to scan
170
+ path.endsWith('Dockerfile') ||
171
+ path.endsWith('Makefile') ||
172
+ /\.env(\.|$)/.test(path));
173
+ }
174
+ const TEXT_EXTENSIONS = new Set([
175
+ 'md',
176
+ 'mdx',
177
+ 'txt',
178
+ 'json',
179
+ 'yaml',
180
+ 'yml',
181
+ 'toml',
182
+ 'ini',
183
+ 'cfg',
184
+ 'conf',
185
+ 'env',
186
+ 'js',
187
+ 'jsx',
188
+ 'ts',
189
+ 'tsx',
190
+ 'mjs',
191
+ 'cjs',
192
+ 'py',
193
+ 'rb',
194
+ 'go',
195
+ 'rs',
196
+ 'sh',
197
+ 'bash',
198
+ 'zsh',
199
+ 'fish',
200
+ 'sql',
201
+ 'graphql',
202
+ 'gql',
203
+ 'html',
204
+ 'xml',
205
+ 'svg',
206
+ 'css',
207
+ 'scss',
208
+ 'less',
209
+ 'lock',
210
+ ]);
211
+ function basename(p) {
212
+ const i = p.lastIndexOf('/');
213
+ return i === -1 ? p : p.slice(i + 1);
214
+ }
@@ -14,4 +14,5 @@ export * from './skill-pr.schema.js';
14
14
  export * from './intelligence.schema.js';
15
15
  export * from './api-keys.schema.js';
16
16
  export * from './skill-report.schema.js';
17
+ export * from './run.schema.js';
17
18
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schemas/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAA;AACxC,cAAc,mBAAmB,CAAA;AACjC,cAAc,2BAA2B,CAAA;AACzC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,wBAAwB,CAAA;AACtC,cAAc,yBAAyB,CAAA;AACvC,cAAc,qBAAqB,CAAA;AACnC,cAAc,qBAAqB,CAAA;AACnC,cAAc,qBAAqB,CAAA;AACnC,cAAc,sBAAsB,CAAA;AACpC,cAAc,0BAA0B,CAAA;AACxC,cAAc,sBAAsB,CAAA;AACpC,cAAc,0BAA0B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schemas/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAA;AACxC,cAAc,mBAAmB,CAAA;AACjC,cAAc,2BAA2B,CAAA;AACzC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,wBAAwB,CAAA;AACtC,cAAc,yBAAyB,CAAA;AACvC,cAAc,qBAAqB,CAAA;AACnC,cAAc,qBAAqB,CAAA;AACnC,cAAc,qBAAqB,CAAA;AACnC,cAAc,sBAAsB,CAAA;AACpC,cAAc,0BAA0B,CAAA;AACxC,cAAc,sBAAsB,CAAA;AACpC,cAAc,0BAA0B,CAAA;AACxC,cAAc,iBAAiB,CAAA"}
@@ -14,3 +14,4 @@ export * from './skill-pr.schema.js';
14
14
  export * from './intelligence.schema.js';
15
15
  export * from './api-keys.schema.js';
16
16
  export * from './skill-report.schema.js';
17
+ export * from './run.schema.js';