@flydocs/cli 0.5.0-beta.6 → 0.5.0-beta.8

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.
@@ -0,0 +1,293 @@
1
+ #!/usr/bin/env python3
2
+ """Context7 API client for fetching library documentation.
3
+
4
+ Queries the Context7 REST API to search for libraries and retrieve
5
+ LLM-ready documentation. Replaces the Context7 MCP server with a
6
+ lightweight script that follows the FlyDocs mechanism pattern.
7
+
8
+ Usage:
9
+ python3 .claude/skills/flydocs-context7/scripts/context7.py \\
10
+ search <library_name> [query]
11
+
12
+ python3 .claude/skills/flydocs-context7/scripts/context7.py \\
13
+ docs <library_id> <query> [--type txt|json] [--tokens N]
14
+ """
15
+
16
+ import argparse
17
+ import json
18
+ import os
19
+ import sys
20
+ import urllib.error
21
+ import urllib.parse
22
+ import urllib.request
23
+ from pathlib import Path
24
+
25
+ BASE_URL = "https://context7.com/api/v2"
26
+ DEFAULT_TIMEOUT = 10
27
+ DEFAULT_TOKENS = 5000
28
+
29
+
30
+ # --- Helpers ---
31
+
32
+
33
+ def fail(message):
34
+ """Print error to stderr and exit 1."""
35
+ print(message, file=sys.stderr)
36
+ sys.exit(1)
37
+
38
+
39
+ def output_json(data):
40
+ """Print JSON to stdout."""
41
+ print(json.dumps(data, indent=2))
42
+
43
+
44
+ def find_project_root(start=None):
45
+ """Walk up from start (or script location) to find the directory containing .flydocs/."""
46
+ current = Path(start) if start else Path(__file__).resolve().parent
47
+ for parent in [current] + list(current.parents):
48
+ if (parent / ".flydocs").is_dir():
49
+ return parent
50
+ return None
51
+
52
+
53
+ def read_env_file(root):
54
+ """Read .env file at project root and return a dict of key=value pairs."""
55
+ env_path = root / ".env"
56
+ if not env_path.is_file():
57
+ return {}
58
+
59
+ env_vars = {}
60
+ try:
61
+ with open(env_path, "r", encoding="utf-8") as f:
62
+ for line in f:
63
+ line = line.strip()
64
+ # Skip empty lines and comments
65
+ if not line or line.startswith("#"):
66
+ continue
67
+ if "=" not in line:
68
+ continue
69
+ key, _, value = line.partition("=")
70
+ key = key.strip()
71
+ value = value.strip()
72
+ # Strip surrounding quotes if present
73
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
74
+ value = value[1:-1]
75
+ env_vars[key] = value
76
+ except OSError:
77
+ pass
78
+
79
+ return env_vars
80
+
81
+
82
+ def resolve_api_key(args_key):
83
+ """Resolve the API key from flag, env var, or .env file. Returns None if unavailable."""
84
+ # 1. Explicit flag
85
+ if args_key:
86
+ return args_key
87
+
88
+ # 2. Environment variable
89
+ env_key = os.environ.get("CONTEXT7_API_KEY")
90
+ if env_key:
91
+ return env_key
92
+
93
+ # 3. .env file at project root
94
+ root = find_project_root()
95
+ if root:
96
+ env_vars = read_env_file(root)
97
+ dotenv_key = env_vars.get("CONTEXT7_API_KEY")
98
+ if dotenv_key:
99
+ return dotenv_key
100
+
101
+ # 4. Anonymous (no key)
102
+ return None
103
+
104
+
105
+ def api_request(path, params, api_key=None):
106
+ """Make a GET request to the Context7 API and return parsed response data.
107
+
108
+ Handles HTTP errors, timeouts, and network failures with clear messages.
109
+ """
110
+ query_string = urllib.parse.urlencode(params)
111
+ url = f"{BASE_URL}/{path}?{query_string}"
112
+
113
+ request = urllib.request.Request(url)
114
+ request.add_header("User-Agent", "flydocs-context7/1.0")
115
+
116
+ if api_key:
117
+ request.add_header("Authorization", f"Bearer {api_key}")
118
+
119
+ try:
120
+ with urllib.request.urlopen(request, timeout=DEFAULT_TIMEOUT) as response:
121
+ body = response.read().decode("utf-8")
122
+ content_type = response.headers.get("Content-Type", "")
123
+ if "application/json" in content_type:
124
+ return json.loads(body)
125
+ return body
126
+ except urllib.error.HTTPError as e:
127
+ if e.code == 429:
128
+ fail("Rate limited by Context7 API. Wait a moment and try again.")
129
+ body_text = ""
130
+ try:
131
+ body_text = e.read().decode("utf-8", errors="replace")
132
+ except Exception:
133
+ pass
134
+ fail(f"Context7 API error (HTTP {e.code}): {e.reason}\n{body_text}".strip())
135
+ except urllib.error.URLError as e:
136
+ if "timed out" in str(e.reason).lower():
137
+ fail("Context7 API request timed out (10s). The service may be slow or unavailable.")
138
+ fail(f"Cannot reach Context7 API. Check your network connection.\nDetails: {e.reason}")
139
+ except OSError as e:
140
+ fail(f"Network error connecting to Context7 API: {e}")
141
+
142
+
143
+ # --- Commands ---
144
+
145
+
146
+ def cmd_search(args):
147
+ """Search for libraries by name and optional query."""
148
+ params = {"libraryName": args.library_name}
149
+ if args.query:
150
+ params["query"] = args.query
151
+
152
+ api_key = resolve_api_key(args.key)
153
+ data = api_request("libs/search", params, api_key=api_key)
154
+
155
+ # Normalize response — API wraps results in {"results": [...]}
156
+ if isinstance(data, dict) and "results" in data:
157
+ results = data["results"]
158
+ elif isinstance(data, list):
159
+ results = data
160
+ else:
161
+ results = []
162
+
163
+ if args.type == "json" or args.type is None:
164
+ # Default for search is JSON
165
+ compact = []
166
+ for lib in results:
167
+ compact.append({
168
+ "id": lib.get("id", ""),
169
+ "name": lib.get("title", lib.get("name", "")),
170
+ "description": lib.get("description", ""),
171
+ "trustScore": lib.get("trustScore", 0),
172
+ })
173
+ output_json(compact)
174
+ else:
175
+ # txt format for search — one line per result
176
+ for lib in results:
177
+ name = lib.get("title", lib.get("name", "unknown"))
178
+ lib_id = lib.get("id", "")
179
+ desc = lib.get("description", "")
180
+ score = lib.get("trustScore", 0)
181
+ print(f"{lib_id} {name} (trust: {score})")
182
+ if desc:
183
+ print(f" {desc}")
184
+ print()
185
+
186
+
187
+ def cmd_docs(args):
188
+ """Fetch documentation for a specific library."""
189
+ params = {
190
+ "libraryId": args.library_id,
191
+ "query": args.query,
192
+ }
193
+
194
+ output_type = args.type if args.type else "txt"
195
+ params["type"] = output_type
196
+
197
+ if args.tokens:
198
+ params["tokens"] = str(args.tokens)
199
+
200
+ api_key = resolve_api_key(args.key)
201
+ data = api_request("context", params, api_key=api_key)
202
+
203
+ if output_type == "json":
204
+ if isinstance(data, str):
205
+ # API returned text when we asked for JSON; wrap it
206
+ output_json({"content": data})
207
+ else:
208
+ output_json(data)
209
+ else:
210
+ # txt — print directly
211
+ if isinstance(data, str):
212
+ print(data)
213
+ elif isinstance(data, dict):
214
+ content = data.get("content", data.get("text", ""))
215
+ if content:
216
+ print(content)
217
+ else:
218
+ output_json(data)
219
+ else:
220
+ output_json(data)
221
+
222
+
223
+ # --- Main ---
224
+
225
+
226
+ def main():
227
+ parser = argparse.ArgumentParser(
228
+ description="Context7 API client — search libraries and fetch documentation"
229
+ )
230
+ parser.add_argument(
231
+ "--key", type=str, default=None,
232
+ help="API key override (default: CONTEXT7_API_KEY env var or .env file)"
233
+ )
234
+
235
+ subparsers = parser.add_subparsers(dest="command", required=True)
236
+
237
+ # search subcommand
238
+ search_parser = subparsers.add_parser(
239
+ "search",
240
+ help="Search for libraries by name"
241
+ )
242
+ search_parser.add_argument(
243
+ "library_name",
244
+ help="Library name to search for (e.g., 'react', 'next.js')"
245
+ )
246
+ search_parser.add_argument(
247
+ "query", nargs="?", default=None,
248
+ help="Optional search query to refine results"
249
+ )
250
+ search_parser.add_argument(
251
+ "--type", choices=["txt", "json"], default=None,
252
+ help="Output format (default: json for search)"
253
+ )
254
+ search_parser.add_argument(
255
+ "--tokens", type=int, default=DEFAULT_TOKENS,
256
+ help=f"Max tokens budget hint (default: {DEFAULT_TOKENS})"
257
+ )
258
+
259
+ # docs subcommand
260
+ docs_parser = subparsers.add_parser(
261
+ "docs",
262
+ help="Fetch documentation for a library"
263
+ )
264
+ docs_parser.add_argument(
265
+ "library_id",
266
+ help="Library ID from search results (e.g., '/vercel/next.js')"
267
+ )
268
+ docs_parser.add_argument(
269
+ "query",
270
+ help="Documentation query (e.g., 'server components', 'routing')"
271
+ )
272
+ docs_parser.add_argument(
273
+ "--type", choices=["txt", "json"], default=None,
274
+ help="Output format (default: txt for docs)"
275
+ )
276
+ docs_parser.add_argument(
277
+ "--tokens", type=int, default=DEFAULT_TOKENS,
278
+ help=f"Max tokens budget hint (default: {DEFAULT_TOKENS})"
279
+ )
280
+
281
+ args = parser.parse_args()
282
+
283
+ if args.command == "search":
284
+ cmd_search(args)
285
+ elif args.command == "docs":
286
+ cmd_docs(args)
287
+ else:
288
+ parser.print_help()
289
+ sys.exit(1)
290
+
291
+
292
+ if __name__ == "__main__":
293
+ main()
@@ -6,11 +6,6 @@
6
6
  "matcher": "Bash",
