skannr 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/languages/TreeSitterAdapter.d.ts +30 -0
- package/dist/languages/TreeSitterAdapter.d.ts.map +1 -0
- package/dist/languages/TreeSitterAdapter.js +466 -0
- package/dist/languages/TreeSitterAdapter.js.map +1 -0
- package/dist/languages/grammars/tree-sitter-go.wasm +0 -0
- package/dist/languages/grammars/tree-sitter-java.wasm +0 -0
- package/dist/languages/grammars/tree-sitter-python.wasm +0 -0
- package/dist/languages/grammars/tree-sitter-rust.wasm +0 -0
- package/dist/languages/lang-config.d.ts +38 -0
- package/dist/languages/lang-config.d.ts.map +1 -0
- package/dist/languages/lang-config.js +85 -0
- package/dist/languages/lang-config.js.map +1 -0
- package/dist/languages/registry.d.ts +6 -0
- package/dist/languages/registry.d.ts.map +1 -1
- package/dist/languages/registry.js +18 -4
- package/dist/languages/registry.js.map +1 -1
- package/package.json +4 -7
- package/src/languages/TreeSitterAdapter.ts +431 -0
- package/src/languages/lang-config.ts +113 -0
- package/src/languages/registry.ts +65 -49
- package/src/languages/PythonAdapter.ts +0 -302
- package/tests/agent.tools.test.ts +0 -81
- package/tests/benchmark.test.ts +0 -31
- package/tests/blast-radius.test.ts +0 -290
- package/tests/fixtures/sample.py +0 -17
- package/tests/fixtures/sample.ts +0 -13
- package/tests/fixtures/src/api/routes.ts +0 -1
- package/tests/fixtures/src/auth/permission.ts +0 -3
- package/tests/python-adapter.test.ts +0 -31
- package/tests/ranker-enhanced.test.ts +0 -70
- package/tests/ranker.test.ts +0 -79
- package/tests/scanner.scope.test.ts +0 -67
- package/tests/setup-fixtures.js +0 -29
- package/tests/skeletonizer.test.ts +0 -149
- package/uca-landing/index.html +0 -17
- package/uca-landing/package.json +0 -23
- package/uca-landing/postcss.config.js +0 -6
- package/uca-landing/src/App.jsx +0 -45
- package/uca-landing/src/components/AgentMode.jsx +0 -45
- package/uca-landing/src/components/CliReference.jsx +0 -89
- package/uca-landing/src/components/Features.jsx +0 -88
- package/uca-landing/src/components/Footer.jsx +0 -35
- package/uca-landing/src/components/Hero.jsx +0 -125
- package/uca-landing/src/components/HowItWorks.jsx +0 -65
- package/uca-landing/src/components/Install.jsx +0 -103
- package/uca-landing/src/components/LanguageSupport.jsx +0 -63
- package/uca-landing/src/components/Navbar.jsx +0 -86
- package/uca-landing/src/components/Problem.jsx +0 -51
- package/uca-landing/src/components/Reveal.jsx +0 -40
- package/uca-landing/src/components/TokenSavings.jsx +0 -126
- package/uca-landing/src/components/WorksWith.jsx +0 -59
- package/uca-landing/src/hooks/useScrollNav.js +0 -13
- package/uca-landing/src/hooks/useTypewriter.js +0 -41
- package/uca-landing/src/index.css +0 -13
- package/uca-landing/src/main.jsx +0 -10
- package/uca-landing/tailwind.config.js +0 -68
- package/uca-landing/vercel.json +0 -4
- package/uca-landing/vite.config.js +0 -6
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import Reveal from './Reveal'
|
|
2
|
-
|
|
3
|
-
const steps = [
|
|
4
|
-
{
|
|
5
|
-
title: 'Point it at any repo',
|
|
6
|
-
code: 'skannr "how does auth work?"',
|
|
7
|
-
desc: "No configuration needed. Works on repos you've never touched before. Auto-detects language and module structure from the folder layout.",
|
|
8
|
-
},
|
|
9
|
-
{
|
|
10
|
-
title: 'Modules discovered automatically',
|
|
11
|
-
desc: 'Reads the folder structure and identifies logical modules - src/auth, src/api, lib/db. No config file required, though one is supported for fine-tuned control.',
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
title: 'Hybrid ranking finds the right files',
|
|
15
|
-
desc: 'Three signals combined: lexical match (keyword relevance), structural analysis (export/import density), and dependency graph centrality. Finds architecturally important files - not just keyword matches.',
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
title: 'Skeletons compress without losing meaning',
|
|
19
|
-
desc: 'Function bodies stripped. Signatures, types, interfaces, and imports remain. The AI gets the full architectural picture at 3% of the token cost. Citations tell you exactly which files were used.',
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
title: 'Check risk before you push',
|
|
23
|
-
code: 'skannr risk',
|
|
24
|
-
desc: 'Analyzes your uncommitted changes, traverses the dependency graph to find downstream affected files, flags untested code, and gives you a 0-10 risk score. Deterministic, instant, no API call.',
|
|
25
|
-
},
|
|
26
|
-
]
|
|
27
|
-
|
|
28
|
-
export default function HowItWorks() {
|
|
29
|
-
return (
|
|
30
|
-
<section id="how-it-works" className="py-24 px-5">
|
|
31
|
-
<div className="max-w-5xl mx-auto">
|
|
32
|
-
<Reveal>
|
|
33
|
-
<h2 className="text-3xl md:text-5xl font-bold text-center tracking-tight">
|
|
34
|
-
Simple to use. Sophisticated underneath.
|
|
35
|
-
</h2>
|
|
36
|
-
</Reveal>
|
|
37
|
-
|
|
38
|
-
<div className="mt-14 overflow-hidden rounded-xl border border-border bg-card px-5 md:px-10">
|
|
39
|
-
{steps.map((step, idx) => (
|
|
40
|
-
<Reveal key={step.title} delay={idx * 100} className="w-full">
|
|
41
|
-
<div
|
|
42
|
-
className={`group grid gap-6 py-10 md:grid-cols-[120px,1fr] ${idx < steps.length - 1 ? 'border-b border-white/5' : ''}`}
|
|
43
|
-
>
|
|
44
|
-
<span className="font-mono text-7xl font-bold leading-none text-white/5 transition-colors duration-500 group-hover:text-accent/25">
|
|
45
|
-
{String(idx + 1).padStart(2, '0')}
|
|
46
|
-
</span>
|
|
47
|
-
<div>
|
|
48
|
-
<h3 className="text-2xl font-semibold transition-colors duration-300 group-hover:text-white">
|
|
49
|
-
{step.title}
|
|
50
|
-
</h3>
|
|
51
|
-
{step.code && (
|
|
52
|
-
<code className="mt-4 block w-fit rounded-lg border border-border px-3 py-2 font-mono text-sm text-accent transition-all duration-300 group-hover:border-accent/30 group-hover:shadow-[0_0_20px_-8px_rgba(0,212,255,0.35)]">
|
|
53
|
-
{step.code}
|
|
54
|
-
</code>
|
|
55
|
-
)}
|
|
56
|
-
<p className="mt-4 text-[#888]">{step.desc}</p>
|
|
57
|
-
</div>
|
|
58
|
-
</div>
|
|
59
|
-
</Reveal>
|
|
60
|
-
))}
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
</section>
|
|
64
|
-
)
|
|
65
|
-
}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
|
-
import Reveal from './Reveal'
|
|
3
|
-
|
|
4
|
-
const tabs = ['CLI Install', 'npx', 'MCP Server']
|
|
5
|
-
|
|
6
|
-
const snippets = {
|
|
7
|
-
'CLI Install': `npm install -g skannr
|
|
8
|
-
|
|
9
|
-
# Ask about any codebase
|
|
10
|
-
skannr "how does auth work?"
|
|
11
|
-
|
|
12
|
-
# Limit results, JSON output
|
|
13
|
-
skannr "database queries" -n 5 --json
|
|
14
|
-
|
|
15
|
-
# Check risk before pushing
|
|
16
|
-
skannr risk
|
|
17
|
-
skannr risk --diff feature.patch --json
|
|
18
|
-
|
|
19
|
-
# Interactive agent mode
|
|
20
|
-
skannr agent`,
|
|
21
|
-
npx: `# Run without installing
|
|
22
|
-
npx skannr "how does this work?"
|
|
23
|
-
|
|
24
|
-
# Risk check (no install needed)
|
|
25
|
-
npx skannr risk
|
|
26
|
-
|
|
27
|
-
# Health report
|
|
28
|
-
npx skannr report
|
|
29
|
-
|
|
30
|
-
# Cache management
|
|
31
|
-
npx skannr cache stats`,
|
|
32
|
-
'MCP Server': `{
|
|
33
|
-
"mcpServers": {
|
|
34
|
-
"skannr": {
|
|
35
|
-
"command": "npx",
|
|
36
|
-
"args": ["-y", "skannr", "--mcp"]
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}`,
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export default function Install() {
|
|
43
|
-
const [active, setActive] = useState('CLI Install')
|
|
44
|
-
const [copied, setCopied] = useState(false)
|
|
45
|
-
|
|
46
|
-
const doCopy = async () => {
|
|
47
|
-
await navigator.clipboard.writeText(snippets[active])
|
|
48
|
-
setCopied(true)
|
|
49
|
-
setTimeout(() => setCopied(false), 2000)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<section id="install" className="py-24 px-5">
|
|
54
|
-
<div className="max-w-5xl mx-auto">
|
|
55
|
-
<Reveal>
|
|
56
|
-
<h2 className="text-3xl md:text-5xl font-bold text-center tracking-tight">Up in 30 seconds.</h2>
|
|
57
|
-
</Reveal>
|
|
58
|
-
|
|
59
|
-
<div className="mt-10 flex flex-wrap justify-center gap-2">
|
|
60
|
-
{tabs.map((tab) => (
|
|
61
|
-
<button
|
|
62
|
-
key={tab}
|
|
63
|
-
onClick={() => setActive(tab)}
|
|
64
|
-
className={`rounded-lg border px-4 py-2 text-sm transition-all duration-300 ${
|
|
65
|
-
active === tab
|
|
66
|
-
? 'border-accent bg-accent/10 text-accent shadow-[0_0_24px_-8px_rgba(0,212,255,0.35)]'
|
|
67
|
-
: 'border-white/10 text-[#888] hover:border-white/25 hover:text-white'
|
|
68
|
-
}`}
|
|
69
|
-
>
|
|
70
|
-
{tab}
|
|
71
|
-
</button>
|
|
72
|
-
))}
|
|
73
|
-
</div>
|
|
74
|
-
|
|
75
|
-
<div
|
|
76
|
-
key={active}
|
|
77
|
-
className="mt-8 overflow-hidden rounded-xl border border-border bg-card motion-safe:animate-fade-in motion-reduce:animate-none"
|
|
78
|
-
>
|
|
79
|
-
<div className="flex items-center justify-between border-b border-white/5 px-4 py-3">
|
|
80
|
-
<div className="flex gap-2">
|
|
81
|
-
<span className="h-3 w-3 rounded-full bg-[#ff5f57]" />
|
|
82
|
-
<span className="h-3 w-3 rounded-full bg-[#febc2e]" />
|
|
83
|
-
<span className="h-3 w-3 rounded-full bg-[#28c840]" />
|
|
84
|
-
</div>
|
|
85
|
-
<button
|
|
86
|
-
onClick={doCopy}
|
|
87
|
-
className="rounded-md border border-white/15 px-3 py-1 text-sm transition-all duration-300 hover:border-accent hover:text-accent hover:shadow-[0_0_16px_-4px_rgba(0,212,255,0.4)] active:scale-95"
|
|
88
|
-
>
|
|
89
|
-
{copied ? 'Copied!' : 'Copy'}
|
|
90
|
-
</button>
|
|
91
|
-
</div>
|
|
92
|
-
<pre className="overflow-x-auto p-5 font-mono text-sm text-[#e8e8e8]">{snippets[active]}</pre>
|
|
93
|
-
</div>
|
|
94
|
-
|
|
95
|
-
{active === 'MCP Server' && (
|
|
96
|
-
<p className="mt-4 text-sm text-[#888]">
|
|
97
|
-
Add this to ~/.cursor/mcp.json, ~/.gemini/config.json, or Claude Desktop config.
|
|
98
|
-
</p>
|
|
99
|
-
)}
|
|
100
|
-
</div>
|
|
101
|
-
</section>
|
|
102
|
-
)
|
|
103
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import Reveal from './Reveal'
|
|
2
|
-
|
|
3
|
-
const rows = [
|
|
4
|
-
['TypeScript / TSX', 'Full', 'AST via ts-morph'],
|
|
5
|
-
['JavaScript / JSX', 'Full', 'AST via ts-morph'],
|
|
6
|
-
['Python', 'Structural', 'Regex (signatures + type hints)'],
|
|
7
|
-
['Go, Rust, Java, others', 'Basic', 'First 50 lines fallback'],
|
|
8
|
-
]
|
|
9
|
-
|
|
10
|
-
function badgeClass(level) {
|
|
11
|
-
if (level === 'Full') return 'bg-green/15 text-green'
|
|
12
|
-
if (level === 'Structural') return 'bg-yellow/20 text-yellow'
|
|
13
|
-
return 'bg-white/10 text-[#c5c5c5]'
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export default function LanguageSupport() {
|
|
17
|
-
return (
|
|
18
|
-
<section className="py-24 px-5">
|
|
19
|
-
<div className="max-w-5xl mx-auto">
|
|
20
|
-
<Reveal>
|
|
21
|
-
<h2 className="text-3xl md:text-5xl font-bold text-center tracking-tight">Works on your stack.</h2>
|
|
22
|
-
</Reveal>
|
|
23
|
-
<Reveal delay={80}>
|
|
24
|
-
<div className="mt-12 overflow-hidden rounded-xl border border-border bg-card transition-colors duration-300 hover:border-white/10">
|
|
25
|
-
<table className="w-full text-left">
|
|
26
|
-
<thead className="border-b border-white/5">
|
|
27
|
-
<tr className="text-sm text-[#888]">
|
|
28
|
-
<th className="px-6 py-4 font-medium">Language</th>
|
|
29
|
-
<th className="px-6 py-4 font-medium">Support</th>
|
|
30
|
-
<th className="px-6 py-4 font-medium">Method</th>
|
|
31
|
-
</tr>
|
|
32
|
-
</thead>
|
|
33
|
-
<tbody>
|
|
34
|
-
{rows.map((row) => (
|
|
35
|
-
<tr
|
|
36
|
-
key={row[0]}
|
|
37
|
-
className="border-b border-white/5 transition-colors duration-200 last:border-b-0 hover:bg-white/[0.03]"
|
|
38
|
-
>
|
|
39
|
-
<td className="px-6 py-4">{row[0]}</td>
|
|
40
|
-
<td className="px-6 py-4">
|
|
41
|
-
<span className={`inline-flex rounded-full px-3 py-1 text-sm ${badgeClass(row[1])}`}>
|
|
42
|
-
{row[1]}
|
|
43
|
-
</span>
|
|
44
|
-
</td>
|
|
45
|
-
<td className="px-6 py-4 text-[#888]">{row[2]}</td>
|
|
46
|
-
</tr>
|
|
47
|
-
))}
|
|
48
|
-
</tbody>
|
|
49
|
-
</table>
|
|
50
|
-
</div>
|
|
51
|
-
</Reveal>
|
|
52
|
-
<Reveal delay={120}>
|
|
53
|
-
<a
|
|
54
|
-
href="https://github.com/7vignesh/code-analyzer"
|
|
55
|
-
className="mt-6 inline-block text-[#888] transition-all duration-300 hover:text-accent hover:translate-x-1"
|
|
56
|
-
>
|
|
57
|
-
More language adapters coming. PRs welcome →
|
|
58
|
-
</a>
|
|
59
|
-
</Reveal>
|
|
60
|
-
</div>
|
|
61
|
-
</section>
|
|
62
|
-
)
|
|
63
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
|
-
import { Menu, X } from 'lucide-react'
|
|
3
|
-
import { useScrollNav } from '../hooks/useScrollNav'
|
|
4
|
-
|
|
5
|
-
const links = [
|
|
6
|
-
{ label: 'How it works', href: '#how-it-works' },
|
|
7
|
-
{ label: 'Features', href: '#features' },
|
|
8
|
-
{ label: 'Install', href: '#install' },
|
|
9
|
-
{ label: 'Works with', href: '#works-with' },
|
|
10
|
-
]
|
|
11
|
-
|
|
12
|
-
export default function Navbar() {
|
|
13
|
-
const [open, setOpen] = useState(false)
|
|
14
|
-
const scrolled = useScrollNav(60)
|
|
15
|
-
|
|
16
|
-
return (
|
|
17
|
-
<nav
|
|
18
|
-
className={`fixed top-0 z-50 w-full transition-all duration-300 ${
|
|
19
|
-
scrolled ? 'backdrop-blur-md bg-black/60 border-b border-white/5' : ''
|
|
20
|
-
}`}
|
|
21
|
-
>
|
|
22
|
-
<div className="mx-auto max-w-6xl px-5 py-4">
|
|
23
|
-
<div className="flex items-center justify-between">
|
|
24
|
-
<a
|
|
25
|
-
href="#"
|
|
26
|
-
className="inline-flex items-center gap-2 transition-transform duration-300 hover:scale-[1.03] motion-reduce:hover:scale-100"
|
|
27
|
-
>
|
|
28
|
-
<span className="font-mono text-lg font-bold text-accent">skannr</span>
|
|
29
|
-
<span className="h-2 w-2 rounded-full bg-green motion-safe:animate-pulse" />
|
|
30
|
-
</a>
|
|
31
|
-
|
|
32
|
-
<div className="hidden md:flex items-center gap-7">
|
|
33
|
-
{links.map((link) => (
|
|
34
|
-
<a
|
|
35
|
-
key={link.label}
|
|
36
|
-
href={link.href}
|
|
37
|
-
className="group relative py-1 text-sm text-[#888] transition-colors hover:text-white"
|
|
38
|
-
>
|
|
39
|
-
{link.label}
|
|
40
|
-
<span className="absolute bottom-0 left-0 h-[2px] w-0 bg-accent transition-all duration-300 ease-out group-hover:w-full" />
|
|
41
|
-
</a>
|
|
42
|
-
))}
|
|
43
|
-
<a
|
|
44
|
-
href="https://github.com/7vignesh/code-analyzer"
|
|
45
|
-
className="border border-white/20 text-sm px-4 py-1.5 rounded-lg transition-all duration-300 hover:border-accent hover:text-accent hover:shadow-[0_0_20px_-6px_rgba(0,212,255,0.45)] hover:-translate-y-px"
|
|
46
|
-
>
|
|
47
|
-
GitHub
|
|
48
|
-
</a>
|
|
49
|
-
</div>
|
|
50
|
-
|
|
51
|
-
<button
|
|
52
|
-
className="md:hidden text-white/80 hover:text-accent transition-colors"
|
|
53
|
-
onClick={() => setOpen((v) => !v)}
|
|
54
|
-
aria-label="Toggle menu"
|
|
55
|
-
>
|
|
56
|
-
{open ? <X size={20} /> : <Menu size={20} />}
|
|
57
|
-
</button>
|
|
58
|
-
</div>
|
|
59
|
-
|
|
60
|
-
{open && (
|
|
61
|
-
<div className="mt-4 md:hidden rounded-xl border border-border bg-card p-4">
|
|
62
|
-
<div className="flex flex-col gap-3">
|
|
63
|
-
{links.map((link) => (
|
|
64
|
-
<a
|
|
65
|
-
key={link.label}
|
|
66
|
-
href={link.href}
|
|
67
|
-
className="text-sm text-[#888] hover:text-white transition-colors"
|
|
68
|
-
onClick={() => setOpen(false)}
|
|
69
|
-
>
|
|
70
|
-
{link.label}
|
|
71
|
-
</a>
|
|
72
|
-
))}
|
|
73
|
-
<a
|
|
74
|
-
href="https://github.com/7vignesh/code-analyzer"
|
|
75
|
-
className="mt-1 w-fit border border-white/20 text-sm px-4 py-1.5 rounded-lg hover:border-accent hover:text-accent transition-all"
|
|
76
|
-
onClick={() => setOpen(false)}
|
|
77
|
-
>
|
|
78
|
-
GitHub
|
|
79
|
-
</a>
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
|
-
)}
|
|
83
|
-
</div>
|
|
84
|
-
</nav>
|
|
85
|
-
)
|
|
86
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { AppWindow, Clock, GitBranch } from 'lucide-react'
|
|
2
|
-
import Reveal from './Reveal'
|
|
3
|
-
|
|
4
|
-
const cards = [
|
|
5
|
-
{
|
|
6
|
-
icon: AppWindow,
|
|
7
|
-
title: 'Context window limits',
|
|
8
|
-
body: "Large repos have hundreds of files. You can't paste them all. The AI only sees what you show it - and guesses the rest.",
|
|
9
|
-
},
|
|
10
|
-
{
|
|
11
|
-
icon: Clock,
|
|
12
|
-
title: 'One file at a time is slow',
|
|
13
|
-
body: "You manually hunt for relevant files, paste them one by one, and re-explain the architecture every session. It's exhausting.",
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
icon: GitBranch,
|
|
17
|
-
title: 'No structural awareness',
|
|
18
|
-
body: "Keyword search finds files that mention your term. It doesn't find the files that ARE the answer architecturally - the central import hubs and interfaces.",
|
|
19
|
-
},
|
|
20
|
-
]
|
|
21
|
-
|
|
22
|
-
export default function Problem() {
|
|
23
|
-
return (
|
|
24
|
-
<section className="py-24 px-5">
|
|
25
|
-
<div className="max-w-6xl mx-auto">
|
|
26
|
-
<Reveal>
|
|
27
|
-
<h2 className="text-3xl md:text-5xl font-bold text-center tracking-tight">
|
|
28
|
-
AI is brilliant. But it can't read your whole codebase.
|
|
29
|
-
</h2>
|
|
30
|
-
</Reveal>
|
|
31
|
-
<div className="mt-14 grid grid-cols-1 md:grid-cols-3 gap-5">
|
|
32
|
-
{cards.map(({ icon: Icon, title, body }, i) => (
|
|
33
|
-
<Reveal key={title} delay={i * 100} className="h-full">
|
|
34
|
-
<article className="group h-full rounded-xl border border-border bg-card p-6 transition-all duration-300 hover:-translate-y-1 hover:border-accent/20 hover:bg-card-hover hover:shadow-[0_20px_40px_-24px_rgba(0,0,0,0.8)]">
|
|
35
|
-
<Icon
|
|
36
|
-
className="mb-5 text-accent transition-transform duration-300 group-hover:scale-110 group-hover:text-accent"
|
|
37
|
-
size={24}
|
|
38
|
-
/>
|
|
39
|
-
<h3 className="mb-2 text-xl font-semibold">{title}</h3>
|
|
40
|
-
<p className="text-[#888]">{body}</p>
|
|
41
|
-
</article>
|
|
42
|
-
</Reveal>
|
|
43
|
-
))}
|
|
44
|
-
</div>
|
|
45
|
-
<Reveal delay={200}>
|
|
46
|
-
<p className="mt-10 text-center text-[#888]">Universal Code Analyzer fixes all three.</p>
|
|
47
|
-
</Reveal>
|
|
48
|
-
</div>
|
|
49
|
-
</section>
|
|
50
|
-
)
|
|
51
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react'
|
|
2
|
-
|
|
3
|
-
export default function Reveal({ children, delay = 0, className = '' }) {
|
|
4
|
-
const ref = useRef(null)
|
|
5
|
-
const [visible, setVisible] = useState(false)
|
|
6
|
-
|
|
7
|
-
useEffect(() => {
|
|
8
|
-
if (typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
9
|
-
setVisible(true)
|
|
10
|
-
return
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const el = ref.current
|
|
14
|
-
if (!el) return
|
|
15
|
-
|
|
16
|
-
const observer = new IntersectionObserver(
|
|
17
|
-
([entry]) => {
|
|
18
|
-
if (entry.isIntersecting) setVisible(true)
|
|
19
|
-
},
|
|
20
|
-
{ threshold: 0.12, rootMargin: '0px 0px -6% 0px' }
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
observer.observe(el)
|
|
24
|
-
return () => observer.disconnect()
|
|
25
|
-
}, [])
|
|
26
|
-
|
|
27
|
-
return (
|
|
28
|
-
<div
|
|
29
|
-
ref={ref}
|
|
30
|
-
className={[
|
|
31
|
-
'transition-[opacity,transform] duration-700 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none',
|
|
32
|
-
visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8',
|
|
33
|
-
className,
|
|
34
|
-
].join(' ')}
|
|
35
|
-
style={{ transitionDelay: visible ? `${delay}ms` : '0ms' }}
|
|
36
|
-
>
|
|
37
|
-
{children}
|
|
38
|
-
</div>
|
|
39
|
-
)
|
|
40
|
-
}
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { ArrowDown, Flame, Zap } from 'lucide-react'
|
|
2
|
-
import Reveal from './Reveal'
|
|
3
|
-
|
|
4
|
-
const comparison = [
|
|
5
|
-
{
|
|
6
|
-
label: 'Without Skannr',
|
|
7
|
-
scenario: 'AI reads full files to answer your question',
|
|
8
|
-
files: '10 files × 300 lines',
|
|
9
|
-
tokens: '~15,000 tokens',
|
|
10
|
-
cost: 'Full context burned every time',
|
|
11
|
-
highlight: false,
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
label: 'With Skannr',
|
|
15
|
-
scenario: 'Skannr returns ranked skeletons (signatures + types only)',
|
|
16
|
-
files: '8 relevant files, compressed',
|
|
17
|
-
tokens: '~500 tokens',
|
|
18
|
-
cost: '96.5% less context used',
|
|
19
|
-
highlight: true,
|
|
20
|
-
},
|
|
21
|
-
]
|
|
22
|
-
|
|
23
|
-
const flows = [
|
|
24
|
-
{
|
|
25
|
-
icon: Flame,
|
|
26
|
-
title: 'Direct CLI — zero tokens',
|
|
27
|
-
command: 'skannr "how does auth work?"',
|
|
28
|
-
desc: 'Answers locally. No AI API call, no tokens burned. Free.',
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
icon: Zap,
|
|
32
|
-
title: 'MCP — 96% fewer tokens',
|
|
33
|
-
command: 'AI calls scan_codebase via MCP',
|
|
34
|
-
desc: 'Instead of reading 10+ full files, your assistant gets compressed skeletons in one tool call. Same answer, 30x cheaper.',
|
|
35
|
-
},
|
|
36
|
-
]
|
|
37
|
-
|
|
38
|
-
export default function TokenSavings() {
|
|
39
|
-
return (
|
|
40
|
-
<section className="py-24 px-5">
|
|
41
|
-
<div className="max-w-5xl mx-auto">
|
|
42
|
-
<Reveal>
|
|
43
|
-
<h2 className="text-3xl md:text-5xl font-bold text-center tracking-tight">
|
|
44
|
-
Stop burning tokens on full file reads.
|
|
45
|
-
</h2>
|
|
46
|
-
<p className="mt-4 text-center text-[#888] max-w-2xl mx-auto">
|
|
47
|
-
Every time your AI reads a file, you pay for every line — including the function bodies
|
|
48
|
-
it doesn't need. Skannr strips the noise and returns only structure.
|
|
49
|
-
</p>
|
|
50
|
-
</Reveal>
|
|
51
|
-
|
|
52
|
-
{/* Before/After comparison cards */}
|
|
53
|
-
<div className="mt-14 grid grid-cols-1 md:grid-cols-2 gap-5">
|
|
54
|
-
{comparison.map((item) => (
|
|
55
|
-
<Reveal key={item.label} delay={item.highlight ? 100 : 0} className="h-full">
|
|
56
|
-
<article
|
|
57
|
-
className={`group h-full rounded-xl border p-6 transition-all duration-300 ${
|
|
58
|
-
item.highlight
|
|
59
|
-
? 'border-accent/40 bg-accent/[0.04] hover:border-accent/60 hover:shadow-[0_0_36px_rgba(0,212,255,0.12)]'
|
|
60
|
-
: 'border-border bg-card hover:border-white/10 hover:bg-card-hover'
|
|
61
|
-
}`}
|
|
62
|
-
>
|
|
63
|
-
<h3 className={`text-lg font-semibold mb-3 ${item.highlight ? 'text-accent' : 'text-white'}`}>
|
|
64
|
-
{item.label}
|
|
65
|
-
</h3>
|
|
66
|
-
<p className="text-[#888] text-sm mb-4">{item.scenario}</p>
|
|
67
|
-
<div className="space-y-2">
|
|
68
|
-
<div className="flex justify-between text-sm">
|
|
69
|
-
<span className="text-[#888]">Files processed</span>
|
|
70
|
-
<span className="font-mono text-[#d0d0d0]">{item.files}</span>
|
|
71
|
-
</div>
|
|
72
|
-
<div className="flex justify-between text-sm">
|
|
73
|
-
<span className="text-[#888]">Tokens in context</span>
|
|
74
|
-
<span className={`font-mono font-semibold ${item.highlight ? 'text-accent' : 'text-yellow'}`}>
|
|
75
|
-
{item.tokens}
|
|
76
|
-
</span>
|
|
77
|
-
</div>
|
|
78
|
-
<div className="pt-2 border-t border-white/5">
|
|
79
|
-
<span className={`text-sm font-medium ${item.highlight ? 'text-green' : 'text-[#888]'}`}>
|
|
80
|
-
{item.cost}
|
|
81
|
-
</span>
|
|
82
|
-
</div>
|
|
83
|
-
</div>
|
|
84
|
-
</article>
|
|
85
|
-
</Reveal>
|
|
86
|
-
))}
|
|
87
|
-
</div>
|
|
88
|
-
|
|
89
|
-
{/* Arrow indicator */}
|
|
90
|
-
<Reveal delay={150}>
|
|
91
|
-
<div className="flex justify-center my-8">
|
|
92
|
-
<div className="flex items-center gap-3 rounded-full border border-border bg-card px-5 py-2">
|
|
93
|
-
<ArrowDown size={16} className="text-green" />
|
|
94
|
-
<span className="text-sm font-mono text-green">96.5% reduction</span>
|
|
95
|
-
<ArrowDown size={16} className="text-green" />
|
|
96
|
-
</div>
|
|
97
|
-
</div>
|
|
98
|
-
</Reveal>
|
|
99
|
-
|
|
100
|
-
{/* Two modes of saving */}
|
|
101
|
-
<div className="mt-8 grid grid-cols-1 md:grid-cols-2 gap-5">
|
|
102
|
-
{flows.map(({ icon: Icon, title, command, desc }, i) => (
|
|
103
|
-
<Reveal key={title} delay={i * 80} className="h-full">
|
|
104
|
-
<article className="group h-full rounded-xl border border-border bg-card p-6 transition-all duration-300 hover:-translate-y-1 hover:border-accent/30 hover:bg-card-hover">
|
|
105
|
-
<Icon size={20} className="mb-3 text-accent" />
|
|
106
|
-
<h3 className="text-lg font-semibold mb-2">{title}</h3>
|
|
107
|
-
<code className="block w-fit rounded-lg border border-border px-3 py-1.5 font-mono text-xs text-accent mb-3">
|
|
108
|
-
{command}
|
|
109
|
-
</code>
|
|
110
|
-
<p className="text-[#888] text-sm">{desc}</p>
|
|
111
|
-
</article>
|
|
112
|
-
</Reveal>
|
|
113
|
-
))}
|
|
114
|
-
</div>
|
|
115
|
-
|
|
116
|
-
{/* Clarification */}
|
|
117
|
-
<Reveal delay={200}>
|
|
118
|
-
<p className="mt-10 text-center text-sm text-[#888] max-w-xl mx-auto">
|
|
119
|
-
Skeletons show structure — signatures, types, imports. If you need a function body,
|
|
120
|
-
use the symbol mapping to retrieve just that one implementation on demand.
|
|
121
|
-
</p>
|
|
122
|
-
</Reveal>
|
|
123
|
-
</div>
|
|
124
|
-
</section>
|
|
125
|
-
)
|
|
126
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import Reveal from './Reveal'
|
|
2
|
-
|
|
3
|
-
const tools = [
|
|
4
|
-
{
|
|
5
|
-
name: 'Gemini CLI',
|
|
6
|
-
desc: 'Add as MCP server in ~/.gemini/config.json',
|
|
7
|
-
barClass: 'bg-accent',
|
|
8
|
-
},
|
|
9
|
-
{
|
|
10
|
-
name: 'Claude Code',
|
|
11
|
-
desc: 'Works via MCP over stdio',
|
|
12
|
-
barClass: 'bg-purple',
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
name: 'Cursor',
|
|
16
|
-
desc: 'Add to ~/.cursor/mcp.json',
|
|
17
|
-
barClass: 'bg-green',
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
name: 'Any MCP Client',
|
|
21
|
-
desc: 'Standard Model Context Protocol over stdio',
|
|
22
|
-
barClass: 'bg-yellow',
|
|
23
|
-
},
|
|
24
|
-
]
|
|
25
|
-
|
|
26
|
-
export default function WorksWith() {
|
|
27
|
-
return (
|
|
28
|
-
<section id="works-with" className="py-24 px-5">
|
|
29
|
-
<div className="max-w-6xl mx-auto">
|
|
30
|
-
<Reveal>
|
|
31
|
-
<h2 className="text-3xl md:text-5xl font-bold text-center tracking-tight">
|
|
32
|
-
Works with the tools you already use.
|
|
33
|
-
</h2>
|
|
34
|
-
</Reveal>
|
|
35
|
-
<div className="mt-12 grid grid-cols-1 items-stretch gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
|
36
|
-
{tools.map((tool, i) => (
|
|
37
|
-
<Reveal key={tool.name} delay={i * 90} className="h-full">
|
|
38
|
-
<article className="group flex h-full min-h-[160px] gap-4 rounded-xl border border-border bg-card p-6 transition-all duration-300 hover:-translate-y-1.5 hover:border-accent/25 hover:bg-card-hover hover:shadow-[0_24px_48px_-28px_rgba(0,0,0,0.9)]">
|
|
39
|
-
<div
|
|
40
|
-
className={`h-12 w-1 shrink-0 origin-top rounded-full ${tool.barClass} motion-safe:animate-bar-grow motion-reduce:opacity-100 motion-reduce:scale-y-100 transition-transform duration-300 motion-safe:group-hover:scale-y-110`}
|
|
41
|
-
aria-hidden
|
|
42
|
-
/>
|
|
43
|
-
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
|
44
|
-
<h3 className="text-lg font-semibold leading-snug text-[#f0f0f0] transition-colors duration-300 group-hover:text-white">
|
|
45
|
-
{tool.name}
|
|
46
|
-
</h3>
|
|
47
|
-
<p className="flex-1 text-sm leading-relaxed text-[#888]">{tool.desc}</p>
|
|
48
|
-
<span className="mt-3 inline-flex w-fit rounded-full bg-green/15 px-3 py-1 text-xs font-medium text-green transition-transform duration-300 group-hover:scale-105">
|
|
49
|
-
Ready
|
|
50
|
-
</span>
|
|
51
|
-
</div>
|
|
52
|
-
</article>
|
|
53
|
-
</Reveal>
|
|
54
|
-
))}
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
57
|
-
</section>
|
|
58
|
-
)
|
|
59
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react'
|
|
2
|
-
|
|
3
|
-
export function useScrollNav(threshold = 60) {
|
|
4
|
-
const [scrolled, setScrolled] = useState(false)
|
|
5
|
-
|
|
6
|
-
useEffect(() => {
|
|
7
|
-
const handler = () => setScrolled(window.scrollY > threshold)
|
|
8
|
-
window.addEventListener('scroll', handler)
|
|
9
|
-
return () => window.removeEventListener('scroll', handler)
|
|
10
|
-
}, [threshold])
|
|
11
|
-
|
|
12
|
-
return scrolled
|
|
13
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react'
|
|
2
|
-
|
|
3
|
-
export function useTypewriter(lines, charDelay = 25, lineDelay = 350) {
|
|
4
|
-
const [displayed, setDisplayed] = useState([])
|
|
5
|
-
const [currentLine, setCurrentLine] = useState(0)
|
|
6
|
-
const [currentChar, setCurrentChar] = useState(0)
|
|
7
|
-
const [done, setDone] = useState(false)
|
|
8
|
-
|
|
9
|
-
useEffect(() => {
|
|
10
|
-
setDisplayed([])
|
|
11
|
-
setCurrentLine(0)
|
|
12
|
-
setCurrentChar(0)
|
|
13
|
-
setDone(false)
|
|
14
|
-
}, [lines])
|
|
15
|
-
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
if (currentLine >= lines.length) {
|
|
18
|
-
setDone(true)
|
|
19
|
-
return
|
|
20
|
-
}
|
|
21
|
-
if (currentChar < lines[currentLine].length) {
|
|
22
|
-
const t = setTimeout(() => {
|
|
23
|
-
setDisplayed((prev) => {
|
|
24
|
-
const next = [...prev]
|
|
25
|
-
if (!next[currentLine]) next[currentLine] = ''
|
|
26
|
-
next[currentLine] = lines[currentLine].slice(0, currentChar + 1)
|
|
27
|
-
return next
|
|
28
|
-
})
|
|
29
|
-
setCurrentChar((c) => c + 1)
|
|
30
|
-
}, charDelay)
|
|
31
|
-
return () => clearTimeout(t)
|
|
32
|
-
}
|
|
33
|
-
const t = setTimeout(() => {
|
|
34
|
-
setCurrentLine((l) => l + 1)
|
|
35
|
-
setCurrentChar(0)
|
|
36
|
-
}, lineDelay)
|
|
37
|
-
return () => clearTimeout(t)
|
|
38
|
-
}, [currentLine, currentChar, lines, charDelay, lineDelay])
|
|
39
|
-
|
|
40
|
-
return { displayed, done }
|
|
41
|
-
}
|