intentic-ike 0.2.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.
ike/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """intentic Knowledge Engine — context-efficient knowledge serving for AI agents."""
2
+
3
+ __version__ = "0.1.0"
ike/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from ike.cli import cli
2
+
3
+ cli()
ike/cli.py ADDED
@@ -0,0 +1,56 @@
1
+ """ike CLI — dynamic command loading from ike/commands/.
2
+
3
+ Each *_cmd.py in ike/commands/ exports a Click command as `cmd`.
4
+ Commands are auto-discovered at CLI startup.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import importlib
10
+ from pathlib import Path
11
+
12
+ import click
13
+
14
+ from .core.engine import Engine
15
+
16
+
17
+ class DynamicGroup(click.Group):
18
+ """Click group that auto-discovers commands from ike/commands/*_cmd.py."""
19
+
20
+ def __init__(self, *args, **kwargs):
21
+ super().__init__(*args, **kwargs)
22
+ self._load_commands()
23
+
24
+ def _load_commands(self) -> None:
25
+ commands_dir = Path(__file__).parent / "commands"
26
+ if not commands_dir.exists():
27
+ return
28
+ for f in sorted(commands_dir.glob("*_cmd.py")):
29
+ name = f.stem.removesuffix("_cmd").replace("_", "-")
30
+ try:
31
+ mod = importlib.import_module(f"ike.commands.{f.stem}")
32
+ if hasattr(mod, "cmd"):
33
+ self.add_command(mod.cmd, name)
34
+ except Exception:
35
+ pass # Graceful: skip broken commands
36
+
37
+
38
+ @click.group(cls=DynamicGroup)
39
+ @click.version_option(package_name="intentic-ike")
40
+ @click.option(
41
+ "--kb-root",
42
+ type=click.Path(),
43
+ default=None,
44
+ envvar="IKE_KB_ROOT",
45
+ help="KB root directory (default: ~/intentic-kb)",
46
+ )
47
+ @click.pass_context
48
+ def cli(ctx: click.Context, kb_root: str | None) -> None:
49
+ """ike — intentic Knowledge Engine."""
50
+ ctx.ensure_object(dict)
51
+ kb = Path(kb_root).expanduser() if kb_root else None
52
+ ctx.obj["kb_root"] = kb
53
+ try:
54
+ ctx.obj["engine"] = Engine(kb_root=kb)
55
+ except Exception:
56
+ ctx.obj["engine"] = None
@@ -0,0 +1,2 @@
1
+ # ike/commands/ — Dynamic CLI command modules.
2
+ # Each *_cmd.py must export a Click command as `cmd`.
@@ -0,0 +1,33 @@
1
+ """ike doctor — auto-fix missing frontmatter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import click
8
+
9
+
10
+ @click.command()
11
+ @click.option("--yes", "auto_fix", is_flag=True, help="Apply all fixes without prompting")
12
+ @click.option(
13
+ "--kb-root",
14
+ type=click.Path(exists=True),
15
+ default=None,
16
+ help="KB root (defaults to --kb-root from parent group or IKE_KB_ROOT)",
17
+ )
18
+ @click.pass_context
19
+ def cmd(ctx: click.Context, auto_fix: bool, kb_root: str | None) -> None:
20
+ """Auto-fix missing frontmatter in KB documents."""
21
+ from ike.core.doctor import run_doctor
22
+
23
+ if kb_root:
24
+ kb = Path(kb_root).resolve()
25
+ else:
26
+ kb = ctx.obj.get("kb_root")
27
+ if kb is None:
28
+ kb = Path.home() / "intentic-kb"
29
+
30
+ fixed = run_doctor(kb, auto_fix=auto_fix)
31
+ click.echo(f"\nFixed {fixed} file(s).")
32
+ if fixed > 0:
33
+ click.echo("Run `ike lint` to verify.")
@@ -0,0 +1,22 @@
1
+ """Load KB content (entire file or specific section)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+
8
+ @click.command()
9
+ @click.argument("file_path")
10
+ @click.option("--section", default=None, help="Section ID to fetch")
11
+ @click.pass_context
12
+ def cmd(ctx: click.Context, file_path: str, section: str | None) -> None:
13
+ """Load KB content (entire file or specific section)."""
14
+ try:
15
+ result = ctx.obj["engine"].fetch(file_path, section)
16
+ click.echo(result.content)
17
+ except KeyError as e:
18
+ click.echo(f"Error: {e}", err=True)
19
+ raise SystemExit(1)
20
+ except FileNotFoundError:
21
+ click.echo(f"Error: File not found: {file_path}", err=True)
22
+ raise SystemExit(1)
@@ -0,0 +1,19 @@
1
+ """Flag a section as stale."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+
8
+ @click.command("flag-stale")
9
+ @click.argument("file_path")
10
+ @click.option("--section", required=True, help="Section ID to flag")
11
+ @click.option("--reason", required=True, help="Why the section is stale")
12
+ @click.pass_context
13
+ def cmd(ctx: click.Context, file_path: str, section: str, reason: str) -> None:
14
+ """Flag a section as stale."""
15
+ result = ctx.obj["engine"].flag_stale(file_path, section, reason)
16
+ if not result.success:
17
+ click.echo(f"Error: {result.error}", err=True)
18
+ raise SystemExit(1)
19
+ click.echo(f"Flagged {file_path}#{section} as stale: {reason}")
@@ -0,0 +1,19 @@
1
+ """Build or rebuild the search index."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+
8
+ @click.command()
9
+ @click.option("--rebuild", is_flag=True, help="Full rebuild (drop + reindex)")
10
+ @click.pass_context
11
+ def cmd(ctx: click.Context, rebuild: bool) -> None:
12
+ """Build or rebuild the search index."""
13
+ engine = ctx.obj["engine"]
14
+ if rebuild:
15
+ n = engine.rebuild_index()
16
+ click.echo(f"Index rebuilt: {n} chunks indexed.")
17
+ else:
18
+ engine.index.ensure_fresh(engine.parser)
19
+ click.echo("Index is fresh.")
@@ -0,0 +1,37 @@
1
+ """ike init — bootstrap ike for a repository."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import click
8
+
9
+
10
+ @click.command()
11
+ @click.option(
12
+ "--kb-root",
13
+ type=click.Path(exists=True),
14
+ required=True,
15
+ help="Path to the knowledge base directory",
16
+ )
17
+ @click.pass_context
18
+ def cmd(ctx: click.Context, kb_root: str) -> None:
19
+ """Bootstrap ike for a repository. Scans docs and generates discovery artifacts."""
20
+ from ike.core.init import run_init
21
+
22
+ kb = Path(kb_root).resolve()
23
+ cwd = Path.cwd()
24
+
25
+ result = run_init(kb, cwd)
26
+
27
+ click.echo(f"Found {result['total']} markdown files in {kb}")
28
+ click.echo()
29
+ click.echo("Quality Report:")
30
+ click.echo(f" {result['ready']} ready")
31
+ click.echo(f" {result['fixable']} fixable (run `ike doctor` to fix)")
32
+ click.echo(f" {result['needs_review']} need review")
33
+ click.echo()
34
+ for a in result["artifacts"]:
35
+ click.echo(f"Created: {a}")
36
+ click.echo()
37
+ click.echo("Restart your AI tool to activate ike MCP.")
@@ -0,0 +1,19 @@
1
+ """Run consistency checks on the KB."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+
8
+ @click.command()
9
+ @click.option("--freshness", type=int, default=None, help="Flag docs older than N days")
10
+ @click.pass_context
11
+ def cmd(ctx: click.Context, freshness: int | None) -> None:
12
+ """Run consistency checks on the KB."""
13
+ findings = ctx.obj["engine"].lint(freshness)
14
+ if not findings:
15
+ click.echo("No issues found.")
16
+ return
17
+ for f in findings:
18
+ click.echo(f"[{f.severity}] {f.file_path}: {f.check} — {f.message}")
19
+ click.echo(f"\n{len(findings)} issue(s) found.")
@@ -0,0 +1,17 @@
1
+ """List all KB documents and sections."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+
8
+ @click.command("list")
9
+ @click.option("--type", "knowledge_type", default=None, help="Filter by type")
10
+ @click.pass_context
11
+ def cmd(ctx: click.Context, knowledge_type: str | None) -> None:
12
+ """List all KB documents and sections."""
13
+ chunks = ctx.obj["engine"].list_docs(knowledge_type)
14
+ for c in chunks:
15
+ section = f"#{c.section_id}" if c.section_id else ""
16
+ click.echo(f"{c.file_path}{section} ({c.token_count} tokens)")
17
+ click.echo(f"\n{len(chunks)} section(s).")
@@ -0,0 +1,30 @@
1
+ """ike migrate-mcp — migrate .mcp.json to portable pattern."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import click
9
+
10
+
11
+ @click.command("migrate-mcp")
12
+ @click.option("--dry-run", is_flag=True, help="Show changes without applying")
13
+ def cmd(dry_run: bool) -> None:
14
+ """Migrate .mcp.json from absolute paths to portable uvx/pipx pattern."""
15
+ from ike.core.migrate import run_migrate_mcp
16
+
17
+ result = run_migrate_mcp(Path.cwd(), dry_run=dry_run)
18
+
19
+ if not result["changed"]:
20
+ click.echo(f"No migration needed: {result.get('reason', 'unknown')}")
21
+ return
22
+
23
+ if dry_run:
24
+ click.echo("DRY RUN — would change:")
25
+ click.echo(f" Old: {json.dumps(result['old'], indent=2)}")
26
+ click.echo(f" New: {json.dumps(result['new'], indent=2)}")
27
+ else:
28
+ click.echo("Migrated .mcp.json:")
29
+ click.echo(f" Old command: {result['old'].get('command')}")
30
+ click.echo(f" New command: {result['new'].get('command')}")
@@ -0,0 +1,20 @@
1
+ """Route + fetch in one step."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+
8
+ @click.command()
9
+ @click.argument("query_text")
10
+ @click.option("--depth", type=click.Choice(["shallow", "deep"]), default="shallow")
11
+ @click.option("--limit", default=5)
12
+ @click.pass_context
13
+ def cmd(ctx: click.Context, query_text: str, depth: str, limit: int) -> None:
14
+ """Route + fetch in one step."""
15
+ results = ctx.obj["engine"].query_and_fetch(query_text, depth, limit)
16
+ for r in results:
17
+ section_label = f"#{r.section_id}" if r.section_id else ""
18
+ click.echo(f"--- {r.file_path}{section_label} ({r.token_count} tokens) ---")
19
+ click.echo(r.content)
20
+ click.echo()
@@ -0,0 +1,19 @@
1
+ """Route a query to relevant KB sections."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import asdict
7
+
8
+ import click
9
+
10
+
11
+ @click.command()
12
+ @click.argument("query_text")
13
+ @click.option("--limit", default=10, help="Max results")
14
+ @click.option("--type", "knowledge_type", default=None, help="Filter by type")
15
+ @click.pass_context
16
+ def cmd(ctx: click.Context, query_text: str, limit: int, knowledge_type: str | None) -> None:
17
+ """Find relevant KB sections. Returns paths + token counts."""
18
+ result = ctx.obj["engine"].route(query_text, limit, knowledge_type)
19
+ click.echo(json.dumps(asdict(result), indent=2))
@@ -0,0 +1,17 @@
1
+ """ike serve — start the MCP server.
2
+
3
+ CRITICAL: No top-level imports of fastmcp or ike.mcp_server.
4
+ CRITICAL: No print(), sys.stdout.write(), or click.echo() — stdout is for JSON-RPC.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import click
10
+
11
+
12
+ @click.command()
13
+ def cmd() -> None:
14
+ """Start the ike MCP server (stdio transport)."""
15
+ from ike.core.serve import run_serve
16
+
17
+ run_serve()
@@ -0,0 +1,38 @@
1
+ """Modify KB content. Content is read from stdin."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ import click
8
+
9
+
10
+ @click.command("write")
11
+ @click.option("--file", "file_path", required=True, help="File to modify")
12
+ @click.option("--mode", type=click.Choice(["append", "replace"]), required=True)
13
+ @click.option("--section", default=None, help="Section ID (required for replace)")
14
+ @click.option("--agent-id", default="manual", help="Agent identifier for git commit")
15
+ @click.option("--no-commit", is_flag=True, help="Skip git commit")
16
+ @click.pass_context
17
+ def cmd(
18
+ ctx: click.Context,
19
+ file_path: str,
20
+ mode: str,
21
+ section: str | None,
22
+ agent_id: str,
23
+ no_commit: bool,
24
+ ) -> None:
25
+ """Modify KB content. Content is read from stdin."""
26
+ content = sys.stdin.read()
27
+ if not content.strip():
28
+ click.echo("Error: No content provided via stdin", err=True)
29
+ raise SystemExit(1)
30
+
31
+ result = ctx.obj["engine"].write(
32
+ file_path, content, mode, section, agent_id, not no_commit
33
+ )
34
+ if not result.success:
35
+ click.echo(f"Error: {result.error}", err=True)
36
+ raise SystemExit(1)
37
+
38
+ click.echo(f"Written to {result.file_path} (mode={result.mode}, committed={result.committed})")
ike/core/__init__.py ADDED
File without changes
ike/core/artifacts.py ADDED
@@ -0,0 +1,146 @@
1
+ """Generate discovery artifacts for AI tools (.mcp.json, AGENTS.md, etc.)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import shutil
7
+ from pathlib import Path
8
+
9
+
10
+ def _detect_runner() -> tuple[str, list[str]]:
11
+ """Detect best available runner: uvx > pipx > python3 -m."""
12
+ if shutil.which("uvx"):
13
+ return "uvx", ["--from", "intentic-ike[mcp]", "ike", "serve"]
14
+ if shutil.which("pipx"):
15
+ return "pipx", ["run", "intentic-ike[mcp]", "serve"]
16
+ return "ike", ["serve"]
17
+
18
+
19
+ def generate_mcp_json(target: Path) -> Path:
20
+ """Generate or merge ike entry into .mcp.json."""
21
+ mcp_path = target / ".mcp.json"
22
+ runner_cmd, runner_args = _detect_runner()
23
+
24
+ ike_entry = {
25
+ "command": runner_cmd,
26
+ "args": runner_args,
27
+ }
28
+
29
+ if mcp_path.exists():
30
+ data = json.loads(mcp_path.read_text(encoding="utf-8"))
31
+ else:
32
+ data = {}
33
+
34
+ data.setdefault("mcpServers", {})
35
+ data["mcpServers"]["ike"] = ike_entry
36
+
37
+ mcp_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
38
+ return mcp_path
39
+
40
+
41
+ def generate_agents_md(kb_root: Path, target: Path) -> Path:
42
+ """Generate AGENTS.md for AI tool discovery."""
43
+ agents_path = target / "AGENTS.md"
44
+ kb_name = kb_root.name
45
+
46
+ content = f"""# ike — intentic Knowledge Engine
47
+
48
+ This repository uses **ike** to make its knowledge base queryable by AI tools.
49
+ ike indexes the `{kb_name}/` directory and serves content via 2-step retrieval.
50
+
51
+ ## How to query (CLI)
52
+
53
+ **IMPORTANT:** `ike route` returns file paths relative to the KB root. Use those
54
+ EXACT paths with `ike fetch` — do NOT prepend the KB directory name.
55
+
56
+ ```bash
57
+ # Step 1: Find relevant sections
58
+ ike route "your question"
59
+ # Returns: {{"file_path": "architecture/auth.md", "section_id": "rate-limiting", ...}}
60
+
61
+ # Step 2: Fetch using the EXACT file_path from route results
62
+ ike fetch architecture/auth.md --section rate-limiting
63
+
64
+ # Or combine both steps:
65
+ ike query "your question" --depth deep
66
+ ```
67
+
68
+ ### All CLI commands
69
+
70
+ | Command | Purpose |
71
+ |---------|---------|
72
+ | `ike route "query"` | Find relevant sections (returns paths + token counts) |
73
+ | `ike fetch <path>` | Load entire file (path from route results) |
74
+ | `ike fetch <path> --section <id>` | Load specific section |
75
+ | `ike query "text" --depth deep` | Route + fetch combined |
76
+ | `ike lint` | Check KB consistency |
77
+ | `ike list` | List all indexed sections with paths |
78
+
79
+ ## How to query (MCP)
80
+
81
+ For MCP-capable tools (Claude Code, Cursor, Windsurf, Zed), the server is
82
+ configured in `.mcp.json`. Available tools:
83
+
84
+ | Tool | Description |
85
+ |------|-------------|
86
+ | `route(query, limit)` | Find relevant sections (~100 tokens) |
87
+ | `fetch(file_path, section_id)` | Load content (use paths from route) |
88
+ | `query(query_text, depth)` | Route + fetch in one step |
89
+ | `lint(freshness_days)` | Check KB health |
90
+
91
+ ## Example workflow
92
+
93
+ ```bash
94
+ $ ike route "authentication"
95
+ architecture/auth.md#api-keys (score: 3, 120 tokens)
96
+ architecture/auth.md#oauth2-flow (score: 2, 150 tokens)
97
+ architecture/auth.md#rate-limiting (score: 1, 80 tokens)
98
+
99
+ $ ike fetch architecture/auth.md --section rate-limiting
100
+ ## Rate Limiting
101
+ | Tier | Requests/min | Burst |
102
+ ...
103
+ ```
104
+
105
+ ## Knowledge Base
106
+
107
+ - **Root:** `{kb_name}/`
108
+ - **Paths:** All file_paths in ike are relative to this root
109
+ - **Format:** Markdown with YAML frontmatter (title, domain, summary)
110
+ """
111
+ agents_path.write_text(content, encoding="utf-8")
112
+ return agents_path
113
+
114
+
115
+ def generate_cursorrules(target: Path) -> Path:
116
+ """Generate or append ike snippet to .cursorrules."""
117
+ rules_path = target / ".cursorrules"
118
+
119
+ snippet = """
120
+ # ike Knowledge Engine
121
+ # Use `ike route "query"` to find relevant KB sections, then `ike fetch <path>` to load content.
122
+ # MCP tools are also available if configured in .mcp.json.
123
+ """
124
+
125
+ if rules_path.exists():
126
+ existing = rules_path.read_text(encoding="utf-8")
127
+ if "ike" not in existing:
128
+ rules_path.write_text(existing.rstrip() + "\n" + snippet, encoding="utf-8")
129
+ else:
130
+ rules_path.write_text(snippet.lstrip(), encoding="utf-8")
131
+ return rules_path
132
+
133
+
134
+ def generate_llms_txt(kb_root: Path, target: Path) -> Path:
135
+ """Generate llms.txt listing KB structure."""
136
+ llms_path = target / "llms.txt"
137
+ lines = [f"# {kb_root.name} Knowledge Base", "", "## Documents", ""]
138
+
139
+ for md in sorted(kb_root.rglob("*.md")):
140
+ if md.name.startswith("."):
141
+ continue
142
+ rel = md.relative_to(kb_root)
143
+ lines.append(f"- {rel}")
144
+
145
+ llms_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
146
+ return llms_path