ghost-api-cli 0.2.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 (125) hide show
  1. ghost_api/__init__.py +3 -0
  2. ghost_api/__main__.py +5 -0
  3. ghost_api/adopt.py +173 -0
  4. ghost_api/agent/__init__.py +0 -0
  5. ghost_api/agent/orchestrator.py +413 -0
  6. ghost_api/agent/prompts.py +149 -0
  7. ghost_api/agent/tools.py +84 -0
  8. ghost_api/analysis/__init__.py +0 -0
  9. ghost_api/analysis/analyzer.py +130 -0
  10. ghost_api/analysis/detectors.py +188 -0
  11. ghost_api/analysis/exposure.py +80 -0
  12. ghost_api/analysis/graphql.py +67 -0
  13. ghost_api/analysis/models.py +64 -0
  14. ghost_api/analysis/schema.py +109 -0
  15. ghost_api/analysis/url_grouping.py +217 -0
  16. ghost_api/api.py +264 -0
  17. ghost_api/audit.py +78 -0
  18. ghost_api/browser/__init__.py +0 -0
  19. ghost_api/browser/auth_state.py +172 -0
  20. ghost_api/browser/driver.py +273 -0
  21. ghost_api/browser/egress.py +183 -0
  22. ghost_api/browser/gate.py +127 -0
  23. ghost_api/browser/install.py +28 -0
  24. ghost_api/browser/snapshot.py +221 -0
  25. ghost_api/capture/__init__.py +0 -0
  26. ghost_api/capture/filters.py +255 -0
  27. ghost_api/capture/har.py +148 -0
  28. ghost_api/capture/models.py +41 -0
  29. ghost_api/capture/personal.py +188 -0
  30. ghost_api/capture/recorder.py +181 -0
  31. ghost_api/cli/__init__.py +44 -0
  32. ghost_api/cli/_common.py +219 -0
  33. ghost_api/cli/client.py +487 -0
  34. ghost_api/cli/data.py +492 -0
  35. ghost_api/cli/explore.py +379 -0
  36. ghost_api/cli/manage.py +437 -0
  37. ghost_api/config.py +132 -0
  38. ghost_api/drift.py +344 -0
  39. ghost_api/drive.py +211 -0
  40. ghost_api/errors.py +106 -0
  41. ghost_api/events.py +213 -0
  42. ghost_api/exitcodes.py +21 -0
  43. ghost_api/export/__init__.py +71 -0
  44. ghost_api/export/confidence.py +45 -0
  45. ghost_api/export/html.py +41 -0
  46. ghost_api/export/openapi.py +201 -0
  47. ghost_api/export/report.py +227 -0
  48. ghost_api/export/site_data.py +196 -0
  49. ghost_api/export/tabular.py +88 -0
  50. ghost_api/export/viewer/report.html +286 -0
  51. ghost_api/generator/__init__.py +221 -0
  52. ghost_api/generator/cassettes.py +80 -0
  53. ghost_api/generator/models_gen.py +144 -0
  54. ghost_api/generator/naming.py +174 -0
  55. ghost_api/generator/package.py +197 -0
  56. ghost_api/generator/python_gen.py +172 -0
  57. ghost_api/generator/spec.py +486 -0
  58. ghost_api/generator/templates/client.py.j2 +518 -0
  59. ghost_api/generator/templates/client.test.ts.j2 +106 -0
  60. ghost_api/generator/templates/client.ts.j2 +339 -0
  61. ghost_api/generator/templates/models.py.j2 +45 -0
  62. ghost_api/generator/templates/package_readme.md.j2 +111 -0
  63. ghost_api/generator/templates/pyproject.toml.j2 +22 -0
  64. ghost_api/generator/templates/test_client.py.j2 +105 -0
  65. ghost_api/generator/ts_gen.py +232 -0
  66. ghost_api/integrations/__init__.py +0 -0
  67. ghost_api/integrations/claude_code/commands/ghost-data.md +10 -0
  68. ghost_api/integrations/claude_code/commands/ghost-diff.md +10 -0
  69. ghost_api/integrations/claude_code/commands/ghost-explore.md +10 -0
  70. ghost_api/integrations/claude_code/skills/ghost-api/SKILL.md +46 -0
  71. ghost_api/integrations/setup.py +84 -0
  72. ghost_api/llm/__init__.py +0 -0
  73. ghost_api/llm/doctor.py +60 -0
  74. ghost_api/llm/gateway.py +476 -0
  75. ghost_api/llm/providers.py +124 -0
  76. ghost_api/memory/__init__.py +0 -0
  77. ghost_api/memory/knowledge.py +157 -0
  78. ghost_api/memory/store.py +883 -0
  79. ghost_api/net.py +235 -0
  80. ghost_api/netguard.py +203 -0
  81. ghost_api/pipeline.py +745 -0
  82. ghost_api/probe/__init__.py +0 -0
  83. ghost_api/probe/prober.py +311 -0
  84. ghost_api/probe/replay.py +313 -0
  85. ghost_api/project.py +186 -0
  86. ghost_api/py.typed +0 -0
  87. ghost_api/runtime.py +397 -0
  88. ghost_api/serve/__init__.py +101 -0
  89. ghost_api/serve/explorer_mcp.py +305 -0
  90. ghost_api/serve/mcp.py +121 -0
  91. ghost_api/serve/mock.py +130 -0
  92. ghost_api/serve/rest.py +132 -0
  93. ghost_api/site.py +84 -0
  94. ghost_api/specdiff.py +122 -0
  95. ghost_api/ui/__init__.py +0 -0
  96. ghost_api/ui/keys.py +134 -0
  97. ghost_api/ui/repl.py +453 -0
  98. ghost_api/ui/terminal.py +573 -0
  99. ghost_api/verify/__init__.py +0 -0
  100. ghost_api/verify/classify.py +84 -0
  101. ghost_api/verify/repair.py +40 -0
  102. ghost_api/verify/runner.py +110 -0
  103. ghost_api/verify/verifier.py +291 -0
  104. ghost_api/watch.py +313 -0
  105. ghost_api/web/__init__.py +1 -0
  106. ghost_api/web/server.py +690 -0
  107. ghost_api/web/static/assets/Endpoints-zPnjlcM8.js +46 -0
  108. ghost_api/web/static/assets/Run-D85zWHey.js +91 -0
  109. ghost_api/web/static/assets/Settings-D0aWHFAt.js +11 -0
  110. ghost_api/web/static/assets/Site-a88shEmP.js +71 -0
  111. ghost_api/web/static/assets/Watches-CqZWIa9G.js +16 -0
  112. ghost_api/web/static/assets/circle-x-BcTE_1tr.js +11 -0
  113. ghost_api/web/static/assets/index-Ds2e5B9f.js +141 -0
  114. ghost_api/web/static/assets/index-xQpyIBbG.css +1 -0
  115. ghost_api/web/static/assets/minus-C2xqP1eQ.js +6 -0
  116. ghost_api/web/static/assets/refresh-cw-CxdR_8Oi.js +6 -0
  117. ghost_api/web/static/favicon.svg +1 -0
  118. ghost_api/web/static/index.html +26 -0
  119. ghost_api/workflows.py +203 -0
  120. ghost_api_cli-0.2.0.dist-info/METADATA +1019 -0
  121. ghost_api_cli-0.2.0.dist-info/RECORD +125 -0
  122. ghost_api_cli-0.2.0.dist-info/WHEEL +5 -0
  123. ghost_api_cli-0.2.0.dist-info/entry_points.txt +2 -0
  124. ghost_api_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
  125. ghost_api_cli-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,157 @@
