clew-code 0.2.29 → 0.2.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +20 -4
  2. package/bin/clew.cjs +2 -2
  3. package/dist/main.js +2015 -2015
  4. package/package.json +5 -2
  5. package/scripts/auto-close-duplicates.ts +277 -0
  6. package/scripts/backfill-duplicate-comments.ts +213 -0
  7. package/scripts/codegraph.ts +221 -0
  8. package/scripts/comment-on-duplicates.sh +95 -0
  9. package/scripts/edit-issue-labels.sh +84 -0
  10. package/scripts/final-peer-rename.mjs +46 -0
  11. package/scripts/fix-encoding.mjs +83 -0
  12. package/scripts/fix-inconsistencies.mjs +67 -0
  13. package/scripts/generate-docs.ts +307 -0
  14. package/scripts/gh.sh +96 -0
  15. package/scripts/install.ps1 +40 -0
  16. package/scripts/install.sh +65 -0
  17. package/scripts/issue-lifecycle.ts +38 -0
  18. package/scripts/lifecycle-comment.ts +53 -0
  19. package/scripts/normalize-html.mjs +37 -0
  20. package/scripts/preload.ts +159 -0
  21. package/scripts/rename-content-peer-to-swarm.mjs +67 -0
  22. package/scripts/rename-hooks-mesh.mjs +6 -0
  23. package/scripts/rename-peer-to-swarm.mjs +62 -0
  24. package/scripts/run_devcontainer_claude_code.ps1 +152 -0
  25. package/scripts/scrape.py +82 -0
  26. package/scripts/session.ts +195 -0
  27. package/scripts/sweep.ts +168 -0
  28. package/scripts/write-discovery-test.cjs +93 -0
  29. package/scripts/write-discovery-test2.cjs +65 -0
  30. package/scripts/write-server-final.cjs +108 -0
  31. package/scripts/write-server-test.cjs +69 -0
  32. package/scripts/write-server-test2.cjs +99 -0
  33. package/scripts/write-server-test3.cjs +59 -0
  34. package/scripts/write-server-test4.cjs +108 -0
  35. package/scripts/write-server-v2.cjs +142 -0
  36. package/scripts/write-server-v2b.cjs +129 -0
  37. package/scripts/write-server-v2c.cjs +136 -0
  38. package/scripts/write-store-test.cjs +12 -0
  39. package/scripts/write-store-test2.cjs +163 -0
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Context Preloader
4
+ *
5
+ * Prepares module context before Claude Code starts editing.
6
+ * Usage: bun run scripts/preload.ts <module-name>
7
+ * bun run scripts/preload.ts src/bridge
8
+ * bun run scripts/preload.ts src/services/ai
9
+ */
10
+
11
+ import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs'
12
+ import { join, dirname, basename } from 'path'
13
+ import { execSync } from 'child_process'
14
+
15
+ const ROOT = join(import.meta.dirname, '..')
16
+ const SRC = join(ROOT, 'src')
17
+ const CONTEXT_DIR = join(ROOT, '.clew', 'context')
18
+
19
+ const moduleArg = process.argv[2]
20
+ if (!moduleArg) {
21
+ console.error('Usage: bun run scripts/preload.ts <module-path>')
22
+ console.error(' e.g. bun run scripts/preload.ts bridge')
23
+ console.error(' e.g. bun run scripts/preload.ts src/bridge')
24
+ process.exit(1)
25
+ }
26
+
27
+ const modPath = moduleArg.replace(/\\/g, '/').replace(/^src\//, '')
28
+ const targetDir = join(SRC, modPath)
29
+
30
+ function run(cmd: string): string {
31
+ try {
32
+ return execSync(cmd, { cwd: ROOT, encoding: 'utf-8', timeout: 15000, stdio: ['pipe', 'pipe', 'ignore'] })
33
+ } catch (e: any) {
34
+ return e.stdout || ''
35
+ }
36
+ }
37
+
38
+ function findFiles(dir: string): string[] {
39
+ try {
40
+ const entries = readdirSync(dir)
41
+ return entries
42
+ .filter(f => f.endsWith('.ts') || f.endsWith('.tsx'))
43
+ .map(f => join(dir, f))
44
+ .filter(f => statSync(f).isFile())
45
+ } catch {
46
+ return []
47
+ }
48
+ }
49
+
50
+ // ── Gather data ──
51
+ const files = findFiles(targetDir)
52
+
53
+ const md: string[] = []
54
+ md.push(`# Context: ${modPath}`)
55
+ md.push(`_Preloaded ${new Date().toISOString().slice(0, 16).replace('T', ' ')}_`)
56
+ md.push('')
57
+
58
+ // Summary
59
+ md.push(`## Scope`)
60
+ md.push('')
61
+ md.push(`Module: \`${modPath}\``)
62
+ md.push(`Files: ${files.length}`)
63
+ md.push('')
64
+
65
+ // File listing with sizes
66
+ md.push(`## Files`)
67
+ md.push('')
68
+ md.push(`| File | Lines | Exports | TODOs |`)
69
+ md.push(`|------|-------|---------|-------|`)
70
+
71
+ let totalLines = 0
72
+ for (const file of files) {
73
+ const relName = file.replace(/\\/g, '/').replace(ROOT.replace(/\\/g, '/') + '/', '')
74
+ const content = readFileSync(file, 'utf-8')
75
+ const lines = content.split('\n').length
76
+ totalLines += lines
77
+
78
+ const exports = content.match(/export (async )?(function|const|class|interface|type) [a-zA-Z_$][a-zA-Z0-9_$]*/g) || []
79
+ const exportList = exports.map(e => e.split(/\s+/).pop()).filter(Boolean).slice(0, 8).join(', ')
80
+ const todos = (content.match(/\bTODO|FIXME\b/g) || []).length
81
+
82
+ md.push(`| ${relName} | ${lines} | ${exportList || '-'} | ${todos || '-'} |`)
83
+ }
84
+
85
+ md.push(`| **Total** | **${totalLines}** | | |`)
86
+ md.push('')
87
+
88
+ // Internal dependencies
89
+ md.push(`## Internal Dependencies`)
90
+ md.push('')
91
+ md.push('```')
92
+ const deps = files.map(f => {
93
+ const c = readFileSync(f, 'utf-8')
94
+ return [...c.matchAll(/from\s+['"](src\/[^'"]+)['"]/g)].map(m => m[1])
95
+ }).flat()
96
+ const uniqueDeps = [...new Set(deps)].sort()
97
+ for (const d of uniqueDeps) md.push(d)
98
+ if (!uniqueDeps.length) md.push('(none)')
99
+ md.push('```')
100
+ md.push('')
101
+
102
+ // Recent git history
103
+ md.push(`## Recent Changes`)
104
+ md.push('')
105
+ md.push('```')
106
+ try {
107
+ const gitLog = run(`git log --oneline -10 -- "src/${modPath}/"`).trim()
108
+ md.push(gitLog || '(no recent changes)')
109
+ } catch {
110
+ md.push('(no git history)')
111
+ }
112
+ md.push('```')
113
+ md.push('')
114
+
115
+ // Key types
116
+ const typePattern = /export (interface|type) (\w+)/g
117
+ const allTypes: string[] = []
118
+ for (const file of files) {
119
+ const c = readFileSync(file, 'utf-8')
120
+ let m: RegExpExecArray | null
121
+ while ((m = typePattern.exec(c)) !== null) allTypes.push(m[2])
122
+ }
123
+ if (allTypes.length) {
124
+ md.push(`## Key Types`)
125
+ md.push('')
126
+ md.push('- ' + [...new Set(allTypes)].join('\n- '))
127
+ md.push('')
128
+ }
129
+
130
+ // Open TODOs
131
+ const allTodos: { file: string; line: string }[] = []
132
+ for (const file of files) {
133
+ const c = readFileSync(file, 'utf-8')
134
+ const lines = c.split('\n')
135
+ lines.forEach((l, i) => {
136
+ if (/\bTODO|FIXME\b/.test(l)) {
137
+ const text = l.replace(/^\s*\/\/\s*/, '').replace(/^\s*/, '').slice(0, 80)
138
+ allTodos.push({ file: file.replace(/\\/g, '/').replace(ROOT.replace(/\\/g, '/') + '/', ''), line: text })
139
+ }
140
+ })
141
+ }
142
+
143
+ if (allTodos.length) {
144
+ md.push(`## Open TODOs`)
145
+ md.push('')
146
+ for (const t of allTodos) md.push(`- \`${t.file}\`: ${t.line}`)
147
+ md.push('')
148
+ }
149
+
150
+ // Write
151
+ mkdirSync(CONTEXT_DIR, { recursive: true })
152
+ const outPath = join(CONTEXT_DIR, `${modPath.replace(/\//g, '-')}.md`)
153
+ writeFileSync(outPath, md.join('\n'), 'utf-8')
154
+
155
+ console.log(`\n✅ Preloaded: ${modPath}`)
156
+ console.log(` ${files.length} files, ${totalLines} lines`)
157
+ console.log(` Saved to: ${outPath}`)
158
+ console.log('')
159
+ console.log('Claude Code can now read this context via Read tool.')
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Content rename: peer -> swarm in all Swarm* tool files
3
+ */
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+
7
+ function walk(dir) {
8
+ const r = [];
9
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
10
+ const f = path.join(dir, e.name);
11
+ if (e.isDirectory()) r.push(...walk(f));
12
+ else if (e.name.endsWith('.ts') || e.name.endsWith('.tsx')) r.push(f);
13
+ }
14
+ return r;
15
+ }
16
+
17
+ const dirs = [
18
+ 'src/tools/SwarmBroadcastTool',
19
+ 'src/tools/SwarmDisconnectTool',
20
+ 'src/tools/SwarmDiscoverTool',
21
+ 'src/tools/SwarmJoinTool',
22
+ 'src/tools/SwarmListMessagesTool',
23
+ 'src/tools/SwarmListRolesTool',
24
+ 'src/tools/SwarmPingTool',
25
+ 'src/tools/SwarmRunTool',
26
+ 'src/tools/SwarmSendMessageTool',
27
+ 'src/tools/SwarmSetNameTool',
28
+ 'src/tools/SwarmSetRoleTool',
29
+ 'src/tools/SwarmShareTool',
30
+ 'src/tools/SwarmSpawnTool',
31
+ 'src/tools/ProcessSwarmTool',
32
+ 'src/tools/CodexSwarmTool',
33
+ 'src/swarm',
34
+ ];
35
+
36
+ const files = [];
37
+ for (const d of dirs) {
38
+ if (fs.existsSync(d)) files.push(...walk(d));
39
+ }
40
+
41
+ for (const f of files) {
42
+ let c = fs.readFileSync(f, 'utf-8');
43
+ const orig = c;
44
+
45
+ // PeerXxx -> SwarmXxx (class/identifier names)
46
+ c = c.replace(/\bPeer(Broadcast|Disconnect|Discover|Join|ListMessages|ListRoles|Ping|Run|SendMessage|SetName|SetRole|Share|Spawn|Info|Help|Server|Store|Discovery|StatusLine)\b/g, 'Swarm$1');
47
+ c = c.replace(/\bProcessPeer(Provider|Tool)\b/g, 'ProcessSwarm$1');
48
+ c = c.replace(/\bCodexPeerTool\b/g, 'CodexSwarmTool');
49
+
50
+ // peer_xxx -> swarm_xxx (tool name constants)
51
+ c = c.replace(/peer_/g, 'swarm_');
52
+
53
+ // peerXxx -> swarmXxx (camelCase variables)
54
+ c = c.replace(/\bpeer(Server|Store|Discovery|Port|Name|Info|List|Count|Connection|Sharing|Health)\b/g, 'swarm$1');
55
+
56
+ // usePeerAutoInject -> useSwarmAutoInject
57
+ c = c.replace(/\busePeerAutoInject\b/g, 'useSwarmAutoInject');
58
+
59
+ // /peer -> /swarm (command paths)
60
+ c = c.replace(/\/peer\b/g, '/swarm');
61
+
62
+ if (c !== orig) {
63
+ fs.writeFileSync(f, c, 'utf-8');
64
+ console.log('OK:', f);
65
+ }
66
+ }
67
+ console.log('DONE');
@@ -0,0 +1,6 @@
1
+ import fs from 'fs';
2
+ for (const f of fs.readdirSync('src/hooks')) {
3
+ const nn = f.replace(/A2A/g, 'Mesh').replace(/a2a/g, 'mesh');
4
+ if (nn !== f) fs.renameSync('src/hooks/' + f, 'src/hooks/' + nn);
5
+ }
6
+ console.log('DONE');
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Bulk rename: peer -> swarm across all source files
3
+ */
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+
7
+ function walk(dir) {
8
+ const results = [];
9
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
10
+ const full = path.join(dir, entry.name);
11
+ if (entry.isDirectory()) {
12
+ if (!entry.name.startsWith('.') && entry.name !== 'node_modules' && entry.name !== 'dist') {
13
+ results.push(...walk(full));
14
+ }
15
+ } else if (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx')) {
16
+ results.push(full);
17
+ }
18
+ }
19
+ return results;
20
+ }
21
+
22
+ // Only replace import paths — safe, targeted changes
23
+ const replacements = [
24
+ // Import paths for peer/ directory
25
+ [/from ['"]\.\.\/peer\//g, `from '../swarm/`],
26
+ [/from ['"]\.\/peer\//g, `from './swarm/`],
27
+ [/from ['"]\.\.\/\.\.\/peer\//g, `from '../../swarm/`],
28
+ [/from ['"]\.\.\/\.\.\/\.\.\/peer\//g, `from '../../../swarm/`],
29
+
30
+ // Import paths for tools/Peer* -> tools/Swarm*
31
+ [/from ['"]\.\.\/\.\.\/tools\/PeerInfoTool\//g, `from '../../tools/SwarmInfoTool/`],
32
+ [/from ['"]\.\.\/tools\/PeerInfoTool\//g, `from '../tools/SwarmInfoTool/`],
33
+ [/from ['"]\.\/PeerInfoTool\//g, `from './SwarmInfoTool/`],
34
+ [/from ['"]\.\.\/\.\.\/tools\/PeerHelpTool\//g, `from '../../tools/SwarmHelpTool/`],
35
+ [/from ['"]\.\.\/tools\/PeerHelpTool\//g, `from '../tools/SwarmHelpTool/`],
36
+ [/from ['"]\.\/PeerHelpTool\//g, `from './SwarmHelpTool/`],
37
+ [/from ['"]\.\.\/peer\//g, `from '../swarm/`],
38
+ ];
39
+
40
+ const srcDir = 'src';
41
+ const files = walk(srcDir);
42
+ let totalChanged = 0;
43
+
44
+ for (const file of files) {
45
+ let content = fs.readFileSync(file, 'utf-8');
46
+ let changed = false;
47
+
48
+ for (const [pattern, replacement] of replacements) {
49
+ if (pattern.test(content)) {
50
+ content = content.replace(pattern, replacement);
51
+ changed = true;
52
+ }
53
+ }
54
+
55
+ if (changed) {
56
+ fs.writeFileSync(file, content, 'utf-8');
57
+ totalChanged++;
58
+ console.log('Updated:', file);
59
+ }
60
+ }
61
+
62
+ console.log(`\nTotal files updated: ${totalChanged}`);
@@ -0,0 +1,152 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Automates the setup and connection to a DevContainer environment using either Docker or Podman on Windows.
4
+
5
+ .DESCRIPTION
6
+ This script automates the process of initializing, starting, and connecting to a DevContainer
7
+ using either Docker or Podman as the container backend. It must be executed from the root
8
+ directory of your project and assumes the script is located in a 'Script' subdirectory.
9
+
10
+ .PARAMETER Backend
11
+ Specifies the container backend to use. Valid values are 'docker' or 'podman'.
12
+
13
+ .EXAMPLE
14
+ .\Script\run_devcontainer_claude_code.ps1 -Backend docker
15
+ Uses Docker as the container backend.
16
+
17
+ .EXAMPLE
18
+ .\Script\run_devcontainer_claude_code.ps1 -Backend podman
19
+ Uses Podman as the container backend.
20
+
21
+ .NOTES
22
+ Project Structure:
23
+ Project/
24
+ ├── .devcontainer/
25
+ └── Script/
26
+ └── run_devcontainer_claude_code.ps1
27
+ #>
28
+
29
+ [CmdletBinding()]
30
+ param(
31
+ [Parameter(Mandatory=$true)]
32
+ [ValidateSet('docker','podman')]
33
+ [string]$Backend
34
+ )
35
+
36
+ # Notify script start
37
+ Write-Host "--- DevContainer Startup & Connection Script ---"
38
+ Write-Host "Using backend: $($Backend)"
39
+
40
+ # --- Prerequisite Check ---
41
+ Write-Host "Checking for required commands..."
42
+ try {
43
+ if (-not (Get-Command $Backend -ErrorAction SilentlyContinue)) {
44
+ throw "Required command '$($Backend)' not found."
45
+ }
46
+ Write-Host "- $($Backend) command found."
47
+ if (-not (Get-Command devcontainer -ErrorAction SilentlyContinue)) {
48
+ throw "Required command 'devcontainer' not found."
49
+ }
50
+ Write-Host "- devcontainer command found."
51
+ }
52
+ catch {
53
+ Write-Error "A required command is not installed or not in your PATH. $($_.Exception.Message)"
54
+ Write-Error "Please ensure both '$Backend' and 'devcontainer' are installed and accessible in your system's PATH."
55
+ exit 1
56
+ }
57
+
58
+
59
+ # --- Backend-Specific Initialization ---
60
+ if ($Backend -eq 'podman') {
61
+ Write-Host "--- Podman Backend Initialization ---"
62
+
63
+ # --- Step 1a: Initialize Podman machine ---
64
+ Write-Host "Initializing Podman machine 'claudeVM'..."
65
+ try {
66
+ & podman machine init claudeVM
67
+ Write-Host "Podman machine 'claudeVM' initialized or already exists."
68
+ } catch {
69
+ Write-Error "Failed to initialize Podman machine: $($_.Exception.Message)"
70
+ exit 1 # Exit script on error
71
+ }
72
+
73
+ # --- Step 1b: Start Podman machine ---
74
+ Write-Host "Starting Podman machine 'claudeVM'..."
75
+ try {
76
+ & podman machine start claudeVM -q
77
+ Write-Host "Podman machine started or already running."
78
+ } catch {
79
+ Write-Error "Failed to start Podman machine: $($_.Exception.Message)"
80
+ exit 1
81
+ }
82
+
83
+ # --- Step 2: Set default connection ---
84
+ Write-Host "Setting default Podman connection to 'claudeVM'..."
85
+ try {
86
+ & podman system connection default claudeVM
87
+ Write-Host "Default connection set."
88
+ } catch {
89
+ Write-Warning "Failed to set default Podman connection (may be already set or machine issue): $($_.Exception.Message)"
90
+ }
91
+
92
+ } elseif ($Backend -eq 'docker') {
93
+ Write-Host "--- Docker Backend Initialization ---"
94
+
95
+ # --- Step 1 & 2: Check Docker Desktop ---
96
+ Write-Host "Checking if Docker Desktop is running and docker command is available..."
97
+ try {
98
+ docker info | Out-Null
99
+ Write-Host "Docker Desktop (daemon) is running."
100
+ } catch {
101
+ Write-Error "Docker Desktop is not running or docker command not found."
102
+ Write-Error "Please ensure Docker Desktop is running."
103
+ exit 1
104
+ }
105
+ }
106
+
107
+ # --- Step 3: Bring up DevContainer ---
108
+ Write-Host "Bringing up DevContainer in the current folder..."
109
+ try {
110
+ $arguments = @('up', '--workspace-folder', '.')
111
+ if ($Backend -eq 'podman') {
112
+ $arguments += '--docker-path', 'podman'
113
+ }
114
+ & devcontainer @arguments
115
+ Write-Host "DevContainer startup process completed."
116
+ } catch {
117
+ Write-Error "Failed to bring up DevContainer: $($_.Exception.Message)"
118
+ exit 1
119
+ }
120
+
121
+ # --- Step 4: Get DevContainer ID ---
122
+ Write-Host "Finding the DevContainer ID..."
123
+ $currentFolder = (Get-Location).Path
124
+
125
+ try {
126
+ $containerId = (& $Backend ps --filter "label=devcontainer.local_folder=$currentFolder" --format '{{.ID}}').Trim()
127
+ } catch {
128
+ $displayCommand = "$Backend ps --filter `"label=devcontainer.local_folder=$currentFolder`" --format '{{.ID}}'"
129
+ Write-Error "Failed to get container ID (Command: $displayCommand): $($_.Exception.Message)"
130
+ exit 1
131
+ }
132
+
133
+ if (-not $containerId) {
134
+ Write-Error "Could not find DevContainer ID for the current folder ('$currentFolder')."
135
+ Write-Error "Please check if 'devcontainer up' was successful and the container is running."
136
+ exit 1
137
+ }
138
+ Write-Host "Found container ID: $containerId"
139
+
140
+ # --- Step 5 & 6: Execute command and enter interactive shell inside container ---
141
+ Write-Host "Executing 'claude' command and then starting zsh session inside container $($containerId)..."
142
+ try {
143
+ & $Backend exec -it $containerId zsh -c 'claude; exec zsh'
144
+ Write-Host "Interactive session ended."
145
+ } catch {
146
+ $displayCommand = "$Backend exec -it $containerId zsh -c 'claude; exec zsh'"
147
+ Write-Error "Failed to execute command inside container (Command: $displayCommand): $($_.Exception.Message)"
148
+ exit 1
149
+ }
150
+
151
+ # Notify script completion
152
+ Write-Host "--- Script completed ---"
@@ -0,0 +1,82 @@
1
+ import sys
2
+ import json
3
+ import argparse
4
+ from duckduckgo_search import DDGS
5
+ from scrapling import Fetcher
6
+ import trafilatura
7
+
8
+ def run_search(query, max_results=5):
9
+ try:
10
+ with DDGS() as ddgs:
11
+ results = list(ddgs.text(query, max_results=max_results))
12
+ # Format to simple dict list
13
+ formatted = []
14
+ for r in results:
15
+ formatted.append({
16
+ "title": r.get("title", ""),
17
+ "url": r.get("href", ""),
18
+ "body": r.get("body", "")
19
+ })
20
+ return {"status": "ok", "results": formatted}
21
+ except Exception as e:
22
+ return {"status": "failed", "error": str(e)}
23
+
24
+ def run_scrape(url):
25
+ try:
26
+ # 1. Initialize Scrapling Fetcher
27
+ f = Fetcher()
28
+
29
+ # 2. Get the response object stealthily
30
+ res = f.get(url)
31
+
32
+ # 3. Retrieve HTML content and text
33
+ html = res.html_content
34
+ text = res.text
35
+
36
+ # 4. Extract title via XPath selector
37
+ title_node = res.xpath('//title/text()').get()
38
+ title = title_node.strip() if title_node else ""
39
+
40
+ # 5. Extract clean main text/Markdown and metadata using Trafilatura
41
+ clean_text = trafilatura.extract(
42
+ html,
43
+ output_format='markdown',
44
+ include_comments=False,
45
+ include_tables=True
46
+ )
47
+
48
+ # 6. Extract page metadata
49
+ meta = trafilatura.extract_metadata(html)
50
+
51
+ result = {
52
+ "status": "ok",
53
+ "title": title or (meta.title if meta else ""),
54
+ "author": meta.author if meta else "",
55
+ "published_at": meta.date if meta else "",
56
+ "markdown": clean_text or text or "",
57
+ "url": url
58
+ }
59
+ return result
60
+ except Exception as e:
61
+ return {"status": "failed", "error": str(e)}
62
+
63
+ def main():
64
+ parser = argparse.ArgumentParser(description="Stealth Web Search and Scraper Helper")
65
+ parser.add_argument("--search", type=str, help="Search query to run on DuckDuckGo")
66
+ parser.add_argument("--url", type=str, help="Web page URL to scrape")
67
+ parser.add_argument("--max-results", type=int, default=5, help="Max search results to return")
68
+
69
+ args = parser.parse_args()
70
+
71
+ if args.search:
72
+ result = run_search(args.search, args.max_results)
73
+ print(json.dumps(result))
74
+ elif args.url:
75
+ result = run_scrape(args.url)
76
+ print(json.dumps(result))
77
+ else:
78
+ print(json.dumps({"status": "failed", "error": "Specify either --search or --url"}))
79
+ sys.exit(1)
80
+
81
+ if __name__ == "__main__":
82
+ main()