stackresolve 0.1.0__tar.gz

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,10 @@
1
+ node_modules/
2
+ .next/
3
+ .env*
4
+ !.env.example
5
+ .DS_Store
6
+ *.log
7
+ /.claude/worktrees/
8
+
9
+ # prior-work semantic vector cache (machine-local)
10
+ .claude/cache/
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.5
2
+ Name: stackresolve
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the StackResolve API (AgentReady + CompanyData).
5
+ Project-URL: Homepage, https://stackresolve.dev
6
+ Author: StackResolve
7
+ License: MIT
8
+ Keywords: agent,agentready,companydata,mcp,sdk,stackresolve
9
+ Requires-Python: >=3.9
10
+ Requires-Dist: httpx>=0.24
11
+ Description-Content-Type: text/markdown
12
+
13
+ # stackresolve
14
+
15
+ Official Python SDK for the [StackResolve](https://stackresolve.dev) API.
16
+
17
+ StackResolve helps agents discover, evaluate, select, install, and use software
18
+ (AgentReady), and compresses web research into structured company data calls
19
+ (CompanyData). This SDK is a thin wrapper over the live REST API.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pip install stackresolve
25
+ ```
26
+
27
+ Requires Python 3.9+. Depends on `httpx`.
28
+
29
+ ## Quickstart
30
+
31
+ ```python
32
+ from stackresolve import StackResolve
33
+
34
+ sr = StackResolve(api_key="ar_...") # or set STACKRESOLVE_API_KEY
35
+
36
+ # 1. Audit a domain: AgentReady scores, facts, and issues.
37
+ report = sr.audit("stripe.com")
38
+ print(report["scores"]["agentready"], report["issues"])
39
+
40
+ # 2. Structured company data.
41
+ company = sr.get_company("vercel.com")
42
+ print(company["name"], company.get("description"))
43
+
44
+ # 3. Task -> ranked, agent-ready tools.
45
+ tools = sr.find_tools("send transactional email from a Python service")
46
+ print(tools["results"])
47
+
48
+ # 4. Run an Agent Discovery check (needs an API key).
49
+ discovery = sr.run_discovery("firecrawl")
50
+ print(discovery)
51
+ ```
52
+
53
+ ## Authentication
54
+
55
+ Get an API key at [stackresolve.dev](https://stackresolve.dev). Pass it to the
56
+ constructor, or set the `STACKRESOLVE_API_KEY` environment variable:
57
+
58
+ ```python
59
+ sr = StackResolve(api_key="ar_...")
60
+ # or, reading STACKRESOLVE_API_KEY from the environment:
61
+ sr = StackResolve()
62
+ ```
63
+
64
+ The key is sent as the `x-api-key` header on every request. Public endpoints
65
+ (audit, search, profiles, company data, ...) work without a key, subject to an
66
+ anonymous rate limit. Gated endpoints (monitors, usage, discovery runs) need one.
67
+
68
+ Point the client at a different host with `base_url` or the
69
+ `STACKRESOLVE_BASE_URL` environment variable:
70
+
71
+ ```python
72
+ sr = StackResolve(api_key="ar_...", base_url="https://api.stackresolve.dev")
73
+ ```
74
+
75
+ ## Errors
76
+
77
+ Any non-2xx response raises a `StackResolveError` carrying the HTTP `status` and
78
+ the parsed `body`:
79
+
80
+ ```python
81
+ from stackresolve import StackResolve, StackResolveError
82
+
83
+ sr = StackResolve()
84
+ try:
85
+ sr.get_usage()
86
+ except StackResolveError as err:
87
+ print(err.status, err.body)
88
+ ```
89
+
90
+ ## Methods
91
+
92
+ AgentReady:
93
+ `audit(domain)`, `find_tools(task)`, `search(query, requirements=None)`,
94
+ `compare(slugs)`, `get_profile(slug)`, `list_registry(...)`, `get_categories()`,
95
+ `get_category(slug)`, `get_discovery(slug)`, `run_discovery(slug)`,
96
+ `get_score_history(slug)`, `generate(domain, openapi_url=None)`,
97
+ `deploy(domain, openapi_url=None)`.
98
+
99
+ CompanyData:
100
+ `get_company(domain)`, `get_pricing(domain)`, `get_competitors(domain)`,
101
+ `research(domain, question=None)`, `compare_companies(domains)`.
102
+
103
+ Monitors + account (API key required):
104
+ `list_monitors()`, `add_monitor(slug, ...)`, `run_monitor_now(slug)`,
105
+ `get_usage()`, `get_my_profile()`.
106
+
107
+ ## License
108
+
109
+ MIT
@@ -0,0 +1,97 @@
1
+ # stackresolve
2
+
3
+ Official Python SDK for the [StackResolve](https://stackresolve.dev) API.
4
+
5
+ StackResolve helps agents discover, evaluate, select, install, and use software
6
+ (AgentReady), and compresses web research into structured company data calls
7
+ (CompanyData). This SDK is a thin wrapper over the live REST API.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install stackresolve
13
+ ```
14
+
15
+ Requires Python 3.9+. Depends on `httpx`.
16
+
17
+ ## Quickstart
18
+
19
+ ```python
20
+ from stackresolve import StackResolve
21
+
22
+ sr = StackResolve(api_key="ar_...") # or set STACKRESOLVE_API_KEY
23
+
24
+ # 1. Audit a domain: AgentReady scores, facts, and issues.
25
+ report = sr.audit("stripe.com")
26
+ print(report["scores"]["agentready"], report["issues"])
27
+
28
+ # 2. Structured company data.
29
+ company = sr.get_company("vercel.com")
30
+ print(company["name"], company.get("description"))
31
+
32
+ # 3. Task -> ranked, agent-ready tools.
33
+ tools = sr.find_tools("send transactional email from a Python service")
34
+ print(tools["results"])
35
+
36
+ # 4. Run an Agent Discovery check (needs an API key).
37
+ discovery = sr.run_discovery("firecrawl")
38
+ print(discovery)
39
+ ```
40
+
41
+ ## Authentication
42
+
43
+ Get an API key at [stackresolve.dev](https://stackresolve.dev). Pass it to the
44
+ constructor, or set the `STACKRESOLVE_API_KEY` environment variable:
45
+
46
+ ```python
47
+ sr = StackResolve(api_key="ar_...")
48
+ # or, reading STACKRESOLVE_API_KEY from the environment:
49
+ sr = StackResolve()
50
+ ```
51
+
52
+ The key is sent as the `x-api-key` header on every request. Public endpoints
53
+ (audit, search, profiles, company data, ...) work without a key, subject to an
54
+ anonymous rate limit. Gated endpoints (monitors, usage, discovery runs) need one.
55
+
56
+ Point the client at a different host with `base_url` or the
57
+ `STACKRESOLVE_BASE_URL` environment variable:
58
+
59
+ ```python
60
+ sr = StackResolve(api_key="ar_...", base_url="https://api.stackresolve.dev")
61
+ ```
62
+
63
+ ## Errors
64
+
65
+ Any non-2xx response raises a `StackResolveError` carrying the HTTP `status` and
66
+ the parsed `body`:
67
+
68
+ ```python
69
+ from stackresolve import StackResolve, StackResolveError
70
+
71
+ sr = StackResolve()
72
+ try:
73
+ sr.get_usage()
74
+ except StackResolveError as err:
75
+ print(err.status, err.body)
76
+ ```
77
+
78
+ ## Methods
79
+
80
+ AgentReady:
81
+ `audit(domain)`, `find_tools(task)`, `search(query, requirements=None)`,
82
+ `compare(slugs)`, `get_profile(slug)`, `list_registry(...)`, `get_categories()`,
83
+ `get_category(slug)`, `get_discovery(slug)`, `run_discovery(slug)`,
84
+ `get_score_history(slug)`, `generate(domain, openapi_url=None)`,
85
+ `deploy(domain, openapi_url=None)`.
86
+
87
+ CompanyData:
88
+ `get_company(domain)`, `get_pricing(domain)`, `get_competitors(domain)`,
89
+ `research(domain, question=None)`, `compare_companies(domains)`.
90
+
91
+ Monitors + account (API key required):
92
+ `list_monitors()`, `add_monitor(slug, ...)`, `run_monitor_now(slug)`,
93
+ `get_usage()`, `get_my_profile()`.
94
+
95
+ ## License
96
+
97
+ MIT
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "stackresolve"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for the StackResolve API (AgentReady + CompanyData)."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "StackResolve" }]
13
+ keywords = ["stackresolve", "agentready", "companydata", "agent", "mcp", "sdk"]
14
+ dependencies = ["httpx>=0.24"]
15
+
16
+ [project.urls]
17
+ Homepage = "https://stackresolve.dev"
18
+
19
+ [tool.hatch.build.targets.wheel]
20
+ packages = ["stackresolve"]
@@ -0,0 +1,6 @@
1
+ """StackResolve official Python SDK."""
2
+
3
+ from .client import StackResolve, StackResolveError
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = ["StackResolve", "StackResolveError", "__version__"]
@@ -0,0 +1,243 @@
1
+ """StackResolve official Python SDK.
2
+
3
+ A thin wrapper over the StackResolve REST API. It gives agents and developers
4
+ Firecrawl/Exa-style ergonomics over AgentReady (discover, evaluate, select,
5
+ install, and use software) and CompanyData (structured company research).
6
+
7
+ from stackresolve import StackResolve
8
+
9
+ sr = StackResolve(api_key="ar_...")
10
+ report = sr.audit("stripe.com")
11
+ print(report["scores"]["agentready"])
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ from typing import Any, Dict, List, Optional
18
+
19
+ import httpx
20
+
21
+ DEFAULT_BASE_URL = "https://api.stackresolve.dev"
22
+
23
+ JsonDict = Dict[str, Any]
24
+
25
+
26
+ class StackResolveError(Exception):
27
+ """Raised on any non-2xx API response. Carries the HTTP status and parsed body."""
28
+
29
+ def __init__(self, status: int, body: Any, message: Optional[str] = None) -> None:
30
+ self.status = status
31
+ self.body = body
32
+ super().__init__(message or f"StackResolve API error {status}")
33
+
34
+
35
+ class StackResolve:
36
+ """Client for the StackResolve API.
37
+
38
+ Args:
39
+ api_key: API key. Falls back to the STACKRESOLVE_API_KEY env var.
40
+ Optional for public endpoints, required for gated ones.
41
+ base_url: Base URL override. Falls back to STACKRESOLVE_BASE_URL,
42
+ then https://api.stackresolve.dev .
43
+ timeout: Per-request timeout in seconds (default 60).
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ api_key: Optional[str] = None,
49
+ base_url: Optional[str] = None,
50
+ timeout: float = 60.0,
51
+ ) -> None:
52
+ self.api_key = api_key or os.environ.get("STACKRESOLVE_API_KEY")
53
+ base = base_url or os.environ.get("STACKRESOLVE_BASE_URL") or DEFAULT_BASE_URL
54
+ self.base_url = base.rstrip("/")
55
+ headers = {"accept": "application/json"}
56
+ if self.api_key:
57
+ headers["x-api-key"] = self.api_key
58
+ self._client = httpx.Client(base_url=self.base_url, headers=headers, timeout=timeout)
59
+
60
+ # ---- context manager + cleanup ----
61
+
62
+ def close(self) -> None:
63
+ self._client.close()
64
+
65
+ def __enter__(self) -> "StackResolve":
66
+ return self
67
+
68
+ def __exit__(self, *exc: Any) -> None:
69
+ self.close()
70
+
71
+ # ---- core transport ----
72
+
73
+ def _request(
74
+ self,
75
+ method: str,
76
+ path: str,
77
+ json: Optional[JsonDict] = None,
78
+ params: Optional[Dict[str, Any]] = None,
79
+ ) -> Any:
80
+ clean_params = None
81
+ if params:
82
+ clean_params = {k: v for k, v in params.items() if v is not None and v != ""}
83
+ resp = self._client.request(method, path, json=json, params=clean_params)
84
+ text = resp.text
85
+ parsed: Any = None
86
+ if text:
87
+ try:
88
+ parsed = resp.json()
89
+ except ValueError:
90
+ parsed = text
91
+ if resp.status_code < 200 or resp.status_code >= 300:
92
+ message = None
93
+ if isinstance(parsed, dict) and "error" in parsed:
94
+ message = str(parsed["error"])
95
+ raise StackResolveError(resp.status_code, parsed, message)
96
+ return parsed
97
+
98
+ # ---- AgentReady: discovery, evaluation, profiles ----
99
+
100
+ def audit(self, domain: str) -> JsonDict:
101
+ """Audit a domain: AgentReady scores, facts, and issues."""
102
+ return self._request("POST", "/v1/audit", json={"domain": domain})
103
+
104
+ def find_tools(self, task: str) -> JsonDict:
105
+ """Turn a natural-language task into ranked, agent-ready tool candidates."""
106
+ return self._request("POST", "/v1/find-tools", json={"task": task})
107
+
108
+ def search(self, query: str, requirements: Optional[JsonDict] = None) -> List[JsonDict]:
109
+ """Keyword search over the registry, optionally filtered by requirements."""
110
+ body: JsonDict = {"query": query}
111
+ if requirements:
112
+ body["requirements"] = requirements
113
+ return self._request("POST", "/v1/search", json=body)
114
+
115
+ def compare(self, slugs: List[str]) -> JsonDict:
116
+ """Compare products side by side by slug."""
117
+ return self._request("POST", "/v1/compare", json={"slugs": slugs})
118
+
119
+ def get_profile(self, slug: str) -> Optional[JsonDict]:
120
+ """Fetch a single product profile (scores + facts) by slug."""
121
+ return self._request("GET", f"/v1/profile/{slug}")
122
+
123
+ def list_registry(
124
+ self,
125
+ category: Optional[str] = None,
126
+ min_score: Optional[float] = None,
127
+ limit: Optional[int] = None,
128
+ ) -> List[JsonDict]:
129
+ """List registry rows, optionally filtered by category, minimum score, and limit."""
130
+ return self._request(
131
+ "GET",
132
+ "/v1/registry",
133
+ params={"category": category, "minScore": min_score, "limit": limit},
134
+ )
135
+
136
+ def get_categories(self) -> List[JsonDict]:
137
+ """List all categories."""
138
+ r = self._request("GET", "/v1/categories")
139
+ return r.get("categories", []) if isinstance(r, dict) else r
140
+
141
+ def get_category(self, slug: str) -> Optional[JsonDict]:
142
+ """Fetch one category and its products by slug."""
143
+ return self._request("GET", f"/v1/categories/{slug}")
144
+
145
+ # ---- CompanyData: structured company research ----
146
+
147
+ def get_company(self, domain: str) -> JsonDict:
148
+ """Structured company profile for a domain."""
149
+ return self._request("POST", "/v1/company", json={"domain": domain})
150
+
151
+ def get_pricing(self, domain: str) -> JsonDict:
152
+ """Structured pricing for a domain.
153
+
154
+ Note: the API reads ``domain`` from the query string for this endpoint,
155
+ not the body.
156
+ """
157
+ return self._request("POST", "/v1/pricing", params={"domain": domain})
158
+
159
+ def get_competitors(self, domain: str) -> JsonDict:
160
+ """Competitors and alternatives for a domain.
161
+
162
+ Note: the API reads ``domain`` from the query string for this endpoint,
163
+ not the body.
164
+ """
165
+ return self._request("POST", "/v1/competitors", params={"domain": domain})
166
+
167
+ def research(self, domain: str, question: Optional[str] = None) -> JsonDict:
168
+ """Grounded research answer about a domain, optionally scoped to a question."""
169
+ body: JsonDict = {"domain": domain}
170
+ if question:
171
+ body["question"] = question
172
+ return self._request("POST", "/v1/research", json=body)
173
+
174
+ def compare_companies(self, domains: List[str]) -> JsonDict:
175
+ """Compare multiple companies by domain."""
176
+ return self._request("POST", "/v1/compare-companies", json={"domains": domains})
177
+
178
+ # ---- Agent-native interface generation + hosting ----
179
+
180
+ def generate(self, domain: str, openapi_url: Optional[str] = None) -> JsonDict:
181
+ """Generate agent-native interfaces (MCP server, CLI, llms.txt, snippets)."""
182
+ body: JsonDict = {"domain": domain}
183
+ if openapi_url:
184
+ body["openapiUrl"] = openapi_url
185
+ return self._request("POST", "/v1/generate", json=body)
186
+
187
+ def deploy(self, domain: str, openapi_url: Optional[str] = None) -> JsonDict:
188
+ """Deploy a hosted MCP server for a domain."""
189
+ body: JsonDict = {"domain": domain}
190
+ if openapi_url:
191
+ body["openapiUrl"] = openapi_url
192
+ return self._request("POST", "/v1/deploy", json=body)
193
+
194
+ # ---- Agent Discovery + Score History ----
195
+
196
+ def get_discovery(self, slug: str) -> JsonDict:
197
+ """Read the latest Agent Discovery view for a slug."""
198
+ return self._request("GET", "/v1/discovery", params={"slug": slug})
199
+
200
+ def run_discovery(self, slug: str) -> JsonDict:
201
+ """Run an Agent Discovery check for a slug (requires an API key; spends credits)."""
202
+ return self._request("POST", "/v1/discovery/run", json={"slug": slug})
203
+
204
+ def get_score_history(self, slug: str) -> JsonDict:
205
+ """Read the score-history timeseries for a slug."""
206
+ return self._request("GET", "/v1/score-history", params={"slug": slug})
207
+
208
+ # ---- Monitors (require an API key) ----
209
+
210
+ def list_monitors(self) -> JsonDict:
211
+ """List monitors for the authenticated workspace."""
212
+ return self._request("GET", "/v1/monitors")
213
+
214
+ def add_monitor(
215
+ self,
216
+ slug: str,
217
+ cadence: Optional[str] = None,
218
+ kinds: Optional[List[str]] = None,
219
+ enabled: Optional[bool] = None,
220
+ ) -> JsonDict:
221
+ """Create or update a monitor for a slug."""
222
+ body: JsonDict = {"slug": slug}
223
+ if cadence is not None:
224
+ body["cadence"] = cadence
225
+ if kinds is not None:
226
+ body["kinds"] = kinds
227
+ if enabled is not None:
228
+ body["enabled"] = enabled
229
+ return self._request("POST", "/v1/monitors", json=body)
230
+
231
+ def run_monitor_now(self, slug: str) -> JsonDict:
232
+ """Run a monitor immediately for a slug."""
233
+ return self._request("POST", "/v1/monitors/run-now", json={"slug": slug})
234
+
235
+ # ---- Account (require an API key) ----
236
+
237
+ def get_usage(self) -> JsonDict:
238
+ """Usage for the authenticated workspace."""
239
+ return self._request("GET", "/v1/usage")
240
+
241
+ def get_my_profile(self) -> JsonDict:
242
+ """The authenticated workspace's own claimed profile (or null)."""
243
+ return self._request("GET", "/v1/me/profile")