ki-manager 2.3.0.dev3__tar.gz → 2.3.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.
Files changed (32) hide show
  1. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/.gitignore +3 -0
  2. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/CHANGELOG.md +28 -1
  3. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/PKG-INFO +1 -1
  4. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/pyproject.toml +1 -1
  5. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/src/ki_manager/__init__.py +1 -1
  6. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/src/ki_manager/cli.py +6 -3
  7. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/src/ki_manager/scripts/analyze_module.py +3 -0
  8. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/src/ki_manager/scripts/audit_coverage.py +1 -1
  9. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/src/ki_manager/scripts/check_ki_drift.py +1 -1
  10. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/src/ki_manager/scripts/finalize_ki_scaffolds.py +1 -1
  11. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/src/ki_manager/scripts/find_unmapped_files.py +2 -0
  12. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/src/ki_manager/scripts/generate_ki_scaffolds.py +1 -1
  13. ki_manager-2.3.1/src/ki_manager/scripts/git_diff_secured.py +81 -0
  14. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/src/ki_manager/scripts/ki_search.py +1 -1
  15. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/src/ki_manager/scripts/ki_utils.py +44 -2
  16. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/src/ki_manager/scripts/knowledge_engine.py +5 -4
  17. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/src/ki_manager/scripts/sync_agents_md.py +8 -4
  18. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/src/ki_manager/server.py +329 -143
  19. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/src/ki_manager/tools/facade.py +45 -1
  20. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/src/ki_manager/tools/scaffold.py +7 -6
  21. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/src/ki_manager/workflows/expand-knowledge.md +4 -4
  22. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/src/ki_manager/workflows/sync-knowledge.md +2 -2
  23. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/LICENSE +0 -0
  24. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/README.md +0 -0
  25. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/src/ki_manager/scripts/add_ki_to_config.py +0 -0
  26. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/src/ki_manager/scripts/generate_dir_index.py +0 -0
  27. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/src/ki_manager/scripts/ki_dependency_analyzer.py +0 -0
  28. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/src/ki_manager/scripts/ki_graph_visualize.py +0 -0
  29. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/src/ki_manager/templates/KI_template.md +0 -0
  30. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/src/ki_manager/tools/__init__.py +0 -0
  31. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/src/ki_manager/workflows/create-adr.md +0 -0
  32. {ki_manager-2.3.0.dev3 → ki_manager-2.3.1}/src/ki_manager/workflows/scaffold-knowledge.md +0 -0
@@ -29,3 +29,6 @@ posts/
29
29
 
30
30
  # Private plans (moved to KI-enterprise)
31
31
  plans/
