open-knowledge-studio 0.2.3__tar.gz → 0.2.4__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.
- {open_knowledge_studio-0.2.3/open_knowledge_studio.egg-info → open_knowledge_studio-0.2.4}/PKG-INFO +11 -4
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/README.md +10 -3
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/claude/hooks/pre-compact.sh +11 -1
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/claude/hooks/session-start.sh +7 -1
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/cli.py +86 -21
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/config.py +49 -11
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/recall.py +2 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/store.py +9 -22
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4/open_knowledge_studio.egg-info}/PKG-INFO +11 -4
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/pyproject.toml +1 -1
- open_knowledge_studio-0.2.4/tests/test_init.py +90 -0
- open_knowledge_studio-0.2.3/tests/test_init.py +0 -50
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/LICENSE +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/MANIFEST.in +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/__init__.py +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/_meta/frontmatter-schema.md +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/_meta/learning-schema.json +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/_meta/raw-evidence-schema.md +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/claude/hooks/user-prompt-recall.py +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/claude/hooks/user-prompt-recall.sh +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/claude/hooks/validate-wiki-write.sh +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/claude/rules/raw-intake.md +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/claude/rules/wiki-writing.md +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/claude/settings.json +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/claude/skills/archive/SKILL.md +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/claude/skills/compile/SKILL.md +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/claude/skills/ingest/SKILL.md +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/claude/skills/lint/SKILL.md +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/claude/skills/promote/SKILL.md +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/claude/skills/query/SKILL.md +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/claude/skills/start/SKILL.md +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/claude/skills/status/SKILL.md +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/settings/handlers.json +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/settings/input-sources.json +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/settings/raw-tools.example.json +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/templates/anti-pattern.md +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/templates/concept.md +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/templates/draft.md +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/_assets/templates/strategy.md +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/distiller.py +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/health.py +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/knowledge_studio/metrics.py +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/open_knowledge_studio.egg-info/SOURCES.txt +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/open_knowledge_studio.egg-info/dependency_links.txt +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/open_knowledge_studio.egg-info/entry_points.txt +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/open_knowledge_studio.egg-info/requires.txt +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/open_knowledge_studio.egg-info/top_level.txt +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/setup.cfg +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/setup.py +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/tests/test_distiller.py +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/tests/test_health.py +0 -0
- {open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4}/tests/test_recall.py +0 -0
{open_knowledge_studio-0.2.3/open_knowledge_studio.egg-info → open_knowledge_studio-0.2.4}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: open-knowledge-studio
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: File-based knowledge engineering CLI for Claude Code
|
|
5
5
|
Author: open-agent-power
|
|
6
6
|
License: MIT
|
|
@@ -42,16 +42,23 @@ a file-based knowledge base that turns raw material into a recallable, self-deca
|
|
|
42
42
|
## Install
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
|
-
|
|
45
|
+
pipx install open-knowledge-studio && pipx ensurepath
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
We recommend pipx because modern Linux (Ubuntu 24.04+) and macOS Homebrew Pythons are
|
|
49
|
+
PEP 668 externally-managed, so a bare `pip install` fails. If your mirror lags behind
|
|
50
|
+
PyPI, add `--pip-args="-i https://pypi.org/simple"`.
|
|
51
|
+
|
|
48
52
|
Optional multimodal ingest (PDF / audio / video / formula extraction) lives in a
|
|
49
53
|
separate, heavier package that you can pull in on demand:
|
|
50
54
|
|
|
51
55
|
```bash
|
|
52
|
-
|
|
56
|
+
pipx inject open-knowledge-studio oks-connector
|
|
53
57
|
```
|
|
54
58
|
|
|
59
|
+
(pipx apps share one venv without a standalone pip executable — `pipx inject` is how
|
|
60
|
+
you add extras. In a plain venv, use `pip install "open-knowledge-studio[connector]"`.)
|
|
61
|
+
|
|
55
62
|
## What you get
|
|
56
63
|
|
|
57
64
|
- **6+1-factor recall engine** — token overlap, substring, topic trace, type boost,
|
|
@@ -67,7 +74,7 @@ orchestrate the pipeline around it.
|
|
|
67
74
|
## Quick start
|
|
68
75
|
|
|
69
76
|
```bash
|
|
70
|
-
|
|
77
|
+
pipx install open-knowledge-studio && pipx ensurepath # 1. install the CLI
|
|
71
78
|
oks init my-knowledge-base # 2. scaffold an instance (skills + buckets)
|
|
72
79
|
cd my-knowledge-base
|
|
73
80
|
oks status # 3. use it
|
|
@@ -8,16 +8,23 @@ a file-based knowledge base that turns raw material into a recallable, self-deca
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
|
|
11
|
+
pipx install open-knowledge-studio && pipx ensurepath
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
We recommend pipx because modern Linux (Ubuntu 24.04+) and macOS Homebrew Pythons are
|
|
15
|
+
PEP 668 externally-managed, so a bare `pip install` fails. If your mirror lags behind
|
|
16
|
+
PyPI, add `--pip-args="-i https://pypi.org/simple"`.
|
|
17
|
+
|
|
14
18
|
Optional multimodal ingest (PDF / audio / video / formula extraction) lives in a
|
|
15
19
|
separate, heavier package that you can pull in on demand:
|
|
16
20
|
|
|
17
21
|
```bash
|
|
18
|
-
|
|
22
|
+
pipx inject open-knowledge-studio oks-connector
|
|
19
23
|
```
|
|
20
24
|
|
|
25
|
+
(pipx apps share one venv without a standalone pip executable — `pipx inject` is how
|
|
26
|
+
you add extras. In a plain venv, use `pip install "open-knowledge-studio[connector]"`.)
|
|
27
|
+
|
|
21
28
|
## What you get
|
|
22
29
|
|
|
23
30
|
- **6+1-factor recall engine** — token overlap, substring, topic trace, type boost,
|
|
@@ -33,7 +40,7 @@ orchestrate the pipeline around it.
|
|
|
33
40
|
## Quick start
|
|
34
41
|
|
|
35
42
|
```bash
|
|
36
|
-
|
|
43
|
+
pipx install open-knowledge-studio && pipx ensurepath # 1. install the CLI
|
|
37
44
|
oks init my-knowledge-base # 2. scaffold an instance (skills + buckets)
|
|
38
45
|
cd my-knowledge-base
|
|
39
46
|
oks status # 3. use it
|
|
@@ -4,7 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
set -euo pipefail
|
|
6
6
|
|
|
7
|
-
REPO_ROOT="${OKS_ROOT
|
|
7
|
+
REPO_ROOT="${OKS_ROOT:-}"
|
|
8
|
+
if [ -z "$REPO_ROOT" ]; then
|
|
9
|
+
REPO_ROOT="$(python3 -c "import json,os;print(json.load(open(os.path.expanduser('~/.oks/config.json'))).get('knowledge_base_path',''))" 2>/dev/null || true)"
|
|
10
|
+
fi
|
|
11
|
+
if [ -z "$REPO_ROOT" ]; then
|
|
12
|
+
REPO_ROOT="$(pwd)"
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
# Only snapshot inside a real knowledge base — never litter other dirs.
|
|
16
|
+
[ -d "$REPO_ROOT/wiki" ] || exit 0
|
|
17
|
+
|
|
8
18
|
SNAPSHOT_DIR="$REPO_ROOT/.oks/snapshots"
|
|
9
19
|
mkdir -p "$SNAPSHOT_DIR"
|
|
10
20
|
|
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
set -euo pipefail
|
|
6
6
|
|
|
7
|
-
REPO_ROOT="${OKS_ROOT
|
|
7
|
+
REPO_ROOT="${OKS_ROOT:-}"
|
|
8
|
+
if [ -z "$REPO_ROOT" ]; then
|
|
9
|
+
REPO_ROOT="$(python3 -c "import json,os;print(json.load(open(os.path.expanduser('~/.oks/config.json'))).get('knowledge_base_path',''))" 2>/dev/null || true)"
|
|
10
|
+
fi
|
|
11
|
+
if [ -z "$REPO_ROOT" ]; then
|
|
12
|
+
REPO_ROOT="$(pwd)"
|
|
13
|
+
fi
|
|
8
14
|
|
|
9
15
|
if [ ! -d "$REPO_ROOT/wiki" ]; then
|
|
10
16
|
exit 0
|
|
@@ -475,7 +475,11 @@ def config_init(
|
|
|
475
475
|
"""Initialize global config at ~/.oks/config.json."""
|
|
476
476
|
from knowledge_studio.config import init_config
|
|
477
477
|
|
|
478
|
-
|
|
478
|
+
try:
|
|
479
|
+
path = init_config(kb_path)
|
|
480
|
+
except ValueError as e:
|
|
481
|
+
console.print(f"[red]{e}[/red]")
|
|
482
|
+
raise typer.Exit(1)
|
|
479
483
|
console.print(f"[green]Config created:[/green] {path}")
|
|
480
484
|
|
|
481
485
|
from knowledge_studio.config import load_config
|
|
@@ -526,7 +530,15 @@ def config_set(
|
|
|
526
530
|
target[k] = {}
|
|
527
531
|
target = target[k]
|
|
528
532
|
|
|
529
|
-
if
|
|
533
|
+
if key == "knowledge_base_path":
|
|
534
|
+
resolved = Path(value).expanduser().resolve()
|
|
535
|
+
if not resolved.is_dir():
|
|
536
|
+
console.print(
|
|
537
|
+
f"[yellow]Warning:[/yellow] directory does not exist: {resolved}"
|
|
538
|
+
)
|
|
539
|
+
value = str(resolved)
|
|
540
|
+
target[keys[-1]] = value
|
|
541
|
+
elif value.lower() in ("true", "false"):
|
|
530
542
|
target[keys[-1]] = value.lower() == "true"
|
|
531
543
|
elif value.isdigit():
|
|
532
544
|
target[keys[-1]] = int(value)
|
|
@@ -607,18 +619,17 @@ def _materialize_assets(root: Path, base: Path, is_packaged: bool, overwrite: bo
|
|
|
607
619
|
if not src.is_dir():
|
|
608
620
|
continue
|
|
609
621
|
dest = root / dest_name
|
|
610
|
-
if dest.exists():
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
shutil.copytree(src, dest)
|
|
622
|
+
if dest.exists() and not overwrite:
|
|
623
|
+
continue
|
|
624
|
+
# Merge-copy: refresh bundled files in place, keep user-owned files.
|
|
625
|
+
shutil.copytree(src, dest, dirs_exist_ok=True)
|
|
615
626
|
done.append(dest_name)
|
|
616
627
|
return done
|
|
617
628
|
|
|
618
629
|
|
|
619
630
|
@app.command()
|
|
620
631
|
def init(
|
|
621
|
-
path: str = typer.Argument(
|
|
632
|
+
path: str = typer.Argument(..., help="Target directory for the new knowledge instance"),
|
|
622
633
|
set_default: bool = typer.Option(
|
|
623
634
|
True, "--set-default/--no-set-default",
|
|
624
635
|
help="Register this folder as the active KB in ~/.oks/config.json",
|
|
@@ -630,6 +641,10 @@ def init(
|
|
|
630
641
|
False, "--upgrade",
|
|
631
642
|
help="Re-copy bundled assets (skills/templates/_meta/settings), overwriting them; your memory (wiki/drafts/profiles) is untouched",
|
|
632
643
|
),
|
|
644
|
+
force: bool = typer.Option(
|
|
645
|
+
False, "--force",
|
|
646
|
+
help="Scaffold into a non-empty directory that is not already a knowledge base",
|
|
647
|
+
),
|
|
633
648
|
):
|
|
634
649
|
"""Scaffold a new knowledge INSTANCE folder (e.g. your personal artboy-knowledge-studio).
|
|
635
650
|
|
|
@@ -639,6 +654,26 @@ def init(
|
|
|
639
654
|
from anywhere.
|
|
640
655
|
"""
|
|
641
656
|
root = Path(path).expanduser().resolve()
|
|
657
|
+
|
|
658
|
+
# Refuse to scaffold into an existing non-empty directory that is not
|
|
659
|
+
# already a KB (missing wiki/) — protects arbitrary folders from being
|
|
660
|
+
# hijacked. Re-running on an existing KB is idempotent and allowed.
|
|
661
|
+
if (
|
|
662
|
+
root.is_dir()
|
|
663
|
+
and any(root.iterdir())
|
|
664
|
+
and not (root / "wiki").is_dir()
|
|
665
|
+
and not force
|
|
666
|
+
):
|
|
667
|
+
console.print(
|
|
668
|
+
f"[red]Refusing to scaffold into non-empty directory:[/red] {root}\n"
|
|
669
|
+
f"It does not look like a knowledge base (no wiki/). Init would create:\n"
|
|
670
|
+
+ "\n".join(f" - {d}/" for d in _INSTANCE_DIRS)
|
|
671
|
+
+ "\n - .claude/ templates/ _meta/ settings/ (bundled assets)"
|
|
672
|
+
+ "\n - .gitignore"
|
|
673
|
+
+ "\n\nRe-run with [bold]--force[/bold] to proceed anyway."
|
|
674
|
+
)
|
|
675
|
+
raise typer.Exit(1)
|
|
676
|
+
|
|
642
677
|
root.mkdir(parents=True, exist_ok=True)
|
|
643
678
|
|
|
644
679
|
for d in _INSTANCE_DIRS:
|
|
@@ -693,7 +728,10 @@ def init(
|
|
|
693
728
|
|
|
694
729
|
# ── Optional editor hooks (opt-in auto-recall) ───────────────────
|
|
695
730
|
|
|
696
|
-
|
|
731
|
+
# Hook commands are written as absolute paths (see hook_install). Old
|
|
732
|
+
# installs wired the relative path below; matching is done by script name
|
|
733
|
+
# so both forms are recognized.
|
|
734
|
+
_RECALL_HOOK_SCRIPT_NAME = "user-prompt-recall.sh"
|
|
697
735
|
_RECALL_HOOK_SCRIPTS = ("user-prompt-recall.py", "user-prompt-recall.sh")
|
|
698
736
|
_HOOK_EDITORS = {
|
|
699
737
|
"claude": ".claude/settings.json",
|
|
@@ -709,9 +747,16 @@ def _instance_root(path: str | None) -> Path:
|
|
|
709
747
|
|
|
710
748
|
|
|
711
749
|
def _ensure_recall_scripts(root: Path) -> list[str]:
|
|
712
|
-
"""Copy the recall hook scripts
|
|
750
|
+
"""Copy/refresh the recall hook scripts in <root>/.claude/hooks/.
|
|
751
|
+
|
|
752
|
+
The .sh wrapper gets the current interpreter baked into its OKS_PYTHON
|
|
753
|
+
fallback. If an existing .sh lacks the current bake (fresh copy still on
|
|
754
|
+
`python3`, or baked against a stale interpreter), it is re-copied from
|
|
755
|
+
the asset source and re-baked. The .py engine is only copied if missing.
|
|
756
|
+
"""
|
|
713
757
|
import shutil
|
|
714
758
|
import stat
|
|
759
|
+
import sys
|
|
715
760
|
|
|
716
761
|
hooks_dir = root / ".claude" / "hooks"
|
|
717
762
|
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -721,20 +766,27 @@ def _ensure_recall_scripts(root: Path) -> list[str]:
|
|
|
721
766
|
if base is not None:
|
|
722
767
|
src_dir = base / ("claude/hooks" if is_packaged else ".claude/hooks")
|
|
723
768
|
|
|
769
|
+
baked = f'"${{OKS_PYTHON:-{sys.executable}}}"'
|
|
724
770
|
created: list[str] = []
|
|
725
771
|
for name in _RECALL_HOOK_SCRIPTS:
|
|
726
772
|
dest = hooks_dir / name
|
|
727
773
|
if dest.exists():
|
|
728
|
-
|
|
774
|
+
if not name.endswith(".sh"):
|
|
775
|
+
continue
|
|
776
|
+
try:
|
|
777
|
+
if baked in dest.read_text(encoding="utf-8"):
|
|
778
|
+
continue
|
|
779
|
+
except OSError:
|
|
780
|
+
pass
|
|
781
|
+
# Stale interpreter bake — fall through to re-copy + re-bake.
|
|
729
782
|
if src_dir is None or not (src_dir / name).is_file():
|
|
730
783
|
raise FileNotFoundError(
|
|
731
784
|
f"bundled hook script not found: {name} (asset source: {src_dir})"
|
|
732
785
|
)
|
|
733
786
|
shutil.copy2(src_dir / name, dest)
|
|
734
787
|
if name.endswith(".sh"):
|
|
735
|
-
import sys
|
|
736
788
|
text = dest.read_text(encoding="utf-8").replace(
|
|
737
|
-
'"${OKS_PYTHON:-python3}"',
|
|
789
|
+
'"${OKS_PYTHON:-python3}"', baked
|
|
738
790
|
)
|
|
739
791
|
dest.write_text(text, encoding="utf-8")
|
|
740
792
|
dest.chmod(dest.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
@@ -743,7 +795,11 @@ def _ensure_recall_scripts(root: Path) -> list[str]:
|
|
|
743
795
|
|
|
744
796
|
|
|
745
797
|
def _wire_userpromptsubmit(settings_path: Path, command: str) -> str:
|
|
746
|
-
"""Idempotently add a UserPromptSubmit command hook. Returns 'wired'|'exists'.
|
|
798
|
+
"""Idempotently add a UserPromptSubmit command hook. Returns 'wired'|'exists'.
|
|
799
|
+
|
|
800
|
+
Recognizes previously wired entries (old relative or stale absolute
|
|
801
|
+
paths) by script name and rewrites them in place instead of duplicating.
|
|
802
|
+
"""
|
|
747
803
|
data: dict = {}
|
|
748
804
|
if settings_path.exists():
|
|
749
805
|
try:
|
|
@@ -752,11 +808,18 @@ def _wire_userpromptsubmit(settings_path: Path, command: str) -> str:
|
|
|
752
808
|
raise ValueError(f"{settings_path} is not valid JSON: {e}") from e
|
|
753
809
|
hooks = data.setdefault("hooks", {})
|
|
754
810
|
ups = hooks.setdefault("UserPromptSubmit", [])
|
|
811
|
+
stale: dict | None = None
|
|
755
812
|
for group in ups:
|
|
756
813
|
for h in group.get("hooks", []):
|
|
757
|
-
|
|
814
|
+
cmd = h.get("command", "")
|
|
815
|
+
if cmd == command:
|
|
758
816
|
return "exists"
|
|
759
|
-
|
|
817
|
+
if cmd.endswith(_RECALL_HOOK_SCRIPT_NAME):
|
|
818
|
+
stale = h
|
|
819
|
+
if stale is not None:
|
|
820
|
+
stale["command"] = command
|
|
821
|
+
else:
|
|
822
|
+
ups.append({"hooks": [{"type": "command", "command": command}]})
|
|
760
823
|
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
761
824
|
settings_path.write_text(
|
|
762
825
|
json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
|
|
@@ -764,7 +827,7 @@ def _wire_userpromptsubmit(settings_path: Path, command: str) -> str:
|
|
|
764
827
|
return "wired"
|
|
765
828
|
|
|
766
829
|
|
|
767
|
-
def _hook_is_wired(settings_path: Path
|
|
830
|
+
def _hook_is_wired(settings_path: Path) -> bool:
|
|
768
831
|
if not settings_path.exists():
|
|
769
832
|
return False
|
|
770
833
|
try:
|
|
@@ -773,7 +836,7 @@ def _hook_is_wired(settings_path: Path, command: str) -> bool:
|
|
|
773
836
|
return False
|
|
774
837
|
for group in data.get("hooks", {}).get("UserPromptSubmit", []):
|
|
775
838
|
for h in group.get("hooks", []):
|
|
776
|
-
if h.get("command")
|
|
839
|
+
if h.get("command", "").endswith(_RECALL_HOOK_SCRIPT_NAME):
|
|
777
840
|
return True
|
|
778
841
|
return False
|
|
779
842
|
|
|
@@ -824,10 +887,11 @@ def hook_install(
|
|
|
824
887
|
if created:
|
|
825
888
|
console.print(f"[green]Installed hook script:[/green] {', '.join(created)}")
|
|
826
889
|
|
|
890
|
+
hook_cmd = (root / ".claude" / "hooks" / _RECALL_HOOK_SCRIPT_NAME).resolve().as_posix()
|
|
827
891
|
editors = ("claude", "qoder") if editor == "both" else (editor,)
|
|
828
892
|
for name in editors:
|
|
829
893
|
settings_path = root / _HOOK_EDITORS[name]
|
|
830
|
-
result = _wire_userpromptsubmit(settings_path,
|
|
894
|
+
result = _wire_userpromptsubmit(settings_path, hook_cmd)
|
|
831
895
|
label = "[green]wired[/green]" if result == "wired" else "[dim]already wired[/dim]"
|
|
832
896
|
console.print(f" {name}: {label} → {settings_path}")
|
|
833
897
|
|
|
@@ -860,11 +924,12 @@ def hook_status(
|
|
|
860
924
|
except (OSError, subprocess.TimeoutExpired):
|
|
861
925
|
ok = False
|
|
862
926
|
state = ("[green]importable[/green]" if ok
|
|
863
|
-
else "[red]
|
|
927
|
+
else "[red]hook script has stale interpreter — "
|
|
928
|
+
"run `oks hook install` to re-bake[/red]")
|
|
864
929
|
console.print(f" engine: {state} (python: {py})")
|
|
865
930
|
for name, rel in _HOOK_EDITORS.items():
|
|
866
931
|
settings_path = root / rel
|
|
867
|
-
wired = _hook_is_wired(settings_path
|
|
932
|
+
wired = _hook_is_wired(settings_path)
|
|
868
933
|
state = "[green]wired[/green]" if wired else "[dim]not wired[/dim]"
|
|
869
934
|
console.print(f" {name}: {state}")
|
|
870
935
|
|
|
@@ -90,37 +90,75 @@ def save_config(config: dict[str, Any]) -> None:
|
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
def init_config(kb_path: str | None = None) -> Path:
|
|
93
|
-
"""Initialize global config. Returns the config path.
|
|
93
|
+
"""Initialize global config. Returns the config path.
|
|
94
|
+
|
|
95
|
+
Raises ValueError when no kb_path is given and the existing config has
|
|
96
|
+
no knowledge_base_path — we never silently default to cwd.
|
|
97
|
+
"""
|
|
94
98
|
config = load_config()
|
|
95
99
|
|
|
96
100
|
if kb_path:
|
|
97
|
-
config["knowledge_base_path"] = kb_path
|
|
101
|
+
config["knowledge_base_path"] = str(Path(kb_path).expanduser().resolve())
|
|
98
102
|
elif not config.get("knowledge_base_path"):
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
except Exception:
|
|
103
|
-
config["knowledge_base_path"] = str(Path.cwd())
|
|
103
|
+
raise ValueError(
|
|
104
|
+
"knowledge_base_path required: pass --kb-path or run `oks init <path>`"
|
|
105
|
+
)
|
|
104
106
|
|
|
105
107
|
save_config(config)
|
|
106
108
|
return config_path()
|
|
107
109
|
|
|
108
110
|
|
|
111
|
+
# Warn at most once per process when the configured root lacks wiki/.
|
|
112
|
+
_warned_missing_wiki = False
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _warn_if_not_kb(root: Path) -> None:
|
|
116
|
+
global _warned_missing_wiki
|
|
117
|
+
if _warned_missing_wiki:
|
|
118
|
+
return
|
|
119
|
+
if not (root / "wiki").is_dir():
|
|
120
|
+
_warned_missing_wiki = True
|
|
121
|
+
import sys
|
|
122
|
+
print(
|
|
123
|
+
f"oks: warning: configured KB path {root} does not look like a "
|
|
124
|
+
f"knowledge base (missing wiki/); run `oks init <path>` or "
|
|
125
|
+
f"`oks config set knowledge_base_path <path>`",
|
|
126
|
+
file=sys.stderr,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
109
130
|
def get_kb_root() -> Path:
|
|
110
|
-
"""Get the knowledge base root path.
|
|
131
|
+
"""Get the knowledge base root path (single source of truth).
|
|
111
132
|
|
|
112
133
|
Priority:
|
|
113
134
|
1. OKS_ROOT env var
|
|
114
135
|
2. ~/.oks/config.json → knowledge_base_path
|
|
115
136
|
3. Current working directory
|
|
137
|
+
|
|
138
|
+
A corrupt config warns on stderr and falls back to cwd instead of
|
|
139
|
+
raising, matching the historical store.repo_root() behavior.
|
|
116
140
|
"""
|
|
117
141
|
env_root = os.environ.get("OKS_ROOT")
|
|
118
142
|
if env_root:
|
|
119
|
-
|
|
143
|
+
root = Path(env_root).expanduser().resolve()
|
|
144
|
+
_warn_if_not_kb(root)
|
|
145
|
+
return root
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
config = load_config()
|
|
149
|
+
except Exception as e:
|
|
150
|
+
import sys
|
|
151
|
+
print(
|
|
152
|
+
f"oks: warning: could not read {config_path()} ({e}); "
|
|
153
|
+
f"falling back to current directory as KB root",
|
|
154
|
+
file=sys.stderr,
|
|
155
|
+
)
|
|
156
|
+
return Path.cwd()
|
|
120
157
|
|
|
121
|
-
config = load_config()
|
|
122
158
|
kb_path = config.get("knowledge_base_path")
|
|
123
159
|
if kb_path:
|
|
124
|
-
|
|
160
|
+
root = Path(kb_path).expanduser().resolve()
|
|
161
|
+
_warn_if_not_kb(root)
|
|
162
|
+
return root
|
|
125
163
|
|
|
126
164
|
return Path.cwd()
|
|
@@ -221,6 +221,8 @@ def _tokenize(text: str) -> set[str]:
|
|
|
221
221
|
try:
|
|
222
222
|
import jieba
|
|
223
223
|
|
|
224
|
+
import logging as _logging
|
|
225
|
+
jieba.setLogLevel(_logging.WARNING) # silence "Building prefix dict" chatter
|
|
224
226
|
raw_words = list(jieba.cut_for_search(text))
|
|
225
227
|
except Exception:
|
|
226
228
|
raw_words = text.split()
|
|
@@ -35,23 +35,10 @@ DEFAULT_CONFIG: dict = {
|
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
def repo_root() -> Path:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return Path(env_root)
|
|
41
|
-
try:
|
|
42
|
-
from knowledge_studio.config import load_config
|
|
38
|
+
"""Thin delegate — config.get_kb_root() is the single root resolver."""
|
|
39
|
+
from knowledge_studio.config import get_kb_root
|
|
43
40
|
|
|
44
|
-
|
|
45
|
-
if kb_path:
|
|
46
|
-
return Path(kb_path)
|
|
47
|
-
except Exception as e:
|
|
48
|
-
import sys
|
|
49
|
-
print(
|
|
50
|
-
f"oks: warning: could not read ~/.oks/config.json ({e}); "
|
|
51
|
-
f"falling back to current directory as KB root",
|
|
52
|
-
file=sys.stderr,
|
|
53
|
-
)
|
|
54
|
-
return Path(os.getcwd())
|
|
41
|
+
return get_kb_root()
|
|
55
42
|
|
|
56
43
|
|
|
57
44
|
def wiki_dir() -> Path:
|
|
@@ -108,9 +95,9 @@ def load_active_goals() -> list[dict]:
|
|
|
108
95
|
|
|
109
96
|
|
|
110
97
|
def _access_log_path() -> Path:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return
|
|
98
|
+
# Read-only: no mkdir here. Writers go through _atomic_write, which
|
|
99
|
+
# creates the parent directory.
|
|
100
|
+
return repo_root() / ".oks" / "access.json"
|
|
114
101
|
|
|
115
102
|
|
|
116
103
|
def _load_access_counts() -> dict[str, int]:
|
|
@@ -255,9 +242,9 @@ def _fingerprint(content: str) -> str:
|
|
|
255
242
|
|
|
256
243
|
|
|
257
244
|
def _fingerprint_index_path() -> Path:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
return
|
|
245
|
+
# Read-only: no mkdir here. Writers go through _atomic_write, which
|
|
246
|
+
# creates the parent directory.
|
|
247
|
+
return repo_root() / ".oks" / "fingerprints.json"
|
|
261
248
|
|
|
262
249
|
|
|
263
250
|
def _load_fingerprint_index() -> dict[str, str]:
|
{open_knowledge_studio-0.2.3 → open_knowledge_studio-0.2.4/open_knowledge_studio.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: open-knowledge-studio
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: File-based knowledge engineering CLI for Claude Code
|
|
5
5
|
Author: open-agent-power
|
|
6
6
|
License: MIT
|
|
@@ -42,16 +42,23 @@ a file-based knowledge base that turns raw material into a recallable, self-deca
|
|
|
42
42
|
## Install
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
|
-
|
|
45
|
+
pipx install open-knowledge-studio && pipx ensurepath
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
We recommend pipx because modern Linux (Ubuntu 24.04+) and macOS Homebrew Pythons are
|
|
49
|
+
PEP 668 externally-managed, so a bare `pip install` fails. If your mirror lags behind
|
|
50
|
+
PyPI, add `--pip-args="-i https://pypi.org/simple"`.
|
|
51
|
+
|
|
48
52
|
Optional multimodal ingest (PDF / audio / video / formula extraction) lives in a
|
|
49
53
|
separate, heavier package that you can pull in on demand:
|
|
50
54
|
|
|
51
55
|
```bash
|
|
52
|
-
|
|
56
|
+
pipx inject open-knowledge-studio oks-connector
|
|
53
57
|
```
|
|
54
58
|
|
|
59
|
+
(pipx apps share one venv without a standalone pip executable — `pipx inject` is how
|
|
60
|
+
you add extras. In a plain venv, use `pip install "open-knowledge-studio[connector]"`.)
|
|
61
|
+
|
|
55
62
|
## What you get
|
|
56
63
|
|
|
57
64
|
- **6+1-factor recall engine** — token overlap, substring, topic trace, type boost,
|
|
@@ -67,7 +74,7 @@ orchestrate the pipeline around it.
|
|
|
67
74
|
## Quick start
|
|
68
75
|
|
|
69
76
|
```bash
|
|
70
|
-
|
|
77
|
+
pipx install open-knowledge-studio && pipx ensurepath # 1. install the CLI
|
|
71
78
|
oks init my-knowledge-base # 2. scaffold an instance (skills + buckets)
|
|
72
79
|
cd my-knowledge-base
|
|
73
80
|
oks status # 3. use it
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Tests for `oks init` — instance scaffolding + shareable-asset materialization."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from typer.testing import CliRunner
|
|
5
|
+
|
|
6
|
+
from knowledge_studio.cli import app
|
|
7
|
+
|
|
8
|
+
runner = CliRunner()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_init_scaffolds_buckets_and_data_gitignore(tmp_path):
|
|
12
|
+
target = tmp_path / "kb"
|
|
13
|
+
result = runner.invoke(app, ["init", str(target), "--no-git", "--no-set-default"])
|
|
14
|
+
assert result.exit_code == 0, result.output
|
|
15
|
+
|
|
16
|
+
for d in ["wiki", "drafts", "raw", "profiles/goals", "settings", "_meta", "templates"]:
|
|
17
|
+
assert (target / d).is_dir(), f"missing bucket {d}"
|
|
18
|
+
|
|
19
|
+
gi = (target / ".gitignore").read_text(encoding="utf-8")
|
|
20
|
+
# instance gitignore ignores only per-machine state, and TRACKS memory
|
|
21
|
+
assert ".oks/" in gi
|
|
22
|
+
assert "wiki/**/*.md" not in gi
|
|
23
|
+
assert "drafts/*.md" not in gi
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_init_materializes_shareable_assets(tmp_path):
|
|
27
|
+
target = tmp_path / "kb"
|
|
28
|
+
result = runner.invoke(app, ["init", str(target), "--no-git", "--no-set-default"])
|
|
29
|
+
assert result.exit_code == 0, result.output
|
|
30
|
+
|
|
31
|
+
# skills + templates arrive so the Claude Code experience works out of the box
|
|
32
|
+
assert (target / ".claude" / "skills" / "ingest").is_dir()
|
|
33
|
+
assert (target / ".claude" / "settings.json").is_file()
|
|
34
|
+
assert (target / "templates").is_dir()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_init_upgrade_refreshes_assets_but_keeps_user_files(tmp_path):
|
|
38
|
+
target = tmp_path / "kb"
|
|
39
|
+
runner.invoke(app, ["init", str(target), "--no-git", "--no-set-default"])
|
|
40
|
+
|
|
41
|
+
marker = target / ".claude" / "MARKER.txt"
|
|
42
|
+
marker.write_text("local edit", encoding="utf-8")
|
|
43
|
+
|
|
44
|
+
bundled = target / ".claude" / "settings.json"
|
|
45
|
+
original = bundled.read_text(encoding="utf-8")
|
|
46
|
+
bundled.write_text("{}", encoding="utf-8")
|
|
47
|
+
|
|
48
|
+
# re-init without --upgrade keeps existing assets untouched
|
|
49
|
+
runner.invoke(app, ["init", str(target), "--no-git", "--no-set-default"])
|
|
50
|
+
assert marker.exists()
|
|
51
|
+
assert bundled.read_text(encoding="utf-8") == "{}"
|
|
52
|
+
|
|
53
|
+
# --upgrade merge-copies bundled assets: bundled files refreshed,
|
|
54
|
+
# user-owned files (marker) survive — no more whole-tree deletion
|
|
55
|
+
runner.invoke(app, ["init", str(target), "--no-git", "--no-set-default", "--upgrade"])
|
|
56
|
+
assert marker.exists()
|
|
57
|
+
assert bundled.read_text(encoding="utf-8") == original
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_init_requires_path_argument():
|
|
61
|
+
result = runner.invoke(app, ["init"])
|
|
62
|
+
assert result.exit_code != 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_init_aborts_on_nonempty_non_kb_dir(tmp_path):
|
|
66
|
+
target = tmp_path / "documents"
|
|
67
|
+
target.mkdir()
|
|
68
|
+
(target / "important.txt").write_text("do not touch", encoding="utf-8")
|
|
69
|
+
|
|
70
|
+
result = runner.invoke(app, ["init", str(target), "--no-git", "--no-set-default"])
|
|
71
|
+
assert result.exit_code == 1
|
|
72
|
+
assert not (target / "wiki").exists()
|
|
73
|
+
|
|
74
|
+
# --force overrides the guard
|
|
75
|
+
result = runner.invoke(
|
|
76
|
+
app, ["init", str(target), "--no-git", "--no-set-default", "--force"]
|
|
77
|
+
)
|
|
78
|
+
assert result.exit_code == 0, result.output
|
|
79
|
+
assert (target / "wiki").is_dir()
|
|
80
|
+
assert (target / "important.txt").exists()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_init_rerun_on_existing_kb_is_idempotent(tmp_path):
|
|
84
|
+
target = tmp_path / "kb"
|
|
85
|
+
result = runner.invoke(app, ["init", str(target), "--no-git", "--no-set-default"])
|
|
86
|
+
assert result.exit_code == 0, result.output
|
|
87
|
+
|
|
88
|
+
# target now contains wiki/ → treated as an existing KB, no --force needed
|
|
89
|
+
result = runner.invoke(app, ["init", str(target), "--no-git", "--no-set-default"])
|
|
90
|
+
assert result.exit_code == 0, result.output
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
"""Tests for `oks init` — instance scaffolding + shareable-asset materialization."""
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
from typer.testing import CliRunner
|
|
5
|
-
|
|
6
|
-
from knowledge_studio.cli import app
|
|
7
|
-
|
|
8
|
-
runner = CliRunner()
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def test_init_scaffolds_buckets_and_data_gitignore(tmp_path):
|
|
12
|
-
target = tmp_path / "kb"
|
|
13
|
-
result = runner.invoke(app, ["init", str(target), "--no-git", "--no-set-default"])
|
|
14
|
-
assert result.exit_code == 0, result.output
|
|
15
|
-
|
|
16
|
-
for d in ["wiki", "drafts", "raw", "profiles/goals", "settings", "_meta", "templates"]:
|
|
17
|
-
assert (target / d).is_dir(), f"missing bucket {d}"
|
|
18
|
-
|
|
19
|
-
gi = (target / ".gitignore").read_text(encoding="utf-8")
|
|
20
|
-
# instance gitignore ignores only per-machine state, and TRACKS memory
|
|
21
|
-
assert ".oks/" in gi
|
|
22
|
-
assert "wiki/**/*.md" not in gi
|
|
23
|
-
assert "drafts/*.md" not in gi
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def test_init_materializes_shareable_assets(tmp_path):
|
|
27
|
-
target = tmp_path / "kb"
|
|
28
|
-
result = runner.invoke(app, ["init", str(target), "--no-git", "--no-set-default"])
|
|
29
|
-
assert result.exit_code == 0, result.output
|
|
30
|
-
|
|
31
|
-
# skills + templates arrive so the Claude Code experience works out of the box
|
|
32
|
-
assert (target / ".claude" / "skills" / "ingest").is_dir()
|
|
33
|
-
assert (target / ".claude" / "settings.json").is_file()
|
|
34
|
-
assert (target / "templates").is_dir()
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def test_init_upgrade_refreshes_assets(tmp_path):
|
|
38
|
-
target = tmp_path / "kb"
|
|
39
|
-
runner.invoke(app, ["init", str(target), "--no-git", "--no-set-default"])
|
|
40
|
-
|
|
41
|
-
marker = target / ".claude" / "MARKER.txt"
|
|
42
|
-
marker.write_text("local edit", encoding="utf-8")
|
|
43
|
-
|
|
44
|
-
# re-init without --upgrade keeps existing assets (marker survives)
|
|
45
|
-
runner.invoke(app, ["init", str(target), "--no-git", "--no-set-default"])
|
|
46
|
-
assert marker.exists()
|
|
47
|
-
|
|
48
|
-
# --upgrade re-copies bundled assets, dropping the local marker
|
|
49
|
-
runner.invoke(app, ["init", str(target), "--no-git", "--no-set-default", "--upgrade"])
|
|
50
|
-
assert not marker.exists()
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|