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
@@ -0,0 +1,265 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ KnowFlow CLI — Agent Knowledge Pipeline
4
+
5
+ Usage:
6
+ knowflow init <template> [--dir DIR] [--storage git|local] [--kb obsidian|local]
7
+ knowflow run [--plugin NAME]
8
+ knowflow status
9
+ knowflow commit [MESSAGE]
10
+ knowflow archive [--dry-run]
11
+ knowflow list
12
+ knowflow version
13
+ """
14
+ import sys
15
+ import os
16
+ from pathlib import Path
17
+
18
+ def main():
19
+ args = sys.argv[1:]
20
+ if not args or args[0] in ("-h", "--help"):
21
+ print(__doc__.strip())
22
+ return
23
+
24
+ cmd = args[0]
25
+
26
+ if cmd == "version":
27
+ from knowflow import __version__
28
+ print(f"KnowFlow v{__version__}")
29
+
30
+ elif cmd == "list":
31
+ from knowflow.cli.template_manager import list_templates
32
+ templates = list_templates()
33
+ print(f"\nšŸ“¦ Available templates ({len(templates)}):\n")
34
+ for t in templates:
35
+ print(f" {t['display']:25s} {t['description']}")
36
+ print()
37
+
38
+ elif cmd == "init":
39
+ _init(args[1:])
40
+
41
+ elif cmd == "run":
42
+ _run(args[1:])
43
+
44
+ elif cmd == "status":
45
+ _status()
46
+
47
+ elif cmd == "commit":
48
+ msg = " ".join(args[1:]) if len(args) > 1 else ""
49
+ _commit(msg)
50
+
51
+ elif cmd == "archive":
52
+ dry_run = "--dry-run" in args
53
+ _archive(dry_run)
54
+
55
+ elif cmd == "dashboard":
56
+ _dashboard(args[1:])
57
+
58
+ else:
59
+ print(f"Unknown command: {cmd}")
60
+ print(__doc__.strip())
61
+
62
+
63
+ def _init(args):
64
+ from knowflow.cli.template_manager import init_project, list_templates
65
+
66
+ template = args[0] if args else None
67
+ output_dir = "."
68
+ storage = "git"
69
+ kb = "local"
70
+
71
+ for i, a in enumerate(args):
72
+ if a == "--dir" and i + 1 < len(args): output_dir = args[i + 1]
73
+ if a == "--storage" and i + 1 < len(args): storage = args[i + 1]
74
+ if a == "--kb" and i + 1 < len(args): kb = args[i + 1]
75
+
76
+ if not template:
77
+ print("āŒ Please specify a template. Available:")
78
+ for t in list_templates():
79
+ print(f" {t['name']:25s} {t['description']}")
80
+ return
81
+
82
+ try:
83
+ files = init_project(template, output_dir, storage, kb)
84
+ print(f"\nāœ… Created project '{template}' in {output_dir}/")
85
+ for f in files:
86
+ print(f" šŸ“„ {f}")
87
+ print(f"\n Next: cd {output_dir} && knowflow run")
88
+ except ValueError as e:
89
+ print(f"āŒ {e}")
90
+
91
+
92
+ def _run(args):
93
+ from knowflow.core.pipeline import PipelineRunner
94
+ from knowflow.transforms import MarkdownExport, ProfileIndex
95
+ from knowflow.adapters.storage import create_storage
96
+
97
+ # Detect project root
98
+ root = Path.cwd()
99
+ config_path = root / "knowflow.toml"
100
+ if not config_path.exists():
101
+ print("āŒ No knowflow.toml found. Run 'knowflow init' first.")
102
+ return
103
+
104
+ import yaml
105
+ with open(config_path) as f:
106
+ config = yaml.safe_load(f)
107
+
108
+ # Parse args
109
+ plugin_name = None
110
+ dry_run = "--dry-run" in args
111
+ verbose = "--verbose" in args
112
+ for i, a in enumerate(args):
113
+ if a == "--plugin" and i + 1 < len(args):
114
+ plugin_name = args[i + 1]
115
+
116
+ # Create storage
117
+ storage_cfg = config.get("storage", {"backend": "git", "path": str(root)})
118
+ store = create_storage(storage_cfg["backend"], config=storage_cfg)
119
+
120
+ # Setup pipeline
121
+ runner = PipelineRunner(config)
122
+ runner.register(MarkdownExport())
123
+ runner.register(ProfileIndex())
124
+
125
+ data_dir = root / "data" if (root / "data").exists() else root
126
+ output_dir = root / "output"
127
+ output_dir.mkdir(exist_ok=True)
128
+
129
+ if plugin_name:
130
+ results = [runner.run_plugin(plugin_name, store, data_dir, output_dir, dry_run, verbose)]
131
+ else:
132
+ plugins = [p.stem for p in sorted(data_dir.iterdir()) if p.is_dir() and p.name not in ("output", ".git", ".github")]
133
+ if not plugins:
134
+ # Fall back to the data directory itself as a single plugin
135
+ plugins = [root.name]
136
+ results = runner.run_all(plugins, store, data_dir, output_dir, dry_run, verbose)
137
+
138
+ for r in results:
139
+ icon = "āœ…" if r["success"] else "āŒ"
140
+ print(f" {icon} {r['plugin']}")
141
+ for sname, sres in r["stages"].items():
142
+ status = sres.get("status", "?")
143
+ if status == "ok":
144
+ gen = sres.get("generated", 0)
145
+ print(f" ā”œā”€ {sname}: {gen} files generated" if gen else f" ā”œā”€ {sname}: āœ…")
146
+ elif status == "skipped":
147
+ print(f" ā”œā”€ {sname}: ā­ļø {sres.get('reason', '')}")
148
+
149
+
150
+ def _status():
151
+ from knowflow.adapters.storage import create_storage
152
+
153
+ root = Path.cwd()
154
+ config_path = root / "knowflow.toml"
155
+ backend = "git"
156
+ if config_path.exists():
157
+ import yaml
158
+ with open(config_path) as f:
159
+ cfg = yaml.safe_load(f)
160
+ backend = cfg.get("storage", {}).get("backend", "git")
161
+
162
+ try:
163
+ store = create_storage(backend, config={"path": str(root)})
164
+ items = store.walk() if hasattr(store, 'walk') else []
165
+ commits = store.log(5) if hasattr(store, 'log') else []
166
+
167
+ print(f"\nšŸ“Š KnowFlow Status")
168
+ print(f" Root: {root}")
169
+ print(f" Backend: {backend}")
170
+ print(f" Files: {len(items)}")
171
+ if commits:
172
+ print(f" Last commits:")
173
+ for c in commits:
174
+ short = c['id'][:8]
175
+ msg = c['message'][:50]
176
+ print(f" {short} {msg}")
177
+ else:
178
+ print(f" (no version history)")
179
+ print()
180
+ except Exception as e:
181
+ print(f"āŒ Status error: {e}")
182
+
183
+
184
+ def _commit(message: str = ""):
185
+ from knowflow.adapters.storage import create_storage
186
+ root = Path.cwd()
187
+ store = create_storage("git", config={"path": str(root)})
188
+ rev = store.commit(message)
189
+ if rev:
190
+ print(f"āœ… Committed: {rev[:8]} — {message or 'auto'}")
191
+ else:
192
+ print(" (nothing to commit)")
193
+
194
+
195
+ def _archive(dry_run: bool = False):
196
+ from knowflow.transforms.archive import ArchiveStage
197
+ from knowflow.core.pipeline import PipelineContext
198
+
199
+ root = Path.cwd()
200
+ data_dir = root / "data"
201
+ if not data_dir.exists():
202
+ print("āŒ No data/ directory found")
203
+ return
204
+
205
+ stage = ArchiveStage()
206
+ for plugin_dir in sorted(data_dir.iterdir()):
207
+ if plugin_dir.is_dir():
208
+ ctx = PipelineContext(
209
+ storage=None, plugin_name=plugin_dir.name,
210
+ plugin_config={}, data_dir=plugin_dir,
211
+ output_dir=root / "output" / plugin_dir.name,
212
+ dry_run=dry_run,
213
+ )
214
+ result = stage.process(ctx)
215
+ if dry_run:
216
+ print(f" {plugin_dir}: {result.get('candidates', 0)} files to archive")
217
+ else:
218
+ print(f" {plugin_dir}: {result.get('archives', 0)} archives, {result.get('freed_kb', 0)}KB freed")
219
+
220
+
221
+ def _dashboard(args):
222
+ """Generate web dashboard from knowledge data."""
223
+ from knowflow.dashboard import generate
224
+
225
+ root = Path.cwd()
226
+ knowledge_dir = root
227
+ auto_detect = True
228
+ password = None
229
+
230
+ for i, a in enumerate(args):
231
+ if a == "--dir" and i + 1 < len(args):
232
+ knowledge_dir = Path(args[i + 1])
233
+ auto_detect = False
234
+ if a == "--password" and i + 1 < len(args):
235
+ password = args[i + 1]
236
+
237
+ if auto_detect and not (knowledge_dir / "MANIFEST.yaml").exists():
238
+ home_kb = Path.home() / "knowledge"
239
+ if home_kb.exists():
240
+ knowledge_dir = home_kb
241
+ else:
242
+ print("āŒ No knowledge directory found. Specify: knowflow dashboard --dir /path/to/knowledge")
243
+ return
244
+
245
+ output = knowledge_dir / "dashboard"
246
+ for i, a in enumerate(args):
247
+ if a == "--output" and i + 1 < len(args):
248
+ output = Path(args[i + 1])
249
+
250
+ if not password:
251
+ import getpass
252
+ password = getpass.getpass("č®¾ē½®ä»Ŗč”Øē›˜åÆ†ē : ")
253
+
254
+ try:
255
+ path = generate(knowledge_dir, output, password=password)
256
+ print(f" Open: file://{path.absolute()}")
257
+ print(f" Host with: cd {output} && python3 -m http.server 8080")
258
+ except Exception as e:
259
+ print(f"āŒ Dashboard generation failed: {e}")
260
+ import traceback
261
+ traceback.print_exc()
262
+
263
+
264
+ if __name__ == "__main__":
265
+ main()
@@ -0,0 +1,100 @@
1
+ """
2
+ knowflow/cli/template_manager.py — Template system
3
+ ====================================================
4
+ Init new projects from built-in templates.
5
+
6
+ Usage:
7
+ knowflow init football # Create football prediction project
8
+ knowflow list # List available templates
9
+ """
10
+ from pathlib import Path
11
+ import shutil
12
+ import yaml
13
+
14
+ TEMPLATES_DIR = Path(__file__).resolve().parent.parent / "templates"
15
+
16
+
17
+ def list_templates() -> list[dict]:
18
+ """List all available templates with descriptions"""
19
+ templates = []
20
+ for tdir in sorted(TEMPLATES_DIR.iterdir()):
21
+ if tdir.is_dir():
22
+ cfg_path = tdir / "plugin.yaml"
23
+ if cfg_path.exists():
24
+ with open(cfg_path) as f:
25
+ cfg = yaml.safe_load(f) or {}
26
+ templates.append({
27
+ "name": tdir.name,
28
+ "display": cfg.get("display_name", tdir.name),
29
+ "description": cfg.get("description", ""),
30
+ })
31
+ return templates
32
+
33
+
34
+ def init_project(template_name: str, output_dir: str, storage_backend: str = "git",
35
+ kb_backend: str = "local") -> list[str]:
36
+ """
37
+ Initialize a new project from a template.
38
+
39
+ Args:
40
+ template_name: Name of template (e.g. "football-prediction")
41
+ output_dir: Where to create the project
42
+ storage_backend: Storage type (git, local, s3, etc.)
43
+ kb_backend: Knowledge base type (obsidian, local, notion, etc.)
44
+
45
+ Returns:
46
+ List of created files (relative to output_dir)
47
+ """
48
+ src = TEMPLATES_DIR / template_name
49
+ if not src.exists():
50
+ raise ValueError(f"Template '{template_name}' not found. "
51
+ f"Available: {[t['name'] for t in list_templates()]}")
52
+
53
+ dst = Path(output_dir).expanduser().resolve()
54
+ dst.mkdir(parents=True, exist_ok=True)
55
+
56
+ created = []
57
+
58
+ # Copy plugin config
59
+ if (src / "plugin.yaml").exists():
60
+ shutil.copy2(src / "plugin.yaml", dst / "plugin.yaml")
61
+ created.append("plugin.yaml")
62
+
63
+ # Create data directory
64
+ plugin_name = template_name.split("-")[0]
65
+ (dst / plugin_name).mkdir(parents=True, exist_ok=True)
66
+ created.append(f"{plugin_name}/")
67
+
68
+ # Create knowflow.toml config
69
+ config = {
70
+ "project": {"name": template_name, "version": "0.1.0"},
71
+ "storage": {"backend": storage_backend, "path": str(dst)},
72
+ "knowledge": {"backend": kb_backend, "path": str(dst / "output")},
73
+ "pipeline": ["markdown_export", "profile_index"],
74
+ }
75
+ config_path = dst / "knowflow.toml"
76
+ config_path.write_text(yaml.dump(config, default_flow_style=False), encoding="utf-8")
77
+ created.append("knowflow.toml")
78
+
79
+ # Create README
80
+ readme = f"""# {template_name}
81
+
82
+ Generated from KnowFlow template: {template_name}
83
+
84
+ ## Usage
85
+
86
+ ```bash
87
+ # Add data
88
+ echo '{{"key": "value"}}' > {plugin_name}/data/my-data.json
89
+
90
+ # Run pipeline
91
+ knowflow run
92
+
93
+ # View output
94
+ ls {plugin_name}/output/
95
+ ```
96
+ """
97
+ (dst / "README.md").write_text(readme, encoding="utf-8")
98
+ created.append("README.md")
99
+
100
+ return created
@@ -0,0 +1,10 @@
1
+ """knowflow/core/__init__.py"""
2
+ from .storage import StorageAdapter, StorageItem, StorageError, StorageNotFoundError
3
+ from .knowledge import KnowledgeAdapter, KnowledgePage, KnowledgeError
4
+ from .pipeline import TransformStage, PipelineRunner, PipelineContext, PipelineError
5
+
6
+ __all__ = [
7
+ 'StorageAdapter', 'StorageItem', 'StorageError', 'StorageNotFoundError',
8
+ 'KnowledgeAdapter', 'KnowledgePage', 'KnowledgeError',
9
+ 'TransformStage', 'PipelineRunner', 'PipelineContext', 'PipelineError',
10
+ ]
@@ -0,0 +1,85 @@
1
+ """
2
+ knowflow/core/knowledge.py — Knowledge Base Adapter Interface
3
+ ==============================================================
4
+ Knowledge Base = HOW humans read and interact with the data.
5
+ Different from Storage (where data lives).
6
+
7
+ Built-in adapters:
8
+ - local: Plain markdown directory
9
+ - obsidian: Obsidian vault (markdown + .obsidian config)
10
+ - notion: Notion database (via API)
11
+ - ima: 腾讯IMAēŸ„čÆ†åŗ“ (via OpenAPI)
12
+ - mkdocs: MkDocs static site
13
+ - gitbook: GitBook-style documentation
14
+
15
+ Usage:
16
+ from knowflow.adapters.knowledge import create_knowledge, create_kb # create_kb äøŗåˆ«å
17
+ kb = create_knowledge("obsidian", path="~/my-vault")
18
+ kb.sync([KnowledgePage(title="å‘ØęŠ„", content="# å‘ØęŠ„\\n…", path="weekly/2026-10-02.md")])
19
+ kb.search("é˜…čÆ»ä¹ ęƒÆ", limit=5)
20
+ """
21
+
22
+ from abc import ABC, abstractmethod
23
+ from dataclasses import dataclass
24
+ from typing import Optional
25
+
26
+
27
+ class KnowledgeError(Exception): pass
28
+
29
+
30
+ @dataclass
31
+ class KnowledgePage:
32
+ """A page/document in the knowledge base"""
33
+ title: str
34
+ content: str # Markdown content
35
+ path: str # Relative path within KB
36
+ tags: list[str] = None
37
+ metadata: dict = None
38
+
39
+
40
+ class KnowledgeAdapter(ABC):
41
+ """
42
+ Knowledge Base Adapter Interface
43
+
44
+ Transforms structured data from Storage into human-readable
45
+ documents in the target knowledge base system.
46
+
47
+ A single data source can sync to multiple KBs simultaneously
48
+ (e.g., Push to Obsidian + Notion + IMA at the same time).
49
+ """
50
+
51
+ def __init__(self, config: dict = None):
52
+ self.config = config or {}
53
+
54
+ @abstractmethod
55
+ def name(self) -> str:
56
+ """Human-readable name of this KB backend"""
57
+ ...
58
+
59
+ @abstractmethod
60
+ def sync(self, pages: list[KnowledgePage]) -> dict:
61
+ """
62
+ Sync pages to the knowledge base.
63
+
64
+ Args:
65
+ pages: List of KnowledgePage objects to sync
66
+
67
+ Returns:
68
+ {"synced": 5, "skipped": 2, "errors": [...]}
69
+ """
70
+ ...
71
+
72
+ @abstractmethod
73
+ def search(self, query: str, limit: int = 10) -> list[KnowledgePage]:
74
+ """
75
+ Search the knowledge base.
76
+ Returns matching pages.
77
+ """
78
+ ...
79
+
80
+ def validate_config(self) -> list[str]:
81
+ """
82
+ Check if configuration is valid.
83
+ Returns list of error messages (empty = valid).
84
+ """
85
+ return []
@@ -0,0 +1,119 @@
1
+ """
2
+ knowflow/core/pipeline.py — Data Pipeline Framework
3
+ ====================================================
4
+ Transforms raw data (JSON) → formatted output (MD/docs).
5
+ Each transform is a pluggable stage.
6
+
7
+ Built-in stages:
8
+ - markdown_export: JSON → Markdown
9
+ - profile_index: Auto-generate PROFILE.md
10
+ - archive: Hot → Warm → Cold lifecycle
11
+ - token_compress: TokenJuice-style compression
12
+ - validate_schema: Validate against plugin schema
13
+ """
14
+
15
+ from abc import ABC, abstractmethod
16
+ from dataclasses import dataclass, field
17
+ from pathlib import Path
18
+ import importlib
19
+ import inspect
20
+ import yaml
21
+
22
+
23
+ @dataclass
24
+ class PipelineContext:
25
+ """Context passed through each pipeline stage"""
26
+ storage: object # StorageAdapter instance
27
+ plugin_name: str # e.g. "football"
28
+ plugin_config: dict # Parsed plugin.yaml
29
+ data_dir: Path # Where data files live
30
+ output_dir: Path # Where output goes (obsidian/)
31
+ dry_run: bool = False
32
+ verbose: bool = False
33
+
34
+
35
+ class PipelineError(Exception): pass
36
+
37
+
38
+ class TransformStage(ABC):
39
+ """
40
+ A single stage in the data pipeline.
41
+
42
+ Stages are loaded from knowflow.toml [pipeline] config.
43
+ To add a custom stage: subclass TransformStage and register.
44
+ """
45
+
46
+ def __init__(self, name: str):
47
+ self.name = name
48
+
49
+ @abstractmethod
50
+ def process(self, ctx: PipelineContext) -> dict:
51
+ """
52
+ Execute this stage.
53
+ Returns: {"status": "ok"|"skipped"|"error", "detail": "..."}
54
+ """
55
+ ...
56
+
57
+ def should_run(self, ctx: PipelineContext) -> bool:
58
+ """Override to add conditional execution."""
59
+ return True
60
+
61
+
62
+ class PipelineRunner:
63
+ """Orchestrates pipeline execution"""
64
+
65
+ def __init__(self, config: dict):
66
+ self.config = config
67
+ self.stages: dict[str, TransformStage] = {}
68
+
69
+ def register(self, stage: TransformStage):
70
+ """Register a pipeline stage"""
71
+ self.stages[stage.name] = stage
72
+
73
+ def load_plugin_config(self, plugin_dir: Path) -> dict:
74
+ """Load plugin.yaml from a plugin directory"""
75
+ path = plugin_dir / "plugin.yaml"
76
+ if path.exists():
77
+ with open(path) as f:
78
+ return yaml.safe_load(f) or {}
79
+ return {}
80
+
81
+ def run_plugin(self, plugin_name: str, storage, data_dir: Path, output_dir: Path,
82
+ dry_run: bool = False, verbose: bool = False) -> dict:
83
+ """Run pipeline for one plugin"""
84
+ result = {"plugin": plugin_name, "stages": {}, "success": True}
85
+
86
+ ctx = PipelineContext(
87
+ storage=storage,
88
+ plugin_name=plugin_name,
89
+ plugin_config={},
90
+ data_dir=data_dir / plugin_name,
91
+ output_dir=output_dir / plugin_name,
92
+ dry_run=dry_run,
93
+ verbose=verbose,
94
+ )
95
+
96
+ for stage_name, stage in self.stages.items():
97
+ if not stage.should_run(ctx):
98
+ result["stages"][stage_name] = {"status": "skipped"}
99
+ continue
100
+
101
+ try:
102
+ stage_result = stage.process(ctx)
103
+ result["stages"][stage_name] = stage_result
104
+ if stage_result.get("status") == "error":
105
+ result["success"] = False
106
+ except Exception as e:
107
+ result["stages"][stage_name] = {"status": "error", "detail": str(e)}
108
+ result["success"] = False
109
+
110
+ return result
111
+
112
+ def run_all(self, plugins: list, storage, data_dir: Path, output_dir: Path,
113
+ dry_run: bool = False, verbose: bool = False) -> list[dict]:
114
+ """Run pipeline for all plugins"""
115
+ results = []
116
+ for plugin in plugins:
117
+ result = self.run_plugin(plugin, storage, data_dir, output_dir, dry_run, verbose)
118
+ results.append(result)
119
+ return results