32
+
33
+ # Agent/MCP state
34
+ .serena/
@@ -9,7 +9,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
- ## [2.3.0.dev3] — 2026-10-09
12
+ ## [2.3.1] — 2026-10-18
13
+
14
+ ### Security & Hardening
15
+ - **Path Traversal Protection**: Enforced strict directory boundary containment across all file discovery and inspection tools (`find_unmapped_files`, `analyze_module`, `ki_search`, and custom agent paths) to prevent prefix-based sandbox traversal attacks.
16
+ - **Secured Git Operations (`git_diff_secured`)**: Added hardened diff inspection tool protecting against flag injection (`--output`, etc.), shell command chaining, and repository boundary escapes.
17
+ - **Restricted File Modifications**: Extended write protection to block direct modifications of shell/executable scripts (`.sh`, `.bash`, `.zsh`, `.js`, `.ts`, `.vbs`, `.wsf`) and internal system state files (`doc_config.json`, `ki_config.json`, `config.json`, `doc_state.json`) via direct file editing tools.
18
+
19
+ ### Changed
20
+ - **Unified Tool Dispatch Schema**: In compact facade mode, dynamically bind `ki_call` and `ki_mutate` tool validation schemas (`enum` and `description`) directly to tool `readOnlyHint` annotations, eliminating dispatch conflicts and preventing model confusion.
21
+ - **Date Format Resilience in `update_last_verified`**: Extended `update_last_verified` to automatically detect and update all supported date notation variants (HTML comment `<!-- last_verified: YYYY-MM-DD -->`, YAML frontmatter `last_verified: "YYYY-MM-DD"`, and Markdown bold `**Last verified**: YYYY-MM-DD`), and automatically insert the verification tag if missing.
22
+ - **Workflow Instructions Alignment**: Synchronized tool arguments and signatures across `/expand-knowledge` and `/sync-knowledge` instructions, ensuring seamless execution in both compact facade and full tool modes.
23
+ - **Daily MCP Log Rotation**: Server diagnostics now rotate daily (`mcp_YYYY-MM-DD.log`) with payload truncation to prevent runaway disk usage during high-frequency sessions.
24
+
25
+ ## [2.3.0] — 2026-10-11
26
+
27
+ ### Added
28
+ - **Unified Cascade Path Resolution (`resolve_know_path`)**: `read_know_file`, `write_know_file`, and `edit_know_file` now share `ki_read`'s smart cascade resolver. AI agents can access and edit Knowledge Items using flat basenames (e.g. `KI_data_persistence.md`) without requiring explicit `knowledge/` directory prefixes.
29
+ - **Smart Directory Placement for New KIs**: `write_know_file` now updates existing files in place across `knowledge/` rather than creating duplicates in the sandbox root, and automatically routes newly created flat KI files (`KI_*.md`, `*.ki.md`) into `<jail>/knowledge/`.
30
+ - **ADR Name & Prefix Tolerance**: `resolve_know_path` and `ki_read` gracefully handle variations with or without `ADR_`/`adr_`/`ADR-` prefixes (e.g., resolving `ADR_002_node_status_queue_and_ui_feedback.md` to `002_node_status_queue_and_ui_feedback.md`) as well as numeric ADR IDs.
31
+ - **Accurate Document Listings**: `get_ki_list_table()` dynamically reconciles registered entries in `doc_config.json` against filesystem reality, outputting exact on-disk filenames in document overviews.
32
+ - **Comprehensive Project Overview**: `ki_instructions(document="knowledge-items")`, prompts, and `ki://knowledge-items.md` now present both Knowledge Items and Architecture Decision Records (ADRs) tables together when ADRs exist.
33
+
34
+ ### Highlights from 2.3.0 Development Cycle
35
+ - Compact Facade Mode (`--tool-mode compact`) reducing MCP tools footprint from 34 schemas to 6 tools (`ki_instructions`, `ki_search`, `ki_read`, `ki_tools`, `ki_call`, `ki_mutate`), saving thousands of context tokens.
36
+ - Native BM25 search engine (`ki_search`) with bilingual EN/RU stemming and field-weighted ranking.
37
+ - Token-efficient reader with section filtering and character offset pagination (`ki_read`, `read_know_file`).
38
+ - Batch ADR superseding (`create_adr`) with automatic status transitions, warning banners, git/environment snapshots, and quality sections (`rejected_alternatives`, `invariants`, `verification`).
39
+ - Automated drift analysis (`check_ki_drift`) and Python AST pre-enrichment during scaffolding.
13
40
 
14
41
  ### Added
15
42
  - `create_adr` batch `supersedes` support: accept a list of previous ADR IDs (`supersedes: ["001", "002"]`) to supersede multiple older records simultaneously; automatically updates deprecated ADR files with warning banners and `Superseded by ADR XXX` statuses, updates `doc_config.json`, and embeds bi-directional cross-links in the generated ADR.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ki-manager
3
- Version: 2.3.0.dev3
3
+ Version: 2.3.1
4
4
  Summary: Knowledge Item (KI) management MCP server for AI-assisted development
5
5
  Project-URL: Homepage, https://github.com/Laeryid/KI-base
6
6
  Project-URL: Repository, https://github.com/Laeryid/KI-base
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ki-manager"
7
- version = "2.3.0.dev3"
7
+ version = "2.3.1"
8
8
  description = "Knowledge Item (KI) management MCP server for AI-assisted development"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -1,3 +1,3 @@
1
1
 
2
2
  """ki-manager — Knowledge Item MCP server for AI-assisted development."""
3
- __version__ = "2.3.0.dev3"
3
+ __version__ = "2.3.1"
@@ -144,11 +144,14 @@ def main():
144
144
  if command == "install-skills":
145
145
  install_skills(sys.argv[2:])
146
146
  elif command in ("--version", "-v"):
147
- import importlib.metadata
148
147
  try:
149
- v = importlib.metadata.version("ki-manager")
148
+ from . import __version__ as v
150
149
  except Exception:
151
- v = "unknown"
150
+ import importlib.metadata
151
+ try:
152
+ v = importlib.metadata.version("ki-manager")
153
+ except Exception:
154
+ v = "unknown"
152
155
  print(f"ki-manager {v}")
153
156
  sys.exit(0)
154
157
  else:
@@ -32,6 +32,9 @@ def analyze_path(target_path: str, recursive: bool = False):
32
32
 
33
33
  # Resolve absolute path
34
34
  abs_target = os.path.normpath(os.path.join(project_root, target_path))
35
+ if not ki_utils.is_subpath(abs_target, project_root):
36
+ print(f"Error: Path '{target_path}' escapes project root.")
37
+ return
35
38
 
36
39
  if not os.path.exists(abs_target):
37
40
  print(f"Error: Path {target_path} not found.")
@@ -65,7 +65,7 @@ def load_ki_files() -> list:
65
65
  ki_dir = os.path.join(get_knowledge_root(), "knowledge")
66
66
  if not os.path.isdir(ki_dir):
67
67
  return []
68
- return [f for f in os.listdir(ki_dir) if f.startswith("KI_") and f.endswith(".md")]
68
+ return [f for f in os.listdir(ki_dir) if ki_utils.is_ki_filename(f)]
69
69
 
70
70
 
71
71
  # ─── Metrics ──────────────────────────────────────────────────────────────────