7
7
  "command": "python3 ./.flydocs/hooks/auto-approve.py",
8
8
  "timeout": 5
9
- },
10
- {
11
- "matcher": "mcp__linear.*",
12
- "command": "python3 ./.flydocs/hooks/prefer-scripts.py",
13
- "timeout": 5
14
9
  }
15
10
  ],
16
11
  "afterFileEdit": [
@@ -22,8 +22,9 @@
22
22
  #
23
23
 
24
24
  # ===========================================
25
- # REQUIRED: Linear API Key
25
+ # CLOUD TIER ONLY: Linear API Key
26
26
  # ===========================================
27
+ # Only needed if you use cloud tier (tier: "cloud" in .flydocs/config.json)
27
28
  # Get from: Linear → Settings → API → Personal API Keys
28
29
  # Format: lin_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
29
30
  LINEAR_API_KEY=
@@ -32,9 +33,17 @@ LINEAR_API_KEY=
32
33
  # OPTIONAL: Figma Access Token
33
34
  # ===========================================
34
35
  # Get from: Figma → Settings → Personal Access Tokens
35
- # Only needed if using Figma MCP for design integration
36
+ # Only needed if using the flydocs-figma skill for design integration
36
37
  FIGMA_ACCESS_TOKEN=
37
38
 
39
+ # ===========================================
40
+ # OPTIONAL: Context7 API Key
41
+ # ===========================================
42
+ # Enables higher rate limits for library documentation lookup
43
+ # Get from: context7.com/dashboard (free account)
44
+ # Works without a key at lower rate limits (~1,000 calls/month)
45
+ CONTEXT7_API_KEY=
46
+
38
47
  # ===========================================
39
48
  # NOTE: Team ID and Project ID are discovered automatically
40
49
  # ===========================================
@@ -1,13 +1,13 @@
1
1
  {
2
- "version": "0.5.0-beta.6",
2
+ "version": "0.5.0-beta.8",
3
3
  "sourceRepo": "github.com/plastrlab/flydocs-core",
4
- "tier": "cloud",
4
+ "tier": "local",
5
5
  "setupComplete": false,
6
6
  "paths": {
7
7
  "content": "flydocs"
8
8
  },
9
9
  "provider": {
10
- "type": "linear",
10
+ "type": null,
11
11
  "teamId": null
12
12
  },
13
13
  "workspace": {
@@ -46,11 +46,6 @@
46
46
  "auth": [],
47
47
  "styling": []
48
48
  },
49
- "mcp": {
50
- "preferred": [],
51
- "fallbackOnly": ["linear"],
52
- "stackSpecific": {}
53
- },
54
49
  "skills": {
55
50
  "installed": [],
56
51
  "custom": []
@@ -19,9 +19,9 @@ import re
19
19
 
20
20
 
21
21
  # Pattern matches any flydocs skill scripts directory
22
- # Covers: flydocs-local, flydocs-cloud, flydocs-workflow
22
+ # Covers: flydocs-local, flydocs-cloud, flydocs-workflow, flydocs-context-graph
23
23
  APPROVED_PATTERN = re.compile(
24
- r'\.claude/skills/flydocs-(?:local|cloud|workflow)/scripts/\w+\.py'
24
+ r'python3?\s+(?:["\']?(?:\$CLAUDE_PROJECT_DIR|\$\{CLAUDE_PROJECT_DIR\}|\.)["\']?/)?\.?claude/skills/flydocs-(?:local|cloud|workflow|context-graph|context7)/scripts/\w+\.py'
25
25
  )
26
26
 
27
27
 
@@ -11,6 +11,7 @@ Exit codes:
11
11
  """
12
12
 
13
13
  import json
14
+ import os
14
15
  import subprocess
15
16
  import sys
16
17
  from pathlib import Path
@@ -59,6 +60,18 @@ def main() -> None:
59
60
  print('{}')
60
61
  sys.exit(0)
61
62
 
63
+ # Validate file_path is within project directory
64
+ project_dir = os.environ.get('CLAUDE_PROJECT_DIR', os.getcwd())
65
+ try:
66
+ resolved = os.path.realpath(file_path)
67
+ project_resolved = os.path.realpath(project_dir)
68
+ if not resolved.startswith(project_resolved + os.sep) and resolved != project_resolved:
69
+ print('{}')
70
+ sys.exit(0)
71
+ except (OSError, ValueError):
72
+ print('{}')
73
+ sys.exit(0)
74
+
62
75
  # Get file extension and format
63
76
  ext = get_file_extension(file_path)
64
77
  format_file(file_path, ext)
@@ -22,8 +22,9 @@
22
22
  <!-- Skills installed based on detected stack -->
23
23
 
24
24
  {{#each skills}}
25
+
25
26
  - `{{this}}` - See `.claude/skills/{{this}}/SKILL.md`
26
- {{/each}}
27
+ {{/each}}
27
28
 
28
29
  ---
29
30
 
@@ -72,10 +73,10 @@ src/
72
73
 
73
74
  <!-- Link to ADRs or explain key architectural choices -->
74
75
 
75
- | Decision | Rationale | Date |
76
- |----------|-----------|------|
76
+ | Decision | Rationale | Date |
77
+ | -------------------------------- | ---------------------------------------- | -------- |
77
78
  | Use Server Components by default | Better performance, simpler mental model | {{date}} |
78
- | Convex for real-time | Built-in subscriptions, type safety | {{date}} |
79
+ | Convex for real-time | Built-in subscriptions, type safety | {{date}} |
79
80
 
80
81
  ---
81
82
 
@@ -105,59 +106,6 @@ async function Page() {
105
106
 
106
107
  ---
107
108
 
108
- ## MCP Configuration
109
-
110
- <!-- Stack-specific MCP preferences for this project -->
111
-
112
- ### MCP Strategy
113
-
114
- Configure in `.flydocs/config.json`:
115
-
116
- ```json
117
- {
118
- "mcp": {
119
- "preferred": ["context7", "figma"],
120
- "fallbackOnly": ["linear"],
121
- "stackSpecific": {
122
- "convex": {
123
- "note": "Use Context7 for docs, CLI for schema changes"
124
- }
125
- }
126
- }
127
- }
128
- ```
129
-
130
- - **preferred**: MCPs to use proactively (unique capabilities like Figma visuals, Context7 docs)
131
- - **fallbackOnly**: MCPs with script alternatives (use scripts first, MCP if script fails)
132
- - **stackSpecific**: Per-stack notes and usage guidance
133
-
134
- ### Stack-Specific MCP Notes
135
-
136
- <!-- Uncomment and customize for your stack -->
137
-
138
- <!--
139
- #### Convex
140
- - Use Context7 MCP for Convex API documentation
141
- - Prefer `convex dev` CLI for schema changes over direct mutations
142
- - Query patterns: Use `.collect()` for small datasets, pagination for large
143
- -->
144
-
145
- <!--
146
- #### Supabase
147
- - Use Supabase MCP for database operations when available
148
- - Prefer RLS policies over application-level auth checks
149
- - Use database functions for complex operations
150
- -->
151
-
152
- <!--
153
- #### Firebase
154
- - Use Firebase MCP for Firestore operations
155
- - Prefer security rules over client-side validation
156
- - Use batch operations for multiple writes
157
- -->
158
-
159
- ---
160
-
161
109
  ## Skill Overrides
162
110
 
163
111
  <!-- Override specific skill recommendations for this project -->
@@ -168,7 +116,7 @@ Configure in `.flydocs/config.json`:
168
116
 
169
117
  ```typescript
170
118
  // This project uses shorter cache times due to real-time requirements
171
- cacheLife('seconds'); // Instead of skill default 'hours'
119
+ cacheLife("seconds"); // Instead of skill default 'hours'
172
120
  ```
173
121
 
174
122
  ### convex
@@ -181,7 +129,7 @@ defineTable({
181
129
  // ... fields
182
130
  createdAt: v.number(),
183
131
  updatedAt: v.number(),
184
- createdBy: v.optional(v.id('users')),
132
+ createdBy: v.optional(v.id("users")),
185
133
  });
186
134
  ```
187
135
 
@@ -191,11 +139,11 @@ defineTable({
191
139
 
192
140
  <!-- Document required env vars for this project -->
193
141
 
194
- | Variable | Purpose | Required |
195
- |----------|---------|----------|
196
- | `DATABASE_URL` | Database connection | Yes |
197
- | `NEXT_PUBLIC_API_URL` | Public API endpoint | Yes |
198
- | `AUTH_SECRET` | Auth session encryption | Yes |
142
+ | Variable | Purpose | Required |
143
+ | --------------------- | ----------------------- | -------- |
144
+ | `DATABASE_URL` | Database connection | Yes |
145
+ | `NEXT_PUBLIC_API_URL` | Public API endpoint | Yes |
146
+ | `AUTH_SECRET` | Auth session encryption | Yes |
199
147
 
200
148
  ---
201
149
 
@@ -208,10 +156,10 @@ defineTable({
208
156
  // Test location: Co-located with source files
209
157
 
210
158
  // Example test structure
211
- describe('UserService', () => {
212
- it('creates a user with valid data', async () => {
159
+ describe("UserService", () => {
160
+ it("creates a user with valid data", async () => {
213
161
  // Arrange
214
- const input = { name: 'Test', email: 'test@example.com' };
162
+ const input = { name: "Test", email: "test@example.com" };
215
163
 
216
164
  // Act
217
165
  const result = await createUser(input);
@@ -224,5 +172,5 @@ describe('UserService', () => {
224
172
 
225
173
  ---
226
174
 
227
- *Last updated: {{date}}*
228
- *FlyDocs version: 6.3.0*
175
+ _Last updated: {{date}}_
176
+ _FlyDocs version: 6.3.0_
@@ -1 +1 @@
1
- 0.5.0-beta.6
1
+ 0.5.0-beta.8
@@ -95,6 +95,7 @@ Consult the relevant skill BEFORE writing code or making workflow decisions.
95
95
  | Skill | Triggers | Entry |
96
96
  | ----------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
97
97
  | flydocs-cloud | create issue, transition, comment, list issues, assign, update description, update issue, project update, Linear, cloud | .claude/skills/flydocs-cloud/SKILL.md |
98
+ | flydocs-context7 | context7, library docs, documentation lookup, framework docs, package docs, API reference | .claude/skills/flydocs-context7/SKILL.md |
98
99
  | flydocs-estimates | estimate, cost, token usage, API cost, labor estimate, sizing, effort | .claude/skills/flydocs-estimates/SKILL.md |
99
100
  | flydocs-figma | Figma, design, screenshot, token mapping, component from design, pixel-perfect, design system | .claude/skills/flydocs-figma/SKILL.md |
100
101
  | flydocs-local | create issue, transition, comment, list issues, assign, update description, status summary, local | .claude/skills/flydocs-local/SKILL.md |