docforge-cli 0.4.0__tar.gz → 0.4.1__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.
Files changed (46) hide show
  1. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/PKG-INFO +1 -1
  2. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/pyproject.toml +1 -1
  3. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/cli.py +12 -8
  4. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/mcp_server.py +41 -23
  5. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/remote_client.py +61 -47
  6. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge_cli.egg-info/PKG-INFO +1 -1
  7. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/LICENSE +0 -0
  8. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/README.md +0 -0
  9. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/setup.cfg +0 -0
  10. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/__init__.py +0 -0
  11. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/__main__.py +0 -0
  12. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/api.py +0 -0
  13. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/config.py +0 -0
  14. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/crawlers/__init__.py +0 -0
  15. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/crawlers/confluence.py +0 -0
  16. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/crawlers/git.py +0 -0
  17. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/db.py +0 -0
  18. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/embedder_api.py +0 -0
  19. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/ingest.py +0 -0
  20. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/lint.py +0 -0
  21. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/processors/__init__.py +0 -0
  22. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/processors/chunker.py +0 -0
  23. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/processors/embedder.py +0 -0
  24. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/processors/parser.py +0 -0
  25. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/query_log.py +0 -0
  26. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/ranking.py +0 -0
  27. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/scripts/__init__.py +0 -0
  28. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/scripts/eval_search.py +0 -0
  29. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/scripts/latency_report.py +0 -0
  30. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/sources.py +0 -0
  31. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/sql/migrations/001_add_source_identifier.sql +0 -0
  32. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/sql/migrations/002_add_status_index.sql +0 -0
  33. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/sql/migrations/003_add_source_tags.sql +0 -0
  34. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/sql/migrations/004_add_query_log.sql +0 -0
  35. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/sql/migrations/005_add_query_log_user_oid.sql +0 -0
  36. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/sql/migrations/006_add_query_log_request_ms.sql +0 -0
  37. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/sql/schema.sql +0 -0
  38. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/templates/docforge.yml +0 -0
  39. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/templates/docker-compose.yml +0 -0
  40. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/templates/mcp_client.py +0 -0
  41. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge/templates/sources.yml +0 -0
  42. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge_cli.egg-info/SOURCES.txt +0 -0
  43. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge_cli.egg-info/dependency_links.txt +0 -0
  44. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge_cli.egg-info/entry_points.txt +0 -0
  45. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge_cli.egg-info/requires.txt +0 -0
  46. {docforge_cli-0.4.0 → docforge_cli-0.4.1}/src/docforge_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docforge-cli
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: Forge searchable context from Confluence and git repos for AI coding assistants
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://GranatenUdo.github.io/docforge/
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "docforge-cli"
7
- version = "0.4.0"
7
+ version = "0.4.1"
8
8
  description = "Forge searchable context from Confluence and git repos for AI coding assistants"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -8,6 +8,8 @@ from pathlib import Path
8
8
 
9
9
  import typer
10
10
 
11
+ from docforge.remote_client import AuthName
12
+
11
13
  app = typer.Typer(
12
14
  help="Forge searchable context from Confluence and git repos for AI coding assistants.",
13
15
  )
@@ -125,24 +127,26 @@ def serve(
125
127
  help="Run MCP backed by a remote search API at this URL",
126
128
  envvar="DOCFORGE_API_URL",
127
129
  ),
128
- auth: str = typer.Option(
129
- "none",
130
+ auth: AuthName = typer.Option(
131
+ AuthName.none,
130
132
  "--auth",
131
- help="Auth provider for --remote-api: none | bearer | azure",
133
+ help="Auth provider for --remote-api",
132
134
  envvar="DOCFORGE_AUTH",
133
135
  ),
134
136
  ) -> None:
135
137
  """Run the MCP server (or FastAPI API with --api, or remote-backed MCP with --remote-api)."""
136
138
  _setup_logging()
139
+ if remote_api and api:
140
+ typer.echo("Error: --api and --remote-api are mutually exclusive.", err=True)
141
+ raise typer.Exit(1)
142
+ if auth is not AuthName.none and not remote_api:
143
+ typer.echo("Warning: --auth has no effect without --remote-api.", err=True)
144
+
137
145
  if remote_api:
138
- if api:
139
- typer.echo("Error: --api and --remote-api are mutually exclusive.", err=True)
140
- raise typer.Exit(1)
141
146
  from docforge.remote_client import run_remote_mcp
142
147
 
143
148
  run_remote_mcp(url=remote_api, auth_name=auth)
144
- return
145
- if api:
149
+ elif api:
146
150
  import uvicorn
147
151
 
148
152
  from docforge.api import app as fastapi_app
@@ -49,6 +49,32 @@ def _get_embedder() -> EmbedderProtocol:
49
49
  return _embedder
50
50
 
51
51
 
