first-customer-finder-skill 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.
- package/.claude-plugin/marketplace.json +13 -0
- package/.claude-plugin/plugin.json +11 -0
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/first-customer-finder/SKILL.md +122 -0
- package/first-customer-finder/references/locale-playbooks.md +69 -0
- package/first-customer-finder/references/report-artifact.md +76 -0
- package/first-customer-finder/references/research-framework.md +123 -0
- package/first-customer-finder/scripts/generate_report.py +285 -0
- package/package.json +39 -0
- package/scripts/install.js +91 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "z0rats",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "z0rats"
|
|
5
|
+
},
|
|
6
|
+
"plugins": [
|
|
7
|
+
{
|
|
8
|
+
"name": "first-customer-finder",
|
|
9
|
+
"source": "./",
|
|
10
|
+
"description": "Find and qualify evidence-backed potential first customers for a startup from recent public signals — ICP analysis, verified pain/buying signals, scored shortlist, drafted outreach openers, locale playbooks for non-English markets, and a polished HTML report. Never sends messages."
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "first-customer-finder",
|
|
3
|
+
"description": "Find and qualify evidence-backed potential first customers, early adopters, and design partners for a startup from recent public signals, with locale playbooks for non-English markets. Research-only: never sends outreach.",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Francesco Mistero"
|
|
7
|
+
},
|
|
8
|
+
"homepage": "https://github.com/z0rats/first-customer-finder-skill",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"skills": ["./first-customer-finder"]
|
|
11
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Francesco Mistero
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Claude First Customer Finder Skill
|
|
2
|
+
|
|
3
|
+
A Claude Code skill that turns a startup URL or product idea into a qualified shortlist of potential first customers using recent public pain, demand, and timing signals.
|
|
4
|
+
|
|
5
|
+
It defines the ideal customer profile, researches public sources, links the evidence behind every prospect, ranks fit and timing, drafts a source-based opener with a concrete manual CTA, and creates a polished HTML report. It never sends outreach automatically.
|
|
6
|
+
|
|
7
|
+
## What It Does
|
|
8
|
+
|
|
9
|
+
- Analyzes a startup URL, repository, or product description
|
|
10
|
+
- Defines the primary and adjacent ideal customer profiles
|
|
11
|
+
- Finds explicit demand, pain, workaround, switching, and timing signals
|
|
12
|
+
- Qualifies prospects with an evidence-based score
|
|
13
|
+
- Links every primary prospect to the original public source
|
|
14
|
+
- Drafts respectful, source-based outreach openers with concrete manual CTAs
|
|
15
|
+
- Recommends official/public contact routes without private enrichment
|
|
16
|
+
- Creates a responsive standalone HTML report
|
|
17
|
+
- Keeps all outreach manual by default
|
|
18
|
+
- Avoids private contact enrichment and sensitive personal data
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
**npm installer** (fastest):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx --yes first-customer-finder-skill@latest
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This installs the skill into:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
~/.claude/skills/first-customer-finder
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Claude Code plugin** (this repo is its own marketplace):
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
/plugin marketplace add z0rats/first-customer-finder-skill
|
|
38
|
+
/plugin install first-customer-finder@z0rats
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Restart Claude Code after installation.
|
|
42
|
+
|
|
43
|
+
## How long it takes
|
|
44
|
+
|
|
45
|
+
This is a deep-research skill, not a chat answer — it searches, and in `standard`/`deep` mode fetches and verifies real pages, before anything reaches the shortlist. Expect roughly **10–15 minutes for a `standard` run** in Claude Code; `deep` mode (up to twenty prospects) takes noticeably longer.
|
|
46
|
+
|
|
47
|
+
To speed things up:
|
|
48
|
+
|
|
49
|
+
- Say **"quick mode"** — five prospects instead of ten roughly halves the research.
|
|
50
|
+
- You can shrink scope **mid-run**: say *"quick mode is fine — wrap up with what's verified"* and the skill re-plans instead of finishing every search bucket.
|
|
51
|
+
- If your environment prompts for fetch/browse permission per domain, approving "allow all for this site" once avoids repeat prompts — research tends to cluster on a handful of domains, so prompts stop quickly.
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
54
|
+
|
|
55
|
+
Claude Code activates the skill automatically when a request matches its description. Just ask naturally, for example:
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
Find ten evidence-backed potential first customers for https://example.com and create the final HTML report.
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Find design partners:
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
Find first customers in design-partners mode for this startup: [URL]. Prioritize people publicly describing the problem and likely to give product feedback.
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
B2B research:
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
Find first customers in b2b mode for [URL]. Find public business triggers, qualify the relevant companies, and draft one opener per prospect without sending anything.
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Output
|
|
74
|
+
|
|
75
|
+
The report includes:
|
|
76
|
+
|
|
77
|
+
1. Early-customer verdict
|
|
78
|
+
2. Primary ICP and disqualifiers
|
|
79
|
+
3. Highest-confidence prospect
|
|
80
|
+
4. Evidence-backed prospect shortlist
|
|
81
|
+
5. Fit and timing scores
|
|
82
|
+
6. Source links and signal dates
|
|
83
|
+
7. Personalized outreach openers with concrete CTAs
|
|
84
|
+
8. Repeated pain patterns
|
|
85
|
+
9. Seven-day manual outreach plan
|
|
86
|
+
10. Research limitations
|
|
87
|
+
|
|
88
|
+
Prospects are hypotheses based on public signals, not confirmed customers or guaranteed buyers.
|
|
89
|
+
|
|
90
|
+
## Modes
|
|
91
|
+
|
|
92
|
+
- `quick`: up to five strong prospects
|
|
93
|
+
- `standard`: up to ten prospects across several source types
|
|
94
|
+
- `deep`: up to twenty prospects and repeated-pattern analysis
|
|
95
|
+
- `design-partners`: feedback-oriented early adopters
|
|
96
|
+
- `b2b`: companies and public business triggers
|
|
97
|
+
- `community`: explicit requests and public discussion signals
|
|
98
|
+
|
|
99
|
+
## Manual Installation
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
git clone https://github.com/z0rats/first-customer-finder-skill.git
|
|
103
|
+
mkdir -p ~/.claude/skills
|
|
104
|
+
cp -R first-customer-finder-skill/first-customer-finder ~/.claude/skills/first-customer-finder
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Restart Claude Code after installation.
|
|
108
|
+
|
|
109
|
+
## Credits
|
|
110
|
+
|
|
111
|
+
This repo started as a fork of [codex-first-customer-finder-skill](https://github.com/Kappaemme-git/codex-first-customer-finder-skill) by Francesco Mistero (the Codex-agent original) — see `LICENSE` for that project's copyright. Later revisions also borrowed structure, wording, and workflow ideas from [carolinacherry/claude-first-customer-finder-skill](https://github.com/carolinacherry/claude-first-customer-finder-skill) by Daniel An. This fork adds locale playbooks for non-English markets (starting with CIS/Russian-speaking) and other workflow changes on top of both.
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: first-customer-finder
|
|
3
|
+
description: Find and qualify evidence-backed potential first customers, early adopters, design partners, or beta users for a startup using recent public pain and buying signals. Use when analyzing a product URL or idea, defining an ideal customer profile, researching public discussions and business pages, identifying first-user prospects, ranking lead fit and timing, preparing source-based outreach drafts, or creating a shareable early-customer prospecting report without sending messages automatically.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# First Customer Finder
|
|
7
|
+
|
|
8
|
+
Turn a startup URL or product description into a short, evidence-backed list of plausible first customers. Use public signals, preserve privacy, and distinguish a prospect from a confirmed buyer.
|
|
9
|
+
|
|
10
|
+
Read [references/research-framework.md](references/research-framework.md) before researching or scoring prospects. Read [references/report-artifact.md](references/report-artifact.md) before creating the final report. Read [references/locale-playbooks.md](references/locale-playbooks.md) whenever the product's buyer, language, or stated geography is not English-language/US-EU-centric, and apply the matching playbook before building the search plan.
|
|
11
|
+
|
|
12
|
+
## Workflow
|
|
13
|
+
|
|
14
|
+
### 1. Understand the product
|
|
15
|
+
|
|
16
|
+
- Inspect the supplied URL, repository, landing-page copy, or product description.
|
|
17
|
+
- Identify the product, outcome, buyer, user, price or buying motion, geography, and strongest use case.
|
|
18
|
+
- Define one primary ICP, one adjacent ICP, pain triggers, positive signals, and disqualifiers.
|
|
19
|
+
- Infer missing context when safe and label the inference. Ask one concise question only when ambiguity would materially change the search.
|
|
20
|
+
- Determine the buyer's likely language and market. If it is not English-language/US-EU-centric (e.g., CIS, Russian-speaking), read [references/locale-playbooks.md](references/locale-playbooks.md) and use the matching playbook for the rest of the workflow.
|
|
21
|
+
|
|
22
|
+
### 2. Build a public-signal search plan
|
|
23
|
+
|
|
24
|
+
Search current public sources for:
|
|
25
|
+
|
|
26
|
+
- explicit tool or alternative requests
|
|
27
|
+
- first-person descriptions of the target problem
|
|
28
|
+
- manual workflows and repeated workaround complaints
|
|
29
|
+
- migration, churn, or competitor-frustration signals
|
|
30
|
+
- public company changes that create timing, such as hiring, launching, expanding, or adopting a relevant workflow
|
|
31
|
+
|
|
32
|
+
Use the query buckets and source mix from the applicable locale playbook when one applies; otherwise use the base buckets above with general web search. When subagent delegation is available, run each query bucket as its own parallel research subagent — one bucket per agent, each returning candidate signals with source URLs, dates, and a summary close to the original wording. Give every agent the product brief and ICP so it can reject weak matches at the source, and instruct it to fetch and quote what it cites; an agent may not return a URL it did not open. Work the buckets sequentially instead if the environment has no subagent support. Prefer original pages over search snippets. Record the source URL, source type, publication date when visible, and the exact evidence supporting qualification.
|
|
33
|
+
|
|
34
|
+
### 3. Research safely
|
|
35
|
+
|
|
36
|
+
- Use public, intentionally shared professional or business information only.
|
|
37
|
+
- Do not bypass login walls, paywalls, access controls, rate limits, or robots restrictions.
|
|
38
|
+
- Do not use data brokers, leaked datasets, private groups, personal email discovery, phone enrichment, or sensitive personal information.
|
|
39
|
+
- Do not infer protected traits or target people using health, financial hardship, political belief, sexuality, religion, or other sensitive attributes.
|
|
40
|
+
- Prefer companies, public professional profiles, public requests, and community posts relevant to the product.
|
|
41
|
+
- Quote minimally and paraphrase by default. Link every material pain or timing signal.
|
|
42
|
+
|
|
43
|
+
### 4. Verify before you qualify
|
|
44
|
+
|
|
45
|
+
Fetch the original page for every candidate signal before it can enter the shortlist. A search snippet, an aggregator summary, or a subagent's unquoted paraphrase is not evidence. Confirm the page exists, the signal says what was claimed, and the date. Drop anything that fails this check.
|
|
46
|
+
|
|
47
|
+
Some high-value sources block server-side fetching (e.g., Reddit, G2/TrustRadius, Cloudflare-fronted job boards). Do not drop a candidate for that reason alone — verify through an alternate legitimate route instead: a browser session, an official API (e.g., HN Algolia for Hacker News), or the site's RSS feed. Third-party archives can run months stale, so prefer the live page and note which route confirmed it. When a subagent already fetched and quoted a source, spot-check its work, and always re-verify the top three prospects yourself before they reach the report.
|
|
48
|
+
|
|
49
|
+
### 5. Qualify and deduplicate
|
|
50
|
+
|
|
51
|
+
Score each prospect using the bundled framework:
|
|
52
|
+
|
|
53
|
+
- pain strength
|
|
54
|
+
- product fit
|
|
55
|
+
- timing
|
|
56
|
+
- public reachability
|
|
57
|
+
- evidence quality
|
|
58
|
+
|
|
59
|
+
Remove duplicates and weak matches. A prospect without a cited pain, need, or timing signal is only a speculative fit and must not appear in the primary shortlist.
|
|
60
|
+
|
|
61
|
+
Never claim that a prospect is interested, has consented, or will buy. Label the output "potential customer based on public signals."
|
|
62
|
+
|
|
63
|
+
### 6. Draft outreach, never send it
|
|
64
|
+
|
|
65
|
+
- Recommend the most natural public or professional channel already associated with the source. Prefer concrete official/public routes such as a company form, public business email, relevant public thread, or professional profile. If no direct public route is found, say so instead of guessing.
|
|
66
|
+
- Identify the likely target role or function, not just the company name.
|
|
67
|
+
- Translate the product into the buyer's problem language. Do not lead with implementation-layer terms unless the source proves that audience already cares about them.
|
|
68
|
+
- Make the next step concrete enough to accept, reject, or forward, such as a scenario, teardown, worksheet, checklist, benchmark, mockup, or sandbox walkthrough.
|
|
69
|
+
- Write one short opener grounded only in the cited public context.
|
|
70
|
+
- Include a specific CTA, preferably a routing or yes/no ask tied to that next step.
|
|
71
|
+
- Note the likely target function and objection when useful for manual outreach planning.
|
|
72
|
+
- Avoid pretending to know the person, overstating familiarity, or mentioning unrelated personal details.
|
|
73
|
+
- Do not send messages, submit forms, connect, follow, comment, or create CRM records unless the user separately requests and authorizes that action.
|
|
74
|
+
|
|
75
|
+
### 7. Produce the report
|
|
76
|
+
|
|
77
|
+
Lead with the most actionable evidence. Use this order:
|
|
78
|
+
|
|
79
|
+
1. **Verdict** — whether the startup has reachable early-customer signals.
|
|
80
|
+
2. **ICP** — buyer, job, trigger, and disqualifiers.
|
|
81
|
+
3. **Top prospect** — strongest evidence-backed candidate and why now.
|
|
82
|
+
4. **Prospect shortlist** — source, pain signal, fit score, stage, why now, channel, and opener with a concrete CTA.
|
|
83
|
+
5. **Repeated patterns** — pains and triggers appearing across prospects.
|
|
84
|
+
6. **Seven-day outreach plan** — a manual, low-volume validation sequence.
|
|
85
|
+
7. **Limits** — missing evidence and what must be confirmed through real conversations.
|
|
86
|
+
|
|
87
|
+
Create a standalone HTML report unless the user explicitly requests chat-only output:
|
|
88
|
+
|
|
89
|
+
1. Write structured JSON using `references/report-artifact.md`.
|
|
90
|
+
2. Run `python3 scripts/generate_report.py <analysis.json> <report.html>`.
|
|
91
|
+
3. Save the report in the workspace `outputs/` directory (create it if missing).
|
|
92
|
+
4. Verify prospect cards, source links, scores, concrete contact routes, CTAs, patterns, outreach plan, and limitations.
|
|
93
|
+
5. Return a clickable absolute file link (`file://...`) in the final response, or publish it as an Artifact if the user is working in an environment that supports that.
|
|
94
|
+
|
|
95
|
+
## Modes
|
|
96
|
+
|
|
97
|
+
- **quick**: Find and qualify up to five strong prospects.
|
|
98
|
+
- **standard**: Find up to ten prospects across several public source types.
|
|
99
|
+
- **deep**: Research up to twenty prospects and map repeated pain patterns.
|
|
100
|
+
- **design-partners**: Prioritize users willing to test and give feedback over immediate buyers.
|
|
101
|
+
- **b2b**: Prioritize companies, public business triggers, and relevant decision roles.
|
|
102
|
+
- **community**: Prioritize public discussion and explicit request signals.
|
|
103
|
+
|
|
104
|
+
Use `standard` by default. State the mode explicitly in the request, e.g. "find first customers for https://example.com in deep mode".
|
|
105
|
+
|
|
106
|
+
## Gotchas
|
|
107
|
+
|
|
108
|
+
- **Fabrication is the failure mode that kills this skill.** A plausible prospect with a dead or mismatched link is worse than no prospect — step 4's verification is not optional, even in quick mode.
|
|
109
|
+
- **Two-sided products** (e.g., marketplaces) return a mixed bag of both sides from research. Filter to the primary ICP before scoring; note the adjacent side under patterns instead of blending both into one shortlist.
|
|
110
|
+
- **Old signals** can still qualify — an explicit request from 18 months ago is fine to include — but cut its timing score and print the date next to it so the reader can judge freshness themselves.
|
|
111
|
+
- **Ten strong beats thirty generic.** If verification leaves you with four great prospects instead of ten mediocre ones, ship four and say why.
|
|
112
|
+
- **Respect impatience.** If the user asks you to hurry or wrap up mid-run, switch to quick mode immediately: stop waiting on unfinished search buckets, verify and score what you already have, and ship the report with a note about which buckets were cut short.
|
|
113
|
+
|
|
114
|
+
## Quality bar
|
|
115
|
+
|
|
116
|
+
- Link every prospect to at least one meaningful public signal.
|
|
117
|
+
- Prefer ten strong matches over a long generic lead list.
|
|
118
|
+
- Make uncertainty and stale evidence visible.
|
|
119
|
+
- Personalize from the source, not from invented assumptions.
|
|
120
|
+
- Make the manual next step operationally clear: who should receive it, what to ask, and what answer the outreach needs.
|
|
121
|
+
- Keep outreach manual and respectful.
|
|
122
|
+
- Treat the shortlist as a research hypothesis, not a customer database.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Locale Playbooks
|
|
2
|
+
|
|
3
|
+
The default query buckets and source mix in `research-framework.md` are written for English-language, US/EU-centric products. Apply a locale playbook whenever the product's buyer, language, or stated geography points elsewhere, so the search plan does not default to English-only sources.
|
|
4
|
+
|
|
5
|
+
## Detecting the locale
|
|
6
|
+
|
|
7
|
+
Infer from, in order of priority:
|
|
8
|
+
|
|
9
|
+
1. an explicit geography or language stated by the user
|
|
10
|
+
2. the product's own language (landing page copy, README, UI strings)
|
|
11
|
+
3. the TLD, company registration, or pricing currency
|
|
12
|
+
4. the language prospects are likely to complain in, which may differ from the product's language
|
|
13
|
+
|
|
14
|
+
Label the inference. If signals conflict (e.g., English product copy but a CIS-only pricing page), search both the product's language and the buyer's likely language.
|
|
15
|
+
|
|
16
|
+
## CIS / Russian-speaking market
|
|
17
|
+
|
|
18
|
+
Use this playbook when the buyer or product geography is Russia, Ukraine, Belarus, Kazakhstan, or another Russian-speaking market.
|
|
19
|
+
|
|
20
|
+
### Query buckets (Russian-language equivalents)
|
|
21
|
+
|
|
22
|
+
Search these alongside, not instead of, the English buckets if the audience is bilingual:
|
|
23
|
+
|
|
24
|
+
1. **Explicit demand:** "ищу сервис", "посоветуйте инструмент", "кто чем пользуется для", "аналог [competitor]", "есть что-то для".
|
|
25
|
+
2. **Pain:** "задолбался вручную", "занимает часы", "бесит когда", "неудобно", "постоянно ломается", "костыль".
|
|
26
|
+
3. **Workaround:** таблицы (Google Sheets/Excel), копипаста между сервисами, боты в Telegram "на коленке", фрилансер/VA для рутины.
|
|
27
|
+
4. **Switching:** "отказался от [конкурент]", "подорожал", "перестал устраивать", жалобы на цены/поддержку/блокировку.
|
|
28
|
+
5. **Timing:** вакансии на hh.ru под смежную роль, анонсы запуска/расширения в Telegram-каналах компании, посты о найме или пивоте.
|
|
29
|
+
|
|
30
|
+
Russian is heavily inflected — a single fixed phrase per bucket misses most matches. Vary case and person rather than searching one form only: "ищу" / "ищем" / "искали" / "ищет кто-нибудь", "задолбался" / "задолбались" / "задолбало", "аналог Notion" / "аналоги Notion" / "альтернатива Notion". Treat each bucket as a cluster of word forms, not a single string.
|
|
31
|
+
|
|
32
|
+
### Source mix
|
|
33
|
+
|
|
34
|
+
- **Habr (habr.com)** — technical/product audience; strong for developer tools, SaaS, infra pain posts and comments.
|
|
35
|
+
- **VC.ru** — startup/business audience; launch posts, business-model discussions, comment threads with pain signals.
|
|
36
|
+
- **Pikabu** — broad consumer audience; useful for consumer-facing products, less reliable for B2B.
|
|
37
|
+
- **hh.ru** — vacancies as a timing signal (a company hiring for a role your product supports is a trigger); job descriptions also reveal current workflow and tooling.
|
|
38
|
+
- **TenChat** — closest RU-market analog to LinkedIn-style professional posts; use for professional/business signals.
|
|
39
|
+
- **VK public pages and communities** — company pages, industry groups; treat like public company pages, not private groups.
|
|
40
|
+
- **Telegram public channels and chats** — browse via the public web preview (`t.me/s/<channel>`) or a public directory (e.g., TGStat, Telemetr) rather than trying to search inside the app; only cite channels/messages that are openly public, not invite-only groups.
|
|
41
|
+
- **Public company registries** — a strong, underused timing source. For Russia: `rusprofile.ru` or `list-org.com` (registration date, OKVED activity codes, leadership/ownership changes, active/liquidation status). A recent registration, a leadership change, or a newly added activity code relevant to the product is a legitimate timing signal — cite the registry page itself, not a summary.
|
|
42
|
+
|
|
43
|
+
### Payment rails and product fit
|
|
44
|
+
|
|
45
|
+
Stripe and PayPal do not operate inside Russia, which affects `product_fit` and reachability for any product whose buying motion assumes them (checkout, subscription billing, payouts). When qualifying a Russia-based prospect for a product billed through Stripe/PayPal, note this as a disqualifier or a material caveat rather than silently scoring fit as if global checkout worked normally. Local alternatives prospects may already use — useful for recognizing existing workarounds under the "workaround" query bucket — include ЮKassa, CloudPayments, Т-Банк (Tinkoff) Acquiring, and Robokassa. This constraint is Russia-specific; Ukraine, Kazakhstan, and Belarus each have their own separate payment-rail landscape and shouldn't be assumed to share it.
|
|
46
|
+
|
|
47
|
+
### Compliance caution
|
|
48
|
+
|
|
49
|
+
When the product/company is based in a jurisdiction with export controls or sanctions programs (e.g., US, EU, UK) and a prospect is Russia-based, sanctions or export-control restrictions may prohibit selling or providing services to that prospect, depending on the product category, the specific entity, and current regulations. This is not something to resolve automatically — flag it, don't decide it:
|
|
50
|
+
|
|
51
|
+
- Do not silently draft outreach to a Russia-based prospect for a Western-owned product without surfacing this.
|
|
52
|
+
- Set the prospect's `caution` field to note the jurisdiction mismatch and that sanctions/export-control screening should happen before any outreach.
|
|
53
|
+
- This is a flag for the user to verify, not legal advice — never assert that a prospect is or isn't compliant to contact.
|
|
54
|
+
|
|
55
|
+
### Platform caveats
|
|
56
|
+
|
|
57
|
+
- **LinkedIn is formally blocked in Russia** (Roskomnadzor, since 2016), but VPN use is widespread enough among the professional/tech audience that it remains a usable source — do not exclude it by default. Treat it as secondary to TenChat/VK/Habr for Russia-based prospects rather than off-limits, and expect lower coverage for less tech-savvy or non-VPN audiences.
|
|
58
|
+
- **Telegram and VK content is often not indexed by general web search.** Do not assume a WebSearch query surfaces relevant channels or posts; fetch known public channels/communities directly, or use a public directory site to discover them first.
|
|
59
|
+
- **Private/closed Telegram groups and VK communities remain off-limits**, same as any other private group under the base research-safety rules — do not join, request access, or use a member list to enrich contacts.
|
|
60
|
+
|
|
61
|
+
### Reporting and outreach adjustments
|
|
62
|
+
|
|
63
|
+
- Set `search_scope` in the report JSON to reflect the actual scope searched, e.g. `"Public Russian-language sources (Habr, VC.ru, hh.ru, Telegram), last 12 months"` instead of the English-language default.
|
|
64
|
+
- Draft the opener in the same language as the cited public source, not automatically in English.
|
|
65
|
+
- Base the seven-day outreach plan's cadence on the prospect's local business hours, not the product's home timezone. Russia, Belarus, and most of Kazakhstan sit roughly UTC+3 to UTC+6 (Moscow Time and east of it); Ukraine is UTC+2/+3. Note the relevant timezone in the plan when it would otherwise default to a Western business-hours assumption.
|
|
66
|
+
|
|
67
|
+
## Adding another locale
|
|
68
|
+
|
|
69
|
+
Follow the same shape: translated query buckets, a ranked source mix specific to that market, platform caveats (blocked/unreliable platforms, search-indexing gaps), and any reporting-language adjustment. Keep each playbook self-contained so the workflow can read only the one that applies.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Report Artifact
|
|
2
|
+
|
|
3
|
+
Create a standalone HTML report from the final qualified prospect data. Use the bundled generator instead of writing report markup manually.
|
|
4
|
+
|
|
5
|
+
## Generate
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python3 scripts/generate_report.py analysis.json outputs/first-customer-finder-report.html
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Return a clickable absolute `file://` link in the final response, or publish the HTML as an Artifact if the current environment supports it. Keep the JSON in a work or temporary directory unless the user asks for raw data.
|
|
12
|
+
|
|
13
|
+
## JSON schema
|
|
14
|
+
|
|
15
|
+
Fields are intentionally flat (`source_url`, `source_type`, `signal_date`, `pain_strength`, ...) rather than nested under a `source`/`scores` object. A model writing this JSON by hand is less likely to misplace or drop a field in a flat shape, and `generate_report.py` reads these exact keys — keep new fields flat too unless there's a concrete reason to nest.
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"title": "First Customer Finder",
|
|
20
|
+
"product": "Example product",
|
|
21
|
+
"product_url": "https://example.com",
|
|
22
|
+
"target_customer": "Independent gym owners",
|
|
23
|
+
"search_scope": "Public English-language sources, last 12 months",
|
|
24
|
+
"generated_at": "2026-07-12",
|
|
25
|
+
"verdict": "The strongest early demand comes from owners manually chasing failed membership payments.",
|
|
26
|
+
"prospects": [
|
|
27
|
+
{
|
|
28
|
+
"name": "Example Gym",
|
|
29
|
+
"type": "Company",
|
|
30
|
+
"stage": "Problem aware",
|
|
31
|
+
"score": 82,
|
|
32
|
+
"pain_signal": "The owner publicly described manually following up on overdue memberships.",
|
|
33
|
+
"evidence": "A recent public post describes the workflow and time cost.",
|
|
34
|
+
"why_fit": "The product automates the exact reminder and payment step.",
|
|
35
|
+
"why_now": "The post is recent and asks how other owners handle the process.",
|
|
36
|
+
"source_title": "How do you handle failed payments?",
|
|
37
|
+
"source_url": "https://example.com/public-post",
|
|
38
|
+
"source_type": "Public forum",
|
|
39
|
+
"signal_date": "2026-07-01",
|
|
40
|
+
"suggested_channel": "Reply to the public discussion",
|
|
41
|
+
"opener": "Saw your question about failed-payment follow-up. We are testing a checklist for reducing manual payment chasing. Worth sending to whoever owns membership billing?",
|
|
42
|
+
"caution": "Confirm the workflow is still active before pitching.",
|
|
43
|
+
"dimensions": {
|
|
44
|
+
"pain_strength": 5,
|
|
45
|
+
"product_fit": 5,
|
|
46
|
+
"timing": 4,
|
|
47
|
+
"reachability": 4,
|
|
48
|
+
"evidence_quality": 4
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"patterns": [
|
|
53
|
+
{
|
|
54
|
+
"title": "Manual follow-up",
|
|
55
|
+
"count": 4,
|
|
56
|
+
"insight": "Owners rely on spreadsheets and messaging apps after payments fail."
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"outreach_plan": {
|
|
60
|
+
"angle": "Offer a manual concierge test before asking for software adoption.",
|
|
61
|
+
"first_step": "Contact the three highest-scoring prospects with one source-based question.",
|
|
62
|
+
"follow_up": "Share a two-minute mockup only after they confirm the pain.",
|
|
63
|
+
"success": "Three conversations and one design-partner commitment within seven days."
|
|
64
|
+
},
|
|
65
|
+
"limits": [
|
|
66
|
+
"These are potential customers inferred from public signals, not confirmed buyers.",
|
|
67
|
+
"Verify current relevance before any outreach and keep contact manual."
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Normal mode should contain up to ten qualified prospects. Every primary prospect must include a valid public source URL and a score from 0 to 100.
|
|
73
|
+
|
|
74
|
+
Set `search_scope` to the sources actually searched, not the example above by default. When a locale playbook applied (see [locale-playbooks.md](locale-playbooks.md)), name the actual sources and language, e.g. `"Public Russian-language sources (Habr, VC.ru, hh.ru, Telegram), last 12 months"`.
|
|
75
|
+
|
|
76
|
+
Use `opener` for a short source-grounded message with a concrete manual CTA. Avoid vague CTAs such as "would this be useful?" when a sharper routing ask is available.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Research and Qualification Framework
|
|
2
|
+
|
|
3
|
+
Use this framework to keep prospect research evidence-based, current, and respectful.
|
|
4
|
+
|
|
5
|
+
## Research sequence
|
|
6
|
+
|
|
7
|
+
### Product brief
|
|
8
|
+
|
|
9
|
+
Define:
|
|
10
|
+
|
|
11
|
+
- product and promised outcome
|
|
12
|
+
- primary user and economic buyer
|
|
13
|
+
- urgent job to be done
|
|
14
|
+
- current alternative or workaround
|
|
15
|
+
- likely adoption trigger
|
|
16
|
+
- geography or language constraint
|
|
17
|
+
- clear disqualifiers
|
|
18
|
+
|
|
19
|
+
Do not begin broad lead collection until this brief is specific enough to reject weak matches.
|
|
20
|
+
|
|
21
|
+
### Query buckets
|
|
22
|
+
|
|
23
|
+
Search several buckets rather than repeating one query:
|
|
24
|
+
|
|
25
|
+
1. **Explicit demand:** "looking for," "recommend a tool," "alternative to," "does anything exist."
|
|
26
|
+
2. **Pain:** "takes hours," "manual," "frustrating," "hate," "difficult," "keeps breaking."
|
|
27
|
+
3. **Workaround:** spreadsheets, copy-paste, virtual assistants, scripts, templates, or repeated manual steps.
|
|
28
|
+
4. **Switching:** cancellation, migration, missing feature, pricing complaint, or competitor frustration.
|
|
29
|
+
5. **Timing:** public launch, hiring, expansion, new workflow, regulation, integration, or process change relevant to the product.
|
|
30
|
+
|
|
31
|
+
Adapt wording to the audience's language. Search the original public page and do not qualify from a search snippet alone.
|
|
32
|
+
|
|
33
|
+
### Source mix
|
|
34
|
+
|
|
35
|
+
Useful public sources include:
|
|
36
|
+
|
|
37
|
+
- forums and public community discussions
|
|
38
|
+
- public social posts and replies
|
|
39
|
+
- product reviews and app marketplace reviews
|
|
40
|
+
- GitHub issues and public feature requests
|
|
41
|
+
- public company pages, job posts, changelogs, or announcements
|
|
42
|
+
- public "looking for a tool" posts and directories
|
|
43
|
+
|
|
44
|
+
Avoid private groups, gated communities, data brokers, scraped contact databases, and sources that prohibit access.
|
|
45
|
+
|
|
46
|
+
## Verification rule
|
|
47
|
+
|
|
48
|
+
Qualify only from the original public page, fetched and read — not from a search snippet, an aggregator summary, or memory. Each signal that reaches the shortlist needs: source URL, source title, source type, visible publication date (or "date unavailable"), and a note of what was directly observed versus inferred.
|
|
49
|
+
|
|
50
|
+
Some high-value sources block server-side fetching (Reddit, G2/TrustRadius, Cloudflare-fronted job boards, among others). Do not drop a candidate for that alone — verify through an alternate legitimate route: a browser session, an official API (e.g., HN Algolia for Hacker News), or the site's RSS feed. Third-party archives and cached copies can run months stale; prefer the live page and record which route verified it.
|
|
51
|
+
|
|
52
|
+
## Qualification score
|
|
53
|
+
|
|
54
|
+
Score every dimension from 0 to 5:
|
|
55
|
+
|
|
56
|
+
- **Pain strength (25%)** — directness, severity, repetition, and cost of the stated problem.
|
|
57
|
+
- **Product fit (25%)** — how directly the startup solves the evidenced job.
|
|
58
|
+
- **Timing (20%)** — freshness and presence of a current trigger.
|
|
59
|
+
- **Public reachability (15%)** — a natural, relevant public or professional contact path exists.
|
|
60
|
+
- **Evidence quality (15%)** — specificity, source reliability, and confidence that the signal belongs to the prospect.
|
|
61
|
+
|
|
62
|
+
Calculate:
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
score = pain_strength/5*25
|
|
66
|
+
+ product_fit/5*25
|
|
67
|
+
+ timing/5*20
|
|
68
|
+
+ reachability/5*15
|
|
69
|
+
+ evidence_quality/5*15
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Interpretation:
|
|
73
|
+
|
|
74
|
+
- **80–100:** strong first-customer candidate
|
|
75
|
+
- **65–79:** promising, validate quickly
|
|
76
|
+
- **50–64:** plausible but missing a material signal
|
|
77
|
+
- **Below 50:** do not include in the primary shortlist
|
|
78
|
+
|
|
79
|
+
An old explicit request can still be relevant, but reduce timing and label the date. A company that merely matches the industry without an evidenced trigger is not a qualified prospect.
|
|
80
|
+
|
|
81
|
+
## Prospect stages
|
|
82
|
+
|
|
83
|
+
- **High intent:** publicly requesting a solution or actively switching.
|
|
84
|
+
- **Problem aware:** clearly describing the pain or expensive workaround.
|
|
85
|
+
- **Trigger present:** a current business event makes the product relevant.
|
|
86
|
+
- **Potential fit:** ICP match with incomplete evidence; keep outside the primary shortlist.
|
|
87
|
+
|
|
88
|
+
## Outreach rules
|
|
89
|
+
|
|
90
|
+
Draft one opener using this shape:
|
|
91
|
+
|
|
92
|
+
1. mention the public context naturally
|
|
93
|
+
2. connect it to the buyer's exact business problem
|
|
94
|
+
3. offer a concrete next step
|
|
95
|
+
4. ask one low-friction CTA that can be answered, forwarded, or declined
|
|
96
|
+
|
|
97
|
+
Good CTAs are specific: "Worth sending to whoever owns billing?", "Should I send the teardown?", or "Is a 10-minute workflow review useful this week?" Weak CTAs ask only whether the product "would be useful" without giving the prospect a clear next action.
|
|
98
|
+
|
|
99
|
+
For each prospect, make the outreach recommendation cover:
|
|
100
|
+
|
|
101
|
+
- target role or function
|
|
102
|
+
- official/public contact route
|
|
103
|
+
- primary CTA
|
|
104
|
+
- likely objection or risk
|
|
105
|
+
|
|
106
|
+
Translate the product into the prospect's language. Avoid implementation details, platform mechanics, or internal jargon unless the public source shows the prospect already cares about those details.
|
|
107
|
+
|
|
108
|
+
Keep the opener under 90 words by default. Never claim the message was sent. Do not include private emails, phone numbers, personal addresses, family information, or sensitive traits.
|
|
109
|
+
|
|
110
|
+
## Evidence ledger
|
|
111
|
+
|
|
112
|
+
For each qualified prospect record:
|
|
113
|
+
|
|
114
|
+
- displayed company, project, or public professional name
|
|
115
|
+
- source title and URL
|
|
116
|
+
- visible publication date or "date unavailable"
|
|
117
|
+
- source type
|
|
118
|
+
- concise pain or timing signal
|
|
119
|
+
- observed evidence versus inference
|
|
120
|
+
- score breakdown
|
|
121
|
+
- freshness warning when relevant
|
|
122
|
+
|
|
123
|
+
Cite sources in the chat response whenever web research was performed.
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate a standalone First Customer Finder HTML report from JSON."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import html
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
from urllib.parse import urlparse
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DIMENSIONS = {
|
|
15
|
+
"pain_strength": "Pain strength",
|
|
16
|
+
"product_fit": "Product fit",
|
|
17
|
+
"timing": "Timing",
|
|
18
|
+
"reachability": "Reachability",
|
|
19
|
+
"evidence_quality": "Evidence quality",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
TOOLBAR_STYLES = """
|
|
23
|
+
.toolbar{border:1px solid var(--line);border-radius:var(--radius);background:var(--panel);padding:16px;margin-bottom:16px}
|
|
24
|
+
.toolbar-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
|
|
25
|
+
.toolbar input[type=search],.toolbar select,.toolbar input[type=number]{background:var(--panel2);border:1px solid var(--line);color:var(--ink);border-radius:10px;padding:9px 12px;font:600 13px inherit}
|
|
26
|
+
.toolbar input[type=search]{flex:1 1 220px;min-width:160px}
|
|
27
|
+
.toolbar select{flex:0 0 auto}
|
|
28
|
+
.score-filter{display:flex;align-items:center;gap:8px;color:var(--muted);font:700 12px ui-monospace,monospace;white-space:nowrap}
|
|
29
|
+
.score-filter input{width:64px}
|
|
30
|
+
.toolbar button{background:transparent;border:1px solid var(--line);color:var(--muted);border-radius:999px;padding:9px 14px;font:700 12px inherit;cursor:pointer}
|
|
31
|
+
.toolbar button:hover{color:var(--ink);border-color:var(--acid)}
|
|
32
|
+
.toolbar-status{margin:12px 2px 0;color:var(--muted);font:700 11px ui-monospace,monospace;text-transform:uppercase;letter-spacing:.06em}
|
|
33
|
+
#noResults{color:var(--muted);padding:24px;text-align:center;border:1px dashed var(--line);border-radius:var(--radius);margin-bottom:76px}
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
TOOLBAR_SCRIPT = """
|
|
37
|
+
(function(){
|
|
38
|
+
var list = document.getElementById('prospectsList');
|
|
39
|
+
if(!list) return;
|
|
40
|
+
var cards = Array.prototype.slice.call(list.querySelectorAll('.prospect'));
|
|
41
|
+
var searchInput = document.getElementById('searchInput');
|
|
42
|
+
var stageFilter = document.getElementById('stageFilter');
|
|
43
|
+
var sourceFilter = document.getElementById('sourceFilter');
|
|
44
|
+
var minScore = document.getElementById('minScore');
|
|
45
|
+
var sortSelect = document.getElementById('sortSelect');
|
|
46
|
+
var resetBtn = document.getElementById('resetFilters');
|
|
47
|
+
var resultCount = document.getElementById('resultCount');
|
|
48
|
+
var noResults = document.getElementById('noResults');
|
|
49
|
+
var total = cards.length;
|
|
50
|
+
if(!searchInput || !stageFilter || !sourceFilter || !minScore || !sortSelect) return;
|
|
51
|
+
|
|
52
|
+
function sortCards(list){
|
|
53
|
+
var mode = sortSelect.value;
|
|
54
|
+
return list.slice().sort(function(a, b){
|
|
55
|
+
if(mode === 'name-asc') return a.dataset.name.localeCompare(b.dataset.name);
|
|
56
|
+
var sa = Number(a.dataset.score), sb = Number(b.dataset.score);
|
|
57
|
+
return mode === 'score-asc' ? sa - sb : sb - sa;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function apply(){
|
|
62
|
+
var q = searchInput.value.trim().toLowerCase();
|
|
63
|
+
var stage = stageFilter.value;
|
|
64
|
+
var source = sourceFilter.value;
|
|
65
|
+
var min = Number(minScore.value) || 0;
|
|
66
|
+
var visible = 0;
|
|
67
|
+
sortCards(cards).forEach(function(card, i){
|
|
68
|
+
var matches = Number(card.dataset.score) >= min
|
|
69
|
+
&& (!stage || card.dataset.stage === stage)
|
|
70
|
+
&& (!source || card.dataset.source === source)
|
|
71
|
+
&& (!q || card.dataset.name.toLowerCase().indexOf(q) !== -1);
|
|
72
|
+
card.style.display = matches ? '' : 'none';
|
|
73
|
+
card.style.order = String(i);
|
|
74
|
+
if(matches){
|
|
75
|
+
visible += 1;
|
|
76
|
+
var rank = card.querySelector('.rank');
|
|
77
|
+
if(rank) rank.textContent = String(visible).padStart(2, '0');
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
if(resultCount) resultCount.textContent = 'Showing ' + visible + ' of ' + total + ' prospects';
|
|
81
|
+
if(noResults) noResults.style.display = visible ? 'none' : '';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
[searchInput, stageFilter, sourceFilter, minScore, sortSelect].forEach(function(el){
|
|
85
|
+
el.addEventListener('input', apply);
|
|
86
|
+
el.addEventListener('change', apply);
|
|
87
|
+
});
|
|
88
|
+
if(resetBtn){
|
|
89
|
+
resetBtn.addEventListener('click', function(){
|
|
90
|
+
searchInput.value = '';
|
|
91
|
+
stageFilter.value = '';
|
|
92
|
+
sourceFilter.value = '';
|
|
93
|
+
minScore.value = '0';
|
|
94
|
+
sortSelect.value = 'score-desc';
|
|
95
|
+
apply();
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
apply();
|
|
99
|
+
})();
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def esc(value: Any) -> str:
|
|
104
|
+
return html.escape(str(value if value is not None else ""), quote=True)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def clamp(value: Any, maximum: int = 100) -> int:
|
|
108
|
+
try:
|
|
109
|
+
number = round(float(value))
|
|
110
|
+
except (TypeError, ValueError):
|
|
111
|
+
number = 0
|
|
112
|
+
return max(0, min(maximum, number))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def items(value: Any) -> list[Any]:
|
|
116
|
+
if value is None:
|
|
117
|
+
return []
|
|
118
|
+
return value if isinstance(value, list) else [value]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def safe_url(value: Any) -> str:
|
|
122
|
+
raw = str(value or "").strip()
|
|
123
|
+
parsed = urlparse(raw)
|
|
124
|
+
return esc(raw) if parsed.scheme in {"http", "https"} and parsed.netloc else "#"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def stage_class(stage: Any) -> str:
|
|
128
|
+
value = str(stage or "").lower()
|
|
129
|
+
if "high" in value:
|
|
130
|
+
return "hot"
|
|
131
|
+
if "problem" in value or "trigger" in value:
|
|
132
|
+
return "warm"
|
|
133
|
+
return "cool"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def render_dimensions(data: dict[str, Any]) -> str:
|
|
137
|
+
rows = []
|
|
138
|
+
for key, label in DIMENSIONS.items():
|
|
139
|
+
score = clamp(data.get(key, 0), 5)
|
|
140
|
+
rows.append(
|
|
141
|
+
f'<div class="metric"><span>{esc(label)}</span><div class="track"><i style="width:{score * 20}%"></i></div><b>{score}/5</b></div>'
|
|
142
|
+
)
|
|
143
|
+
return "".join(rows)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def render_filter_options(prospects: list[dict[str, Any]], key: str, default: str) -> str:
|
|
147
|
+
seen: dict[str, None] = {}
|
|
148
|
+
for prospect in prospects:
|
|
149
|
+
value = str(prospect.get(key) or default).strip()
|
|
150
|
+
if value:
|
|
151
|
+
seen.setdefault(value, None)
|
|
152
|
+
return "".join(f'<option value="{esc(v)}">{esc(v)}</option>' for v in sorted(seen))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def render_prospect(prospect: dict[str, Any], index: int) -> str:
|
|
156
|
+
score = clamp(prospect.get("score"))
|
|
157
|
+
source = safe_url(prospect.get("source_url"))
|
|
158
|
+
name = esc(prospect.get("name", f"Prospect {index}"))
|
|
159
|
+
stage = esc(prospect.get("stage", "Potential fit"))
|
|
160
|
+
source_type = esc(prospect.get("source_type", "Public source"))
|
|
161
|
+
return f"""
|
|
162
|
+
<article class="prospect reveal" data-score="{score}" data-stage="{stage}" data-source="{source_type}" data-name="{name}">
|
|
163
|
+
<header class="prospect-head">
|
|
164
|
+
<div class="rank">{index:02d}</div>
|
|
165
|
+
<div class="identity">
|
|
166
|
+
<span class="eyebrow">{esc(prospect.get('type', 'Public prospect'))}</span>
|
|
167
|
+
<h3>{name}</h3>
|
|
168
|
+
<span class="stage {stage_class(prospect.get('stage'))}">{stage}</span>
|
|
169
|
+
</div>
|
|
170
|
+
<div class="score" style="--score:{score}" aria-label="Fit score {score} out of 100"><strong>{score}</strong><small>/100</small></div>
|
|
171
|
+
</header>
|
|
172
|
+
<div class="signal"><span>Public signal</span><p>{esc(prospect.get('pain_signal', ''))}</p></div>
|
|
173
|
+
<div class="prospect-grid">
|
|
174
|
+
<div><span>Why it fits</span><p>{esc(prospect.get('why_fit', ''))}</p></div>
|
|
175
|
+
<div><span>Why now</span><p>{esc(prospect.get('why_now', ''))}</p></div>
|
|
176
|
+
<div><span>Suggested channel</span><p>{esc(prospect.get('suggested_channel', ''))}</p></div>
|
|
177
|
+
<div><span>Caution</span><p>{esc(prospect.get('caution', 'Confirm current relevance before outreach.'))}</p></div>
|
|
178
|
+
</div>
|
|
179
|
+
<blockquote><span>Suggested opener</span>{esc(prospect.get('opener', ''))}</blockquote>
|
|
180
|
+
<details>
|
|
181
|
+
<summary>Evidence and score breakdown</summary>
|
|
182
|
+
<div class="evidence"><div><span>Evidence</span><p>{esc(prospect.get('evidence', ''))}</p></div><div><span>Source</span><p>{source_type} · {esc(prospect.get('signal_date', 'Date unavailable'))}</p><a href="{source}" target="_blank" rel="noreferrer">{esc(prospect.get('source_title', 'Open original source'))} ↗</a></div></div>
|
|
183
|
+
<div class="metrics">{render_dimensions(prospect.get('dimensions') if isinstance(prospect.get('dimensions'), dict) else {})}</div>
|
|
184
|
+
</details>
|
|
185
|
+
</article>"""
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def render_pattern(pattern: dict[str, Any], index: int) -> str:
|
|
189
|
+
return f"""
|
|
190
|
+
<article class="pattern reveal"><span class="pattern-num">{index:02d}</span><div><h3>{esc(pattern.get('title', 'Repeated signal'))}</h3><p>{esc(pattern.get('insight', ''))}</p></div><strong>{clamp(pattern.get('count'), 999)}×</strong></article>"""
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def build_html(data: dict[str, Any]) -> str:
|
|
194
|
+
prospects = [x for x in items(data.get("prospects")) if isinstance(x, dict)]
|
|
195
|
+
patterns = [x for x in items(data.get("patterns")) if isinstance(x, dict)]
|
|
196
|
+
scores = [clamp(x.get("score")) for x in prospects]
|
|
197
|
+
high_intent = sum(1 for x in prospects if "high" in str(x.get("stage", "")).lower())
|
|
198
|
+
average = round(sum(scores) / len(scores)) if scores else 0
|
|
199
|
+
top = max(prospects, key=lambda x: clamp(x.get("score")), default={})
|
|
200
|
+
plan = data.get("outreach_plan") if isinstance(data.get("outreach_plan"), dict) else {}
|
|
201
|
+
limits = "".join(f"<li>{esc(x)}</li>" for x in items(data.get("limits")))
|
|
202
|
+
prospect_html = "".join(render_prospect(x, i) for i, x in enumerate(prospects, 1))
|
|
203
|
+
pattern_html = "".join(render_pattern(x, i) for i, x in enumerate(patterns, 1))
|
|
204
|
+
product_url = safe_url(data.get("product_url"))
|
|
205
|
+
stage_options = render_filter_options(prospects, "stage", "Potential fit")
|
|
206
|
+
source_options = render_filter_options(prospects, "source_type", "Public source")
|
|
207
|
+
toolbar_html = f"""
|
|
208
|
+
<div class="toolbar" role="search" aria-label="Filter and sort prospects">
|
|
209
|
+
<div class="toolbar-row">
|
|
210
|
+
<input type="search" id="searchInput" placeholder="Search name or company…" aria-label="Search prospects">
|
|
211
|
+
<select id="stageFilter" aria-label="Filter by stage"><option value="">All stages</option>{stage_options}</select>
|
|
212
|
+
<select id="sourceFilter" aria-label="Filter by source type"><option value="">All sources</option>{source_options}</select>
|
|
213
|
+
<label class="score-filter">Min score <input type="number" id="minScore" min="0" max="100" value="0" aria-label="Minimum score"></label>
|
|
214
|
+
<select id="sortSelect" aria-label="Sort by">
|
|
215
|
+
<option value="score-desc">Score: high to low</option>
|
|
216
|
+
<option value="score-asc">Score: low to high</option>
|
|
217
|
+
<option value="name-asc">Name: A-Z</option>
|
|
218
|
+
</select>
|
|
219
|
+
<button type="button" id="resetFilters">Reset</button>
|
|
220
|
+
</div>
|
|
221
|
+
<p class="toolbar-status" id="resultCount" aria-live="polite"></p>
|
|
222
|
+
</div>"""
|
|
223
|
+
|
|
224
|
+
return f"""<!doctype html>
|
|
225
|
+
<html lang="en">
|
|
226
|
+
<head>
|
|
227
|
+
<meta charset="utf-8">
|
|
228
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
229
|
+
<meta name="color-scheme" content="dark">
|
|
230
|
+
<title>{esc(data.get('title', 'First Customer Finder'))}</title>
|
|
231
|
+
<style>
|
|
232
|
+
:root{{--bg:#090b0f;--panel:#12161d;--panel2:#191f28;--ink:#f5f2ea;--muted:#9ca6b5;--line:#2b3340;--acid:#d9ff63;--blue:#69b7ff;--orange:#ff8f5a;--cyan:#5ee8d0;--radius:18px;--shadow:0 24px 80px rgba(0,0,0,.38)}}
|
|
233
|
+
*{{box-sizing:border-box}}html{{scroll-behavior:smooth}}body{{margin:0;background:var(--bg);color:var(--ink);font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;line-height:1.55}}body:before{{content:"";position:fixed;inset:0;pointer-events:none;opacity:.3;background:repeating-linear-gradient(135deg,rgba(255,255,255,.018) 0 1px,transparent 1px 12px);mask-image:linear-gradient(#000,transparent 82%)}}a{{color:inherit}}.skip{{position:absolute;left:-9999px}}.skip:focus{{left:16px;top:16px;z-index:9;background:var(--acid);color:#111;padding:10px}}
|
|
234
|
+
.shell{{width:min(1180px,calc(100% - 40px));margin:auto;position:relative}}.top{{display:flex;justify-content:space-between;align-items:center;padding:22px 0;border-bottom:1px solid var(--line)}}.brand{{font-weight:850;display:flex;align-items:center;gap:10px}}.brand i{{width:12px;height:12px;border-radius:50%;background:var(--acid);box-shadow:0 0 22px var(--acid)}}.meta{{display:flex;gap:8px;align-items:center}}.chip,.stage{{border:1px solid var(--line);border-radius:999px;padding:7px 10px;color:var(--muted);font:750 11px ui-monospace,monospace;text-transform:uppercase;letter-spacing:.07em}}button{{background:var(--ink);color:#111;border:0;border-radius:999px;padding:9px 14px;font:800 13px inherit;cursor:pointer}}button:focus-visible,summary:focus-visible,a:focus-visible{{outline:3px solid var(--blue);outline-offset:3px}}
|
|
235
|
+
.hero{{display:grid;grid-template-columns:1.45fr .55fr;gap:36px;align-items:end;padding:72px 0 42px}}.eyebrow{{color:var(--acid);font:750 11px ui-monospace,monospace;text-transform:uppercase;letter-spacing:.11em}}h1{{font-size:clamp(48px,7vw,92px);line-height:.92;letter-spacing:-.065em;margin:13px 0 24px}}.verdict{{font-size:clamp(18px,2.1vw,25px);color:#dce1e9;max-width:820px;margin:0}}.hero-card{{background:var(--acid);color:#10120d;padding:26px;border-radius:var(--radius);box-shadow:var(--shadow);transform:rotate(1deg)}}.hero-card span{{font:800 11px ui-monospace,monospace;text-transform:uppercase;letter-spacing:.08em}}.hero-card strong{{display:block;font-size:64px;line-height:1;letter-spacing:-.08em;margin:15px 0 6px}}.hero-card p{{margin:0;font-weight:700;font-size:13px}}
|
|
236
|
+
.stats{{display:grid;grid-template-columns:repeat(4,1fr);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;margin-bottom:28px}}.stats>div{{padding:18px;background:var(--panel)}}.stats>div+div{{border-left:1px solid var(--line)}}.stats span,.prospect-grid span,.signal span,.evidence span,blockquote span{{display:block;color:var(--muted);font:700 10px ui-monospace,monospace;text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px}}.stats strong{{font-size:17px}}
|
|
237
|
+
.best{{display:grid;grid-template-columns:180px 1fr auto;gap:24px;align-items:center;padding:25px;background:var(--orange);color:#1b0f09;border-radius:var(--radius);box-shadow:var(--shadow);margin:0 0 72px}}.best-label{{font:800 11px ui-monospace,monospace;text-transform:uppercase;letter-spacing:.1em}}.best h2{{font-size:clamp(28px,4vw,46px);letter-spacing:-.045em;line-height:1;margin:0 0 8px}}.best p{{margin:0}}.best strong{{font-size:44px}}
|
|
238
|
+
.section-head{{display:flex;justify-content:space-between;align-items:end;gap:24px;padding-bottom:18px;border-bottom:1px solid var(--line);margin-bottom:18px}}.section-head h2{{font-size:clamp(34px,5vw,62px);line-height:.98;letter-spacing:-.055em;margin:0}}.section-head p{{color:var(--muted);max-width:470px;margin:0}}
|
|
239
|
+
.prospects{{display:grid;gap:16px;margin-bottom:76px}}.prospect{{background:linear-gradient(135deg,var(--panel),#0f1319);border:1px solid var(--line);border-radius:var(--radius);padding:25px;box-shadow:0 12px 38px rgba(0,0,0,.18)}}.prospect-head{{display:grid;grid-template-columns:54px 1fr 92px;gap:17px;align-items:start}}.rank{{font:850 24px ui-monospace,monospace;color:var(--acid);padding-top:8px}}.identity h3{{font-size:29px;letter-spacing:-.04em;margin:6px 0 10px}}.stage{{display:inline-block}}.stage.hot{{color:var(--orange);border-color:rgba(255,143,90,.5)}}.stage.warm{{color:var(--cyan);border-color:rgba(94,232,208,.45)}}.score{{--score:0;width:88px;height:88px;border-radius:50%;display:grid;place-content:center;text-align:center;background:radial-gradient(circle,var(--panel) 58%,transparent 60%),conic-gradient(var(--acid) calc(var(--score)*1%),var(--line) 0)}}.score strong{{font-size:26px;line-height:1}}.score small{{color:var(--muted)}}
|
|
240
|
+
.signal{{margin:21px 0 12px;padding:16px;background:var(--panel2);border-left:4px solid var(--acid)}}.signal p{{font-size:17px;margin:0}}.prospect-grid{{display:grid;grid-template-columns:1fr 1fr;gap:9px}}.prospect-grid>div,.evidence>div{{border:1px solid var(--line);border-radius:12px;padding:15px;background:rgba(255,255,255,.018)}}.prospect-grid p,.evidence p{{margin:0}}blockquote{{margin:12px 0 0;padding:16px;border:1px dashed rgba(217,255,99,.55);border-radius:12px;color:#eaf6c6}}details{{margin-top:10px}}summary{{cursor:pointer;color:var(--acid);font-weight:800;padding:10px 0}}.evidence{{display:grid;grid-template-columns:1fr 1fr;gap:9px}}.evidence a{{display:inline-block;color:var(--blue);margin-top:8px;word-break:break-word}}.metrics{{display:grid;gap:8px;margin-top:14px}}.metric{{display:grid;grid-template-columns:145px 1fr 40px;gap:12px;align-items:center;font-size:13px}}.track{{height:8px;background:var(--line);border-radius:8px;overflow:hidden}}.track i{{display:block;height:100%;background:linear-gradient(90deg,var(--blue),var(--acid))}}
|
|
241
|
+
.patterns{{display:grid;grid-template-columns:repeat(2,1fr);gap:12px;margin-bottom:76px}}.pattern{{display:grid;grid-template-columns:42px 1fr auto;gap:15px;align-items:start;padding:20px;border:1px solid var(--line);border-radius:14px;background:var(--panel)}}.pattern-num{{color:var(--acid);font:800 13px ui-monospace,monospace}}.pattern h3{{margin:0 0 5px}}.pattern p{{margin:0;color:var(--muted)}}.pattern>strong{{font-size:30px;color:var(--acid)}}
|
|
242
|
+
.plan{{display:grid;grid-template-columns:.8fr 1.2fr;gap:28px;background:var(--acid);color:#111;padding:29px;border-radius:var(--radius);margin-bottom:30px}}.plan h2{{font-size:38px;line-height:1;letter-spacing:-.045em;margin:10px 0}}.plan-grid{{display:grid;grid-template-columns:1fr 1fr;gap:9px}}.plan-grid>div{{border:1px solid rgba(0,0,0,.24);border-radius:11px;padding:13px}}.plan-grid span{{display:block;font:750 10px ui-monospace,monospace;text-transform:uppercase;letter-spacing:.07em;margin-bottom:5px}}.plan-grid p{{margin:0}}.limits{{border:1px solid var(--line);border-radius:var(--radius);padding:24px;color:var(--muted);margin-bottom:60px}}.limits h2{{color:var(--ink);margin-top:0}}footer{{display:flex;justify-content:space-between;gap:20px;border-top:1px solid var(--line);padding:22px 0 40px;color:var(--muted);font-size:12px}}.reveal{{animation:rise .4s ease both}}@keyframes rise{{from{{opacity:0;transform:translateY(12px)}}}}
|
|
243
|
+
@media(max-width:820px){{.shell{{width:min(100% - 24px,1180px)}}.hero,.plan{{grid-template-columns:1fr}}.hero{{padding-top:44px}}.hero-card{{transform:none}}.stats{{grid-template-columns:1fr 1fr}}.stats>div+div{{border-left:0;border-top:1px solid var(--line)}}.best{{grid-template-columns:1fr}}.prospect-head{{grid-template-columns:38px 1fr}}.score{{grid-column:1/-1}}.prospect-grid,.evidence,.patterns,.plan-grid{{grid-template-columns:1fr}}.meta .chip{{display:none}}}}
|
|
244
|
+
@media(prefers-reduced-motion:reduce){{*{{animation:none!important;transition:none!important;scroll-behavior:auto!important}}}}@media print{{body{{background:#fff;color:#111}}body:before,.top button,.toolbar{{display:none}}.shell{{width:100%}}.prospect,.pattern,.limits{{background:#fff;color:#111;break-inside:avoid}}.prospect-grid span,.signal span,.evidence span,blockquote span{{color:#444}}}}
|
|
245
|
+
{TOOLBAR_STYLES}
|
|
246
|
+
</style>
|
|
247
|
+
</head>
|
|
248
|
+
<body>
|
|
249
|
+
<a class="skip" href="#main">Skip to report</a>
|
|
250
|
+
<div class="shell">
|
|
251
|
+
<header class="top"><div class="brand"><i></i> First Customer Finder</div><div class="meta"><span class="chip">Public signals only</span><button type="button" onclick="window.print()">Print / Save PDF</button></div></header>
|
|
252
|
+
<main id="main">
|
|
253
|
+
<section class="hero"><div><span class="eyebrow">Early-customer report · {esc(data.get('generated_at', ''))}</span><h1>{esc(data.get('title', 'First Customer Finder'))}</h1><p class="verdict">{esc(data.get('verdict', 'No verdict supplied.'))}</p></div><aside class="hero-card"><span>Qualified prospects</span><strong>{len(prospects)}</strong><p>Potential customers based on public signals.</p></aside></section>
|
|
254
|
+
<section class="stats"><div><span>Product</span><strong><a href="{product_url}" target="_blank" rel="noreferrer">{esc(data.get('product', 'Not specified'))}</a></strong></div><div><span>Target customer</span><strong>{esc(data.get('target_customer', 'Not specified'))}</strong></div><div><span>High intent</span><strong>{high_intent}</strong></div><div><span>Average fit score</span><strong>{average}/100</strong></div></section>
|
|
255
|
+
<section class="best"><div class="best-label">Highest-confidence prospect</div><div><h2>{esc(top.get('name', 'No qualified prospect'))}</h2><p>{esc(top.get('why_now', top.get('pain_signal', '')))}</p></div><strong>{clamp(top.get('score'))}</strong></section>
|
|
256
|
+
<section><header class="section-head"><h2>People with a reason<br>to care now.</h2><p>Every primary prospect is tied to a public pain, demand, or timing signal. Open the evidence before considering outreach.</p></header>{toolbar_html if prospects else ''}<div class="prospects" id="prospectsList">{prospect_html or '<p>No qualified prospects supplied.</p>'}</div><p id="noResults" class="hidden" style="display:none">No prospects match these filters.</p></section>
|
|
257
|
+
<section><header class="section-head"><h2>Signals that repeat.</h2><p>Patterns across prospects reveal the strongest positioning, workflow, and outreach angles.</p></header><div class="patterns">{pattern_html or '<p>No repeated patterns supplied.</p>'}</div></section>
|
|
258
|
+
<section class="plan"><div><span class="eyebrow" style="color:#111">Seven-day manual plan</span><h2>{esc(plan.get('angle', 'Validate the pain before pitching the product.'))}</h2></div><div class="plan-grid"><div><span>First step</span><p>{esc(plan.get('first_step', ''))}</p></div><div><span>Follow-up</span><p>{esc(plan.get('follow_up', ''))}</p></div><div><span>Success signal</span><p>{esc(plan.get('success', ''))}</p></div><div><span>Research scope</span><p>{esc(data.get('search_scope', 'Not specified'))}</p></div></div></section>
|
|
259
|
+
<section class="limits"><h2>Use this shortlist responsibly</h2><ul>{limits or '<li>These are potential customers inferred from public signals, not confirmed buyers.</li>'}</ul></section>
|
|
260
|
+
</main>
|
|
261
|
+
<footer><span>Generated by $first-customer-finder</span><span>Outreach is never sent automatically.</span></footer>
|
|
262
|
+
</div>
|
|
263
|
+
<script>{TOOLBAR_SCRIPT}</script>
|
|
264
|
+
</body>
|
|
265
|
+
</html>"""
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def main() -> None:
|
|
269
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
270
|
+
parser.add_argument("input", type=Path, help="Path to report JSON")
|
|
271
|
+
parser.add_argument("output", type=Path, help="Path to output HTML")
|
|
272
|
+
args = parser.parse_args()
|
|
273
|
+
|
|
274
|
+
with args.input.open("r", encoding="utf-8") as handle:
|
|
275
|
+
data = json.load(handle)
|
|
276
|
+
if not isinstance(data, dict):
|
|
277
|
+
raise SystemExit("Input JSON must contain an object at the top level.")
|
|
278
|
+
|
|
279
|
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
280
|
+
args.output.write_text(build_html(data), encoding="utf-8")
|
|
281
|
+
print(f"Created report: {args.output.resolve()}")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
if __name__ == "__main__":
|
|
285
|
+
main()
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "first-customer-finder-skill",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A Claude Code skill that finds evidence-backed potential first customers from recent public signals.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "z0rats",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/z0rats/first-customer-finder-skill.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/z0rats/first-customer-finder-skill#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/z0rats/first-customer-finder-skill/issues"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"first-customer-finder-skill": "scripts/install.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"first-customer-finder",
|
|
20
|
+
".claude-plugin",
|
|
21
|
+
"scripts/install.js",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"keywords": [
|
|
26
|
+
"claude",
|
|
27
|
+
"claude-code",
|
|
28
|
+
"skill",
|
|
29
|
+
"startup",
|
|
30
|
+
"customers",
|
|
31
|
+
"early-adopters",
|
|
32
|
+
"prospecting",
|
|
33
|
+
"customer-discovery",
|
|
34
|
+
"founders"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
function usage() {
|
|
8
|
+
console.log(`
|
|
9
|
+
Claude First Customer Finder Skill installer
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
npx first-customer-finder-skill
|
|
13
|
+
first-customer-finder-skill --skills-dir ~/.claude/skills
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--skills-dir PATH Install into a custom Claude Code skills directory
|
|
17
|
+
--help Show this help
|
|
18
|
+
`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function expandHome(value) {
|
|
22
|
+
if (!value) return value;
|
|
23
|
+
if (value === "~") return os.homedir();
|
|
24
|
+
if (value.startsWith("~/")) return path.join(os.homedir(), value.slice(2));
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseArgs(argv) {
|
|
29
|
+
const options = {};
|
|
30
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
31
|
+
const arg = argv[index];
|
|
32
|
+
if (arg === "--help" || arg === "-h") {
|
|
33
|
+
options.help = true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (arg === "--skills-dir") {
|
|
37
|
+
const value = argv[index + 1];
|
|
38
|
+
if (!value) throw new Error("--skills-dir requires a value");
|
|
39
|
+
options.skillsDir = expandHome(value);
|
|
40
|
+
index += 1;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
44
|
+
}
|
|
45
|
+
return options;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function defaultSkillsDir() {
|
|
49
|
+
const claudeHome = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
|
|
50
|
+
return path.join(claudeHome, "skills");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function copyDirectory(source, destination) {
|
|
54
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
55
|
+
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
56
|
+
const sourcePath = path.join(source, entry.name);
|
|
57
|
+
const destinationPath = path.join(destination, entry.name);
|
|
58
|
+
if (entry.isDirectory()) copyDirectory(sourcePath, destinationPath);
|
|
59
|
+
else if (entry.isFile()) fs.copyFileSync(sourcePath, destinationPath);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function main() {
|
|
64
|
+
const options = parseArgs(process.argv.slice(2));
|
|
65
|
+
if (options.help) {
|
|
66
|
+
usage();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const source = path.resolve(__dirname, "..", "first-customer-finder");
|
|
71
|
+
const skillsDir = path.resolve(options.skillsDir || defaultSkillsDir());
|
|
72
|
+
const destination = path.join(skillsDir, "first-customer-finder");
|
|
73
|
+
if (!fs.existsSync(source)) throw new Error(`Cannot find bundled skill at ${source}`);
|
|
74
|
+
|
|
75
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
76
|
+
fs.rmSync(destination, { recursive: true, force: true });
|
|
77
|
+
copyDirectory(source, destination);
|
|
78
|
+
|
|
79
|
+
console.log("Installed first-customer-finder skill.");
|
|
80
|
+
console.log(`Location: ${destination}`);
|
|
81
|
+
console.log("");
|
|
82
|
+
console.log("Restart Claude Code, then run:");
|
|
83
|
+
console.log(" Find ten potential first customers for https://example.com.");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
main();
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error(`Error: ${error.message}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|