folderlake 0.2.0__tar.gz → 0.2.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.
- {folderlake-0.2.0/src/folderlake.egg-info → folderlake-0.2.1}/PKG-INFO +7 -6
- {folderlake-0.2.0 → folderlake-0.2.1}/README.md +6 -5
- {folderlake-0.2.0 → folderlake-0.2.1}/pyproject.toml +1 -1
- {folderlake-0.2.0 → folderlake-0.2.1}/src/folderlake/appserver.py +126 -37
- {folderlake-0.2.0 → folderlake-0.2.1/src/folderlake.egg-info}/PKG-INFO +7 -6
- {folderlake-0.2.0 → folderlake-0.2.1}/tests/test_app.py +10 -1
- {folderlake-0.2.0 → folderlake-0.2.1}/LICENSE +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/setup.cfg +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/src/folderlake/__init__.py +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/src/folderlake/assets/echarts.min.js +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/src/folderlake/catalog.py +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/src/folderlake/cli.py +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/src/folderlake/cypher.py +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/src/folderlake/extractors.py +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/src/folderlake/hierarchy.py +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/src/folderlake/home.html +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/src/folderlake/llm.py +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/src/folderlake/mcp_server.py +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/src/folderlake/preview.py +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/src/folderlake/registry.py +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/src/folderlake/relations.py +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/src/folderlake/server.py +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/src/folderlake/topics.py +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/src/folderlake/viewer.html +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/src/folderlake.egg-info/SOURCES.txt +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/src/folderlake.egg-info/dependency_links.txt +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/src/folderlake.egg-info/entry_points.txt +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/src/folderlake.egg-info/requires.txt +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/src/folderlake.egg-info/top_level.txt +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/tests/test_cypher.py +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/tests/test_extractors.py +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/tests/test_hierarchy.py +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/tests/test_incremental.py +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/tests/test_mcp.py +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/tests/test_phrases_llm.py +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/tests/test_relations.py +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/tests/test_server.py +0 -0
- {folderlake-0.2.0 → folderlake-0.2.1}/tests/test_topics.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: folderlake
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Point it at a folder. Know what's in it. A local file catalog with topics, metadata and a relationship graph, stored in one DuckDB file.
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -70,8 +70,7 @@ of shared topics, shared entities and explicit filename references.
|
|
|
70
70
|
## Quick start
|
|
71
71
|
|
|
72
72
|
```bash
|
|
73
|
-
pip install
|
|
74
|
-
# (not on PyPI yet — GitHub is the source; or `pip install -e .` from a clone)
|
|
73
|
+
pip install folderlake # from PyPI (or: pip install -e . from a clone)
|
|
75
74
|
|
|
76
75
|
folderlake app # opens the FolderLake home page
|
|
77
76
|
```
|
|
@@ -86,6 +85,9 @@ page (stored in a small SQLite registry in `~/.folderlake/`): reopen
|
|
|
86
85
|
one in a click, **Refresh** it to pick up added/removed files, or
|
|
87
86
|
**Delete** it. Each history entry shows the settings it was scanned
|
|
88
87
|
with. Inside a catalog, a **← FolderLake Home** link brings you back.
|
|
88
|
+
Everything runs on a single port — each catalog lives under its own
|
|
89
|
+
`/c/…/` path — so the app works through SSH tunnels, Docker port
|
|
90
|
+
mappings and other single-port forwarding.
|
|
89
91
|
|
|
90
92
|
The catalog viewer itself: filters and the topic tree on the left,
|
|
91
93
|
the file list and the relationship graph side by side in the middle
|
|
@@ -117,8 +119,7 @@ variables needed; the environment variables below remain for the
|
|
|
117
119
|
CLI and scripts:
|
|
118
120
|
|
|
119
121
|
```bash
|
|
120
|
-
pip install "folderlake[llm]
|
|
121
|
-
# (adds LiteLLM as the transport)
|
|
122
|
+
pip install "folderlake[llm]" # adds LiteLLM as the transport
|
|
122
123
|
|
|
123
124
|
# Local model via Ollama (default provider)
|
|
124
125
|
FOLDERLAKE_LLM_MODEL=qwen3 folderlake scan ~/notes --llm
|
|
@@ -149,7 +150,7 @@ client (Claude Code, Cursor, your own agent) can ask questions about a
|
|
|
149
150
|
catalog without opening files:
|
|
150
151
|
|
|
151
152
|
```bash
|
|
152
|
-
pip install "folderlake[mcp]
|
|
153
|
+
pip install "folderlake[mcp]"
|
|
153
154
|
folderlake mcp ~/Documents/notes # stdio MCP server
|
|
154
155
|
# or the folderlake-mcp entry point
|
|
155
156
|
```
|
|
@@ -53,8 +53,7 @@ of shared topics, shared entities and explicit filename references.
|
|
|
53
53
|
## Quick start
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
|
-
pip install
|
|
57
|
-
# (not on PyPI yet — GitHub is the source; or `pip install -e .` from a clone)
|
|
56
|
+
pip install folderlake # from PyPI (or: pip install -e . from a clone)
|
|
58
57
|
|
|
59
58
|
folderlake app # opens the FolderLake home page
|
|
60
59
|
```
|
|
@@ -69,6 +68,9 @@ page (stored in a small SQLite registry in `~/.folderlake/`): reopen
|
|
|
69
68
|
one in a click, **Refresh** it to pick up added/removed files, or
|
|
70
69
|
**Delete** it. Each history entry shows the settings it was scanned
|
|
71
70
|
with. Inside a catalog, a **← FolderLake Home** link brings you back.
|
|
71
|
+
Everything runs on a single port — each catalog lives under its own
|
|
72
|
+
`/c/…/` path — so the app works through SSH tunnels, Docker port
|
|
73
|
+
mappings and other single-port forwarding.
|
|
72
74
|
|
|
73
75
|
The catalog viewer itself: filters and the topic tree on the left,
|
|
74
76
|
the file list and the relationship graph side by side in the middle
|
|
@@ -100,8 +102,7 @@ variables needed; the environment variables below remain for the
|
|
|
100
102
|
CLI and scripts:
|
|
101
103
|
|
|
102
104
|
```bash
|
|
103
|
-
pip install "folderlake[llm]
|
|
104
|
-
# (adds LiteLLM as the transport)
|
|
105
|
+
pip install "folderlake[llm]" # adds LiteLLM as the transport
|
|
105
106
|
|
|
106
107
|
# Local model via Ollama (default provider)
|
|
107
108
|
FOLDERLAKE_LLM_MODEL=qwen3 folderlake scan ~/notes --llm
|
|
@@ -132,7 +133,7 @@ client (Claude Code, Cursor, your own agent) can ask questions about a
|
|
|
132
133
|
catalog without opening files:
|
|
133
134
|
|
|
134
135
|
```bash
|
|
135
|
-
pip install "folderlake[mcp]
|
|
136
|
+
pip install "folderlake[mcp]"
|
|
136
137
|
folderlake mcp ~/Documents/notes # stdio MCP server
|
|
137
138
|
# or the folderlake-mcp entry point
|
|
138
139
|
```
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "folderlake"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
description = "Point it at a folder. Know what's in it. A local file catalog with topics, metadata and a relationship graph, stored in one DuckDB file."
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
folder picker, no scan before the page appears. The home page lists
|
|
5
5
|
every catalog built so far (from the SQLite registry), starts new
|
|
6
6
|
scans with live progress, refreshes or deletes old ones, and holds
|
|
7
|
-
the LLM settings.
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
the LLM settings.
|
|
8
|
+
|
|
9
|
+
Everything is served from this one port: each catalog's viewer lives
|
|
10
|
+
under ``/c/<catalog-id>/`` with its API at ``/c/<catalog-id>/api/…``
|
|
11
|
+
(the viewer HTML is rewritten to the prefix when served). A single
|
|
12
|
+
port keeps the app usable through SSH tunnels, Docker port mappings
|
|
13
|
+
and other one-port forwarding — an earlier design ran each catalog
|
|
14
|
+
viewer on its own random port, which broke exactly there.
|
|
10
15
|
"""
|
|
11
16
|
|
|
12
17
|
from __future__ import annotations
|
|
@@ -21,12 +26,13 @@ import uuid
|
|
|
21
26
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
22
27
|
from pathlib import Path
|
|
23
28
|
|
|
24
|
-
from .catalog import CATALOG_NAME, scan
|
|
29
|
+
from .catalog import CATALOG_NAME, scan, try_build_fts
|
|
30
|
+
from .cypher import build_cypher
|
|
25
31
|
from .hierarchy import build_hierarchy
|
|
26
|
-
from .registry import Registry
|
|
32
|
+
from .registry import Registry
|
|
27
33
|
from .relations import build_relations
|
|
28
|
-
from .server import
|
|
29
|
-
|
|
34
|
+
from .server import (ECHARTS_PATH, VIEWER_PATH, facets, file_detail,
|
|
35
|
+
graph, list_files, topics_tree)
|
|
30
36
|
|
|
31
37
|
HOME_PATH = Path(__file__).with_name("home.html")
|
|
32
38
|
|
|
@@ -38,7 +44,6 @@ class App:
|
|
|
38
44
|
self.registry = registry or Registry()
|
|
39
45
|
self.jobs: dict[str, dict] = {}
|
|
40
46
|
self._job_lock = threading.Lock()
|
|
41
|
-
self.viewers: dict[str, tuple[ThreadingHTTPServer, int]] = {}
|
|
42
47
|
handler = type("AppHandler", (_Handler,), {"app": self})
|
|
43
48
|
try:
|
|
44
49
|
self.server = ThreadingHTTPServer((host, port), handler)
|
|
@@ -90,11 +95,7 @@ class App:
|
|
|
90
95
|
return snapshot
|
|
91
96
|
|
|
92
97
|
def _run_scan(self, job: dict, folder: Path, use_llm: bool) -> None:
|
|
93
|
-
cid = catalog_id_for(folder)
|
|
94
|
-
was_open = cid in self.viewers
|
|
95
98
|
try:
|
|
96
|
-
if was_open:
|
|
97
|
-
self._stop_viewer(cid) # DuckDB allows one writer
|
|
98
99
|
snapshot = self._apply_llm_env(use_llm)
|
|
99
100
|
db_path = folder / CATALOG_NAME
|
|
100
101
|
|
|
@@ -114,40 +115,43 @@ class App:
|
|
|
114
115
|
catalog_id=rec["id"])
|
|
115
116
|
except Exception as exc: # surfaced on the home page, not a crash
|
|
116
117
|
job.update(status="error", message=str(exc))
|
|
117
|
-
finally:
|
|
118
|
-
if was_open and job["status"] == "done":
|
|
119
|
-
self.open_catalog(cid)
|
|
120
118
|
|
|
121
|
-
# --
|
|
122
|
-
def
|
|
119
|
+
# -- catalogs under /c/<id>/ ----------------------------------------
|
|
120
|
+
def catalog_rec(self, catalog_id: str) -> dict:
|
|
123
121
|
rec = self.registry.get_catalog(catalog_id)
|
|
124
122
|
if rec is None:
|
|
125
123
|
raise KeyError("no such catalog")
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
db_path = folder / CATALOG_NAME
|
|
132
|
-
if not db_path.exists():
|
|
124
|
+
return rec
|
|
125
|
+
|
|
126
|
+
def open_catalog(self, catalog_id: str) -> str:
|
|
127
|
+
rec = self.catalog_rec(catalog_id)
|
|
128
|
+
if not (Path(rec["path"]) / CATALOG_NAME).exists():
|
|
133
129
|
raise KeyError("catalog file is missing — refresh to rebuild it")
|
|
134
|
-
server = create_server(db_path, folder, self.host, 0,
|
|
135
|
-
home_url=self.home_url)
|
|
136
|
-
port = server.server_address[1]
|
|
137
|
-
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
138
|
-
self.viewers[catalog_id] = (server, port)
|
|
139
130
|
self.registry.touch_opened(catalog_id)
|
|
140
|
-
return f"
|
|
131
|
+
return f"/c/{catalog_id}/"
|
|
141
132
|
|
|
142
|
-
def
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
133
|
+
def catalog_viewer_html(self, catalog_id: str) -> str:
|
|
134
|
+
"""The viewer page with its API/asset paths rewritten to this
|
|
135
|
+
catalog's prefix, plus the back-to-home link."""
|
|
136
|
+
rec = self.catalog_rec(catalog_id)
|
|
137
|
+
if not (Path(rec["path"]) / CATALOG_NAME).exists():
|
|
138
|
+
raise KeyError("catalog file is missing — refresh to rebuild it")
|
|
139
|
+
prefix = f"/c/{catalog_id}"
|
|
140
|
+
page = VIEWER_PATH.read_text(encoding="utf-8")
|
|
141
|
+
page = page.replace("/api/", prefix + "/api/")
|
|
142
|
+
page = page.replace("/assets/", prefix + "/assets/")
|
|
143
|
+
link = (
|
|
144
|
+
'<a href="/" style="margin-left:auto;margin-right:12px;'
|
|
145
|
+
'color:#4a80d0;text-decoration:none;font-size:13px;'
|
|
146
|
+
'white-space:nowrap">← FolderLake Home</a>'
|
|
147
|
+
)
|
|
148
|
+
return page.replace("</header>", link + "</header>", 1)
|
|
148
149
|
|
|
149
150
|
def delete_catalog(self, catalog_id: str) -> dict:
|
|
150
|
-
self.
|
|
151
|
+
running = self.running_job()
|
|
152
|
+
rec = self.catalog_rec(catalog_id)
|
|
153
|
+
if running is not None and running["path"] == rec["path"]:
|
|
154
|
+
raise RuntimeError("a scan is running for this catalog")
|
|
151
155
|
rec = self.registry.delete_catalog(catalog_id)
|
|
152
156
|
if rec is None:
|
|
153
157
|
raise KeyError("no such catalog")
|
|
@@ -193,6 +197,16 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
193
197
|
self.end_headers()
|
|
194
198
|
self.wfile.write(body)
|
|
195
199
|
|
|
200
|
+
def _send_bytes(self, body: bytes, content_type: str,
|
|
201
|
+
disposition: str | None = None) -> None:
|
|
202
|
+
self.send_response(200)
|
|
203
|
+
self.send_header("Content-Type", content_type)
|
|
204
|
+
self.send_header("Content-Length", str(len(body)))
|
|
205
|
+
if disposition:
|
|
206
|
+
self.send_header("Content-Disposition", disposition)
|
|
207
|
+
self.end_headers()
|
|
208
|
+
self.wfile.write(body)
|
|
209
|
+
|
|
196
210
|
def _send_html(self, text: str) -> None:
|
|
197
211
|
body = text.encode()
|
|
198
212
|
self.send_response(200)
|
|
@@ -210,12 +224,84 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
210
224
|
except Exception:
|
|
211
225
|
return {}
|
|
212
226
|
|
|
227
|
+
def _serve_catalog(self, parsed) -> None:
|
|
228
|
+
"""GET /c/<id>/… — the catalog viewer and its API, same port."""
|
|
229
|
+
app = self.app
|
|
230
|
+
parts = parsed.path.split("/") # ['', 'c', '<id>', <rest...>]
|
|
231
|
+
cid = parts[2] if len(parts) > 2 else ""
|
|
232
|
+
rest = "/".join(parts[3:])
|
|
233
|
+
try:
|
|
234
|
+
rec = app.catalog_rec(cid)
|
|
235
|
+
except KeyError:
|
|
236
|
+
self._send_json({"error": "no such catalog"}, 404)
|
|
237
|
+
return
|
|
238
|
+
db_path = Path(rec["path"]) / CATALOG_NAME
|
|
239
|
+
query = urllib.parse.parse_qs(parsed.query)
|
|
240
|
+
try:
|
|
241
|
+
if rest in ("", "/"):
|
|
242
|
+
self._send_html(app.catalog_viewer_html(cid))
|
|
243
|
+
return
|
|
244
|
+
if rest == "assets/echarts.min.js":
|
|
245
|
+
if ECHARTS_PATH.exists():
|
|
246
|
+
self._send_bytes(
|
|
247
|
+
ECHARTS_PATH.read_bytes(),
|
|
248
|
+
"application/javascript; charset=utf-8")
|
|
249
|
+
else:
|
|
250
|
+
self._send_json({"error": "echarts asset missing"}, 404)
|
|
251
|
+
return
|
|
252
|
+
if rest == "api/export.cypher":
|
|
253
|
+
self._send_bytes(build_cypher(db_path).encode(),
|
|
254
|
+
"text/plain; charset=utf-8",
|
|
255
|
+
'attachment; filename="folderlake.cypher"')
|
|
256
|
+
return
|
|
257
|
+
if rest == "api/meta":
|
|
258
|
+
self._send_json({
|
|
259
|
+
"folder": rec["path"],
|
|
260
|
+
"catalog": str(db_path),
|
|
261
|
+
"catalog_bytes": (db_path.stat().st_size
|
|
262
|
+
if db_path.exists() else 0),
|
|
263
|
+
})
|
|
264
|
+
return
|
|
265
|
+
if rest == "api/files":
|
|
266
|
+
kinds = query.get("kind", [None])[0]
|
|
267
|
+
self._send_json(list_files(
|
|
268
|
+
db_path,
|
|
269
|
+
kinds=kinds.split(",") if kinds else None,
|
|
270
|
+
topic=query.get("topic", [None])[0],
|
|
271
|
+
query=query.get("q", [None])[0],
|
|
272
|
+
node=query.get("node", [None])[0]))
|
|
273
|
+
return
|
|
274
|
+
if rest == "api/facets":
|
|
275
|
+
self._send_json(facets(db_path))
|
|
276
|
+
return
|
|
277
|
+
if rest == "api/topics":
|
|
278
|
+
self._send_json(topics_tree(db_path))
|
|
279
|
+
return
|
|
280
|
+
if rest == "api/graph":
|
|
281
|
+
self._send_json(graph(db_path))
|
|
282
|
+
return
|
|
283
|
+
if rest.startswith("api/file/"):
|
|
284
|
+
detail = file_detail(
|
|
285
|
+
db_path, urllib.parse.unquote(rest[len("api/file/"):]))
|
|
286
|
+
if detail is None:
|
|
287
|
+
self._send_json({"error": "file not found"}, 404)
|
|
288
|
+
else:
|
|
289
|
+
self._send_json(detail)
|
|
290
|
+
return
|
|
291
|
+
except KeyError as exc:
|
|
292
|
+
self._send_json({"error": str(exc)}, 404)
|
|
293
|
+
return
|
|
294
|
+
self._send_json({"error": "not found"}, 404)
|
|
295
|
+
|
|
213
296
|
def do_GET(self): # noqa: N802 - stdlib name
|
|
214
297
|
parsed = urllib.parse.urlparse(self.path)
|
|
215
298
|
app = self.app
|
|
216
299
|
if parsed.path == "/":
|
|
217
300
|
self._send_html(HOME_PATH.read_text(encoding="utf-8"))
|
|
218
301
|
return
|
|
302
|
+
if parsed.path.startswith("/c/"):
|
|
303
|
+
self._serve_catalog(parsed)
|
|
304
|
+
return
|
|
219
305
|
if parsed.path == "/api/catalogs":
|
|
220
306
|
self._send_json(app.registry.list_catalogs())
|
|
221
307
|
return
|
|
@@ -288,6 +374,9 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
288
374
|
except KeyError:
|
|
289
375
|
self._send_json({"error": "no such catalog"}, 404)
|
|
290
376
|
return
|
|
377
|
+
except RuntimeError as exc:
|
|
378
|
+
self._send_json({"error": str(exc)}, 409)
|
|
379
|
+
return
|
|
291
380
|
self._send_json({"error": "not found"}, 404)
|
|
292
381
|
|
|
293
382
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: folderlake
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Point it at a folder. Know what's in it. A local file catalog with topics, metadata and a relationship graph, stored in one DuckDB file.
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -70,8 +70,7 @@ of shared topics, shared entities and explicit filename references.
|
|
|
70
70
|
## Quick start
|
|
71
71
|
|
|
72
72
|
```bash
|
|
73
|
-
pip install
|
|
74
|
-
# (not on PyPI yet — GitHub is the source; or `pip install -e .` from a clone)
|
|
73
|
+
pip install folderlake # from PyPI (or: pip install -e . from a clone)
|
|
75
74
|
|
|
76
75
|
folderlake app # opens the FolderLake home page
|
|
77
76
|
```
|
|
@@ -86,6 +85,9 @@ page (stored in a small SQLite registry in `~/.folderlake/`): reopen
|
|
|
86
85
|
one in a click, **Refresh** it to pick up added/removed files, or
|
|
87
86
|
**Delete** it. Each history entry shows the settings it was scanned
|
|
88
87
|
with. Inside a catalog, a **← FolderLake Home** link brings you back.
|
|
88
|
+
Everything runs on a single port — each catalog lives under its own
|
|
89
|
+
`/c/…/` path — so the app works through SSH tunnels, Docker port
|
|
90
|
+
mappings and other single-port forwarding.
|
|
89
91
|
|
|
90
92
|
The catalog viewer itself: filters and the topic tree on the left,
|
|
91
93
|
the file list and the relationship graph side by side in the middle
|
|
@@ -117,8 +119,7 @@ variables needed; the environment variables below remain for the
|
|
|
117
119
|
CLI and scripts:
|
|
118
120
|
|
|
119
121
|
```bash
|
|
120
|
-
pip install "folderlake[llm]
|
|
121
|
-
# (adds LiteLLM as the transport)
|
|
122
|
+
pip install "folderlake[llm]" # adds LiteLLM as the transport
|
|
122
123
|
|
|
123
124
|
# Local model via Ollama (default provider)
|
|
124
125
|
FOLDERLAKE_LLM_MODEL=qwen3 folderlake scan ~/notes --llm
|
|
@@ -149,7 +150,7 @@ client (Claude Code, Cursor, your own agent) can ask questions about a
|
|
|
149
150
|
catalog without opening files:
|
|
150
151
|
|
|
151
152
|
```bash
|
|
152
|
-
pip install "folderlake[mcp]
|
|
153
|
+
pip install "folderlake[mcp]"
|
|
153
154
|
folderlake mcp ~/Documents/notes # stdio MCP server
|
|
154
155
|
# or the folderlake-mcp entry point
|
|
155
156
|
```
|
|
@@ -122,8 +122,17 @@ def test_app_server_flow(tmp_path):
|
|
|
122
122
|
status, opened = client.call(
|
|
123
123
|
"POST", f"/api/catalogs/{cat['id']}/open")
|
|
124
124
|
assert status == 200
|
|
125
|
-
|
|
125
|
+
# Same port, path-based: no second server is involved, so the
|
|
126
|
+
# app works through single-port forwarding (SSH tunnels etc.).
|
|
127
|
+
assert opened["url"] == f"/c/{cat['id']}/"
|
|
128
|
+
viewer = client.get_text(client.base + opened["url"])
|
|
126
129
|
assert "FolderLake Home" in viewer # back-to-home link injected
|
|
130
|
+
assert f"/c/{cat['id']}/api/" in viewer # API paths prefixed
|
|
131
|
+
_, files = client.call("GET", opened["url"] + "api/files")
|
|
132
|
+
assert len(files) == 2
|
|
133
|
+
cypher = client.get_text(client.base + opened["url"]
|
|
134
|
+
+ "api/export.cypher")
|
|
135
|
+
assert cypher.startswith("// FolderLake")
|
|
127
136
|
|
|
128
137
|
# refresh picks up a new file
|
|
129
138
|
(folder / "more.txt").write_text("workshop schedule update",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|