52
+ def format_search_results_markdown(
53
+ results: list[dict],
54
+ *,
55
+ empty_message: str = "No documentation found matching your query.",
56
+ ) -> str:
57
+ """Render a list of search-result dicts as the canonical Markdown shape.
58
+
59
+ Each result must have keys: similarity, source_title, source_url, text.
60
+ Optional: section_title, source_tags.
61
+ """
62
+ if not results:
63
+ return empty_message
64
+
65
+ parts: list[str] = []
66
+ for i, r in enumerate(results, 1):
67
+ header = f"**Result {i}** (relevance: {r['similarity']:.2f}) -- {r['source_title']}"
68
+ if r.get("section_title"):
69
+ header += f" > {r['section_title']}"
70
+ header += f"\nSource: {r['source_url']}"
71
+ tags = r.get("source_tags") or []
72
+ if tags:
73
+ header += f"\nTags: {', '.join(tags)}"
74
+ parts.append(f"{header}\n\n{r['text']}")
75
+ return "\n\n---\n\n".join(parts)
76
+
77
+
52
78
  @mcp.tool()
53
79
  async def search_documentation(
54
80
  query: Annotated[str, Field(max_length=8000)],
@@ -115,31 +141,23 @@ async def search_documentation(
115
141
 
116
142
  await log_query(pool, user_name, team_name, area_name, query, len(rows))
117
143
 
118
- if not rows:
119
- return (
144
+ return format_search_results_markdown(
145
+ [
146
+ {
147
+ "similarity": row["similarity"],
148
+ "source_title": row["source_title"],
149
+ "source_url": row["source_url"],
150
+ "section_title": row["section_title"],
151
+ "source_tags": list(row["source_tags"] or []),
152
+ "text": row["text"],
153
+ }
154
+ for row in rows
155
+ ],
156
+ empty_message=(
120
157
  "No documentation found matching your query. "
121
158
  "The index may be empty -- run `python -m docforge ingest` to populate it."
122
- )
123
-
124
- parts: list[str] = []
125
- for i, row in enumerate(rows, 1):
126
- similarity = row["similarity"]
127
- source = row["source_title"]
128
- url = row["source_url"]
129
- section = row["section_title"]
130
- text = row["text"]
131
- tags = list(row["source_tags"] or [])
132
-
133
- header = f"**Result {i}** (relevance: {similarity:.2f}) — {source}"
134
- if section:
135
- header += f" > {section}"
136
- header += f"\nSource: {url}"
137
- if tags:
138
- header += f"\nTags: {', '.join(tags)}"
139
-
140
- parts.append(f"{header}\n\n{text}")
141
-
142
- return "\n\n---\n\n".join(parts)
159
+ ),
160
+ )
143
161
 
144
162
 
145
163
  @mcp.tool()
@@ -7,12 +7,21 @@ Used by `docforge serve --remote-api $URL --auth ...`. See the
7
7
  from __future__ import annotations
8
8
 
9
9
  import os
10
+ from enum import Enum
10
11
  from typing import Protocol
11
12
 
12
13
  import httpx
13
14
  from fastmcp import FastMCP
14
15
 
15
16
 
17
+ class AuthName(str, Enum):
18
+ """Selectable auth providers for the --remote-api mode."""
19
+
20
+ none = "none"
21
+ bearer = "bearer"
22
+ azure = "azure"
23
+
24
+
16
25
  class AuthProvider(Protocol):
17
26
  """Async source of HTTP headers attached to each remote request."""
18
27
 
@@ -63,15 +72,19 @@ class AzureAuth:
63
72
  return {"Authorization": f"Bearer {token.token}"}
64
73
 
65
74
 
66
- def make_auth_provider(name: str) -> AuthProvider:
75
+ def make_auth_provider(name: AuthName | str) -> AuthProvider:
67
76
  """Return an AuthProvider instance for the given name."""
68
- if name == "none":
77
+ try:
78
+ name = AuthName(name) if isinstance(name, str) else name
79
+ except ValueError as e:
80
+ raise ValueError(f"Unknown auth provider: {name!r}. Valid: none, bearer, azure.") from e
81
+ if name is AuthName.none:
69
82
  return NoneAuth()
70
- if name == "bearer":
83
+ if name is AuthName.bearer:
71
84
  return BearerAuth()
72
- if name == "azure":
85
+ if name is AuthName.azure:
73
86
  return AzureAuth()
74
- raise ValueError(f"Unknown auth provider: {name!r}. Valid: none, bearer, azure.")
87
+ raise ValueError(f"Unknown auth provider: {name!r}.")
75
88
 
76
89
 
77
90
  class RemoteBackend:
@@ -86,7 +99,18 @@ class RemoteBackend:
86
99
  ) -> None:
87
100
  self._url = url.rstrip("/")
88
101
  self._auth = auth
89
- self._transport = transport # for tests
102
+ self._transport = transport
103
+ self._client: httpx.AsyncClient | None = None
104
+
105
+ async def _ensure_client(self) -> httpx.AsyncClient:
106
+ if self._client is None:
107
+ self._client = httpx.AsyncClient(transport=self._transport, timeout=30.0)
108
+ return self._client
109
+
110
+ async def aclose(self) -> None:
111
+ if self._client is not None:
112
+ await self._client.aclose()
113
+ self._client = None
90
114
 
91
115
  def _identity_body(self) -> dict[str, str]:
92
116
  out: dict[str, str] = {}
@@ -100,18 +124,25 @@ class RemoteBackend:
100
124
  out[body_key] = val
101
125
  return out
102
126
 
103
- async def search(self, *, query: str, limit: int = 5) -> str:
104
- """Search the remote API and return Markdown-formatted results."""
105
- body: dict[str, object] = {"query": query, "limit": limit}
106
- body.update(self._identity_body())
127
+ async def _request(
128
+ self,
129
+ method: str,
130
+ path: str,
131
+ *,
132
+ json: dict[str, object] | None = None,
133
+ ) -> httpx.Response | str:
134
+ """Perform an HTTP request with auth and uniform error handling.
135
+
136
+ Returns the Response on 2xx; an already-formatted error string otherwise.
137
+ """
107
138
  try:
108
139
  headers = await self._auth.headers()
109
140
  except Exception as e:
110
141
  return f"Auth provider error: {e}"
111
142
 
143
+ client = await self._ensure_client()
112
144
  try:
113
- async with httpx.AsyncClient(transport=self._transport, timeout=30.0) as client:
114
- resp = await client.post(f"{self._url}/search", json=body, headers=headers)
145
+ resp = await client.request(method, f"{self._url}{path}", json=json, headers=headers)
115
146
  except httpx.ConnectError:
116
147
  return f"Could not reach remote API at {self._url}."
117
148
  except httpx.HTTPError as e:
@@ -123,45 +154,28 @@ class RemoteBackend:
123
154
  return f"Remote API error ({resp.status_code}). Try again in a moment."
124
155
  if resp.status_code != 200:
125
156
  return f"Remote API returned {resp.status_code}: {resp.text[:200]}"
157
+ return resp
126
158
 
127
- data = resp.json()
128
- results = data.get("results", [])
129
- if not results:
130
- return "No documentation found matching your query."
131
-
132
- parts: list[str] = []
133
- for i, r in enumerate(results, 1):
134
- header = f"**Result {i}** (relevance: {r['similarity']:.2f}) -- {r['source_title']}"
135
- if r.get("section_title"):
136
- header += f" > {r['section_title']}"
137
- header += f"\nSource: {r['source_url']}"
138
- tags = r.get("source_tags") or []
139
- if tags:
140
- header += f"\nTags: {', '.join(tags)}"
141
- parts.append(f"{header}\n\n{r['text']}")
142
- return "\n\n---\n\n".join(parts)
159
+ async def search(self, *, query: str, limit: int = 5) -> str:
160
+ """Search the remote API and return Markdown-formatted results."""
161
+ body: dict[str, object] = {"query": query, "limit": limit}
162
+ body.update(self._identity_body())
163
+ result = await self._request("POST", "/search", json=body)
164
+ if isinstance(result, str):
165
+ return result
143
166
 
144
- async def list_sources(self) -> str:
145
- """List indexed sources from the remote API."""
146
- try:
147
- headers = await self._auth.headers()
148
- except Exception as e:
149
- return f"Auth provider error: {e}"
167
+ from docforge.mcp_server import format_search_results_markdown
150
168
 
151
- try:
152
- async with httpx.AsyncClient(transport=self._transport, timeout=10.0) as client:
153
- resp = await client.get(f"{self._url}/sources", headers=headers)
154
- except httpx.ConnectError:
155
- return f"Could not reach remote API at {self._url}."
156
- except httpx.HTTPError as e:
157
- return f"Remote API error: {e}"
169
+ data = result.json()
170
+ return format_search_results_markdown(data.get("results", []))
158
171
 
159
- if resp.status_code == 401:
160
- return "Auth failed (401). Check DOCFORGE_API_URL and the --auth provider."
161
- if resp.status_code != 200:
162
- return f"Remote API returned {resp.status_code}: {resp.text[:200]}"
172
+ async def list_sources(self) -> str:
173
+ """List indexed sources from the remote API."""
174
+ result = await self._request("GET", "/sources")
175
+ if isinstance(result, str):
176
+ return result
163
177
 
164
- data = resp.json()
178
+ data = result.json()
165
179
  sources = data.get("sources", [])
166
180
  if not sources:
167
181
  return "No sources indexed."
@@ -180,7 +194,7 @@ INSTRUCTIONS = (
180
194
  )
181
195
 
182
196
 
183
- def run_remote_mcp(*, url: str, auth_name: str = "none") -> None:
197
+ def run_remote_mcp(*, url: str, auth_name: AuthName | str = AuthName.none) -> None:
184
198
  """Run an MCP server proxying tool calls to a remote docforge search-api."""
185
199
  auth = make_auth_provider(auth_name)
186
200
  backend = RemoteBackend(url=url, auth=auth)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docforge-cli
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: Forge searchable context from Confluence and git repos for AI coding assistants
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://GranatenUdo.github.io/docforge/
File without changes
File without changes
File without changes