forgecommandline 0.8.0__py3-none-any.whl

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 (113) hide show
  1. forgecli/__init__.py +2 -0
  2. forgecli/ads.py +322 -0
  3. forgecli/agent.py +1093 -0
  4. forgecli/art.py +574 -0
  5. forgecli/attach.py +246 -0
  6. forgecli/brief.py +215 -0
  7. forgecli/browsercheck.py +99 -0
  8. forgecli/checkpoints.py +81 -0
  9. forgecli/city.py +833 -0
  10. forgecli/cli.py +1668 -0
  11. forgecli/config.py +194 -0
  12. forgecli/courier.py +177 -0
  13. forgecli/courier_data.py +164 -0
  14. forgecli/courier_hd.py +431 -0
  15. forgecli/courier_hd_data.bin +0 -0
  16. forgecli/courier_hd_data.py +19 -0
  17. forgecli/ghlogin.py +76 -0
  18. forgecli/github.py +54 -0
  19. forgecli/guards.py +403 -0
  20. forgecli/hooks.py +50 -0
  21. forgecli/httpapi.py +434 -0
  22. forgecli/images.py +110 -0
  23. forgecli/keys.py +152 -0
  24. forgecli/keyvault.py +320 -0
  25. forgecli/mcp.py +674 -0
  26. forgecli/memory.py +42 -0
  27. forgecli/openapi.py +331 -0
  28. forgecli/parallel.py +308 -0
  29. forgecli/prices.py +72 -0
  30. forgecli/procs.py +144 -0
  31. forgecli/providers.py +758 -0
  32. forgecli/quality.py +192 -0
  33. forgecli/repomap.py +78 -0
  34. forgecli/roster.py +227 -0
  35. forgecli/safety.py +93 -0
  36. forgecli/sandbox.py +83 -0
  37. forgecli/sessions.py +97 -0
  38. forgecli/skills.py +446 -0
  39. forgecli/tools.py +1184 -0
  40. forgecli/ui.py +860 -0
  41. forgecli/worktrees.py +79 -0
  42. forgecommandline-0.8.0.data/data/skills/commit/SKILL.md +14 -0
  43. forgecommandline-0.8.0.data/data/skills/debug/SKILL.md +17 -0
  44. forgecommandline-0.8.0.data/data/skills/explain/SKILL.md +14 -0
  45. forgecommandline-0.8.0.data/data/skills/init/SKILL.md +19 -0
  46. forgecommandline-0.8.0.data/data/skills/library/LICENSES/agent-skills-MIT.txt +21 -0
  47. forgecommandline-0.8.0.data/data/skills/library/LICENSES/wondelai-MIT.txt +23 -0
  48. forgecommandline-0.8.0.data/data/skills/library/README.md +9 -0
  49. forgecommandline-0.8.0.data/data/skills/library/_references/accessibility-checklist.md +160 -0
  50. forgecommandline-0.8.0.data/data/skills/library/_references/definition-of-done.md +67 -0
  51. forgecommandline-0.8.0.data/data/skills/library/_references/observability-checklist.md +92 -0
  52. forgecommandline-0.8.0.data/data/skills/library/_references/orchestration-patterns.md +370 -0
  53. forgecommandline-0.8.0.data/data/skills/library/_references/performance-checklist.md +238 -0
  54. forgecommandline-0.8.0.data/data/skills/library/_references/security-checklist.md +248 -0
  55. forgecommandline-0.8.0.data/data/skills/library/_references/testing-patterns.md +235 -0
  56. forgecommandline-0.8.0.data/data/skills/library/api-and-interface-design/SKILL.md +367 -0
  57. forgecommandline-0.8.0.data/data/skills/library/architecture-optimization/SKILL.md +279 -0
  58. forgecommandline-0.8.0.data/data/skills/library/architecture-optimization/references/artifact-templates.md +157 -0
  59. forgecommandline-0.8.0.data/data/skills/library/architecture-optimization/references/methods.md +87 -0
  60. forgecommandline-0.8.0.data/data/skills/library/awwwards-tier-web-application/SKILL.md +877 -0
  61. forgecommandline-0.8.0.data/data/skills/library/browser-testing-with-devtools/SKILL.md +317 -0
  62. forgecommandline-0.8.0.data/data/skills/library/ci-cd-and-automation/SKILL.md +390 -0
  63. forgecommandline-0.8.0.data/data/skills/library/code-review-and-quality/SKILL.md +398 -0
  64. forgecommandline-0.8.0.data/data/skills/library/code-simplification/SKILL.md +331 -0
  65. forgecommandline-0.8.0.data/data/skills/library/constraint-driven-development/SKILL.md +311 -0
  66. forgecommandline-0.8.0.data/data/skills/library/constraint-driven-development/references/floor-guard.md +182 -0
  67. forgecommandline-0.8.0.data/data/skills/library/context-engineering/SKILL.md +353 -0
  68. forgecommandline-0.8.0.data/data/skills/library/debugging-and-error-recovery/SKILL.md +300 -0
  69. forgecommandline-0.8.0.data/data/skills/library/deprecation-and-migration/SKILL.md +247 -0
  70. forgecommandline-0.8.0.data/data/skills/library/documentation-and-adrs/SKILL.md +297 -0
  71. forgecommandline-0.8.0.data/data/skills/library/doubt-driven-development/SKILL.md +243 -0
  72. forgecommandline-0.8.0.data/data/skills/library/frontend-ui-engineering/SKILL.md +340 -0
  73. forgecommandline-0.8.0.data/data/skills/library/git-workflow-and-versioning/SKILL.md +355 -0
  74. forgecommandline-0.8.0.data/data/skills/library/idea-refine/SKILL.md +193 -0
  75. forgecommandline-0.8.0.data/data/skills/library/idea-refine/examples.md +238 -0
  76. forgecommandline-0.8.0.data/data/skills/library/idea-refine/frameworks.md +99 -0
  77. forgecommandline-0.8.0.data/data/skills/library/idea-refine/refinement-criteria.md +113 -0
  78. forgecommandline-0.8.0.data/data/skills/library/idea-refine/scripts/idea-refine.sh +15 -0
  79. forgecommandline-0.8.0.data/data/skills/library/improve-code-quality/SKILL.md +275 -0
  80. forgecommandline-0.8.0.data/data/skills/library/improve-code-quality/references/artifact-templates.md +101 -0
  81. forgecommandline-0.8.0.data/data/skills/library/incremental-implementation/SKILL.md +249 -0
  82. forgecommandline-0.8.0.data/data/skills/library/interview-me/SKILL.md +235 -0
  83. forgecommandline-0.8.0.data/data/skills/library/observability-and-instrumentation/SKILL.md +238 -0
  84. forgecommandline-0.8.0.data/data/skills/library/performance-optimization/SKILL.md +267 -0
  85. forgecommandline-0.8.0.data/data/skills/library/performance-optimization/references/optimization-patterns.md +250 -0
  86. forgecommandline-0.8.0.data/data/skills/library/planning-and-task-breakdown/SKILL.md +257 -0
  87. forgecommandline-0.8.0.data/data/skills/library/remove-technical-debt/SKILL.md +203 -0
  88. forgecommandline-0.8.0.data/data/skills/library/remove-technical-debt/references/artifact-templates.md +79 -0
  89. forgecommandline-0.8.0.data/data/skills/library/security-and-hardening/SKILL.md +216 -0
  90. forgecommandline-0.8.0.data/data/skills/library/security-and-hardening/references/hardening-patterns.md +324 -0
  91. forgecommandline-0.8.0.data/data/skills/library/shipping-and-launch/SKILL.md +336 -0
  92. forgecommandline-0.8.0.data/data/skills/library/source-driven-development/SKILL.md +216 -0
  93. forgecommandline-0.8.0.data/data/skills/library/spec-driven-development/SKILL.md +259 -0
  94. forgecommandline-0.8.0.data/data/skills/library/system-design/SKILL.md +207 -0
  95. forgecommandline-0.8.0.data/data/skills/library/system-design/references/building-blocks.md +272 -0
  96. forgecommandline-0.8.0.data/data/skills/library/system-design/references/common-designs.md +257 -0
  97. forgecommandline-0.8.0.data/data/skills/library/system-design/references/database-scaling.md +295 -0
  98. forgecommandline-0.8.0.data/data/skills/library/system-design/references/estimation-numbers.md +250 -0
  99. forgecommandline-0.8.0.data/data/skills/library/system-design/references/four-step-process.md +286 -0
  100. forgecommandline-0.8.0.data/data/skills/library/system-design/references/reliability-operations.md +373 -0
  101. forgecommandline-0.8.0.data/data/skills/library/test-driven-development/SKILL.md +398 -0
  102. forgecommandline-0.8.0.data/data/skills/library/using-agent-skills/SKILL.md +192 -0
  103. forgecommandline-0.8.0.data/data/skills/review/SKILL.md +16 -0
  104. forgecommandline-0.8.0.data/data/skills/run/SKILL.md +17 -0
  105. forgecommandline-0.8.0.data/data/skills/security-review/SKILL.md +20 -0
  106. forgecommandline-0.8.0.data/data/skills/simplify/SKILL.md +18 -0
  107. forgecommandline-0.8.0.data/data/skills/test/SKILL.md +16 -0
  108. forgecommandline-0.8.0.data/data/skills/web-build/SKILL.md +25 -0
  109. forgecommandline-0.8.0.dist-info/METADATA +357 -0
  110. forgecommandline-0.8.0.dist-info/RECORD +113 -0
  111. forgecommandline-0.8.0.dist-info/WHEEL +5 -0
  112. forgecommandline-0.8.0.dist-info/entry_points.txt +2 -0
  113. forgecommandline-0.8.0.dist-info/top_level.txt +1 -0