1
+ """What Ghost API remembers about a site between runs (Stage 8).
2
+
3
+ - Endpoints (with probe results and statuses) and their redacted samples: already in
4
+ the store from earlier stages.
5
+ - Lessons: short notes for future runs. "auto" lessons are written by code (which action
6
+ revealed which endpoint), "llm" lessons by one optional LLM call per run.
7
+ - The naming plan, so regenerating a client needs no LLM call.
8
+
9
+ Lessons come from web content and LLM output, so they are treated as untrusted hints:
10
+ length-capped, single-line, redacted, and labelled as possibly outdated in prompts.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import re
17
+ from collections.abc import Sequence
18
+ from dataclasses import dataclass, field
19
+
20
+ from pydantic import BaseModel, Field
21
+
22
+ from ghost_api.analysis.models import Endpoint
23
+ from ghost_api.capture.filters import REDACTED, redact_text
24
+ from ghost_api.capture.models import CapturedRequest, RawRequest
25
+ from ghost_api.generator.naming import NamingPlan
26
+ from ghost_api.llm.gateway import LLMError, LLMGateway
27
+ from ghost_api.memory.store import Store
28
+
29
+ MAX_LESSON_CHARS = 200
30
+ MAX_LESSONS_IN_PROMPT = 6
31
+
32
+
33
+ @dataclass
34
+ class DomainMemory:
35
+ domain: str
36
+ goal: str
37
+ start_url: str
38
+ run_id: int # the run whose endpoints these are
39
+ endpoints: list[Endpoint]
40
+ lessons: list[str] = field(default_factory=list)
41
+ plan: NamingPlan | None = None
42
+
43
+
44
+ def load_memory(store: Store, domain: str) -> DomainMemory | None:
45
+ run_id = store.latest_endpoint_run(domain)
46
+ runs = [r for r in store.list_runs(domain) if not r.goal.startswith("refresh:")]
47
+ if run_id is None or not runs:
48
+ return None
49
+ plan_json = store.load_naming_plan(domain)
50
+ plan = None
51
+ if plan_json:
52
+ try:
53
+ plan = NamingPlan.model_validate_json(plan_json)
54
+ except ValueError:
55
+ plan = None
56
+ return DomainMemory(
57
+ domain=domain,
58
+ goal=runs[0].goal,
59
+ start_url=runs[0].start_url,
60
+ run_id=run_id,
61
+ endpoints=store.current_endpoints(domain),
62
+ lessons=store.lessons_for(domain),
63
+ plan=plan,
64
+ )
65
+
66
+
67
+ def clean_lesson(text: str) -> str:
68
+ """One line, no secrets, bounded length."""
69
+ text = re.sub(r"\s+", " ", redact_text(text)).strip()
70
+ return text[:MAX_LESSON_CHARS]
71
+
72
+
73
+ def memory_prompt(endpoints: Sequence[Endpoint], lessons: Sequence[str]) -> str:
74
+ """Compact text for the agent's prompt, or '' when there is nothing to say."""
75
+ if not endpoints and not lessons:
76
+ return ""
77
+ lines = ["Earlier runs on this site (may be outdated; the current page is the truth):"]
78
+ for ep in list(endpoints)[:15]:
79
+ status = f" [{ep.status}]" if ep.status not in ("", "discovered") else ""
80
+ lines.append(f"- known endpoint: {ep.method} {ep.host}{ep.path_template}{status}")
81
+ for lesson in list(lessons)[:MAX_LESSONS_IN_PROMPT]:
82
+ lines.append(f"- note: {lesson}")
83
+ return "\n".join(lines)
84
+
85
+
86
+ def discovery_lessons(steps: Sequence[tuple[str, dict[str, object], list[str]]]) -> list[str]:
87
+ """Deterministic lessons: which action revealed which endpoint.
88
+
89
+ `steps` are (action, args, new endpoint labels) tuples from the agent's log.
90
+ """
91
+ lessons = []
92
+ for action, args, new in steps:
93
+ if not new or action in ("finish", "ask_user"):
94
+ continue
95
+ shown = ", ".join(f"{k}={v!r}" for k, v in args.items() if k != "element_id")
96
+ how = f"{action}({shown})" if shown else action
97
+ for label in new:
98
+ lessons.append(clean_lesson(f"{label} appeared after {how}"))
99
+ return lessons
100
+
101
+
102
+ class Lessons(BaseModel):
103
+ lessons: list[str] = Field(
104
+ description="1 to 3 short, specific tips for exploring this site next time.",
105
+ max_length=3,
106
+ )
107
+
108
+
109
+ LESSONS_SYSTEM = """\
110
+ You review a finished exploration of a website's hidden API and write 1 to 3 short,
111
+ specific tips that would make the next exploration of the same site faster: which page
112
+ or action reveals which endpoint, what to avoid, what needs a login. No generic advice."""
113
+
114
+
115
+ def write_lessons(
116
+ llm: LLMGateway,
117
+ goal: str,
118
+ status: str,
119
+ steps: Sequence[tuple[str, dict[str, object], str]],
120
+ endpoints: Sequence[Endpoint],
121
+ ) -> list[str]:
122
+ """One LLM call; returns [] if the LLM is unavailable."""
123
+ history = "\n".join(
124
+ f"{i}. {action}({json.dumps(args)}) -> {result[:160]}"
125
+ for i, (action, args, result) in enumerate(steps, 1)
126
+ )
127
+ found = "\n".join(f"- {e.method} {e.host}{e.path_template}" for e in endpoints) or "(none)"
128
+ prompt = f"GOAL: {goal}\nOUTCOME: {status}\n\nSTEPS:\n{history}\n\nENDPOINTS FOUND:\n{found}"
129
+ try:
130
+ reply = llm.ask(
131
+ [{"role": "system", "content": LESSONS_SYSTEM}, {"role": "user", "content": prompt}],
132
+ response_model=Lessons,
133
+ )
134
+ except LLMError:
135
+ return []
136
+ if reply.parsed is None:
137
+ return []
138
+ return [c for c in (clean_lesson(t) for t in reply.parsed.lessons) if c]
139
+
140
+
141
+ def rebuild_raw(captured: CapturedRequest, required_headers: Sequence[str]) -> RawRequest | None:
142
+ """A replayable request from a stored (redacted) sample, when nothing needed is missing.
143
+
144
+ Returns None if a redacted value would be needed: a secret in the URL or body, or a
145
+ redacted header that probing found to be required. Fresh traffic is needed then.
146
+ """
147
+ if REDACTED in captured.url or (captured.req_body and REDACTED in captured.req_body):
148
+ return None
149
+ required = {h.lower() for h in required_headers}
150
+ headers = {}
151
+ for name, value in captured.req_headers.items():
152
+ if value == REDACTED:
153
+ if name.lower() in required:
154
+ return None
155
+ continue # e.g. a cookie that turned out not to matter
156
+ headers[name] = value
157
+ return RawRequest(captured.method, captured.url, headers, captured.req_body)