knowflow 0.1.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 (35) hide show
  1. knowflow/__init__.py +4 -0
  2. knowflow/__main__.py +3 -0
  3. knowflow/adapters/knowledge/__init__.py +42 -0
  4. knowflow/adapters/knowledge/local.py +43 -0
  5. knowflow/adapters/knowledge/obsidian.py +64 -0
  6. knowflow/adapters/storage/__init__.py +62 -0
  7. knowflow/adapters/storage/git.py +155 -0
  8. knowflow/adapters/storage/local.py +110 -0
  9. knowflow/adapters/storage/quark.py +225 -0
  10. knowflow/adapters/storage/s3.py +118 -0
  11. knowflow/adapters/storage/webdav.py +196 -0
  12. knowflow/cli/__init__.py +265 -0
  13. knowflow/cli/template_manager.py +100 -0
  14. knowflow/core/__init__.py +10 -0
  15. knowflow/core/knowledge.py +85 -0
  16. knowflow/core/pipeline.py +119 -0
  17. knowflow/core/storage.py +167 -0
  18. knowflow/dashboard/__init__.py +355 -0
  19. knowflow/templates/daily-journal/plugin.yaml +21 -0
  20. knowflow/templates/football-prediction/plugin.yaml +22 -0
  21. knowflow/templates/project-wiki/plugin.yaml +20 -0
  22. knowflow/templates/research-notes/plugin.yaml +26 -0
  23. knowflow/templates/stock-analysis/plugin.yaml +22 -0
  24. knowflow/templates/web-monitor/plugin.yaml +20 -0
  25. knowflow/templates/worldcup-tracker/plugin.yaml +18 -0
  26. knowflow/transforms/__init__.py +6 -0
  27. knowflow/transforms/archive.py +87 -0
  28. knowflow/transforms/markdown_export.py +110 -0
  29. knowflow/transforms/profile_index.py +89 -0
  30. knowflow-0.1.0.dist-info/METADATA +264 -0
  31. knowflow-0.1.0.dist-info/RECORD +35 -0
  32. knowflow-0.1.0.dist-info/WHEEL +5 -0
  33. knowflow-0.1.0.dist-info/entry_points.txt +2 -0
  34. knowflow-0.1.0.dist-info/licenses/LICENSE +21 -0
  35. knowflow-0.1.0.dist-info/top_level.txt +1 -0
