@djordje-stojanovic/sigmaskills 0.2.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/LICENSE +21 -0
  3. package/README.md +342 -0
  4. package/bin/sigmaskills.js +7 -0
  5. package/manifest.json +28 -0
  6. package/package.json +42 -0
  7. package/registry/agent-hosts.json +2404 -0
  8. package/registry/schema.json +110 -0
  9. package/registry/skill-baselines.json +4 -0
  10. package/registry/source.json +6 -0
  11. package/sigmabrief/SKILL.md +56 -0
  12. package/sigmabrief/agents/openai.yaml +12 -0
  13. package/sigmabrief/references/brief-method.md +73 -0
  14. package/sigmabrief/references/prompt-contract.md +174 -0
  15. package/sigmaperformance/SKILL.md +118 -0
  16. package/sigmaperformance/agents/openai.yaml +12 -0
  17. package/sigmaperformance/references/audit-method.md +112 -0
  18. package/sigmaperformance/references/calibration.md +45 -0
  19. package/sigmaperformance/references/report-contract.md +103 -0
  20. package/sigmareview/SKILL.md +133 -0
  21. package/sigmareview/agents/openai.yaml +12 -0
  22. package/sigmareview/references/report-contract.md +217 -0
  23. package/sigmareview/references/review-method.md +233 -0
  24. package/sigmawrite/SKILL.md +45 -0
  25. package/sigmawrite/agents/openai.yaml +12 -0
  26. package/src/adoption.js +370 -0
  27. package/src/backup.js +398 -0
  28. package/src/catalog.js +211 -0
  29. package/src/cli.js +657 -0
  30. package/src/customization.js +344 -0
  31. package/src/destinations.js +491 -0
  32. package/src/interactive.js +959 -0
  33. package/src/links.js +157 -0
  34. package/src/plan.js +429 -0
  35. package/src/prepack.js +10 -0
  36. package/src/project-lock.js +169 -0
  37. package/src/purge.js +477 -0
  38. package/src/registry/automation-ci.js +411 -0
  39. package/src/registry/automation.js +554 -0
  40. package/src/registry/diff.js +149 -0
  41. package/src/registry/normalize.js +67 -0
  42. package/src/registry/parse.js +184 -0
  43. package/src/registry/sync.js +230 -0
  44. package/src/registry/validate.js +223 -0
  45. package/src/release-ci.js +12 -0
  46. package/src/release.js +837 -0
  47. package/src/restore.js +518 -0
  48. package/src/revision.js +82 -0
  49. package/src/state.js +480 -0
  50. package/src/status.js +469 -0
  51. package/src/transaction.js +636 -0
  52. package/src/uninstall.js +647 -0
  53. package/src/update.js +815 -0
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * normalize.js — turn parsed upstream host records into the canonical,
5
+ * versioned, attributed registry snapshot consumed by the installer.
6
+ *
7
+ * Only declarative data survives normalization. Detection function bodies
8
+ * are reduced to env-var metadata; destination formulas are kept as safe
9
+ * declarative records (kind: literal | join | conditional | none). Nothing
10
+ * is evaluated. Hosts are sorted bytewise by id so the emitted JSON is
11
+ * reproducible regardless of host locale or ICU settings.
12
+ */
13
+
14
+ /**
15
+ * Bytewise (code-point) id comparator, locale-independent. Guarantees the
16
+ * snapshot ordering is identical on every machine and Node build.
17
+ */
18
+ export function compareIds(a, b) {
19
+ return a < b ? -1 : a > b ? 1 : 0;
20
+ }
21
+
22
+ /**
23
+ * Normalize a parsed host record into a canonical registry entry.
24
+ * Adds attribution (upstream file + line) and a stable normalized shape.
25
+ */
26
+ export function normalizeHost(host, source) {
27
+ return {
28
+ id: host.id,
29
+ name: host.name,
30
+ displayName: host.displayName,
31
+ universal: host.universal,
32
+ universalPrompt: host.universalPrompt,
33
+ destinations: {
34
+ project: host.project ?? { kind: 'none', raw: undefined },
35
+ global: host.global ?? { kind: 'none', raw: undefined },
36
+ },
37
+ aliases: host.aliases ?? [],
38
+ platforms: host.platforms ?? [],
39
+ detection: {
40
+ envVars: host.envVars ?? [],
41
+ },
42
+ attribution: {
43
+ upstreamFile: source.upstreamFile ?? source.file,
44
+ upstreamLine: host.line,
45
+ },
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Build the canonical snapshot container.
51
+ * @param {Array} hosts normalized host records
52
+ * @param {object} source pin record { repository, pinnedRevision, upstreamFile, fetchedAt? }
53
+ */
54
+ export function buildSnapshot(hosts, source) {
55
+ const sorted = [...hosts].sort((a, b) => compareIds(a.id, b.id));
56
+ const generatedFrom = {
57
+ repository: source.repository,
58
+ pinnedRevision: source.pinnedRevision ?? source.revision,
59
+ upstreamFile: source.upstreamFile ?? source.file,
60
+ };
61
+ if (source.fetchedAt) generatedFrom.fetchedAt = source.fetchedAt;
62
+ return {
63
+ schemaVersion: 1,
64
+ generatedFrom,
65
+ hosts: sorted,
66
+ };
67
+ }
@@ -0,0 +1,184 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * parse.js — extract declarative Agent Host data from upstream registry text.
5
+ *
6
+ * Sigma never executes upstream TypeScript, detector, or path-building code.
7
+ * This parser reads upstream `agents.ts` as untrusted text and extracts only
8
+ * literal, declarative fields: host ids, display names, project/global
9
+ * destination formulas, aliases, platform declarations, and env-var
10
+ * detection metadata. Conditional helpers are extracted as declarative
11
+ * case lists (probe + formula), never evaluated.
12
+ */
13
+
14
+ /** Env-var names referenced in a text fragment (metadata, never evaluated). */
15
+ export function extractEnvVars(text) {
16
+ const names = new Set();
17
+ const re = /(?:env|process\.env)\.([A-Z][A-Z0-9_]*)/g;
18
+ let m;
19
+ while ((m = re.exec(text)) !== null) names.add(m[1]);
20
+ return [...names].sort();
21
+ }
22
+
23
+ /** Capture the value of a single-line field like ` skillsDir: '...',`. */
24
+ function field(body, name) {
25
+ const re = new RegExp('^\\s*' + name + '\\s*:\\s*([^\\n]+)', 'm');
26
+ const m = re.exec(body);
27
+ if (!m) return null;
28
+ let value = m[1].trim();
29
+ value = value.replace(/,$/, '').trim();
30
+ return value;
31
+ }
32
+
33
+ /** Unquote a single-quoted upstream literal. Returns null when malformed. */
34
+ function quoted(value) {
35
+ const m = /^'((?:[^'\\]|\\.)*)'$/.exec(value.trim());
36
+ return m ? m[1] : null;
37
+ }
38
+
39
+ const JOIN_RE = /^join\(([A-Za-z0-9_.]+)((?:,\s*'((?:[^'\\]|\\.)*)')*)\)$/;
40
+
41
+ /** Parse a destination formula. Forms: join(...) | '<path>' | undefined | <fn>(). */
42
+ export function parseDirFormula(value) {
43
+ const raw = value.trim();
44
+ if (!raw) return null;
45
+ if (raw === 'undefined') return { kind: 'none', raw };
46
+
47
+ const quotedPath = quoted(raw);
48
+ if (quotedPath !== null) return { kind: 'literal', path: quotedPath, raw };
49
+
50
+ const join = JOIN_RE.exec(raw);
51
+ if (join) {
52
+ const segmentsRe = /'((?:[^'\\]|\\.)*)'/g;
53
+ const segments = [];
54
+ let sm;
55
+ while ((sm = segmentsRe.exec(raw)) !== null) segments.push(sm[1]);
56
+ return { kind: 'join', base: join[1], segments, raw };
57
+ }
58
+
59
+ const fnRe = /^([A-Za-z0-9_]+)\(\)$/;
60
+ const fn = fnRe.exec(raw);
61
+ if (fn) return { kind: 'function', name: fn[1], raw };
62
+
63
+ return { kind: 'unknown', raw };
64
+ }
65
+
66
+ const PROBE_RE = /(?:existsSync|pathExists)\(join\(([A-Za-z0-9_.]+)\s*,\s*'([^']*)'\)\)/g;
67
+ const RETURN_RE = /return\s+(join\([^;]*\)|[^;\n]+);/g;
68
+
69
+ /**
70
+ * Declaratively extract a conditional helper `function <name>(...) { ... }`.
71
+ * Returns { kind: 'conditional', base, cases: [{ probe, formula }] } where
72
+ * cases are (probe, return-join) pairs and the final return is the default.
73
+ * Guards and returns are parsed as text; nothing is evaluated.
74
+ */
75
+ export function parseConditionalHelper(sourceText, name) {
76
+ const anchor = 'function ' + name;
77
+ let idx = sourceText.indexOf(anchor);
78
+ if (idx === -1) return null;
79
+ let p = sourceText.indexOf('(', idx + anchor.length);
80
+ if (p === -1) return null;
81
+ let depth = 1;
82
+ let q = p + 1;
83
+ for (; q < sourceText.length; q++) {
84
+ const ch = sourceText[q];
85
+ if (ch === '(') depth++;
86
+ else if (ch === ')') { depth--; if (depth === 0) break; }
87
+ }
88
+ if (depth !== 0) return null;
89
+ let brace = sourceText.indexOf('{', q);
90
+ if (brace === -1) return null;
91
+ const bodyStart = brace + 1;
92
+ let braceDepth = 1;
93
+ let i = bodyStart;
94
+ for (; i < sourceText.length; i++) {
95
+ const ch = sourceText[i];
96
+ if (ch === '{') braceDepth++;
97
+ else if (ch === '}') {
98
+ braceDepth--;
99
+ if (braceDepth === 0) break;
100
+ }
101
+ }
102
+ const body = sourceText.slice(bodyStart, i);
103
+
104
+ const probes = [];
105
+ let pm;
106
+ while ((pm = PROBE_RE.exec(body)) !== null) {
107
+ probes.push({ index: pm.index, end: pm.index + pm[0].length, probe: pm[2] });
108
+ }
109
+
110
+ const cases = [];
111
+ let rm;
112
+ while ((rm = RETURN_RE.exec(body)) !== null) {
113
+ let probe = null;
114
+ for (const p of probes) {
115
+ if (p.end > rm.index) break;
116
+ const between = body.slice(p.end, rm.index);
117
+ if (!between.includes('}')) probe = p.probe;
118
+ }
119
+ const formula = parseDirFormula(rm[1]);
120
+ cases.push({ probe, formula });
121
+ }
122
+ if (cases.length === 0) return null;
123
+ const first = cases.find((c) => c.formula && c.formula.base);
124
+ const base = first ? first.formula.base : null;
125
+ return { kind: 'conditional', base, cases };
126
+ }
127
+
128
+ /** Boolean value of an upstream literal boolean field. */
129
+ function bool(value) {
130
+ if (value === null) return null;
131
+ if (value === 'true') return true;
132
+ if (value === 'false') return false;
133
+ return null;
134
+ }
135
+
136
+ /** Parse one host entry body into a declarative record. */
137
+ function parseHostBody(id, body, line, sourceText) {
138
+ const name = quoted(field(body, 'name')) ?? id;
139
+ const displayName = quoted(field(body, 'displayName')) ?? name;
140
+ const project = parseDirFormula(field(body, 'skillsDir'));
141
+ const globalRaw = field(body, 'globalSkillsDir');
142
+ let global = parseDirFormula(globalRaw);
143
+ if (global && global.kind === 'function') {
144
+ const conditional = parseConditionalHelper(sourceText, global.name);
145
+ if (conditional) global = conditional;
146
+ }
147
+ const aliasesRaw = field(body, 'aliases');
148
+ const aliases = aliasesRaw ? aliasesRaw.replace(/^\[|\]$/g, '').split(',').map((s) => s.trim()) : [];
149
+ const platformsRaw = field(body, 'platforms');
150
+ const platforms = platformsRaw ? platformsRaw.replace(/^\[|\]$/g, '').split(',').map((s) => s.trim()) : [];
151
+ const showInUniversalList = bool(field(body, 'showInUniversalList'));
152
+ const showInUniversalPrompt = bool(field(body, 'showInUniversalPrompt'));
153
+ const envVars = extractEnvVars(body);
154
+
155
+ return {
156
+ id,
157
+ name,
158
+ displayName,
159
+ project,
160
+ global,
161
+ aliases,
162
+ platforms,
163
+ universal: showInUniversalList !== false,
164
+ universalPrompt: showInUniversalPrompt !== false,
165
+ envVars,
166
+ line,
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Parse upstream agents.ts text into declarative host records.
172
+ */
173
+ export function parseAgents(agentsText) {
174
+ const hosts = [];
175
+ const entryRe = /^ '?([A-Za-z0-9_-]+)'?: \{([\s\S]*?)\n \},$/gm;
176
+ let m;
177
+ while ((m = entryRe.exec(agentsText)) !== null) {
178
+ const id = m[1];
179
+ const body = m[2];
180
+ const line = 1 + agentsText.slice(0, m.index).split('\n').length;
181
+ hosts.push(parseHostBody(id, body, line, agentsText));
182
+ }
183
+ return hosts;
184
+ }
@@ -0,0 +1,230 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * sync.js — deterministic registry synchronization command.
5
+ *
6
+ * node src/registry/sync.js [--fetch] [--dry-run] [--allow-review]
7
+ *
8
+ * Reads upstream `agents.ts` (the pinned revision recorded in
9
+ * `registry/source.json`), verifies its content hash against the recorded
10
+ * pin, parses only declarative fields, normalizes into the canonical
11
+ * attributed snapshot, validates, and emits a normalized semantic diff
12
+ * against the existing snapshot. Unless --dry-run, writes the snapshot only
13
+ * when there are no review-classified changes, or when --allow-review is
14
+ * passed. `--fetch` downloads the pinned revision from the upstream
15
+ * repository and saves it as the local pinned fixture before syncing.
16
+ *
17
+ * The default pin is the real immutable upstream commit recorded in
18
+ * `registry/source.json`; a pseudo-pin is rejected so a release can never
19
+ * masquerade as synchronized from upstream.
20
+ */
21
+
22
+ import crypto from 'node:crypto';
23
+ import fs from 'node:fs';
24
+ import path from 'node:path';
25
+ import { fileURLToPath } from 'node:url';
26
+ import { parseAgents } from './parse.js';
27
+ import { normalizeHost, buildSnapshot } from './normalize.js';
28
+ import { validateSnapshot } from './validate.js';
29
+ import { diffSnapshots } from './diff.js';
30
+
31
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
32
+ const ROOT = path.resolve(__dirname, '..', '..');
33
+ const REGISTRY_DIR = path.join(ROOT, 'registry');
34
+ const FIXTURE_PATH = path.join(ROOT, 'test', 'fixtures', 'vercel-skills', 'src', 'agents.ts');
35
+ const PIN_PATH = path.join(REGISTRY_DIR, 'source.json');
36
+ const SNAPSHOT_PATH = path.join(REGISTRY_DIR, 'agent-hosts.json');
37
+
38
+ const REVISION_PATTERN = /^[0-9a-f]{40}$/;
39
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/;
40
+
41
+ function sha256(text) {
42
+ return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
43
+ }
44
+
45
+ export function canonicalSourceText(text) {
46
+ return String(text || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
47
+ }
48
+
49
+ function parseArgs(argv) {
50
+ const args = { fetch: false, dryRun: false, allowReview: false, source: null };
51
+ for (let i = 0; i < argv.length; i++) {
52
+ const arg = argv[i];
53
+ if (arg === '--dry-run') args.dryRun = true;
54
+ else if (arg === '--allow-review') args.allowReview = true;
55
+ else if (arg === '--fetch') args.fetch = true;
56
+ else if (arg === '--source' && i + 1 < argv.length) args.source = argv[++i];
57
+ }
58
+ return args;
59
+ }
60
+
61
+ function readJson(file) {
62
+ if (!fs.existsSync(file)) return null;
63
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
64
+ }
65
+
66
+ function writeJson(file, value) {
67
+ fs.mkdirSync(path.dirname(file), { recursive: true });
68
+ fs.writeFileSync(file, JSON.stringify(value, null, 2) + '\n', 'utf8');
69
+ }
70
+
71
+ /**
72
+ * Load the pinned source record and reject pseudo-pins.
73
+ * @returns {object} pin { repository, pinnedRevision, upstreamFile, contentSha256 }
74
+ */
75
+ export function loadPin(pinPath = PIN_PATH) {
76
+ const pin = readJson(pinPath);
77
+ if (!pin || !pin.pinnedRevision || !REVISION_PATTERN.test(pin.pinnedRevision)) {
78
+ throw new Error(
79
+ `registry/source.json must pin an immutable 40-hex upstream revision; got ${JSON.stringify(pin && pin.pinnedRevision)}`
80
+ );
81
+ }
82
+ if (typeof pin.repository !== 'string' || pin.repository.length === 0) {
83
+ throw new Error('registry/source.json must record the upstream repository URL');
84
+ }
85
+ if (!pin.upstreamFile || !pin.contentSha256 || !SHA256_PATTERN.test(pin.contentSha256)) {
86
+ throw new Error('registry/source.json must record the upstream file path and contentSha256');
87
+ }
88
+ return pin;
89
+ }
90
+
91
+ /**
92
+ * Fetch the pinned upstream file from the repository at the pinned revision
93
+ * and save it as the local pinned fixture. Verifies the content hash.
94
+ * @param {object} pin loaded pin record
95
+ * @param {string} [fixturePath] where to save the fetched content
96
+ */
97
+ export async function fetchPinnedSource(pin, fixturePath = FIXTURE_PATH) {
98
+ const url = `https://raw.githubusercontent.com/${pin.repository}/${pin.pinnedRevision}/${pin.upstreamFile}`;
99
+ let res;
100
+ try {
101
+ res = await fetch(url);
102
+ } catch (err) {
103
+ throw new Error(`failed to fetch ${url}: ${err.message}`);
104
+ }
105
+ if (!res.ok) {
106
+ throw new Error(`failed to fetch ${url}: HTTP ${res.status}`);
107
+ }
108
+ const text = canonicalSourceText(await res.text());
109
+ const digest = sha256(text);
110
+ if (digest !== pin.contentSha256) {
111
+ throw new Error(
112
+ `fetched content hash ${digest} does not match pinned contentSha256 ${pin.contentSha256} for ${pin.pinnedRevision}`
113
+ );
114
+ }
115
+ fs.mkdirSync(path.dirname(fixturePath), { recursive: true });
116
+ fs.writeFileSync(fixturePath, text, 'utf8');
117
+ return text;
118
+ }
119
+
120
+ /**
121
+ * Synchronize the registry from the pinned upstream source.
122
+ *
123
+ * @param {object} args { fetch, dryRun, allowReview, source? }
124
+ * @param {object} [options] { pin?, previous?, fixturePath? }
125
+ * @returns {object} { ok, snapshot, validation, diff, shouldWrite, errors? }
126
+ */
127
+ export function syncRegistry(args, options = {}) {
128
+ const pin = options.pin ?? loadPin();
129
+ const fixturePath = options.fixturePath ?? FIXTURE_PATH;
130
+ const sourcePath = args.source ?? fixturePath;
131
+
132
+ let sourceText;
133
+ if (args.fetch) {
134
+ sourceText = fetchPinnedSourceSync(pin, fixturePath);
135
+ } else {
136
+ sourceText = canonicalSourceText(fs.readFileSync(sourcePath, 'utf8'));
137
+ const digest = sha256(sourceText);
138
+ if (digest !== pin.contentSha256) {
139
+ return {
140
+ ok: false,
141
+ errors: [
142
+ `source content hash ${digest} does not match pinned contentSha256 ${pin.contentSha256}` +
143
+ ` (source ${sourcePath}); re-fetch with --fetch or update registry/source.json`,
144
+ ],
145
+ };
146
+ }
147
+ }
148
+
149
+ const parsed = parseAgents(sourceText);
150
+ const hosts = parsed.map((h) => normalizeHost(h, pin));
151
+ const snapshot = buildSnapshot(hosts, pin);
152
+
153
+ const validation = validateSnapshot(snapshot);
154
+ if (!validation.valid) {
155
+ return {
156
+ ok: false,
157
+ errors: [...validation.errors, ...Object.values(validation.hostErrors)],
158
+ };
159
+ }
160
+
161
+ const previous = options.previous ?? readJson(SNAPSHOT_PATH);
162
+ const diff = previous ? diffSnapshots(previous, snapshot) : null;
163
+ const reviewCount = diff ? diff.summary.review : 0;
164
+
165
+ const shouldWrite = !args.dryRun && (args.allowReview || reviewCount === 0);
166
+
167
+ if (shouldWrite) {
168
+ writeJson(SNAPSHOT_PATH, snapshot);
169
+ writeJson(PIN_PATH, pin);
170
+ }
171
+
172
+ return {
173
+ ok: true,
174
+ snapshot,
175
+ validation,
176
+ diff,
177
+ shouldWrite,
178
+ };
179
+ }
180
+
181
+ // Small sync wrapper so syncRegistry stays pure for tests.
182
+ function fetchPinnedSourceSync(pin, fixturePath) {
183
+ const url = `https://raw.githubusercontent.com/${pin.repository}/${pin.pinnedRevision}/${pin.upstreamFile}`;
184
+ throw new Error(`sync --fetch requires async fetch; use runSync() instead (${url})`);
185
+ }
186
+
187
+ /**
188
+ * Async entrypoint used by the CLI. Fetches when requested, then syncs.
189
+ * @param {object} args CLI args
190
+ * @returns {Promise<object>} result of syncRegistry
191
+ */
192
+ export async function runSync(args, options = {}) {
193
+ const pin = options.pin ?? loadPin();
194
+ if (args.fetch) {
195
+ await fetchPinnedSource(pin, options.fixturePath ?? FIXTURE_PATH);
196
+ }
197
+ return syncRegistry({ ...args, fetch: false }, options);
198
+ }
199
+
200
+ function main() {
201
+ const args = parseArgs(process.argv.slice(2));
202
+ runSync(args).then((result) => {
203
+ if (!result.ok) {
204
+ for (const err of result.errors) console.error('validation error:', err);
205
+ process.exit(1);
206
+ }
207
+ console.log(`sync: parsed ${result.snapshot.hosts.length} hosts from pinned revision ${loadPin().pinnedRevision}`);
208
+ if (result.diff) {
209
+ console.log('diff summary:', JSON.stringify(result.diff.summary));
210
+ for (const change of result.diff.changes) {
211
+ console.log(` ${change.authority.padEnd(6)} ${change.id} ${change.kind}` +
212
+ (change.fields ? ' [' + change.fields.join(', ') + ']' : ''));
213
+ }
214
+ }
215
+ if (result.shouldWrite) {
216
+ console.log('wrote registry/agent-hosts.json + registry/source.json');
217
+ } else if (args.dryRun) {
218
+ console.log('dry run: no files written');
219
+ } else {
220
+ console.log('review-classified changes present; registry not written (use --allow-review to force)');
221
+ }
222
+ }).catch((err) => {
223
+ console.error('error:', err.message);
224
+ process.exit(1);
225
+ });
226
+ }
227
+
228
+ if (process.argv[1] && path.basename(process.argv[1]) === 'sync.js') {
229
+ main();
230
+ }