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 +10 -0
- dot/api.py +220 -0
- dot/cli.py +636 -0
- dot/config.py +126 -0
- dot/context/__init__.py +11 -0
- dot/context/assembler.py +144 -0
- dot/context/formatter.py +134 -0
- dot/context/ranker.py +122 -0
- dot/conversations/__init__.py +37 -0
- dot/conversations/claude_code.py +346 -0
- dot/conversations/ingest.py +206 -0
- dot/conversations/source.py +93 -0
- dot/conversations/watcher.py +141 -0
- dot/daemon.py +396 -0
- dot/doctor.py +228 -0
- dot/indexer/__init__.py +12 -0
- dot/indexer/chunker.py +235 -0
- dot/indexer/embedder.py +120 -0
- dot/indexer/parser.py +324 -0
- dot/indexer/watcher.py +151 -0
- dot/integrations/__init__.py +1 -0
- dot/integrations/claude.py +106 -0
- dot/integrations/copilot.py +66 -0
- dot/integrations/git.py +148 -0
- dot/integrations/mcp.py +217 -0
- dot/memory/__init__.py +6 -0
- dot/memory/decay.py +50 -0
- dot/memory/decisions.py +169 -0
- dot/memory/shared.py +162 -0
- dot/memory/store.py +661 -0
- dot_context-0.1.0a1.dist-info/METADATA +152 -0
- dot_context-0.1.0a1.dist-info/RECORD +36 -0
- dot_context-0.1.0a1.dist-info/WHEEL +5 -0
- dot_context-0.1.0a1.dist-info/entry_points.txt +2 -0
- dot_context-0.1.0a1.dist-info/licenses/LICENSE +21 -0
- dot_context-0.1.0a1.dist-info/top_level.txt +1 -0
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
|