knowflow/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ # KnowFlow
2
+ # Agent Knowledge Pipeline — Storage-agnostic, KB-agnostic, plug-and-play
3
+
4
+ __version__ = "0.1.0"
knowflow/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ """knowflow.__main__ — Entry point for `python3 -m knowflow`"""
2
+ from knowflow.cli import main
3
+ main()
@@ -0,0 +1,42 @@
1
+ """knowflow/adapters/knowledge/__init__.py — Knowledge adapter registry"""
2
+ from typing import Optional
3
+
4
+ from knowflow.core.knowledge import KnowledgeAdapter, KnowledgePage
5
+
6
+ _REGISTRY = {}
7
+
8
+ def register(name: str, cls):
9
+ _REGISTRY[name.lower()] = cls
10
+
11
+ def create_knowledge(name: str, config: Optional[dict] = None, **kwargs) -> KnowledgeAdapter:
12
+ """Factory: create a knowledge-base adapter by name.
13
+
14
+ 两种调用方式都支持:
15
+ create_knowledge("obsidian", path="~/my-vault") # 扁平参数
16
+ create_knowledge("obsidian", config={"path": "..."}) # 显式 config 字典
17
+ """
18
+ name = name.lower()
19
+ if name not in _REGISTRY:
20
+ available = list(_REGISTRY.keys())
21
+ raise ValueError(
22
+ f"Unknown knowledge base: '{name}'. Available: {available}"
23
+ )
24
+ if config is None:
25
+ config = dict(kwargs) if kwargs else None
26
+ elif kwargs:
27
+ config = {**config, **kwargs}
28
+ return _REGISTRY[name](config)
29
+
30
+
31
+ # 别名:core/knowledge.py 的文档里写的是 create_kb,保持一致(曾导致 ImportError)
32
+ create_kb = create_knowledge
33
+
34
+ def list_backends() -> list[str]:
35
+ return sorted(_REGISTRY.keys())
36
+
37
+ # Built-in adapters
38
+ from . import local as _local
39
+ register("local", _local.LocalKB)
40
+
41
+ from . import obsidian as _obsidian
42
+ register("obsidian", _obsidian.ObsidianKB)
@@ -0,0 +1,43 @@
1
+ """
2
+ knowflow/adapters/knowledge/local.py — Local directory KB
3
+ Simple markdown files in a directory.
4
+ """
5
+ from pathlib import Path
6
+ from knowflow.core.knowledge import KnowledgeAdapter, KnowledgePage, KnowledgeError
7
+
8
+
9
+ class LocalKB(KnowledgeAdapter):
10
+ """Local markdown directory knowledge base"""
11
+
12
+ def __init__(self, config: dict = None):
13
+ super().__init__(config)
14
+ path = config.get("path", "~/knowflow-output") if config else "~/knowflow-output"
15
+ self.root = Path(path).expanduser().resolve()
16
+ self.root.mkdir(parents=True, exist_ok=True)
17
+
18
+ def name(self) -> str:
19
+ return f"Local ({self.root})"
20
+
21
+ def sync(self, pages: list[KnowledgePage]) -> dict:
22
+ synced = 0
23
+ errors = []
24
+ for page in pages:
25
+ try:
26
+ target = self.root / page.path
27
+ target.parent.mkdir(parents=True, exist_ok=True)
28
+ target.write_text(page.content, encoding="utf-8")
29
+ synced += 1
30
+ except Exception as e:
31
+ errors.append(f"{page.path}: {e}")
32
+ return {"synced": synced, "skipped": 0, "errors": errors}
33
+
34
+ def search(self, query: str, limit: int = 10) -> list[KnowledgePage]:
35
+ results = []
36
+ for md_file in sorted(self.root.rglob("*.md"))[:limit]:
37
+ if query.lower() in md_file.read_text(encoding="utf-8").lower():
38
+ results.append(KnowledgePage(
39
+ title=md_file.stem,
40
+ content=md_file.read_text(encoding="utf-8"),
41
+ path=str(md_file.relative_to(self.root)),
42
+ ))
43
+ return results
@@ -0,0 +1,64 @@
1
+ """
2
+ knowflow/adapters/knowledge/obsidian.py — Obsidian vault KB
3
+ Syncs markdown pages to an Obsidian-compatible vault directory.
4
+ """
5
+ from pathlib import Path
6
+ from knowflow.core.knowledge import KnowledgeAdapter, KnowledgePage, KnowledgeError
7
+
8
+
9
+ class ObsidianKB(KnowledgeAdapter):
10
+ """Obsidian vault knowledge base"""
11
+
12
+ def __init__(self, config: dict = None):
13
+ super().__init__(config)
14
+ path = config.get("path", "~/obsidian-vault") if config else "~/obsidian-vault"
15
+ self.root = Path(path).expanduser().resolve()
16
+ self.root.mkdir(parents=True, exist_ok=True)
17
+ self._ensure_vault()
18
+
19
+ def _ensure_vault(self):
20
+ """Ensure .obsidian/ config exists for vault detection"""
21
+ obsidian_dir = self.root / ".obsidian"
22
+ obsidian_dir.mkdir(exist_ok=True)
23
+ app_file = obsidian_dir / "app.json"
24
+ if not app_file.exists():
25
+ import json
26
+ app_file.write_text(json.dumps({"promptDelete": False}, indent=2))
27
+
28
+ def name(self) -> str:
29
+ return f"Obsidian ({self.root})"
30
+
31
+ def sync(self, pages: list[KnowledgePage]) -> dict:
32
+ synced = 0
33
+ errors = []
34
+ for page in pages:
35
+ try:
36
+ target = self.root / page.path
37
+ target.parent.mkdir(parents=True, exist_ok=True)
38
+ # Add Obsidian frontmatter tags if provided
39
+ content = page.content
40
+ if page.tags:
41
+ tags_line = "\n".join(f" - {t}" for t in page.tags)
42
+ frontmatter = f"---\ntags:\n{tags_line}\n---\n\n"
43
+ if not content.startswith("---"):
44
+ content = frontmatter + content
45
+ target.write_text(content, encoding="utf-8")
46
+ synced += 1
47
+ except Exception as e:
48
+ errors.append(f"{page.path}: {e}")
49
+ return {"synced": synced, "skipped": 0, "errors": errors}
50
+
51
+ def search(self, query: str, limit: int = 10) -> list[KnowledgePage]:
52
+ results = []
53
+ for md_file in sorted(self.root.rglob("*.md"))[:limit]:
54
+ try:
55
+ content = md_file.read_text(encoding="utf-8")
56
+ if query.lower() in content.lower():
57
+ results.append(KnowledgePage(
58
+ title=md_file.stem,
59
+ content=content,
60
+ path=str(md_file.relative_to(self.root)),
61
+ ))
62
+ except Exception:
63
+ continue
64
+ return results
@@ -0,0 +1,62 @@
1
+ """knowflow/adapters/storage/__init__.py — Storage adapter registry"""
2
+ from typing import Optional
3
+
4
+ from knowflow.core.storage import StorageAdapter
5
+
6
+
7
+ # Registry: name → class
8
+ _REGISTRY = {}
9
+
10
+
11
+ def register(name: str, cls):
12
+ """Register a storage adapter"""
13
+ _REGISTRY[name.lower()] = cls
14
+
15
+
16
+ def create_storage(name: str, config: Optional[dict] = None, **kwargs) -> StorageAdapter:
17
+ """Factory: create a storage adapter by name.
18
+
19
+ 两种调用方式都支持:
20
+ create_storage("git", path="~/my-data") # 扁平参数(文档里的写法)
21
+ create_storage("git", config={"path": "..."}) # 显式 config 字典
22
+ """
23
+ name = name.lower()
24
+ if name not in _REGISTRY:
25
+ available = list(_REGISTRY.keys())
26
+ raise ValueError(
27
+ f"Unknown storage backend: '{name}'. "
28
+ f"Available: {available}"
29
+ )
30
+ if config is None:
31
+ config = dict(kwargs) if kwargs else None
32
+ elif kwargs:
33
+ config = {**config, **kwargs}
34
+ return _REGISTRY[name](config)
35
+
36
+
37
+ def list_backends() -> list[str]:
38
+ """List all registered storage backends"""
39
+ return sorted(_REGISTRY.keys())
40
+
41
+
42
+ # ─── Import and register built-in adapters ─────────────────
43
+ from . import local as _local_mod
44
+ register("local", _local_mod.LocalStorage)
45
+
46
+ from . import git as _git_mod
47
+ register("git", _git_mod.GitStorage)
48
+
49
+ # Optional adapters — import fails silently, available if deps installed
50
+ try:
51
+ from . import s3 as _s3_mod
52
+ register("s3", _s3_mod.S3Storage)
53
+ except ImportError:
54
+ pass
55
+
56
+ # Quark 夸克网盘 — 依赖 kuake_cli,无额外Python包
57
+ from . import quark as _quark_mod # noqa
58
+ register("quark", _quark_mod.QuarkStorage)
59
+
60
+ # WebDAV — 依赖 requests,已预装
61
+ from . import webdav as _webdav_mod # noqa
62
+ register("webdav", _webdav_mod.WebDAVStorage)
@@ -0,0 +1,155 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ knowflow/adapters/storage/git.py — Git-backed storage
5
+ ======================================================
6
+ Recommended backend: local files + full version history.
7
+
8
+ Config:
9
+ path: str (default: ~/knowflow-data)
10
+ """
11
+ from pathlib import Path
12
+ import subprocess
13
+ import os
14
+ import time
15
+ import json
16
+ from knowflow.core.storage import (
17
+ StorageAdapter, StorageItem, StorageError, StorageNotFoundError
18
+ )
19
+
20
+
21
+ class GitStorage(StorageAdapter):
22
+ """Git-backed storage with version history"""
23
+
24
+ def __init__(self, config: dict = None):
25
+ super().__init__(config)
26
+ root = config.get("path", "~/knowflow-data") if config else "~/knowflow-data"
27
+ self.root = Path(root).expanduser().resolve()
28
+ self.root.mkdir(parents=True, exist_ok=True)
29
+ self._git("init", check=False)
30
+ self._ensure_user()
31
+
32
+ def _resolve(self, path: str) -> Path:
33
+ clean = Path(path).as_posix().lstrip("/")
34
+ resolved = (self.root / clean).resolve()
35
+ if not str(resolved).startswith(str(self.root)):
36
+ raise StorageError(f"Path traversal blocked: {path}")
37
+ return resolved
38
+
39
+ def _git(self, *args, check: bool = True) -> str:
40
+ try:
41
+ result = subprocess.run(
42
+ ["git", "-C", str(self.root), *args],
43
+ capture_output=True, text=True, timeout=30
44
+ )
45
+ if check and result.returncode != 0:
46
+ raise StorageError(f"Git: {result.stderr.strip() or 'unknown error'}")
47
+ return result.stdout.strip()
48
+ except subprocess.TimeoutExpired:
49
+ raise StorageError("Git command timed out")
50
+
51
+ def _ensure_user(self):
52
+ if not self._git("config", "user.name", check=False):
53
+ self._git("config", "user.name", "KnowFlow")
54
+ self._git("config", "user.email", "knowflow@local")
55
+
56
+ def read(self, path: str) -> str:
57
+ p = self._resolve(path)
58
+ if not p.exists():
59
+ raise StorageNotFoundError(f"Not found: {path}")
60
+ return p.read_text(encoding="utf-8")
61
+
62
+ def read_binary(self, path: str) -> bytes:
63
+ p = self._resolve(path)
64
+ if not p.exists():
65
+ raise StorageNotFoundError(f"Not found: {path}")
66
+ return p.read_bytes()
67
+
68
+ def write(self, path: str, content: str) -> str:
69
+ p = self._resolve(path)
70
+ p.parent.mkdir(parents=True, exist_ok=True)
71
+ p.write_text(content, encoding="utf-8")
72
+ return self._git("add", str(p.relative_to(self.root)))
73
+
74
+ def write_binary(self, path: str, content: bytes) -> str:
75
+ p = self._resolve(path)
76
+ p.parent.mkdir(parents=True, exist_ok=True)
77
+ p.write_bytes(content)
78
+ return self._git("add", str(p.relative_to(self.root)))
79
+
80
+ def delete(self, path: str) -> bool:
81
+ p = self._resolve(path)
82
+ if not p.exists():
83
+ return False
84
+ if p.is_file():
85
+ p.unlink()
86
+ else:
87
+ # 与 LocalStorage 行为一致:目录递归删除
88
+ import shutil
89
+ shutil.rmtree(p)
90
+ self._git("add", "-A")
91
+ return True
92
+
93
+ def exists(self, path: str) -> bool:
94
+ try:
95
+ return self._resolve(path).exists()
96
+ except StorageError:
97
+ return False
98
+
99
+ def list(self, prefix: str = "") -> list[StorageItem]:
100
+ p = self._resolve(prefix) if prefix else self.root
101
+ if not p.exists():
102
+ return []
103
+ items = []
104
+ for child in sorted(p.iterdir()):
105
+ try:
106
+ stat = child.stat()
107
+ rel = child.relative_to(self.root).as_posix()
108
+ items.append(StorageItem(
109
+ path=rel, size=stat.st_size if child.is_file() else 0,
110
+ modified_at=stat.st_mtime, is_dir=child.is_dir(),
111
+ ))
112
+ except OSError:
113
+ continue
114
+ return items
115
+
116
+ def walk(self, prefix: str = "") -> list[StorageItem]:
117
+ p = self._resolve(prefix) if prefix else self.root
118
+ if not p.exists():
119
+ return []
120
+ items = []
121
+ for root_dir, dirs, files in os.walk(str(p)):
122
+ root_path = Path(root_dir)
123
+ for name in files:
124
+ child = root_path / name
125
+ try:
126
+ stat = child.stat()
127
+ rel = child.relative_to(self.root).as_posix()
128
+ items.append(StorageItem(path=rel, size=stat.st_size, modified_at=stat.st_mtime))
129
+ except OSError:
130
+ continue
131
+ return sorted(items, key=lambda x: x.path)
132
+
133
+ def commit(self, message: str = "") -> str:
134
+ self._git("add", "-A")
135
+ status = self._git("status", "--porcelain")
136
+ if not status:
137
+ return ""
138
+ if not message:
139
+ message = f"Auto-commit {time.strftime('%Y-%m-%d %H:%M:%S')}"
140
+ self._ensure_user()
141
+ self._git("commit", "-m", message)
142
+ return self._git("rev-parse", "HEAD")
143
+
144
+ def log(self, max_count: int = 10) -> list[dict]:
145
+ fmt = '{"id":"%H","message":"%s","timestamp":"%aI","author":"%an"}'
146
+ output = self._git("log", f"--max-count={max_count}", f"--format={fmt}", check=False)
147
+ if not output:
148
+ return []
149
+ return [json.loads(line) for line in output.split("\n") if line.strip()]
150
+
151
+ def diff(self, rev_a: str, rev_b: str, path: str = "") -> str:
152
+ args = ["diff", f"{rev_a}..{rev_b}"]
153
+ if path:
154
+ args.append("--", str(self._resolve(path)))
155
+ return self._git(*args, check=False)
@@ -0,0 +1,110 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ knowflow/adapters/storage/local.py — Local filesystem storage
5
+ ===============================================================
6
+ Simplest backend: plain directory on disk, no versioning.
7
+
8
+ Config:
9
+ path: str (default: ~/knowflow-data)
10
+ """
11
+ from pathlib import Path
12
+ import os
13
+ import time
14
+ from knowflow.core.storage import (
15
+ StorageAdapter, StorageItem, StorageError, StorageNotFoundError
16
+ )
17
+
18
+
19
+ class LocalStorage(StorageAdapter):
20
+ """Local filesystem storage backend"""
21
+
22
+ def __init__(self, config: dict = None):
23
+ super().__init__(config)
24
+ root = config.get("path", "~/knowflow-data") if config else "~/knowflow-data"
25
+ self.root = Path(root).expanduser().resolve()
26
+ self.root.mkdir(parents=True, exist_ok=True)
27
+
28
+ def _resolve(self, path: str) -> Path:
29
+ clean = Path(path).as_posix().lstrip("/")
30
+ resolved = (self.root / clean).resolve()
31
+ if not str(resolved).startswith(str(self.root)):
32
+ raise StorageError(f"Path traversal blocked: {path}")
33
+ return resolved
34
+
35
+ def read(self, path: str) -> str:
36
+ p = self._resolve(path)
37
+ if not p.exists():
38
+ raise StorageNotFoundError(f"Not found: {path}")
39
+ return p.read_text(encoding="utf-8")
40
+
41
+ def read_binary(self, path: str) -> bytes:
42
+ p = self._resolve(path)
43
+ if not p.exists():
44
+ raise StorageNotFoundError(f"Not found: {path}")
45
+ return p.read_bytes()
46
+
47
+ def write(self, path: str, content: str) -> str:
48
+ p = self._resolve(path)
49
+ p.parent.mkdir(parents=True, exist_ok=True)
50
+ p.write_text(content, encoding="utf-8")
51
+ return ""
52
+
53
+ def write_binary(self, path: str, content: bytes) -> str:
54
+ p = self._resolve(path)
55
+ p.parent.mkdir(parents=True, exist_ok=True)
56
+ p.write_bytes(content)
57
+ return ""
58
+
59
+ def delete(self, path: str) -> bool:
60
+ p = self._resolve(path)
61
+ if not p.exists():
62
+ return False
63
+ if p.is_file():
64
+ p.unlink()
65
+ else:
66
+ import shutil
67
+ shutil.rmtree(p)
68
+ return True
69
+
70
+ def exists(self, path: str) -> bool:
71
+ try:
72
+ return self._resolve(path).exists()
73
+ except StorageError:
74
+ return False
75
+
76
+ def list(self, prefix: str = "") -> list[StorageItem]:
77
+ p = self._resolve(prefix) if prefix else self.root
78
+ if not p.exists():
79
+ return []
80
+ items = []
81
+ for child in sorted(p.iterdir()):
82
+ try:
83
+ stat = child.stat()
84
+ rel = child.relative_to(self.root).as_posix()
85
+ items.append(StorageItem(
86
+ path=rel,
87
+ size=stat.st_size if child.is_file() else 0,
88
+ modified_at=stat.st_mtime,
89
+ is_dir=child.is_dir(),
90
+ ))
91
+ except OSError:
92
+ continue
93
+ return items
94
+
95
+ def walk(self, prefix: str = "") -> list[StorageItem]:
96
+ p = self._resolve(prefix) if prefix else self.root
97
+ if not p.exists():
98
+ return []
99
+ items = []
100
+ for root_dir, dirs, files in os.walk(str(p)):
101
+ root_path = Path(root_dir)
102
+ for name in files:
103
+ child = root_path / name
104
+ try:
105
+ stat = child.stat()
106
+ rel = child.relative_to(self.root).as_posix()
107
+ items.append(StorageItem(path=rel, size=stat.st_size, modified_at=stat.st_mtime))
108
+ except OSError:
109
+ continue
110
+ return sorted(items, key=lambda x: x.path)