@zoralabs/cli 1.2.0 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zoralabs/cli",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Zora CLI tool",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Generates SHA-256 integrity hashes for all skills.
3
+ *
4
+ * Usage:
5
+ * npx tsx scripts/generate-skill-hashes.ts # Print hashes to console
6
+ * npx tsx scripts/generate-skill-hashes.ts --write # Update skills.ts directly
7
+ * npx tsx scripts/generate-skill-hashes.ts --check # Check if hashes are up-to-date (CI)
8
+ */
9
+
10
+ import { createHash } from "node:crypto";
11
+ import { readFileSync, writeFileSync } from "node:fs";
12
+ import { dirname, join } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const SKILLS_TS_PATH = join(__dirname, "../src/commands/skills.ts");
17
+
18
+ const SKILLS_URL = "https://agents.zora.com/skill";
19
+
20
+ const computeIntegrity = (content: string): string => {
21
+ const hash = createHash("sha256").update(content, "utf8").digest("base64");
22
+ return `sha256-${hash}`;
23
+ };
24
+
25
+ /**
26
+ * Parse skill names from skills.ts to avoid import dependency chain.
27
+ * Single source of truth - names are extracted from the SKILLS array.
28
+ */
29
+ function parseSkillNamesFromFile(): string[] {
30
+ const content = readFileSync(SKILLS_TS_PATH, "utf8");
31
+ const names: string[] = [];
32
+
33
+ // Match all name: "skillname" patterns within the SKILLS array
34
+ const namePattern = /name:\s*"([^"]+)"/g;
35
+ let match;
36
+ while ((match = namePattern.exec(content)) !== null) {
37
+ names.push(match[1]);
38
+ }
39
+
40
+ if (names.length === 0) {
41
+ throw new Error("No skill names found in skills.ts");
42
+ }
43
+
44
+ return names;
45
+ }
46
+
47
+ /**
48
+ * Parse current hashes from skills.ts
49
+ */
50
+ function getCurrentHashes(): Map<string, string> {
51
+ const content = readFileSync(SKILLS_TS_PATH, "utf8");
52
+ const hashes = new Map<string, string>();
53
+ const skillNames = parseSkillNamesFromFile();
54
+
55
+ for (const name of skillNames) {
56
+ // Use lazy matching to find the integrity field for each skill
57
+ const pattern = new RegExp(
58
+ `name:\\s*"${name}"[\\s\\S]*?integrity:\\s*"([^"]*)"`,
59
+ );
60
+ const match = content.match(pattern);
61
+ if (match) {
62
+ hashes.set(name, match[1]);
63
+ }
64
+ }
65
+
66
+ return hashes;
67
+ }
68
+
69
+ async function fetchAllHashes(): Promise<Map<string, string>> {
70
+ const hashes = new Map<string, string>();
71
+ const skillNames = parseSkillNamesFromFile();
72
+
73
+ for (const name of skillNames) {
74
+ const url = `${SKILLS_URL}/${name}.md`;
75
+ const response = await fetch(url);
76
+ if (!response.ok) {
77
+ throw new Error(`Failed to fetch ${name}: HTTP ${response.status}`);
78
+ }
79
+ const content = await response.text();
80
+ hashes.set(name, computeIntegrity(content));
81
+ }
82
+
83
+ return hashes;
84
+ }
85
+
86
+ function updateSkillsFile(hashes: Map<string, string>): boolean {
87
+ const content = readFileSync(SKILLS_TS_PATH, "utf8");
88
+ let updated = content;
89
+ let changed = false;
90
+ const errors: string[] = [];
91
+
92
+ for (const [name, hash] of hashes) {
93
+ // First, verify this skill has an integrity field by checking
94
+ // that we can find it within its own object block (before the next skill)
95
+ // Find the position of this skill's name
96
+ const namePattern = new RegExp(`name:\\s*"${name}"`);
97
+ const nameMatch = namePattern.exec(updated);
98
+ if (!nameMatch) {
99
+ errors.push(`Skill "${name}" not found in skills.ts`);
100
+ continue;
101
+ }
102
+
103
+ const namePos = nameMatch.index;
104
+
105
+ // Find the next skill's name (if any) to bound our search
106
+ const remainingContent = updated.slice(namePos + nameMatch[0].length);
107
+ const nextSkillMatch = /name:\s*"[^"]+"/.exec(remainingContent);
108
+ const searchBound = nextSkillMatch
109
+ ? namePos + nameMatch[0].length + nextSkillMatch.index
110
+ : updated.length;
111
+
112
+ // Extract the bounded region for this skill
113
+ const skillRegion = updated.slice(namePos, searchBound);
114
+
115
+ // Check if integrity field exists in this skill's region
116
+ const integrityInRegion = /integrity:\s*"[^"]*"/.exec(skillRegion);
117
+ if (!integrityInRegion) {
118
+ errors.push(
119
+ `Skill "${name}" is missing integrity field - add 'integrity: "sha256-PLACEHOLDER"' to the skill definition`,
120
+ );
121
+ continue;
122
+ }
123
+
124
+ // Now safely replace the integrity value within the bounded region
125
+ const updatedRegion = skillRegion.replace(
126
+ /(integrity:\s*)"[^"]*"/,
127
+ `$1"${hash}"`,
128
+ );
129
+
130
+ if (updatedRegion !== skillRegion) {
131
+ changed = true;
132
+ updated = updated.slice(0, namePos) + updatedRegion + updated.slice(searchBound);
133
+ }
134
+ }
135
+
136
+ if (errors.length > 0) {
137
+ console.error("\nErrors found:");
138
+ for (const err of errors) {
139
+ console.error(` - ${err}`);
140
+ }
141
+ process.exit(1);
142
+ }
143
+
144
+ if (changed) {
145
+ writeFileSync(SKILLS_TS_PATH, updated);
146
+ }
147
+
148
+ return changed;
149
+ }
150
+
151
+ async function main() {
152
+ const args = process.argv.slice(2);
153
+ const writeMode = args.includes("--write");
154
+ const checkMode = args.includes("--check");
155
+
156
+ console.log("Fetching skills from production...\n");
157
+
158
+ let remoteHashes: Map<string, string>;
159
+ try {
160
+ remoteHashes = await fetchAllHashes();
161
+ } catch (err) {
162
+ console.error("Failed to fetch skills:", err);
163
+ process.exit(1);
164
+ }
165
+
166
+ if (checkMode) {
167
+ const currentHashes = getCurrentHashes();
168
+ let hasChanges = false;
169
+
170
+ for (const [name, remoteHash] of remoteHashes) {
171
+ const currentHash = currentHashes.get(name);
172
+ if (currentHash !== remoteHash) {
173
+ console.log(`${name}: CHANGED`);
174
+ console.log(` Current: ${currentHash}`);
175
+ console.log(` Expected: ${remoteHash}\n`);
176
+ hasChanges = true;
177
+ }
178
+ }
179
+
180
+ if (hasChanges) {
181
+ console.log("Skill hashes are out of date. Run with --write to update.");
182
+ process.exit(1);
183
+ } else {
184
+ console.log("All skill hashes are up to date.");
185
+ process.exit(0);
186
+ }
187
+ }
188
+
189
+ if (writeMode) {
190
+ const changed = updateSkillsFile(remoteHashes);
191
+ if (changed) {
192
+ console.log("Updated skills.ts with new hashes:");
193
+ for (const [name, hash] of remoteHashes) {
194
+ console.log(` ${name}: ${hash}`);
195
+ }
196
+ } else {
197
+ console.log("No changes needed - hashes are already up to date.");
198
+ }
199
+ } else {
200
+ console.log("Generated hashes (run with --write to update skills.ts):\n");
201
+ for (const [name, hash] of remoteHashes) {
202
+ console.log(` ${name}: ${hash}`);
203
+ }
204
+ }
205
+ }
206
+
207
+ main().catch((err) => {
208
+ console.error("Fatal error:", err);
209
+ process.exit(1);
210
+ });