forgecli/memory.py ADDED
@@ -0,0 +1,42 @@
1
+ """Small, explicit, project-scoped persistent notes."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import os
6
+ import pathlib
7
+
8
+ from .config import HOME_DIR
9
+
10
+ MAX_CHARS = 50_000
11
+ MAX_NOTE_CHARS = 4_000
12
+
13
+
14
+ def _path(scope: str, cwd: str) -> pathlib.Path:
15
+ if scope == "user":
16
+ return HOME_DIR / "memory" / "user.md"
17
+ if scope != "project":
18
+ raise ValueError("scope must be 'user' or 'project'")
19
+ root = os.path.normcase(str(pathlib.Path(cwd).resolve()))
20
+ key = hashlib.sha256(root.encode("utf-8")).hexdigest()[:20]
21
+ return HOME_DIR / "memory" / "projects" / key / "memory.md"
22
+
23
+
24
+ def read(scope: str, cwd: str) -> str:
25
+ path = _path(scope, cwd)
26
+ if not path.is_file():
27
+ return ""
28
+ return path.read_text(encoding="utf-8")[-MAX_CHARS:]
29
+
30
+
31
+ def remember(scope: str, cwd: str, note: str) -> str:
32
+ note = note.strip()
33
+ if not note:
34
+ raise ValueError("note must not be empty")
35
+ if len(note) > MAX_NOTE_CHARS:
36
+ raise ValueError(f"note must be at most {MAX_NOTE_CHARS} characters")
37
+ path = _path(scope, cwd)
38
+ path.parent.mkdir(parents=True, exist_ok=True)
39
+ existing = read(scope, cwd)
40
+ combined = (existing.rstrip() + "\n- " + note + "\n") if existing else ("- " + note + "\n")
41
+ path.write_text(combined[-MAX_CHARS:], encoding="utf-8")
42
+ return str(path)
forgecli/openapi.py ADDED
@@ -0,0 +1,331 @@
1
+ """Import OpenAPI descriptions and expose their declared operations as tools."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import pathlib
7
+ import re
8
+ import urllib.error
9
+ import urllib.parse
10
+ import urllib.request
11
+
12
+ from . import skills
13
+ from .httpapi import validate_arguments
14
+
15
+ MAX_SPEC = 5_000_000
16
+ METHODS = {"get", "post", "put", "patch", "delete", "head", "options"}
17
+
18
+
19
+ class OpenAPIs:
20
+ def __init__(self, http, root: pathlib.Path):
21
+ self.http = http
22
+ self.root = root
23
+ self.operations: dict[str, dict] = {}
24
+ self.active: set[str] = set()
25
+ self.api_names: set[str] = set()
26
+
27
+ def import_spec(self, source: str, name: str = "") -> str:
28
+ source = str(source or "").strip()
29
+ if not source:
30
+ return "Error: provide an OpenAPI spec URL or file path."
31
+ try:
32
+ base_url = ""
33
+ if urllib.parse.urlsplit(source).scheme in ("http", "https"):
34
+ req = urllib.request.Request(source, headers={"User-Agent": "Forge/0.1"})
35
+ with urllib.request.urlopen(req, timeout=30) as response:
36
+ raw = response.read(MAX_SPEC + 1)
37
+ if len(raw) > MAX_SPEC:
38
+ return "Error: OpenAPI spec exceeds the 5 MB limit."
39
+ base_url = response.geturl()
40
+ else:
41
+ raw = pathlib.Path(source).expanduser().resolve().read_bytes()
42
+ if len(raw) > MAX_SPEC:
43
+ return "Error: OpenAPI spec exceeds the 5 MB limit."
44
+ text = raw.decode("utf-8-sig")
45
+ try:
46
+ spec = json.loads(text)
47
+ except json.JSONDecodeError:
48
+ import yaml
49
+ spec = yaml.safe_load(text)
50
+ if not isinstance(spec, dict) or not str(spec.get("openapi", "")).startswith("3."):
51
+ return "Error: this is not a supported OpenAPI 3.x document."
52
+ except (OSError, UnicodeError, urllib.error.URLError, ValueError, ImportError) as e:
53
+ return f"Error: could not read OpenAPI spec: {type(e).__name__}: {e}"
54
+
55
+ api_name = self._slug(name or self._default_name(source))
56
+ if not api_name:
57
+ api_name = "api"
58
+ digest = hashlib.sha1(source.encode("utf-8")).hexdigest()[:6]
59
+ prefix = f"{api_name}_{digest}"
60
+ paths = spec.get("paths") or {}
61
+ if not isinstance(paths, dict):
62
+ return "Error: OpenAPI paths must be an object."
63
+ components = spec.get("components") or {}
64
+ if not isinstance(components, dict):
65
+ return "Error: OpenAPI components must be an object."
66
+ refs = components.get("schemas") or {}
67
+ if not isinstance(refs, dict):
68
+ return "Error: OpenAPI component schemas must be an object."
69
+ servers = spec.get("servers") or [{"url": "/"}]
70
+ operations = {}
71
+ for path, path_item in paths.items():
72
+ if not isinstance(path_item, dict):
73
+ return f"Error: OpenAPI path {path} must be an object."
74
+ if "$ref" in path_item:
75
+ return f"Error: path-item references are not supported for {path}."
76
+ for method, operation in path_item.items():
77
+ if method.lower() not in METHODS:
78
+ continue
79
+ if not isinstance(operation, dict):
80
+ return f"Error: OpenAPI operation {method.upper()} {path} must be an object."
81
+ try:
82
+ item = self._operation(spec, components, base_url, prefix, str(path),
83
+ method.lower(), path_item, operation, servers)
84
+ except (KeyError, IndexError, TypeError, ValueError) as e:
85
+ return f"Error: invalid OpenAPI operation {method.upper()} {path}: {e}"
86
+ if item["name"] in operations:
87
+ return f"Error: duplicate OpenAPI operation ID {item['name']}."
88
+ operations[item["name"]] = item
89
+ if not operations:
90
+ return "Error: the OpenAPI spec contains no supported operations."
91
+ self.operations.update(operations)
92
+ self.api_names.add(api_name)
93
+ return (f"Imported OpenAPI API '{api_name}' with {len(operations)} operations. "
94
+ "Use openapi_search with a task or operation keyword to load matching tool schemas.")
95
+
96
+ @staticmethod
97
+ def _default_name(source: str) -> str:
98
+ path = urllib.parse.urlsplit(source).path
99
+ return pathlib.Path(path or source).stem or "api"
100
+
101
+ @staticmethod
102
+ def _slug(value: str) -> str:
103
+ return re.sub(r"_+", "_", re.sub(r"[^a-zA-Z0-9_]+", "_", value)).strip("_").lower()[:32]
104
+
105
+ @staticmethod
106
+ def _schema(schema, refs, depth=0):
107
+ if depth > 12:
108
+ raise ValueError("schema reference nesting exceeds 12 levels")
109
+ if not isinstance(schema, dict):
110
+ return {}
111
+ ref = schema.get("$ref")
112
+ if ref:
113
+ if not ref.startswith("#/components/schemas/"):
114
+ raise ValueError(f"unsupported schema reference {ref}")
115
+ component_name = ref.split("/", 3)[-1].replace("~1", "/").replace("~0", "~")
116
+ target = refs.get(component_name)
117
+ if target is None:
118
+ raise ValueError(f"unresolved schema reference {ref}")
119
+ resolved = OpenAPIs._schema(target, refs, depth + 1)
120
+ if schema.get("description"):
121
+ resolved["description"] = schema["description"]
122
+ return resolved
123
+ out = {k: schema[k] for k in ("type", "description", "format", "enum", "default",
124
+ "minimum", "maximum", "minLength", "maxLength")
125
+ if k in schema}
126
+ for key in ("allOf", "anyOf", "oneOf"):
127
+ if key in schema:
128
+ out[key] = [OpenAPIs._schema(item, refs, depth + 1) for item in schema[key]]
129
+ if "properties" in schema:
130
+ out["properties"] = {k: OpenAPIs._schema(v, refs, depth + 1)
131
+ for k, v in schema["properties"].items()}
132
+ if "items" in schema:
133
+ out["items"] = OpenAPIs._schema(schema["items"], refs, depth + 1)
134
+ if schema.get("required"):
135
+ out["required"] = list(schema["required"])
136
+ if "additionalProperties" in schema:
137
+ additional = schema["additionalProperties"]
138
+ out["additionalProperties"] = OpenAPIs._schema(additional, refs, depth + 1) \
139
+ if isinstance(additional, dict) else additional
140
+ return out
141
+
142
+ @classmethod
143
+ def _component_ref(cls, components, section, item):
144
+ ref = item.get("$ref")
145
+ if not ref:
146
+ return item
147
+ prefix = f"#/components/{section}/"
148
+ if not ref.startswith(prefix):
149
+ raise ValueError(f"unsupported {section} reference {ref}")
150
+ name = ref[len(prefix):].replace("~1", "/").replace("~0", "~")
151
+ target = (components.get(section) or {}).get(name)
152
+ if not isinstance(target, dict):
153
+ raise ValueError(f"unresolved {section} reference {ref}")
154
+ return target
155
+
156
+ @classmethod
157
+ def _operation(cls, spec, components, base_url, prefix, path, method, path_item, operation, servers):
158
+ refs = components.get("schemas") or {}
159
+ operation_id = str(operation.get("operationId") or f"{method}_{path}")
160
+ slug = cls._slug(operation_id)
161
+ head = f"openapi__{prefix}__"
162
+ max_slug_len = 64 - len(head)
163
+ if len(slug) > max_slug_len:
164
+ suffix = "_" + hashlib.sha1(operation_id.encode()).hexdigest()[:6]
165
+ slug = slug[:max_slug_len - len(suffix)] + suffix
166
+ name = head + slug
167
+ properties, required = {"path_params": {"type": "object", "properties": {}, "required": []},
168
+ "query": {"type": "object", "properties": {}},
169
+ "headers": {"type": "object", "properties": {}}}, []
170
+ params = {}
171
+ for p in (path_item.get("parameters") or []) + (operation.get("parameters") or []):
172
+ if not isinstance(p, dict):
173
+ continue
174
+ p = cls._component_ref(components, "parameters", p)
175
+ key = (p.get("name"), p.get("in"))
176
+ params[key] = p
177
+ for (param_name, location), param in params.items():
178
+ if not param_name or location not in ("path", "query", "header"):
179
+ continue
180
+ target = {"path": "path_params", "query": "query", "header": "headers"}[location]
181
+ schema = cls._schema(param.get("schema") or {}, refs)
182
+ if param.get("description"):
183
+ schema["description"] = param["description"]
184
+ properties[target]["properties"][param_name] = schema
185
+ if param.get("required"):
186
+ properties[target].setdefault("required", []).append(param_name)
187
+ for key in ("path_params", "query", "headers"):
188
+ if properties[key]["properties"]:
189
+ properties[key]["additionalProperties"] = False
190
+ required.append(key) if key == "path_params" and properties[key].get("required") else None
191
+ elif key != "path_params":
192
+ del properties[key]
193
+
194
+ request_body = operation.get("requestBody") or {}
195
+ if isinstance(request_body, dict):
196
+ request_body = cls._component_ref(components, "requestBodies", request_body)
197
+ content = request_body.get("content") or {}
198
+ media_type = next((key for key in content if key == "application/json" or key.endswith("+json")),
199
+ next((key for key in ("application/x-www-form-urlencoded", "multipart/form-data")
200
+ if key in content), ""))
201
+ media = content.get(media_type)
202
+ if media:
203
+ body_schema = cls._schema(media.get("schema") or {}, refs)
204
+ if media_type == "multipart/form-data":
205
+ body_properties = dict(body_schema.get("properties") or {})
206
+ body_required = list(body_schema.get("required") or [])
207
+ binary = [key for key, value in body_properties.items()
208
+ if value.get("format") in ("binary", "base64")]
209
+ for key in binary:
210
+ body_properties.pop(key, None)
211
+ body_required = [key for key in body_required if key not in binary]
212
+ if body_properties:
213
+ properties["form_body"] = {**body_schema, "properties": body_properties,
214
+ "required": body_required}
215
+ if request_body.get("required"):
216
+ required.append("form_body")
217
+ if binary:
218
+ properties["files"] = {"type": "array", "items": {"type": "object",
219
+ "properties": {"field": {"type": "string", "enum": binary},
220
+ "path": {"type": "string"}, "filename": {"type": "string"}},
221
+ "required": ["field", "path"], "additionalProperties": False}}
222
+ if request_body.get("required") and (
223
+ any(k in body_schema.get("required", []) for k in binary) or not body_properties):
224
+ required.append("files")
225
+ if request_body.get("required") and not body_properties and not binary:
226
+ raise ValueError("required multipart body has no supported fields")
227
+ elif media_type == "application/x-www-form-urlencoded":
228
+ properties["form_body"] = body_schema or {"type": "object"}
229
+ if request_body.get("required"):
230
+ required.append("form_body")
231
+ elif media_type == "application/json" or media_type.endswith("+json"):
232
+ properties["json_body"] = body_schema or {"type": "object"}
233
+ if request_body.get("required"):
234
+ required.append("json_body")
235
+ else:
236
+ raise ValueError(f"unsupported request body media type {media_type}")
237
+ elif request_body:
238
+ raise ValueError("request body has no supported content")
239
+
240
+ server_list = operation.get("servers") or path_item.get("servers") or servers or [{"url": "/"}]
241
+ if not isinstance(server_list, list) or not server_list:
242
+ raise ValueError("server list is empty or invalid")
243
+ server_config = server_list[0]
244
+ if not isinstance(server_config, dict):
245
+ raise ValueError("server entry must be an object")
246
+ server = server_config.get("url", "/")
247
+ if not isinstance(server, str):
248
+ raise ValueError("server URL must be a string")
249
+ server_vars = server_config.get("variables") or {}
250
+ for var, definition in server_vars.items():
251
+ server = server.replace("{" + var + "}", str(definition.get("default", "")))
252
+ if re.search(r"\{[^{}]+\}", server):
253
+ raise ValueError("server URL has a variable without a default")
254
+ if urllib.parse.urlsplit(server).scheme not in ("http", "https"):
255
+ if base_url:
256
+ origin = urllib.parse.urlsplit(base_url)
257
+ server = urllib.parse.urljoin(f"{origin.scheme}://{origin.netloc}/", server)
258
+ else:
259
+ server = urllib.parse.urljoin("http://localhost/", server)
260
+ if urllib.parse.urlsplit(server).scheme not in ("http", "https"):
261
+ raise ValueError("unsupported server URL")
262
+
263
+ summary = operation.get("summary") or operation.get("description") or operation_id
264
+ description = str(operation.get("description") or summary)[:800]
265
+ schema = {"type": "function", "function": {
266
+ "name": name,
267
+ "description": f"OpenAPI {method.upper()} {path}: {summary}. Call only with parameters from this schema.",
268
+ "parameters": {"type": "object", "properties": properties, "required": required,
269
+ "additionalProperties": False},
270
+ }}
271
+ return {"name": name, "api": prefix, "method": method.upper(), "path": path,
272
+ "server": server.rstrip("/"), "summary": str(summary), "description": description,
273
+ "schema": schema}
274
+
275
+ def search(self, query: str, limit: int = 8) -> list[dict]:
276
+ if not self.operations:
277
+ return []
278
+ skills_index = {}
279
+ for name, operation in self.operations.items():
280
+ path = pathlib.Path(name.replace(":", "_"))
281
+ skills_index[name] = skills.Skill(name, f"{operation['summary']} {operation['description']} "
282
+ f"{operation['method']} {operation['path']}",
283
+ path, "openapi")
284
+ matcher = skills.Matcher(skills_index)
285
+ ranked = matcher.scores(query)
286
+ results = []
287
+ for score, skill in ranked[:limit]:
288
+ if score < matcher.min_score * 0.45:
289
+ continue
290
+ operation = self.operations[skill.name]
291
+ self.active.add(skill.name)
292
+ results.append(operation)
293
+ return results
294
+
295
+ def schemas(self, query: str) -> list[dict]:
296
+ matches = self.search(query)
297
+ names = {item["name"] for item in matches} | self.active
298
+ return [self.operations[name]["schema"] for name in sorted(names) if name in self.operations]
299
+
300
+ def report(self) -> str:
301
+ if not self.operations:
302
+ return ""
303
+ total = sum(len(json.dumps(item["schema"], ensure_ascii=False))
304
+ for item in self.operations.values())
305
+ active = sum(len(json.dumps(self.operations[name]["schema"], ensure_ascii=False))
306
+ for name in self.active if name in self.operations)
307
+ deferred = max(0, total - active) // 4
308
+ return (f"OpenAPI operations registered: {len(self.operations)}; "
309
+ f"deferred schemas save about {deferred:,} tokens per unmatched request")
310
+
311
+ def execute(self, name: str, args: dict) -> str:
312
+ operation = self.operations.get(name)
313
+ if not operation:
314
+ return f"Error: OpenAPI operation {name!r} is not registered."
315
+ if name not in self.active:
316
+ return "Error: this OpenAPI operation was not loaded from a matching spec search."
317
+ invalid = validate_arguments(operation["schema"]["function"]["parameters"], args)
318
+ if invalid:
319
+ return f"Error: {invalid}"
320
+ path = operation["path"]
321
+ path_params = args.get("path_params") or {}
322
+ for match in re.findall(r"\{([^{}]+)\}", path):
323
+ if match not in path_params:
324
+ return f"Error: required path parameter {match!r} is missing."
325
+ path = path.replace("{" + match + "}", urllib.parse.quote(str(path_params[match]), safe=""))
326
+ url = operation["server"] + "/" + path.lstrip("/")
327
+ request_args = {"method": operation["method"], "url": url,
328
+ "query": args.get("query"), "headers": args.get("headers"),
329
+ "json_body": args.get("json_body"), "form_body": args.get("form_body"),
330
+ "files": args.get("files")}
331
+ return self.http.request(**request_args)