webctx 0.2.8 → 0.2.10
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/AGENTS.md +12 -1
- package/CONTRIBUTORS.md +2 -2
- package/README.md +27 -2
- package/internal/app/app.go +73 -2
- package/internal/app/app_test.go +7 -0
- package/internal/app/providers.go +165 -0
- package/internal/app/providers_test.go +305 -0
- package/internal/app/scrape.go +1 -1
- package/internal/app/search_keys.go +233 -0
- package/internal/app/search_keys_test.go +227 -0
- package/internal/app/tools.go +116 -62
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -47,13 +47,16 @@ Direct commands:
|
|
|
47
47
|
Preserve these commands unless the user explicitly asks to change them:
|
|
48
48
|
|
|
49
49
|
- `webctx search <query> [--exclude domain1,domain2] [--keyword phrase]`
|
|
50
|
+
- `webctx status` and `webctx providers enable|disable <brave|tavily|exa>`
|
|
51
|
+
- `webctx keys list|add|remove` for macOS Keychain-managed additional search keys; `BRAVE_API_KEYS`, `TAVILY_API_KEYS`, and `EXA_API_KEYS` also work via environment variables
|
|
50
52
|
- `webctx read-link <url>`
|
|
51
53
|
- `webctx map-site <url>`
|
|
52
54
|
- `webctx --version`
|
|
53
55
|
|
|
54
56
|
Behavioral expectations:
|
|
55
57
|
|
|
56
|
-
- `search` combines Brave, Tavily, and Exa results, then re-ranks them with duplicate-aware scoring.
|
|
58
|
+
- `search` combines enabled Brave, Tavily, and Exa results, then re-ranks them with duplicate-aware scoring; an unavailable Exa can fall back to quoted-phrase Brave/Tavily queries for `--keyword` (not a strict text filter).
|
|
59
|
+
- Search key rotation tries plural credentials first, legacy singular credentials afterward; failover applies only to credential/quota/rate/server/transport failures, never bad requests. Keep Firecrawl paid operations outside this rotation.
|
|
57
60
|
- `read-link` keeps the current GitHub raw-content and `.md` fast paths. Firecrawl uses its built-in 30-minute cache, automatic and enhanced proxies, then browser extraction as a final fallback.
|
|
58
61
|
- `map-site` keeps the current Firecrawl map request settings.
|
|
59
62
|
- The CLI should remain agent-friendly and emit plain markdown/text output.
|
|
@@ -100,3 +103,11 @@ When cutting a release, update `src/content/docs/changelog.md` before tagging.
|
|
|
100
103
|
- Do not include docs-site-only changes such as site styling, Zuedocs/package bumps, deploy plumbing, footer/layout changes, or documentation navigation changes.
|
|
101
104
|
- Rewrite commit subjects into clear release notes instead of pasting raw commit messages.
|
|
102
105
|
- If a release contains only tagging/release metadata, write: `Maintenance release. No direct code behavior changes beyond release preparation.`
|
|
106
|
+
|
|
107
|
+
### Docs deployment
|
|
108
|
+
|
|
109
|
+
The Docs GitHub Actions workflow deploys production only for the watched site paths; Vercel Git auto-deployments and `ignoreCommand` stay disabled. Keep the project-scoped `VERCEL_TOKEN` Actions secret and ID variables configured. Check Astro locally; builds run in CI.
|
|
110
|
+
|
|
111
|
+
### Origo deployment
|
|
112
|
+
|
|
113
|
+
The separate `Origo API` GitHub Actions workflow deploys the Go function only for changes to its import graph and Vercel configuration. Use `VERCEL_ORIGO_TOKEN` and `VERCEL_ORIGO_PROJECT_ID` exclusively for Origo; never substitute the WebCTX Docs token or a static `--prebuilt` site bundle. Keep the `/mcp` rewrite and Origo production environment variables intact.
|
package/CONTRIBUTORS.md
CHANGED
|
@@ -108,9 +108,9 @@ gh workflow run npm-publish-retry.yml --repo amxv/webctx --ref main -f version=0
|
|
|
108
108
|
|
|
109
109
|
## Origo deployment
|
|
110
110
|
|
|
111
|
-
The `origo-api` Vercel project
|
|
111
|
+
The `origo-api` Vercel project uses `ORIGO_DEPLOYMENT=1` to select its Go Function configuration. It needs `ORIGO_API_KEY` and `FIRECRAWL_API_KEY` production environment variables; optionally add `GH_TOKEN` for native authenticated GitHub reads. The docs project remains `webctx-docs` and never receives the Origo connection key.
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
The independent `Docs` and `Origo API` GitHub Actions workflows filter their own build inputs before deploying to Vercel. Origo uses the `VERCEL_ORIGO_TOKEN` Actions secret and `VERCEL_ORIGO_PROJECT_ID` variable, separate from the docs credentials. Git-triggered Vercel deployments are disabled; `v*` tags still drive CLI releases.
|
|
114
114
|
|
|
115
115
|
## Notes on package naming
|
|
116
116
|
|
package/README.md
CHANGED
|
@@ -95,7 +95,7 @@ Recognized native GitHub auth, private/not-found, and rate-limit failures stay a
|
|
|
95
95
|
|
|
96
96
|
## Search
|
|
97
97
|
|
|
98
|
-
Normal search asks Brave, Tavily, and Exa, removes duplicate URLs, automatically reads promising sources, follows relevant links, and returns excerpts plus the ranked list. Alexandria discovery is included when available
|
|
98
|
+
Normal search asks the enabled Brave, Tavily, and Exa providers, removes duplicate URLs, automatically reads promising sources, follows relevant links, and returns excerpts plus the ranked list. One working search provider is enough; Alexandria discovery is included when available. Use `execute` when you need provider-native data.
|
|
99
99
|
|
|
100
100
|
```bash
|
|
101
101
|
webctx search "next.js server components"
|
|
@@ -103,6 +103,31 @@ webctx search "react hooks" --exclude youtube.com,medium.com
|
|
|
103
103
|
webctx search "drizzle orm" --keyword "migration guide"
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
+
### Enable or disable search providers
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
webctx status # enabled/disabled providers and locally configured keys
|
|
110
|
+
webctx providers disable exa # stop all Exa search requests, even with a key present
|
|
111
|
+
webctx providers enable exa # turn Exa back on later
|
|
112
|
+
webctx providers disable brave
|
|
113
|
+
webctx providers enable brave
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Provider settings persist across runs, independently of environment variables and Keychain credentials. The settings file lives in the OS user config directory (`webctx/config.json`); set `WEBCTX_CONFIG_FILE` to override its location. Disabled providers are never queried. `status` only reports key presence, **not** live credit balances or API health.
|
|
117
|
+
|
|
118
|
+
Multiple keys per provider are supported. Add extra keys on macOS without placing secrets in command arguments or plaintext config files:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
webctx keys list # count configured and managed keys (never prints them)
|
|
122
|
+
pbpaste | webctx keys add exa --stdin # securely store a copied new key in macOS Keychain
|
|
123
|
+
webctx keys remove exa 1 # remove the newest Keychain-managed Exa key
|
|
124
|
+
webctx providers enable exa
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Or supply `BRAVE_API_KEYS`, `TAVILY_API_KEYS`, or `EXA_API_KEYS` as comma-separated keys (or JSON arrays) in the environment or `.env.local`, alongside the corresponding singular `*_API_KEY`. Keys in the plural list are attempted first, then the legacy single key. On a key-specific authentication, quota/credit, rate-limit, or server error, the next key is tried automatically; invalid requests are not retried. Manage only keys/accounts you are authorized to use and honor each provider's usage restrictions. The Firecrawl/Alexandria integration is separate and **is not included in automatic key rotation**, especially for paid operations.
|
|
128
|
+
|
|
129
|
+
`--keyword` prefers Exa's strict include-text filter when available. If Exa is disabled, unconfigured, or fails, Brave/Tavily search for the quoted phrase instead; the output explicitly notes that this fallback cannot guarantee the phrase appears in the page text. Firecrawl is a separate reading/site-map/Alexandria integration, not one of these three search providers.
|
|
130
|
+
|
|
106
131
|
## Map a site
|
|
107
132
|
|
|
108
133
|
Use `map-site` when you want to discover the useful pages before reading them.
|
|
@@ -131,7 +156,7 @@ FIRECRAWL_API_KEY=...
|
|
|
131
156
|
GH_TOKEN=...
|
|
132
157
|
```
|
|
133
158
|
|
|
134
|
-
- `BRAVE_API_KEY`, `TAVILY_API_KEY`, and `EXA_API_KEY` power normal `search`.
|
|
159
|
+
- `BRAVE_API_KEY`, `TAVILY_API_KEY`, and `EXA_API_KEY` power normal `search`. The plural `*_API_KEYS` names supply additional fallback credentials. Configure any one or more, and optionally disable providers you no longer want to query.
|
|
135
160
|
- `FIRECRAWL_API_KEY` powers `map-site`, page-crawl fallbacks, and Alexandria discovery/inspection/paid execution.
|
|
136
161
|
- `GH_TOKEN` or `GITHUB_TOKEN` is optional. It increases GitHub capacity and unlocks reads such as blame, Discussions, some Actions job logs, private resources the token can access, and richer PR review state.
|
|
137
162
|
|
package/internal/app/app.go
CHANGED
|
@@ -16,8 +16,6 @@ const commandName = "webctx"
|
|
|
16
16
|
var version = buildinfo.CurrentVersion()
|
|
17
17
|
|
|
18
18
|
func Run(args []string, stdout, stderr io.Writer) int {
|
|
19
|
-
loadEnvLocal()
|
|
20
|
-
|
|
21
19
|
if len(args) == 0 || isHelpArg(args[0]) {
|
|
22
20
|
_, _ = fmt.Fprintln(stdout, usageText())
|
|
23
21
|
return 0
|
|
@@ -29,6 +27,71 @@ func Run(args []string, stdout, stderr io.Writer) int {
|
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
tool := args[0]
|
|
30
|
+
if tool == "keys" {
|
|
31
|
+
if len(args) == 2 && args[1] == "list" {
|
|
32
|
+
loadEnvLocal()
|
|
33
|
+
if err := printProviderStatus(stdout); err != nil {
|
|
34
|
+
_, _ = fmt.Fprintln(stderr, err)
|
|
35
|
+
return 1
|
|
36
|
+
}
|
|
37
|
+
if err := printManagedSearchKeySlots(stdout); err != nil {
|
|
38
|
+
_, _ = fmt.Fprintln(stderr, err)
|
|
39
|
+
return 1
|
|
40
|
+
}
|
|
41
|
+
return 0
|
|
42
|
+
}
|
|
43
|
+
if len(args) == 4 && args[1] == "add" && args[3] == "--stdin" {
|
|
44
|
+
count, err := addManagedSearchKey(args[2], os.Stdin)
|
|
45
|
+
if err != nil {
|
|
46
|
+
_, _ = fmt.Fprintln(stderr, err)
|
|
47
|
+
return 1
|
|
48
|
+
}
|
|
49
|
+
_, _ = fmt.Fprintf(stdout, "Saved an additional %s API key in macOS Keychain (%d managed key(s)).\n", strings.ToLower(args[2]), count)
|
|
50
|
+
return 0
|
|
51
|
+
}
|
|
52
|
+
if len(args) == 4 && args[1] == "remove" {
|
|
53
|
+
count, err := removeManagedSearchKey(args[2], args[3])
|
|
54
|
+
if err != nil {
|
|
55
|
+
_, _ = fmt.Fprintln(stderr, err)
|
|
56
|
+
return 1
|
|
57
|
+
}
|
|
58
|
+
_, _ = fmt.Fprintf(stdout, "Removed managed %s API key (%d managed key(s) remain).\n", strings.ToLower(args[2]), count)
|
|
59
|
+
return 0
|
|
60
|
+
}
|
|
61
|
+
_, _ = fmt.Fprintln(stderr, "Usage: webctx keys list | keys add <brave|tavily|exa> --stdin | keys remove <provider> <managed-key-index>")
|
|
62
|
+
return 1
|
|
63
|
+
}
|
|
64
|
+
isProviderTool := tool == "status" || tool == "providers" || tool == "provider"
|
|
65
|
+
isProviderStatus := (tool == "status" && len(args) == 1) ||
|
|
66
|
+
((tool == "providers" || tool == "provider") && (len(args) == 1 || (len(args) == 2 && args[1] == "status")))
|
|
67
|
+
if !isProviderTool || isProviderStatus {
|
|
68
|
+
loadEnvLocal()
|
|
69
|
+
}
|
|
70
|
+
if isProviderTool {
|
|
71
|
+
if isProviderStatus {
|
|
72
|
+
if err := printProviderStatus(stdout); err != nil {
|
|
73
|
+
_, _ = fmt.Fprintln(stderr, err)
|
|
74
|
+
return 1
|
|
75
|
+
}
|
|
76
|
+
return 0
|
|
77
|
+
}
|
|
78
|
+
if tool == "status" || len(args) != 3 || (args[1] != "enable" && args[1] != "disable") {
|
|
79
|
+
_, _ = fmt.Fprintln(stderr, "Usage: webctx providers [status|enable <brave|tavily|exa>|disable <brave|tavily|exa>]")
|
|
80
|
+
return 1
|
|
81
|
+
}
|
|
82
|
+
enabled := args[1] == "enable"
|
|
83
|
+
if _, err := setSearchProviderEnabled(args[2], enabled); err != nil {
|
|
84
|
+
_, _ = fmt.Fprintln(stderr, err)
|
|
85
|
+
return 1
|
|
86
|
+
}
|
|
87
|
+
state := "disabled"
|
|
88
|
+
if enabled {
|
|
89
|
+
state = "enabled"
|
|
90
|
+
}
|
|
91
|
+
_, _ = fmt.Fprintf(stdout, "%s %s. Run 'webctx status' to inspect all providers.\n", strings.ToLower(strings.TrimSpace(args[2])), state)
|
|
92
|
+
return 0
|
|
93
|
+
}
|
|
94
|
+
|
|
32
95
|
flags, positional := parseArgs(args[1:])
|
|
33
96
|
input := ""
|
|
34
97
|
if len(positional) > 0 {
|
|
@@ -197,6 +260,11 @@ func usageText() string {
|
|
|
197
260
|
|
|
198
261
|
Usage:
|
|
199
262
|
webctx search <query> [--exclude domain1,domain2] [--keyword phrase]
|
|
263
|
+
webctx status
|
|
264
|
+
webctx providers [status|enable <brave|tavily|exa>|disable <brave|tavily|exa>]
|
|
265
|
+
webctx keys list
|
|
266
|
+
webctx keys add <brave|tavily|exa> --stdin
|
|
267
|
+
webctx keys remove <brave|tavily|exa> <managed-key-index>
|
|
200
268
|
webctx read-link <url> [--question 'what to find']
|
|
201
269
|
webctx map-site <url> [--query topic] [--limit 30] [--offset 0] [--path-prefix /api] [--language en|all]
|
|
202
270
|
webctx research [question] [--view sources|groups|operations] [--sources IDs] [--include inputs,output,examples] [--limit N] [--offset N]
|
|
@@ -208,6 +276,9 @@ Examples:
|
|
|
208
276
|
webctx search "next.js server components"
|
|
209
277
|
webctx search "react hooks" --exclude youtube.com,vimeo.com
|
|
210
278
|
webctx search "drizzle orm" --keyword "migration guide"
|
|
279
|
+
webctx providers disable exa
|
|
280
|
+
webctx providers enable exa
|
|
281
|
+
webctx keys add exa --stdin < /path/to/private/key.txt
|
|
211
282
|
webctx research "podcast conversations about AI agents"
|
|
212
283
|
webctx research --view sources --limit 5
|
|
213
284
|
webctx inspect particle/podcasts/episodes/search
|
package/internal/app/app_test.go
CHANGED
|
@@ -84,9 +84,13 @@ func TestScoreAndRankResultsPreservesInsertionOrderForTies(t *testing.T) {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
func TestSearchMissingCredentialsErrorIsHelpful(t *testing.T) {
|
|
87
|
+
t.Setenv("WEBCTX_CONFIG_FILE", filepath.Join(t.TempDir(), "config.json"))
|
|
87
88
|
t.Setenv("BRAVE_API_KEY", "")
|
|
88
89
|
t.Setenv("TAVILY_API_KEY", "")
|
|
89
90
|
t.Setenv("EXA_API_KEY", "")
|
|
91
|
+
t.Setenv("BRAVE_API_KEYS", "")
|
|
92
|
+
t.Setenv("TAVILY_API_KEYS", "")
|
|
93
|
+
t.Setenv("EXA_API_KEYS", "")
|
|
90
94
|
|
|
91
95
|
text, err := Search(SearchParams{Query: "openai api"})
|
|
92
96
|
if err == nil {
|
|
@@ -179,6 +183,9 @@ func TestLoadKeychainEnvLoadsMissingOnly(t *testing.T) {
|
|
|
179
183
|
t.Setenv("BRAVE_API_KEY", "already-set")
|
|
180
184
|
t.Setenv("TAVILY_API_KEY", "")
|
|
181
185
|
t.Setenv("EXA_API_KEY", "")
|
|
186
|
+
t.Setenv("BRAVE_API_KEYS", "")
|
|
187
|
+
t.Setenv("TAVILY_API_KEYS", "")
|
|
188
|
+
t.Setenv("EXA_API_KEYS", "")
|
|
182
189
|
t.Setenv("FIRECRAWL_API_KEY", "")
|
|
183
190
|
t.Setenv("GH_TOKEN", "")
|
|
184
191
|
t.Setenv("GITHUB_TOKEN", "")
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
package app
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"io"
|
|
7
|
+
"os"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"slices"
|
|
10
|
+
"strings"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// These are the configurable search providers. Firecrawl powers independent
|
|
14
|
+
// read-link, map-site, and Alexandria operations, not federated web search.
|
|
15
|
+
var searchProviderKeys = []struct {
|
|
16
|
+
name string
|
|
17
|
+
key string
|
|
18
|
+
}{
|
|
19
|
+
{"brave", "BRAVE_API_KEY"},
|
|
20
|
+
{"tavily", "TAVILY_API_KEY"},
|
|
21
|
+
{"exa", "EXA_API_KEY"},
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type providerConfig struct {
|
|
25
|
+
DisabledProviders []string `json:"disabled_providers,omitempty"`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func providerConfigPath() (string, error) {
|
|
29
|
+
if custom := strings.TrimSpace(os.Getenv("WEBCTX_CONFIG_FILE")); custom != "" {
|
|
30
|
+
return custom, nil
|
|
31
|
+
}
|
|
32
|
+
dir, err := os.UserConfigDir()
|
|
33
|
+
if err != nil {
|
|
34
|
+
return "", fmt.Errorf("locate WebCTX config directory: %w", err)
|
|
35
|
+
}
|
|
36
|
+
return filepath.Join(dir, "webctx", "config.json"), nil
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func readProviderConfig(path string) (providerConfig, error) {
|
|
40
|
+
contents, err := os.ReadFile(path)
|
|
41
|
+
if os.IsNotExist(err) {
|
|
42
|
+
return providerConfig{}, nil // all providers default to enabled
|
|
43
|
+
}
|
|
44
|
+
if err != nil {
|
|
45
|
+
return providerConfig{}, fmt.Errorf("read provider settings %s: %w", path, err)
|
|
46
|
+
}
|
|
47
|
+
var config providerConfig
|
|
48
|
+
if err := json.Unmarshal(contents, &config); err != nil {
|
|
49
|
+
return providerConfig{}, fmt.Errorf("invalid provider settings %s: %w", path, err)
|
|
50
|
+
}
|
|
51
|
+
return config, nil
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func currentProviderConfig() (providerConfig, error) {
|
|
55
|
+
path, err := providerConfigPath()
|
|
56
|
+
if err != nil {
|
|
57
|
+
return providerConfig{}, err
|
|
58
|
+
}
|
|
59
|
+
return readProviderConfig(path)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func (c providerConfig) enabled(name string) bool {
|
|
63
|
+
return !slices.Contains(c.DisabledProviders, name)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func validSearchProvider(name string) bool {
|
|
67
|
+
for _, p := range searchProviderKeys {
|
|
68
|
+
if p.name == name {
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func setSearchProviderEnabled(name string, enabled bool) (string, error) {
|
|
76
|
+
name = strings.ToLower(strings.TrimSpace(name))
|
|
77
|
+
if !validSearchProvider(name) {
|
|
78
|
+
return "", fmt.Errorf("unknown search provider %q (choose brave, tavily, or exa)", name)
|
|
79
|
+
}
|
|
80
|
+
path, err := providerConfigPath()
|
|
81
|
+
if err != nil {
|
|
82
|
+
return "", err
|
|
83
|
+
}
|
|
84
|
+
config, err := readProviderConfig(path)
|
|
85
|
+
if err != nil {
|
|
86
|
+
return "", err
|
|
87
|
+
}
|
|
88
|
+
if config.enabled(name) == enabled {
|
|
89
|
+
return path, nil
|
|
90
|
+
}
|
|
91
|
+
if enabled {
|
|
92
|
+
config.DisabledProviders = slices.DeleteFunc(config.DisabledProviders, func(p string) bool { return p == name })
|
|
93
|
+
} else {
|
|
94
|
+
config.DisabledProviders = append(config.DisabledProviders, name)
|
|
95
|
+
}
|
|
96
|
+
slices.Sort(config.DisabledProviders)
|
|
97
|
+
if err := writeProviderConfig(path, config); err != nil {
|
|
98
|
+
return "", err
|
|
99
|
+
}
|
|
100
|
+
return path, nil
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
func writeProviderConfig(path string, config providerConfig) error {
|
|
104
|
+
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
105
|
+
return fmt.Errorf("create provider config directory: %w", err)
|
|
106
|
+
}
|
|
107
|
+
data, err := json.MarshalIndent(config, "", " ")
|
|
108
|
+
if err != nil {
|
|
109
|
+
return err
|
|
110
|
+
}
|
|
111
|
+
file, err := os.CreateTemp(filepath.Dir(path), ".webctx-config-*")
|
|
112
|
+
if err != nil {
|
|
113
|
+
return fmt.Errorf("write provider settings: %w", err)
|
|
114
|
+
}
|
|
115
|
+
defer os.Remove(file.Name())
|
|
116
|
+
if err := file.Chmod(0o600); err != nil {
|
|
117
|
+
_ = file.Close()
|
|
118
|
+
return err
|
|
119
|
+
}
|
|
120
|
+
if _, err := file.Write(append(data, '\n')); err != nil {
|
|
121
|
+
_ = file.Close()
|
|
122
|
+
return err
|
|
123
|
+
}
|
|
124
|
+
if err := file.Close(); err != nil {
|
|
125
|
+
return err
|
|
126
|
+
}
|
|
127
|
+
if err := os.Rename(file.Name(), path); err != nil {
|
|
128
|
+
return fmt.Errorf("save provider settings: %w", err)
|
|
129
|
+
}
|
|
130
|
+
return nil
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
func printProviderStatus(out io.Writer) error {
|
|
134
|
+
path, err := providerConfigPath()
|
|
135
|
+
if err != nil {
|
|
136
|
+
return err
|
|
137
|
+
}
|
|
138
|
+
config, err := readProviderConfig(path)
|
|
139
|
+
if err != nil {
|
|
140
|
+
return err
|
|
141
|
+
}
|
|
142
|
+
_, _ = fmt.Fprintf(out, "Search providers (settings: %s)\n", path)
|
|
143
|
+
for _, p := range searchProviderKeys {
|
|
144
|
+
state, key := "enabled", "key missing"
|
|
145
|
+
if !config.enabled(p.name) {
|
|
146
|
+
state = "disabled"
|
|
147
|
+
}
|
|
148
|
+
count := len(searchKeysForProvider(p.name))
|
|
149
|
+
if count > 0 {
|
|
150
|
+
key = "key configured"
|
|
151
|
+
if count > 1 {
|
|
152
|
+
key = fmt.Sprintf("key configured (%d keys)", count)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
_, _ = fmt.Fprintf(out, " %-7s %-9s %s\n", p.name, state, key)
|
|
156
|
+
}
|
|
157
|
+
_, _ = fmt.Fprintln(out, "\nFirecrawl (page reading, site maps, and Alexandria; separate from search):")
|
|
158
|
+
if strings.TrimSpace(os.Getenv("FIRECRAWL_API_KEY")) != "" {
|
|
159
|
+
_, _ = fmt.Fprintln(out, " key configured")
|
|
160
|
+
} else {
|
|
161
|
+
_, _ = fmt.Fprintln(out, " key missing")
|
|
162
|
+
}
|
|
163
|
+
_, _ = fmt.Fprintln(out, "\nKey presence is local configuration only; quotas and API availability are not checked.")
|
|
164
|
+
return nil
|
|
165
|
+
}
|