@opendirectory.dev/skills 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.
Files changed (212) hide show
  1. package/.claude/skills/claude-md-generator/.env.example +7 -0
  2. package/.claude/skills/claude-md-generator/README.md +78 -0
  3. package/.claude/skills/claude-md-generator/SKILL.md +248 -0
  4. package/.claude/skills/claude-md-generator/evals/evals.json +35 -0
  5. package/.claude/skills/claude-md-generator/references/section-guide.md +175 -0
  6. package/dist/e2e.test.d.ts +1 -0
  7. package/dist/e2e.test.js +62 -0
  8. package/dist/fs-adapters.d.ts +4 -0
  9. package/dist/fs-adapters.js +101 -0
  10. package/dist/fs-adapters.test.d.ts +1 -0
  11. package/dist/fs-adapters.test.js +108 -0
  12. package/dist/index.d.ts +2 -0
  13. package/dist/index.js +211 -0
  14. package/dist/transformers.d.ts +6 -0
  15. package/dist/transformers.js +2 -0
  16. package/package.json +25 -0
  17. package/registry.json +226 -0
  18. package/skills/blog-cover-image-cli/.github/workflows/publish.yml +19 -0
  19. package/skills/blog-cover-image-cli/LICENSE +15 -0
  20. package/skills/blog-cover-image-cli/README.md +126 -0
  21. package/skills/blog-cover-image-cli/SKILL.md +7 -0
  22. package/skills/blog-cover-image-cli/agent-skill/blog-cover-generator/README.md +30 -0
  23. package/skills/blog-cover-image-cli/agent-skill/blog-cover-generator/SKILL.md +72 -0
  24. package/skills/blog-cover-image-cli/bin/cli.js +226 -0
  25. package/skills/blog-cover-image-cli/examples/100x_UX_Research_AI_Agent.png +0 -0
  26. package/skills/blog-cover-image-cli/examples/Firecrawl-supabase-bolt.png +0 -0
  27. package/skills/blog-cover-image-cli/examples/Git-City_Case_study_Cover_Image.jpg +0 -0
  28. package/skills/blog-cover-image-cli/examples/THE DISTRIBUTION LAYER (2).png +0 -0
  29. package/skills/blog-cover-image-cli/examples/canva-perplexity-duolingo-cover-image.png +0 -0
  30. package/skills/blog-cover-image-cli/examples/gamma-mistral-veed.png +0 -0
  31. package/skills/blog-cover-image-cli/examples/server-survival-case-study-cover-image(1).png +0 -0
  32. package/skills/blog-cover-image-cli/examples/viral-meme-automation.png +0 -0
  33. package/skills/blog-cover-image-cli/index.js +2 -0
  34. package/skills/blog-cover-image-cli/package-lock.json +2238 -0
  35. package/skills/blog-cover-image-cli/package.json +37 -0
  36. package/skills/blog-cover-image-cli/src/geminiGenerator.js +126 -0
  37. package/skills/blog-cover-image-cli/src/imageValidator.js +54 -0
  38. package/skills/blog-cover-image-cli/src/logoFetcher.js +86 -0
  39. package/skills/claude-md-generator/.env.example +7 -0
  40. package/skills/claude-md-generator/README.md +78 -0
  41. package/skills/claude-md-generator/SKILL.md +254 -0
  42. package/skills/claude-md-generator/evals/evals.json +35 -0
  43. package/skills/claude-md-generator/references/section-guide.md +175 -0
  44. package/skills/cook-the-blog/README.md +86 -0
  45. package/skills/cook-the-blog/SKILL.md +130 -0
  46. package/skills/dependency-update-bot/.env.example +13 -0
  47. package/skills/dependency-update-bot/README.md +101 -0
  48. package/skills/dependency-update-bot/SKILL.md +376 -0
  49. package/skills/dependency-update-bot/evals/evals.json +45 -0
  50. package/skills/dependency-update-bot/references/changelog-patterns.md +201 -0
  51. package/skills/docs-from-code/.env.example +13 -0
  52. package/skills/docs-from-code/README.md +97 -0
  53. package/skills/docs-from-code/SKILL.md +160 -0
  54. package/skills/docs-from-code/evals/evals.json +29 -0
  55. package/skills/docs-from-code/references/extraction-guide.md +174 -0
  56. package/skills/docs-from-code/references/output-template.md +135 -0
  57. package/skills/docs-from-code/scripts/extract_py.py +238 -0
  58. package/skills/docs-from-code/scripts/extract_ts.ts +284 -0
  59. package/skills/docs-from-code/scripts/package.json +18 -0
  60. package/skills/explain-this-pr/README.md +74 -0
  61. package/skills/explain-this-pr/SKILL.md +130 -0
  62. package/skills/explain-this-pr/evals/evals.json +35 -0
  63. package/skills/google-trends-api-skills/README.md +78 -0
  64. package/skills/google-trends-api-skills/SKILL.md +7 -0
  65. package/skills/google-trends-api-skills/google-trends-api/SKILL.md +163 -0
  66. package/skills/google-trends-api-skills/google-trends-api/references/api-responses.md +188 -0
  67. package/skills/google-trends-api-skills/google-trends-api/scripts/discover_keywords.py +344 -0
  68. package/skills/google-trends-api-skills/seo-keyword-research/SKILL.md +205 -0
  69. package/skills/google-trends-api-skills/seo-keyword-research/references/keyword-placement-guide.md +89 -0
  70. package/skills/google-trends-api-skills/seo-keyword-research/references/tech-blog-examples.md +207 -0
  71. package/skills/google-trends-api-skills/seo-keyword-research/scripts/blog_seo_research.py +373 -0
  72. package/skills/hackernews-intel/.env.example +33 -0
  73. package/skills/hackernews-intel/README.md +161 -0
  74. package/skills/hackernews-intel/SKILL.md +156 -0
  75. package/skills/hackernews-intel/evals/evals.json +35 -0
  76. package/skills/hackernews-intel/package.json +15 -0
  77. package/skills/hackernews-intel/scripts/monitor-hn.js +258 -0
  78. package/skills/kill-the-standup/.env.example +22 -0
  79. package/skills/kill-the-standup/README.md +84 -0
  80. package/skills/kill-the-standup/SKILL.md +169 -0
  81. package/skills/kill-the-standup/evals/evals.json +35 -0
  82. package/skills/kill-the-standup/references/standup-format.md +102 -0
  83. package/skills/linkedin-post-generator/.env.example +14 -0
  84. package/skills/linkedin-post-generator/README.md +107 -0
  85. package/skills/linkedin-post-generator/SKILL.md +228 -0
  86. package/skills/linkedin-post-generator/evals/evals.json +35 -0
  87. package/skills/linkedin-post-generator/references/linkedin-format.md +216 -0
  88. package/skills/linkedin-post-generator/references/output-template.md +154 -0
  89. package/skills/llms-txt-generator/.env.example +18 -0
  90. package/skills/llms-txt-generator/README.md +142 -0
  91. package/skills/llms-txt-generator/SKILL.md +176 -0
  92. package/skills/llms-txt-generator/evals/evals.json +35 -0
  93. package/skills/llms-txt-generator/references/llms-txt-spec.md +88 -0
  94. package/skills/llms-txt-generator/references/output-template.md +76 -0
  95. package/skills/llms-txt-generator/test-output/genzcareer.in/llms.txt +31 -0
  96. package/skills/luma-attendees-scraper/README.md +170 -0
  97. package/skills/luma-attendees-scraper/SKILL.md +7 -0
  98. package/skills/luma-attendees-scraper/luma_attendees_export.js +223 -0
  99. package/skills/meeting-brief-generator/.env.example +21 -0
  100. package/skills/meeting-brief-generator/README.md +90 -0
  101. package/skills/meeting-brief-generator/SKILL.md +275 -0
  102. package/skills/meeting-brief-generator/evals/evals.json +35 -0
  103. package/skills/meeting-brief-generator/references/brief-format.md +114 -0
  104. package/skills/meeting-brief-generator/references/output-template.md +150 -0
  105. package/skills/meta-ads-skill/README.md +100 -0
  106. package/skills/meta-ads-skill/SKILL.md +7 -0
  107. package/skills/meta-ads-skill/meta-ads-skill/SKILL.md +41 -0
  108. package/skills/meta-ads-skill/meta-ads-skill/references/report_templates.md +47 -0
  109. package/skills/meta-ads-skill/meta-ads-skill/references/workflows.md +51 -0
  110. package/skills/meta-ads-skill/meta-ads-skill/scripts/auth_check.py +22 -0
  111. package/skills/meta-ads-skill/meta-ads-skill/scripts/formatters.py +46 -0
  112. package/skills/newsletter-digest/.env.example +20 -0
  113. package/skills/newsletter-digest/README.md +147 -0
  114. package/skills/newsletter-digest/SKILL.md +221 -0
  115. package/skills/newsletter-digest/evals/evals.json +35 -0
  116. package/skills/newsletter-digest/feeds.json +7 -0
  117. package/skills/newsletter-digest/package.json +15 -0
  118. package/skills/newsletter-digest/references/digest-format.md +123 -0
  119. package/skills/newsletter-digest/references/output-template.md +136 -0
  120. package/skills/newsletter-digest/scripts/fetch-feeds.js +141 -0
  121. package/skills/newsletter-digest/scripts/ghost-publish.js +147 -0
  122. package/skills/noise2blog/.env.example +16 -0
  123. package/skills/noise2blog/README.md +107 -0
  124. package/skills/noise2blog/SKILL.md +229 -0
  125. package/skills/noise2blog/evals/evals.json +35 -0
  126. package/skills/noise2blog/references/blog-format.md +188 -0
  127. package/skills/noise2blog/references/output-template.md +184 -0
  128. package/skills/outreach-sequence-builder/.env.example +12 -0
  129. package/skills/outreach-sequence-builder/README.md +108 -0
  130. package/skills/outreach-sequence-builder/SKILL.md +248 -0
  131. package/skills/outreach-sequence-builder/evals/evals.json +36 -0
  132. package/skills/outreach-sequence-builder/references/output-template.md +171 -0
  133. package/skills/outreach-sequence-builder/references/sequence-format.md +167 -0
  134. package/skills/outreach-sequence-builder/references/signal-playbook.md +117 -0
  135. package/skills/position-me/README.md +71 -0
  136. package/skills/position-me/SKILL.md +7 -0
  137. package/skills/position-me/position-me/SKILL.md +50 -0
  138. package/skills/position-me/position-me/references/EVALUATION_SOP.md +40 -0
  139. package/skills/position-me/position-me/references/REPORT_TEMPLATE.md +58 -0
  140. package/skills/position-me/position-me/scripts/extract_links.py +49 -0
  141. package/skills/pr-description-writer/README.md +81 -0
  142. package/skills/pr-description-writer/SKILL.md +141 -0
  143. package/skills/pr-description-writer/evals/evals.json +35 -0
  144. package/skills/pr-description-writer/references/pr-format-guide.md +145 -0
  145. package/skills/producthunt-launch-kit/.env.example +7 -0
  146. package/skills/producthunt-launch-kit/README.md +95 -0
  147. package/skills/producthunt-launch-kit/SKILL.md +380 -0
  148. package/skills/producthunt-launch-kit/evals/evals.json +35 -0
  149. package/skills/producthunt-launch-kit/references/copy-rules.md +124 -0
  150. package/skills/reddit-icp-monitor/.env.example +16 -0
  151. package/skills/reddit-icp-monitor/README.md +117 -0
  152. package/skills/reddit-icp-monitor/SKILL.md +271 -0
  153. package/skills/reddit-icp-monitor/evals/evals.json +40 -0
  154. package/skills/reddit-icp-monitor/references/icp-format.md +131 -0
  155. package/skills/reddit-icp-monitor/references/reply-rules.md +110 -0
  156. package/skills/reddit-post-engine/.env.example +13 -0
  157. package/skills/reddit-post-engine/README.md +103 -0
  158. package/skills/reddit-post-engine/SKILL.md +303 -0
  159. package/skills/reddit-post-engine/evals/evals.json +35 -0
  160. package/skills/reddit-post-engine/references/subreddit-playbook.md +156 -0
  161. package/skills/schema-markup-generator/.env.example +19 -0
  162. package/skills/schema-markup-generator/README.md +114 -0
  163. package/skills/schema-markup-generator/SKILL.md +192 -0
  164. package/skills/schema-markup-generator/evals/evals.json +35 -0
  165. package/skills/schema-markup-generator/references/json-ld-spec.md +263 -0
  166. package/skills/schema-markup-generator/references/output-template.md +556 -0
  167. package/skills/show-hn-writer/.env.example +14 -0
  168. package/skills/show-hn-writer/README.md +88 -0
  169. package/skills/show-hn-writer/SKILL.md +303 -0
  170. package/skills/show-hn-writer/evals/evals.json +35 -0
  171. package/skills/show-hn-writer/references/hn-rules.md +74 -0
  172. package/skills/show-hn-writer/references/title-formulas.md +93 -0
  173. package/skills/stargazer/README.md +79 -0
  174. package/skills/stargazer/SKILL.md +7 -0
  175. package/skills/stargazer/stargazer-skill/SKILL.md +58 -0
  176. package/skills/stargazer/stargazer-skill/assets/.env.example +18 -0
  177. package/skills/stargazer/stargazer-skill/scripts/convert_to_csv.py +63 -0
  178. package/skills/stargazer/stargazer-skill/scripts/count_emails.py +52 -0
  179. package/skills/stargazer/stargazer-skill/scripts/stargazer_deep_extractor.py +450 -0
  180. package/skills/tweet-thread-from-blog/.env.example +14 -0
  181. package/skills/tweet-thread-from-blog/README.md +109 -0
  182. package/skills/tweet-thread-from-blog/SKILL.md +177 -0
  183. package/skills/tweet-thread-from-blog/evals/evals.json +35 -0
  184. package/skills/tweet-thread-from-blog/references/output-template.md +193 -0
  185. package/skills/tweet-thread-from-blog/references/thread-format.md +107 -0
  186. package/skills/twitter-GTM-find-skill/README.md +43 -0
  187. package/skills/twitter-GTM-find-skill/SKILL.md +7 -0
  188. package/skills/twitter-GTM-find-skill/twitter-GTM-find/SKILL.md +37 -0
  189. package/skills/twitter-GTM-find-skill/twitter-GTM-find/references/icp-checklist.md +35 -0
  190. package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/package.json +23 -0
  191. package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/run_pipeline.sh +8 -0
  192. package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/src/debug.ts +23 -0
  193. package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/src/extractor.ts +79 -0
  194. package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/src/icp-filter.ts +87 -0
  195. package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/src/index.ts +94 -0
  196. package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/src/scraper.ts +41 -0
  197. package/skills/twitter-GTM-find-skill/twitter-GTM-find/scripts/tsconfig.json +13 -0
  198. package/skills/yc-intent-radar-skill/README.md +39 -0
  199. package/skills/yc-intent-radar-skill/SKILL.md +7 -0
  200. package/skills/yc-intent-radar-skill/yc-jobs-scraper/SKILL.md +59 -0
  201. package/skills/yc-intent-radar-skill/yc-jobs-scraper/scripts/auth.js +29 -0
  202. package/skills/yc-intent-radar-skill/yc-jobs-scraper/scripts/db.js +62 -0
  203. package/skills/yc-intent-radar-skill/yc-jobs-scraper/scripts/export_radar_candidates.js +40 -0
  204. package/skills/yc-intent-radar-skill/yc-jobs-scraper/scripts/package-lock.json +1525 -0
  205. package/skills/yc-intent-radar-skill/yc-jobs-scraper/scripts/package.json +12 -0
  206. package/skills/yc-intent-radar-skill/yc-jobs-scraper/scripts/scraper.js +217 -0
  207. package/src/e2e.test.ts +35 -0
  208. package/src/fs-adapters.test.ts +91 -0
  209. package/src/fs-adapters.ts +65 -0
  210. package/src/index.ts +182 -0
  211. package/src/transformers.ts +6 -0
  212. package/tsconfig.json +8 -0
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "yc-jobs-scraper",
3
+ "version": "1.0.0",
4
+ "description": "Scrapes Workatastartup and YCombinator for new jobs",
5
+ "main": "scraper.js",
6
+ "dependencies": {
7
+ "better-sqlite3": "^11.10.0",
8
+ "dotenv": "^16.4.7",
9
+ "playwright": "^1.50.1",
10
+ "sqlite3": "^5.1.7"
11
+ }
12
+ }
@@ -0,0 +1,217 @@
1
+ const { chromium } = require('playwright');
2
+ const fs = require('fs');
3
+ const db = require('./db');
4
+
5
+ const QUERY_URLS = [
6
+ 'https://www.workatastartup.com/companies?companySize=seed&demographic=any&hasEquity=any&hasSalary=any&industry=B2B%20Software%20and%20Services&interviewProcess=any&jobType=fulltime&layout=list-compact&query=GTM&role=marketing&role=sales&sortBy=keyword&tab=any&usVisaNotRequired=any',
7
+ 'https://www.workatastartup.com/companies?companySize=seed&demographic=any&hasEquity=any&hasSalary=any&industry=B2B%20Software%20and%20Services&interviewProcess=any&jobType=fulltime&layout=list-compact&query=Growth&role=sales&role=marketing&sortBy=keyword&tab=any&usVisaNotRequired=any',
8
+ 'https://www.workatastartup.com/companies?companySize=seed&demographic=any&hasEquity=any&hasSalary=any&industry=B2B%20Software%20and%20Services&interviewProcess=any&jobType=fulltime&layout=list-compact&query=Founding%20GTM&role=sales&role=marketing&sortBy=keyword&tab=any&usVisaNotRequired=any',
9
+ 'https://www.workatastartup.com/companies?companySize=seed&demographic=any&hasEquity=any&hasSalary=any&industry=B2B%20Software%20and%20Services&interviewProcess=any&jobType=fulltime&layout=list-compact&query=DevRel&role=sales&role=marketing&sortBy=keyword&tab=any&usVisaNotRequired=any',
10
+ 'https://www.workatastartup.com/companies?companySize=seed&demographic=any&hasEquity=any&hasSalary=any&industry=B2B%20Software%20and%20Services&interviewProcess=any&jobType=fulltime&layout=list-compact&query=creator&role=sales&role=marketing&sortBy=keyword&tab=any&usVisaNotRequired=any'
11
+ ];
12
+
13
+ async function extractCompaniesFromQuery(page, url) {
14
+ console.log(`Scraping query URL: ${url.substring(0, 100)}...`);
15
+ await page.goto(url, { waitUntil: 'networkidle' });
16
+
17
+ // Wait specifically for the element that contains the data-page attribute
18
+ try {
19
+ await page.waitForSelector('[data-page]', { timeout: 10000 });
20
+ } catch (e) {
21
+ console.log("Timeout waiting for [data-page]. Page might have failed to load or requires auth.");
22
+ }
23
+
24
+ const companies = await page.evaluate(() => {
25
+ // Some inertia apps use #app, but let's just find anything with data-page
26
+ const appDiv = document.querySelector('[data-page]');
27
+ if (!appDiv) {
28
+ console.log("CRITICAL: No element with data-page found on the page!");
29
+ return [];
30
+ }
31
+
32
+ try {
33
+ const data = JSON.parse(appDiv.dataset.page);
34
+
35
+ console.log("DATA KEYS:", Object.keys(data));
36
+ if (data.props) console.log("PROPS KEYS:", Object.keys(data.props));
37
+ if (data.props && data.props.initialState) console.log("INITIAL STATE KEYS:", Object.keys(data.props.initialState));
38
+
39
+ const results = [];
40
+ const extractedSlugs = new Set();
41
+
42
+ // Let's try to grab from JSON first (the robust way)
43
+ if (data && data.props && data.props.companies) {
44
+ // Usually it's an array or a paginated object
45
+ const comps = Array.isArray(data.props.companies) ? data.props.companies : (data.props.companies.results || data.props.companies.data || []);
46
+ for (const c of comps) {
47
+ if (c.slug) {
48
+ extractedSlugs.add(c.slug);
49
+ results.push({ slug: c.slug, name: c.name || c.slug });
50
+ }
51
+ }
52
+ }
53
+
54
+ // Fallback: If JSON extraction fails, scrape the DOM links
55
+ if (results.length === 0) {
56
+ const links = document.querySelectorAll('a[href^="/companies/"]');
57
+ links.forEach(link => {
58
+ const href = link.getAttribute('href');
59
+ const parts = href.split('/');
60
+ if (parts.length >= 3 && parts[1] === 'companies') {
61
+ const slug = parts[2];
62
+ // Ignore standard routes that aren't company slugs
63
+ if (!['edit', 'new', 'search'].includes(slug) && !extractedSlugs.has(slug)) {
64
+ extractedSlugs.add(slug);
65
+ results.push({ slug, name: slug });
66
+ }
67
+ }
68
+ });
69
+ }
70
+
71
+ return results;
72
+ } catch (e) {
73
+ console.error("Error parsing data-page:", e);
74
+ return [];
75
+ }
76
+ });
77
+
78
+ return companies;
79
+ }
80
+
81
+ async function scrapeCompanyJobs(page, company) {
82
+ console.log(`Scraping jobs for company: ${company.slug}`);
83
+ const internalUrl = `https://www.workatastartup.com/companies/${company.slug}`;
84
+
85
+ try {
86
+ // === METHOD 1: Try authenticated JSON extraction first (Most Reliable) ===
87
+ await page.goto(internalUrl, { waitUntil: 'networkidle' });
88
+
89
+ try {
90
+ await page.waitForSelector('[data-page]', { timeout: 8000 });
91
+
92
+ const internalJobs = await page.evaluate(() => {
93
+ const appDiv = document.querySelector('[data-page]');
94
+ if (!appDiv) return null;
95
+
96
+ try {
97
+ const data = JSON.parse(appDiv.dataset.page);
98
+ const companyData = data.props.company || data.props.rawCompany || {};
99
+ const jobsList = companyData.jobs || [];
100
+
101
+ if (jobsList.length > 0) {
102
+ return jobsList.map(job => ({
103
+ id: job.id ? String(job.id) : null,
104
+ title: job.title || '',
105
+ location: job.location || ''
106
+ }));
107
+ }
108
+ } catch (e) {
109
+ return null;
110
+ }
111
+ return null;
112
+ });
113
+
114
+ if (internalJobs && internalJobs.length > 0) {
115
+ return internalJobs;
116
+ }
117
+ } catch (e) {
118
+ console.log(` -> Internal JSON extraction failed for ${company.slug}, attempting public fallback...`);
119
+ }
120
+
121
+ // === METHOD 2: Fallback to public HTML extraction (User's Idea) ===
122
+ const publicUrl = `https://www.ycombinator.com/companies/${company.slug}/jobs`;
123
+ const response = await page.goto(publicUrl, { waitUntil: 'networkidle' });
124
+
125
+ if (response && response.status() === 404) {
126
+ console.log(` -> No public jobs page found for ${company.slug}`);
127
+ return [];
128
+ }
129
+
130
+ const publicJobs = await page.evaluate(() => {
131
+ const jobCards = Array.from(document.querySelectorAll('.flex.w-full.flex-row.justify-between'));
132
+ const parsedJobs = [];
133
+
134
+ for (let i = 0; i < jobCards.length; i++) {
135
+ const card = jobCards[i];
136
+ const titleEl = card.querySelector('.font-bold');
137
+ const title = titleEl ? titleEl.textContent.trim() : null;
138
+
139
+ if (title) {
140
+ const detailsEl = card.querySelectorAll('div > span');
141
+ let location = "Unknown";
142
+
143
+ if (detailsEl.length > 0) {
144
+ location = detailsEl[0].textContent.trim();
145
+ }
146
+
147
+ parsedJobs.push({
148
+ id: `yc_${Date.now()}_${i}_${title.replace(/\s+/g, '_')}`,
149
+ title: title,
150
+ location: location
151
+ });
152
+ }
153
+ }
154
+ return parsedJobs;
155
+ });
156
+
157
+ return publicJobs;
158
+
159
+ } catch (error) {
160
+ console.error(`Error scraping jobs for ${company.slug}:`, error.message);
161
+ return [];
162
+ }
163
+ }
164
+
165
+ async function runScraper() {
166
+ if (!fs.existsSync('state.json')) {
167
+ console.error('ERROR: state.json not found. Please run auth.js first to log in.');
168
+ process.exit(1);
169
+ }
170
+
171
+ const browser = await chromium.launch({ headless: true });
172
+ const context = await browser.newContext({ storageState: 'state.json' });
173
+ const page = await context.newPage();
174
+
175
+ const allFoundCompanies = new Map();
176
+
177
+ for (const url of QUERY_URLS) {
178
+ const companies = await extractCompaniesFromQuery(page, url);
179
+ for (const c of companies) {
180
+ if (!allFoundCompanies.has(c.slug)) {
181
+ allFoundCompanies.set(c.slug, c);
182
+ }
183
+ }
184
+ }
185
+
186
+ console.log(`Found ${allFoundCompanies.size} unique companies across all queries.`);
187
+
188
+ let newCompaniesCount = 0;
189
+ let newJobsCount = 0;
190
+
191
+ for (const [slug, company] of allFoundCompanies) {
192
+ const isNewCompany = db.insertCompany(slug, company.name);
193
+ if (isNewCompany) newCompaniesCount++;
194
+
195
+ const jobs = await scrapeCompanyJobs(page, company);
196
+
197
+ for (const job of jobs) {
198
+ if (job.id && job.title) {
199
+ const isNewJob = db.insertJob(job.id, slug, job.title, job.location);
200
+ if (isNewJob) {
201
+ console.log(`[NEW JOB] ${slug} - ${job.title} (${job.location})`);
202
+ newJobsCount++;
203
+ }
204
+ }
205
+ }
206
+
207
+ await page.waitForTimeout(1000);
208
+ }
209
+
210
+ console.log('--- SCRAPE COMPLETE ---');
211
+ console.log(`New Companies Added: ${newCompaniesCount}`);
212
+ console.log(`New Jobs Found: ${newJobsCount}`);
213
+
214
+ await browser.close();
215
+ }
216
+
217
+ runScraper().catch(console.error);
@@ -0,0 +1,35 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import { execSync } from 'node:child_process';
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+ import * as os from 'node:os';
6
+
7
+ describe('CLI End-to-End Tests', () => {
8
+ let tempDir: string;
9
+ let cliPath: string;
10
+
11
+ beforeAll(() => {
12
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opendirectory-cli-test-'));
13
+ cliPath = path.resolve(__dirname, '../dist/index.js');
14
+
15
+ if (!fs.existsSync(cliPath)) {
16
+ execSync('pnpm run build', { cwd: path.resolve(__dirname, '..') });
17
+ }
18
+ });
19
+
20
+ afterAll(() => {
21
+ fs.rmSync(tempDir, { recursive: true, force: true });
22
+ });
23
+
24
+ it('should install a skill for opencode locally', () => {
25
+ const skillName = 'claude-md-generator';
26
+
27
+ execSync(`node "${cliPath}" install ${skillName} --target opencode`, { cwd: tempDir });
28
+
29
+ const expectedPath = path.join(tempDir, '.opencode', 'skills', skillName, 'SKILL.md');
30
+ expect(fs.existsSync(expectedPath)).toBe(true);
31
+
32
+ const content = fs.readFileSync(expectedPath, 'utf-8');
33
+ expect(content.length).toBeGreaterThan(0);
34
+ });
35
+ });
@@ -0,0 +1,91 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'node:fs/promises';
3
+ import * as path from 'node:path';
4
+ import * as os from 'node:os';
5
+ import { resolvePath, safeWriteFile, safeAppendFile } from './fs-adapters';
6
+
7
+ describe('fs-adapters', () => {
8
+ let tempDir: string;
9
+
10
+ beforeEach(async () => {
11
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fs-adapters-test-'));
12
+ });
13
+
14
+ afterEach(async () => {
15
+ await fs.rm(tempDir, { recursive: true, force: true });
16
+ });
17
+
18
+ describe('resolvePath', () => {
19
+ it('should resolve ~ to home directory', () => {
20
+ const home = os.homedir();
21
+ expect(resolvePath('~/foo/bar')).toBe(path.join(home, 'foo/bar'));
22
+ expect(resolvePath('~')).toBe(home);
23
+ });
24
+
25
+ it('should resolve absolute paths', () => {
26
+ const absPath = path.resolve('/foo/bar');
27
+ expect(resolvePath('/foo/bar')).toBe(absPath);
28
+ });
29
+
30
+ it('should resolve relative paths', () => {
31
+ const relPath = path.resolve('foo/bar');
32
+ expect(resolvePath('foo/bar')).toBe(relPath);
33
+ });
34
+ });
35
+
36
+ describe('safeWriteFile', () => {
37
+ it('should create directory and write file', async () => {
38
+ const filePath = path.join(tempDir, 'nested', 'dir', 'test.txt');
39
+ await safeWriteFile(filePath, 'hello world');
40
+
41
+ const content = await fs.readFile(filePath, 'utf-8');
42
+ expect(content).toBe('hello world');
43
+ });
44
+
45
+ it('should overwrite existing file', async () => {
46
+ const filePath = path.join(tempDir, 'test.txt');
47
+ await safeWriteFile(filePath, 'hello world');
48
+ await safeWriteFile(filePath, 'new content');
49
+
50
+ const content = await fs.readFile(filePath, 'utf-8');
51
+ expect(content).toBe('new content');
52
+ });
53
+ });
54
+
55
+ describe('safeAppendFile', () => {
56
+ it('should create file if it does not exist', async () => {
57
+ const filePath = path.join(tempDir, 'nested', 'test.txt');
58
+ await safeAppendFile(filePath, 'hello world');
59
+
60
+ const content = await fs.readFile(filePath, 'utf-8');
61
+ expect(content).toBe('hello world');
62
+ });
63
+
64
+ it('should append to existing file', async () => {
65
+ const filePath = path.join(tempDir, 'test.txt');
66
+ await safeWriteFile(filePath, 'line 1\n');
67
+ await safeAppendFile(filePath, 'line 2');
68
+
69
+ const content = await fs.readFile(filePath, 'utf-8');
70
+ expect(content).toBe('line 1\nline 2');
71
+ });
72
+
73
+ it('should add newline if existing file does not end with one', async () => {
74
+ const filePath = path.join(tempDir, 'test.txt');
75
+ await safeWriteFile(filePath, 'line 1');
76
+ await safeAppendFile(filePath, 'line 2');
77
+
78
+ const content = await fs.readFile(filePath, 'utf-8');
79
+ expect(content).toBe('line 1\nline 2');
80
+ });
81
+
82
+ it('should not append if content already exists', async () => {
83
+ const filePath = path.join(tempDir, 'test.txt');
84
+ await safeWriteFile(filePath, 'existing content\nmore stuff');
85
+ await safeAppendFile(filePath, 'existing content');
86
+
87
+ const content = await fs.readFile(filePath, 'utf-8');
88
+ expect(content).toBe('existing content\nmore stuff');
89
+ });
90
+ });
91
+ });
@@ -0,0 +1,65 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import * as os from 'node:os';
4
+
5
+ export function resolvePath(p: string): string {
6
+ if (p.startsWith('~/') || p === '~') {
7
+ return path.resolve(p.replace(/^~/, os.homedir()));
8
+ }
9
+ return path.resolve(p);
10
+ }
11
+
12
+ export async function safeWriteFile(filePath: string, content: string): Promise<void> {
13
+ const resolvedPath = resolvePath(filePath);
14
+ const dir = path.dirname(resolvedPath);
15
+ await fs.mkdir(dir, { recursive: true });
16
+ await fs.writeFile(resolvedPath, content, 'utf-8');
17
+ }
18
+
19
+ export async function safeAppendFile(filePath: string, content: string): Promise<void> {
20
+ const resolvedPath = resolvePath(filePath);
21
+ const dir = path.dirname(resolvedPath);
22
+ await fs.mkdir(dir, { recursive: true });
23
+
24
+ try {
25
+ const existingContent = await fs.readFile(resolvedPath, 'utf-8');
26
+ if (existingContent.includes(content)) {
27
+ return;
28
+ }
29
+
30
+ const prefix = existingContent.length > 0 && !existingContent.endsWith('\n') ? '\n' : '';
31
+ await fs.appendFile(resolvedPath, prefix + content, 'utf-8');
32
+ } catch (error: any) {
33
+ if (error.code === 'ENOENT') {
34
+ await fs.writeFile(resolvedPath, content, 'utf-8');
35
+ } else {
36
+ throw error;
37
+ }
38
+ }
39
+ }
40
+
41
+ export async function updateHermesConfig(): Promise<void> {
42
+ const configPath = resolvePath('~/.hermes/config.yaml');
43
+
44
+ try {
45
+ let content = await fs.readFile(configPath, 'utf-8');
46
+
47
+ if (!content.includes('skills:')) {
48
+ const prefix = content.length > 0 && !content.endsWith('\n') ? '\n' : '';
49
+ content += prefix + 'skills:\n external_dirs:\n - "./.hermes/skills"\n';
50
+ } else if (!content.includes('external_dirs:')) {
51
+ content = content.replace(/(skills:\s*\n)/, '$1 external_dirs:\n - "./.hermes/skills"\n');
52
+ } else if (!content.includes('./.hermes/skills')) {
53
+ content = content.replace(/(external_dirs:\s*\n)/, '$1 - "./.hermes/skills"\n');
54
+ }
55
+
56
+ await fs.writeFile(configPath, content, 'utf-8');
57
+ } catch (error: any) {
58
+ if (error.code === 'ENOENT') {
59
+ const initialContent = `skills:\n external_dirs:\n - "./.hermes/skills"\n`;
60
+ await safeWriteFile('~/.hermes/config.yaml', initialContent);
61
+ } else {
62
+ throw error;
63
+ }
64
+ }
65
+ }
package/src/index.ts ADDED
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import * as fs from 'node:fs/promises';
5
+ import * as path from 'node:path';
6
+ import { Skill } from './transformers';
7
+ import { safeWriteFile } from './fs-adapters';
8
+ import chalk from 'chalk';
9
+ import ora from 'ora';
10
+ import Table from 'cli-table3';
11
+
12
+ const program = new Command();
13
+
14
+ program
15
+ .name('@opendirectory.dev/cli')
16
+ .description(chalk.blue.bold('CLI to install OpenDirectory skills'))
17
+ .version('0.1.0');
18
+
19
+ const getProjectRoot = () => {
20
+ return path.resolve(__dirname, '..');
21
+ };
22
+
23
+ program
24
+ .command('list')
25
+ .description('List available skills in the Open Directory registry')
26
+ .action(async () => {
27
+ const spinner = ora('Fetching available skills...').start();
28
+ try {
29
+ const root = getProjectRoot();
30
+ const registryPath = path.join(root, 'registry.json');
31
+
32
+ let skills: any[] = [];
33
+ try {
34
+ const registryContent = await fs.readFile(registryPath, 'utf-8');
35
+ skills = JSON.parse(registryContent);
36
+ } catch (e) {
37
+ const skillsDir = path.join(root, 'skills');
38
+ const entries = await fs.readdir(skillsDir, { withFileTypes: true });
39
+ skills = entries
40
+ .filter(entry => entry.isDirectory())
41
+ .map(entry => ({ name: entry.name, description: `Skill: ${entry.name}` }));
42
+ }
43
+
44
+ spinner.stop();
45
+ console.log(chalk.green('Successfully loaded Open Directory registry!\n'));
46
+
47
+ const table = new Table({
48
+ head: [chalk.cyan.bold('Skill Name'), chalk.cyan.bold('Description')],
49
+ colWidths: [35, 75],
50
+ wordWrap: true
51
+ });
52
+
53
+ for (const skill of skills) {
54
+ let desc = skill.description || '';
55
+ desc = desc.replace(/<img[^>]*>/g, '').trim();
56
+ if (desc.length > 100) desc = desc.substring(0, 97) + '...';
57
+
58
+ table.push([chalk.yellow(skill.name), desc]);
59
+ }
60
+
61
+ console.log(table.toString());
62
+ console.log(chalk.gray(`\nRun \`${chalk.white('npx @opendirectory.dev/cli install <skill-name> --target <agent>')}\` to install a skill.`));
63
+
64
+ } catch (error) {
65
+ spinner.stop();
66
+ console.error(chalk.red('Failed to list skills.'));
67
+ console.error(error);
68
+ }
69
+ });
70
+
71
+ program
72
+ .command('install <skill>')
73
+ .description('Install a skill for your AI agent')
74
+ .requiredOption('-t, --target <tool>', 'Target agent (opencode, claude, codex, gemini, anti-gravity, openclaw, hermes)')
75
+ .option('-g, --global', 'Install globally for all projects')
76
+ .action(async (skillName, options) => {
77
+ const spinner = ora(`Installing ${chalk.yellow(skillName)}...`).start();
78
+ try {
79
+ const root = getProjectRoot();
80
+ const repoDir = path.join(root, 'skills', skillName);
81
+
82
+ let skillDir = repoDir;
83
+ let skillMdPath = path.join(skillDir, 'SKILL.md');
84
+
85
+ try {
86
+ await fs.access(skillMdPath);
87
+ } catch (e) {
88
+ try {
89
+ const entries = await fs.readdir(repoDir, { withFileTypes: true });
90
+ for (const entry of entries) {
91
+ if (entry.isDirectory()) {
92
+ const possiblePath = path.join(repoDir, entry.name, 'SKILL.md');
93
+ try {
94
+ await fs.access(possiblePath);
95
+ skillDir = path.join(repoDir, entry.name);
96
+ skillMdPath = possiblePath;
97
+ break;
98
+ } catch (err) {}
99
+ }
100
+ }
101
+ if (skillDir === repoDir) {
102
+ for (const entry of entries) {
103
+ if (entry.isDirectory() && entry.name !== 'node_modules' && entry.name !== '.git') {
104
+ const subDir = path.join(repoDir, entry.name);
105
+ const subEntries = await fs.readdir(subDir, { withFileTypes: true });
106
+ for (const subEntry of subEntries) {
107
+ if (subEntry.isDirectory()) {
108
+ const possiblePath = path.join(subDir, subEntry.name, 'SKILL.md');
109
+ try {
110
+ await fs.access(possiblePath);
111
+ skillDir = path.join(subDir, subEntry.name);
112
+ skillMdPath = possiblePath;
113
+ break;
114
+ } catch (err) {}
115
+ }
116
+ }
117
+ }
118
+ }
119
+ }
120
+ } catch (dirErr) {
121
+ spinner.stop();
122
+ console.error(chalk.red(`Error: Repository '${skillName}' not found.`));
123
+ console.log(chalk.gray(`Try running \`${chalk.white('npx @opendirectory.dev/cli list')}\` to see available skills.`));
124
+ process.exit(1);
125
+ }
126
+ }
127
+
128
+ try {
129
+ await fs.access(skillMdPath);
130
+ } catch (e) {
131
+ spinner.stop();
132
+ console.error(chalk.red(`Error: Skill '${skillName}' missing SKILL.md in registry.`));
133
+ process.exit(1);
134
+ }
135
+
136
+ const actualSkillFolderName = path.basename(skillDir);
137
+ const finalSkillName = actualSkillFolderName === skillName ? skillName : actualSkillFolderName;
138
+
139
+ const target = options.target.toLowerCase();
140
+ const isGlobal = options.global;
141
+
142
+ const validTargets = ['opencode', 'claude', 'codex', 'gemini', 'anti-gravity', 'openclaw', 'hermes'];
143
+
144
+ if (validTargets.includes(target)) {
145
+ let targetFolder = '';
146
+ if (target === 'opencode') targetFolder = isGlobal ? `~/.config/opencode/skills/${finalSkillName}` : `./.opencode/skills/${finalSkillName}`;
147
+ if (target === 'claude') targetFolder = isGlobal ? `~/.claude/skills/${finalSkillName}` : `./.claude/skills/${finalSkillName}`;
148
+ if (target === 'codex') targetFolder = isGlobal ? `~/.codex/skills/${finalSkillName}` : `./.codex/skills/${finalSkillName}`;
149
+ if (target === 'gemini') targetFolder = isGlobal ? `~/.gemini/skills/${finalSkillName}` : `./.gemini/skills/${finalSkillName}`;
150
+ if (target === 'anti-gravity') targetFolder = isGlobal ? `~/.gemini/antigravity/skills/${finalSkillName}` : `./.agent/skills/${finalSkillName}`;
151
+ if (target === 'openclaw') targetFolder = isGlobal ? `~/.openclaw/skills/${finalSkillName}` : `./.openclaw/skills/${finalSkillName}`;
152
+ if (target === 'hermes') targetFolder = isGlobal ? `~/.hermes/skills/${finalSkillName}` : `./.hermes/skills/${finalSkillName}`;
153
+
154
+ const { resolvePath, updateHermesConfig } = require('./fs-adapters');
155
+ const resolvedDest = resolvePath(targetFolder);
156
+ await fs.mkdir(resolvedDest, { recursive: true });
157
+ await fs.cp(skillDir, resolvedDest, { recursive: true });
158
+
159
+ if (target === 'hermes' && !isGlobal) {
160
+ await updateHermesConfig();
161
+ }
162
+
163
+ spinner.stop();
164
+ console.log(chalk.green(`Successfully installed ${chalk.bold(finalSkillName)}!`));
165
+ console.log(`\n ${chalk.cyan('Agent:')} ${target}`);
166
+ console.log(` ${chalk.cyan('Scope:')} ${isGlobal ? 'Global' : 'Local Project'}`);
167
+ console.log(` ${chalk.cyan('Path:')} ${targetFolder}\n`);
168
+ } else {
169
+ spinner.stop();
170
+ console.error(chalk.red(`Error: Unsupported target '${target}'.`));
171
+ console.log(chalk.gray(`Supported targets: ${validTargets.join(', ')}`));
172
+ process.exit(1);
173
+ }
174
+ } catch (error) {
175
+ spinner.stop();
176
+ console.error(chalk.red('Failed to install skill.'));
177
+ console.error(error);
178
+ process.exit(1);
179
+ }
180
+ });
181
+
182
+ program.parse();
@@ -0,0 +1,6 @@
1
+ export interface Skill {
2
+ name: string;
3
+ description: string;
4
+ content: string;
5
+ globs?: string;
6
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist"
6
+ },
7
+ "include": ["src/**/*"]
8
+ }