@@ -286,7 +286,7 @@ def run_drift_check(
286
286
  return [check_single_ki(target_name, target_path, doc_config, project_root, knowledge_dir)]
287
287
 
288
288
  results = []
289
- for f in sorted(knowledge_dir.glob("KI_*.md")):
289
+ for f in sorted(f for f in knowledge_dir.glob("*.md") if ki_utils.is_ki_filename(f.name)):
290
290
  results.append(check_single_ki(f.name, f, doc_config, project_root, knowledge_dir))
291
291
 
292
292
  return results
@@ -70,7 +70,7 @@ def finalize_scaffolds(dry_run: bool = False) -> None:
70
70
  skipped_clean = 0
71
71
 
72
72
  for fname in sorted(os.listdir(ki_dir)):
73
- if not (fname.startswith("KI_") and fname.endswith(".md")):
73
+ if not ki_utils.is_ki_filename(fname):
74
74
  continue
75
75
 
76
76
  ki_path = os.path.join(ki_dir, fname)
@@ -29,6 +29,8 @@ def get_unmapped_files(target_path: str = ".") -> List[str]:
29
29
 
30
30
  # 2. Scan the target directory
31
31
  abs_target = os.path.abspath(os.path.join(project_root, target_path))
32
+ if not ki_utils.is_subpath(abs_target, project_root):
33
+ return []
32
34
  if not os.path.exists(abs_target):
33
35
  return []
34
36
 
@@ -514,7 +514,7 @@ def print_scaffold_status() -> None:
514
514
  enriched_count = 0
515
515
 
516
516
  for fname in sorted(os.listdir(ki_dir)):
517
- if not (fname.startswith("KI_") and fname.endswith(".md")):
517
+ if not ki_utils.is_ki_filename(fname):
518
518
  continue
519
519
 
520
520
  ki_path = os.path.join(ki_dir, fname)
@@ -0,0 +1,81 @@
1
+ """
2
+ git_diff_secured.py
3
+
4
+ Safely runs git status and git diff for project files within the project sandbox.
5
+ Prevents argument/flag injection and path traversal.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import argparse
11
+ import subprocess
12
+ from pathlib import Path
13
+ from typing import List
14
+
15
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
16
+ import ki_utils
17
+
18
+
19
+ def git_diff_secured(paths: List[str] = None) -> str:
20
+ project_root = ki_utils.get_project_root()
21
+ if not project_root or not os.path.exists(project_root):
22
+ return "Error: No active project context."
23
+
24
+ safe_paths = []
25
+ if paths:
26
+ for p in paths:
27
+ clean_p = p.strip()
28
+ if not clean_p:
29
+ continue
30
+ if clean_p.startswith("-") or any(char in clean_p for char in [";", "|", "&", ">", "<", "`"]):
31
+ return f"Error: Suspicious argument or path '{clean_p}'."
32
+ abs_p = os.path.abspath(os.path.join(project_root, clean_p))
33
+ if not ki_utils.is_subpath(abs_p, project_root):
34
+ return f"Access Denied: Path '{clean_p}' escapes project root."
35
+ safe_paths.append(os.path.relpath(abs_p, project_root).replace("\\", "/"))
36
+
37
+ try:
38
+ if safe_paths:
39
+ stat_cmd = ["git", "status", "--short", "--"] + safe_paths
40
+ diff_cmd = ["git", "diff", "HEAD", "--"] + safe_paths
41
+ else:
42
+ stat_cmd = ["git", "status", "--short"]
43
+ diff_cmd = ["git", "diff", "HEAD"]
44
+
45
+ stat_res = subprocess.run(
46
+ stat_cmd, cwd=project_root, capture_output=True, text=True, encoding="utf-8"
47
+ )
48
+ diff_res = subprocess.run(
49
+ diff_cmd, cwd=project_root, capture_output=True, text=True, encoding="utf-8"
50
+ )
51
+
52
+ output = []
53
+ if stat_res.stdout.strip():
54
+ output.append("### Git Status:\n" + stat_res.stdout.strip())
55
+ if diff_res.stdout.strip():
56
+ output.append("### Git Diff:\n" + diff_res.stdout.strip())
57
+ if not output:
58
+ output.append("No changes detected in git.")
59
+
60
+ return "\n\n".join(output)
61
+ except Exception as e:
62
+ return f"Git execution error: {str(e)}"
63
+
64
+
65
+ def main():
66
+ if sys.platform == "win32":
67
+ try:
68
+ sys.stdout.reconfigure(encoding="utf-8")
69
+ except Exception:
70
+ pass
71
+
72
+ parser = argparse.ArgumentParser(description="Secured git diff for project files.")
73
+ parser.add_argument("paths", nargs=argparse.REMAINDER, help="File or directory paths relative to project root")
74
+ args = parser.parse_args()
75
+
76
+ result = git_diff_secured(args.paths)
77
+ print(result)
78
+
79
+
80
+ if __name__ == "__main__":
81
+ main()
@@ -218,7 +218,7 @@ def search_knowledge(
218
218
  return {"error": "No project root found. Run ki_init_project or specify workspace.", "results": []}
219
219
 
220
220
  knowledge_root = ki_utils.get_knowledge_root()
221
- if not knowledge_root or not os.path.exists(knowledge_root) or not os.path.normcase(os.path.abspath(knowledge_root)).startswith(os.path.normcase(os.path.abspath(project_root))):
221
+ if not knowledge_root or not os.path.exists(knowledge_root) or not ki_utils.is_subpath(knowledge_root, project_root):
222
222
  for cand in [
223
223
  os.path.join(project_root, ".ki-base", "knowledge"),
224
224
  os.path.join(project_root, "knowledge"),
@@ -58,6 +58,28 @@ def normalize_path(path_str: str, make_absolute: bool = True) -> str:
58
58
  return path_str
59
59
 
60
60
 
61
+ def is_subpath(target: str, root: str) -> bool:
62
+ """Checks whether target path is strictly within or equal to root directory."""
63
+ if not target or not root:
64
+ return False
65
+ try:
66
+ norm_t = os.path.normcase(os.path.abspath(target))
67
+ norm_r = os.path.normcase(os.path.abspath(root))
68
+ return os.path.commonpath([norm_t, norm_r]) == norm_r
69
+ except (ValueError, OSError):
70
+ return False
71
+
72
+
73
+ def is_ki_filename(name: str, include_overview: bool = False) -> bool:
74
+ """Checks if a file name matches Knowledge Item convention (KI_*.md or *.ki.md)."""
75
+ if not name or not name.endswith(".md"):
76
+ return False
77
+ base = os.path.basename(name)
78
+ if not include_overview and base in ("_OVERVIEW.ki.md", "_OVERVIEW.md"):
79
+ return False
80
+ return base.startswith("KI_") or base.endswith(".ki.md")
81
+
82
+
61
83
  # ─── Registry Management ─────────────────────────────────────────────────────
62
84
 
63
85
  def get_registry_path() -> Path:
@@ -139,7 +161,7 @@ def find_project_by_cwd(cwd=None) -> dict:
139
161
 
140
162
  for proj_root, data in registry["projects"].items():
141
163
  norm_proj = normalize_path(proj_root)
142
- if os.path.normcase(cwd).startswith(os.path.normcase(norm_proj)):
164
+ if is_subpath(cwd, norm_proj):
143
165
  if len(norm_proj) > max_len:
144
166
  max_len = len(norm_proj)
145
167
  best_match = data
@@ -409,10 +431,30 @@ def get_ki_list_table() -> str:
409
431
  items = doc_config.get("knowledge_items", {})
410
432
  if not items:
411
433
  return "No Knowledge Items registered yet."
434
+ jail = get_knowledge_root()
435
+ proj = get_project_root()
436
+ dec_dirs = get_decisions_dirs(proj, jail)
437
+
412
438
  rows = ["| File | Topic / Summary |", "|------|-----------------|"]
413
439
  for name, info in sorted(items.items()):
414
440
  summary = info.get("summary", info.get("description", "—"))
415
- rows.append(f"| `{name}` | {summary} |")
441
+ display_name = name
442
+ if "path" in info and info["path"]:
443
+ display_name = info["path"]
444
+ elif jail or proj:
445
+ # Check if name exists literally
446
+ exists_literal = False
447
+ for root_cand in ([jail] if jail else []) + dec_dirs:
448
+ if os.path.exists(os.path.join(root_cand, name)) or os.path.exists(os.path.join(root_cand, "knowledge", name)):
449
+ exists_literal = True
450
+ break
451
+ if not exists_literal and re.match(r"^(?:ADR|adr)[_\-]", name):
452
+ stripped = re.sub(r"^(?:ADR|adr)[_\-]", "", name)
453
+ for d in dec_dirs:
454
+ if os.path.exists(os.path.join(d, stripped)):
455
+ display_name = stripped
456
+ break
457
+ rows.append(f"| `{display_name}` | {summary} |")
416
458
  return "\n".join(rows)
417
459
 
418
460
 
@@ -201,11 +201,11 @@ class KnowledgeEngine:
201
201
  if not knowledge_dir.exists():
202
202
  return "Knowledge directory not found"
203
203
 
204
- path_pattern = re.compile(r"`((?:app|ui|extensions|\.know|\.agent|scripts|tests)/[a-zA-Z0-9_\-\./]+)`")
204
+ path_pattern = re.compile(r"`([a-zA-Z0-9_\-\.]+/[a-zA-Z0-9_\-\./]+)`")
205
205
 
206
206
  updated_count = 0
207
207
  for filename in os.listdir(knowledge_dir):
208
- if not filename.endswith(".md"):
208
+ if not ki_utils.is_ki_filename(filename, include_overview=True):
209
209
  continue
210
210
 
211
211
  ki_path = knowledge_dir / filename
@@ -215,8 +215,9 @@ class KnowledgeEngine:
215
215
  mentioned = path_pattern.findall(content)
216
216
  valid_paths = set()
217
217
  for p in mentioned:
218
- p = p.rstrip(".,")
219
- if (self.project_root / p).exists():
218
+ p = p.rstrip(".,;:)")
219
+ target_file = (self.project_root / p).resolve()
220
+ if ki_utils.is_subpath(str(target_file), str(self.project_root)) and target_file.exists():
220
221
  valid_paths.add(p.replace("\\", "/"))
221
222
 
222
223
  if filename not in ki_items:
@@ -16,12 +16,16 @@ import ki_utils
16
16
 
17
17
 
18
18
  def resolve_agents_path(agents_path: str = None) -> str:
19
- """Finds the AGENTS.md file in the project."""
20
19
  if agents_path:
21
20
  p = ki_utils.normalize_path(agents_path)
22
- if os.path.exists(p):
23
- return p
24
- return p # let caller handle not found
21
+ pr = ki_utils.get_project_root()
22
+ kr = ki_utils.get_knowledge_root()
23
+ allowed = [pr] if pr else []
24
+ if kr and kr not in allowed:
25
+ allowed.append(kr)
26
+ if allowed and not any(ki_utils.is_subpath(p, r) for r in allowed):
27
+ return ""
28
+ return p
25
29
 
26
30
  pr = ki_utils.get_project_root()
27
31
  kr = ki_utils.get_knowledge_root()