dot-context 0.1.0a1__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.
dot/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """Dot — a local-first AI context memory daemon.
2
+
3
+ Dot watches your codebase, indexes it semantically, captures architectural
4
+ decisions, and serves the right context to any AI tool via a local REST API.
5
+ """
6
+
7
+ __version__ = "0.1.0"
8
+
9
+ DEFAULT_PORT = 7337
10
+ DEFAULT_HOST = "127.0.0.1"
dot/api.py ADDED
@@ -0,0 +1,220 @@
1
+ """Local REST API, served on localhost:7337.
2
+
3
+ Endpoints:
4
+ GET /status daemon health + project stats
5
+ GET /context assembled context for a file/query
6
+ POST /memory capture a decision/memory
7
+ GET /memory browse memories
8
+ DELETE /memory/{id} forget a memory
9
+ GET /graph dependency graph as JSON
10
+ POST /ask natural-language query of the codebase
11
+ POST /sync force re-index
12
+ POST /hooks/git/commit git post-commit ping (installed by dot init)
13
+ GET /ui web dashboard (when dashboard/dist is built)
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ import threading
20
+ from pathlib import Path
21
+ from typing import TYPE_CHECKING, Literal
22
+
23
+ from fastapi import FastAPI, HTTPException, Query, Response
24
+ from fastapi.middleware.cors import CORSMiddleware
25
+ from pydantic import BaseModel, Field
26
+
27
+ from dot import __version__
28
+ from dot.context.formatter import FORMATS, context_to_dict, format_context
29
+
30
+ if TYPE_CHECKING:
31
+ from dot.daemon import Daemon
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ class MemoryIn(BaseModel):
37
+ content: str = Field(min_length=3)
38
+ kind: Literal["decision", "rejected", "action_item", "note", "conversation"] = "note"
39
+ source: str = "api"
40
+ file_path: str = ""
41
+ tags: list[str] = []
42
+ confidence: float = Field(default=1.0, ge=0.0, le=1.0)
43
+ share: bool = False # also append to dot-memories.jsonl for the team
44
+
45
+
46
+ class ConversationIn(BaseModel):
47
+ transcript: str = Field(min_length=10)
48
+ source: str = "conversation"
49
+
50
+
51
+ class AskIn(BaseModel):
52
+ question: str = Field(min_length=3)
53
+ current_file: str | None = None
54
+ fmt: str = "markdown"
55
+
56
+
57
+ class SyncIn(BaseModel):
58
+ force: bool = False
59
+
60
+
61
+ def create_app(daemon: Daemon) -> FastAPI:
62
+ app = FastAPI(
63
+ title="Dot",
64
+ version=__version__,
65
+ description="Local-first AI context memory daemon",
66
+ )
67
+ # The dashboard dev server (vite) runs on another port; same machine only.
68
+ app.add_middleware(
69
+ CORSMiddleware,
70
+ allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
71
+ allow_methods=["*"],
72
+ allow_headers=["*"],
73
+ )
74
+
75
+ @app.get("/status")
76
+ def status() -> dict:
77
+ return daemon.status()
78
+
79
+ @app.get("/context")
80
+ def get_context(
81
+ query: str = "",
82
+ file: str | None = Query(default=None, description="current file path"),
83
+ fmt: str = Query(default="raw", description=f"one of {FORMATS}"),
84
+ token_budget: int | None = Query(default=None, ge=100, le=100_000),
85
+ profile: str | None = None,
86
+ ):
87
+ if fmt not in FORMATS:
88
+ raise HTTPException(422, f"fmt must be one of {FORMATS}")
89
+ context = daemon.assembler.assemble(
90
+ query=query, current_file=file, token_budget=token_budget, profile=profile
91
+ )
92
+ if fmt == "raw":
93
+ return context_to_dict(context)
94
+ return Response(
95
+ content=format_context(context, fmt),
96
+ media_type="text/plain; charset=utf-8",
97
+ headers={"x-dot-assembly-ms": f"{context.assembly_ms:.1f}"},
98
+ )
99
+
100
+ @app.post("/memory", status_code=201)
101
+ def add_memory(body: MemoryIn) -> dict:
102
+ memory = daemon.store.add_memory(
103
+ content=body.content,
104
+ kind=body.kind,
105
+ source=body.source,
106
+ file_path=body.file_path,
107
+ tags=body.tags,
108
+ confidence=body.confidence,
109
+ )
110
+ shared = False
111
+ if body.share:
112
+ from dot.memory.shared import export_memory
113
+
114
+ shared = export_memory(daemon.config, memory)
115
+ return {"id": memory.memory_id, "kind": memory.kind, "shared": shared}
116
+
117
+ @app.post("/memory/conversation", status_code=201)
118
+ def capture_conversation(body: ConversationIn) -> dict:
119
+ captured = daemon.decisions.capture_from_conversation(body.transcript, body.source)
120
+ return {"captured": len(captured), "ids": [memory.memory_id for memory in captured]}
121
+
122
+ @app.get("/memory")
123
+ def list_memories(
124
+ kind: str | None = None,
125
+ query: str | None = None,
126
+ limit: int = Query(default=50, ge=1, le=1000),
127
+ ) -> dict:
128
+ if query:
129
+ memories = daemon.store.query_memories(query, n=limit)
130
+ else:
131
+ memories = daemon.store.list_memories(kind=kind, limit=limit)
132
+ return {
133
+ "memories": [
134
+ {
135
+ "id": memory.memory_id,
136
+ "kind": memory.kind,
137
+ "content": memory.content,
138
+ "source": memory.source,
139
+ "file_path": memory.file_path,
140
+ "tags": memory.tags,
141
+ "confidence": memory.confidence,
142
+ "weight": round(memory.weight, 4),
143
+ "created_at": memory.created_at.isoformat() if memory.created_at else None,
144
+ }
145
+ for memory in memories
146
+ ]
147
+ }
148
+
149
+ @app.get("/memory/export")
150
+ def export_memories() -> dict:
151
+ return {"project": daemon.config.project_name, "memories": daemon.store.export_memories()}
152
+
153
+ @app.delete("/memory/{memory_id}")
154
+ def delete_memory(memory_id: str) -> dict:
155
+ if not daemon.store.delete_memory(memory_id):
156
+ raise HTTPException(404, "memory not found")
157
+ return {"deleted": memory_id}
158
+
159
+ @app.get("/graph")
160
+ def graph() -> dict:
161
+ return daemon.store.dependency_graph()
162
+
163
+ @app.post("/ask")
164
+ def ask(body: AskIn):
165
+ """Natural-language query: returns the most relevant code + decisions.
166
+
167
+ Dot is model-agnostic and fully local — it retrieves and ranks; the
168
+ calling tool (Claude, Copilot, a script) does the generation.
169
+ """
170
+ context = daemon.assembler.assemble(body.question, current_file=body.current_file)
171
+ if body.fmt == "raw":
172
+ return context_to_dict(context)
173
+ if body.fmt not in FORMATS:
174
+ raise HTTPException(422, f"fmt must be one of {FORMATS}")
175
+ return Response(
176
+ content=format_context(context, body.fmt),
177
+ media_type="text/plain; charset=utf-8",
178
+ )
179
+
180
+ @app.post("/sync", status_code=202)
181
+ def sync(body: SyncIn | None = None) -> dict:
182
+ force = bool(body and body.force)
183
+ thread = threading.Thread(
184
+ target=daemon.full_sync, kwargs={"force": force}, daemon=True, name="dot-api-sync"
185
+ )
186
+ thread.start()
187
+ return {"status": "sync started", "force": force}
188
+
189
+ @app.post("/conversations/scan", status_code=200)
190
+ def scan_conversations() -> dict:
191
+ """Incrementally scan Claude Code transcripts and capture decisions.
192
+
193
+ Unlike :http:post:`/sync` (which fires a heavy re-index in the
194
+ background), this runs synchronously: a conversation scan is cheap
195
+ (incremental byte-offset reads of local JSONL) so the caller gets the
196
+ real counts back immediately. Returns ``{"enabled": False, ...}`` when
197
+ capture is off rather than erroring, so clients can probe safely.
198
+ """
199
+ return daemon.scan_conversations()
200
+
201
+ @app.post("/hooks/git/commit")
202
+ def git_commit_hook() -> dict:
203
+ captured = daemon.decisions.mine_git(max_count=5)
204
+ return {"decisions_captured": captured}
205
+
206
+ # Serve the built dashboard at /ui when present.
207
+ dist = Path(__file__).resolve().parent.parent / "dashboard" / "dist"
208
+ if dist.is_dir():
209
+ from fastapi.staticfiles import StaticFiles
210
+
211
+ app.mount("/ui", StaticFiles(directory=str(dist), html=True), name="dashboard")
212
+ else:
213
+
214
+ @app.get("/ui")
215
+ def ui_placeholder() -> dict:
216
+ return {
217
+ "message": "dashboard not built — run `npm install && npm run build` in dashboard/",
218
+ }
219
+
220
+ return app