ki-manager 2.3.1.dev1__py3-none-any.whl → 2.3.2.dev1__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.
ki_manager/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
 
2
2
  """ki-manager — Knowledge Item MCP server for AI-assisted development."""
3
- __version__ = "2.3.1.dev1"
3
+ __version__ = "2.3.2.dev1"
ki_manager/cli.py CHANGED
@@ -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:
@@ -76,31 +76,87 @@ def parse_ki_file(ki_path: Path) -> Dict[str, Any]:
76
76
 
77
77
  # Extract Key Components table symbols
78
78
  # Expected table format: | `SymbolName` | `rel/path/file.ext` | Purpose |
79
+ # Only tables that explicitly contain a File/Path column header are inspected.
79
80
  table_symbols: List[Dict[str, str]] = []
80
81
  in_key_components = False
81
- table_row_pattern = re.compile(r"\|\s*`?([A-Za-z0-9_]+)`?\s*\|\s*`?([^`|\s]+)`?\s*\|")
82
+ pending_header: Optional[List[str]] = None
83
+ current_table_active = False
84
+ sym_idx = -1
85
+ file_idx = -1
82
86
 
83
87
  for line in lines:
84
88
  if line.startswith("## Key Components"):
85
89
  in_key_components = True
90
+ pending_header = None
91
+ current_table_active = False
86
92
  continue
87
93
  elif in_key_components and line.startswith("## "):
88
94
  in_key_components = False
95
+ pending_header = None
96
+ current_table_active = False
89
97
  continue
90
98
 
91
- if in_key_components and line.strip().startswith("|"):
92
- lower = line.lower()
93
- if any(k in lower for k in ["class / function", "component", "|---", "|:---", "| — |", "| - |"]):
94
- continue
95
- m = table_row_pattern.search(line)
96
- if m:
97
- sym_name = m.group(1).strip()
98
- file_rel = m.group(2).strip()
99
- header_keywords = {"component", "file", "purpose", "class", "function", "description"}
100
- if sym_name.lower() in header_keywords or file_rel.lower() in header_keywords:
101
- continue
102
- if sym_name and file_rel and not file_rel.startswith("—"):
103
- table_symbols.append({"symbol": sym_name, "file": file_rel})
99
+ if not in_key_components:
100
+ continue
101
+
102
+ stripped = line.strip()
103
+ if not stripped.startswith("|"):
104
+ pending_header = None
105
+ current_table_active = False
106
+ continue
107
+
108
+ raw_cells = [c.strip() for c in stripped.strip("|").split("|")]
109
+ # Check if separator row (e.g. |---|---| or |:---|---:|)
110
+ is_separator = (
111
+ len(raw_cells) > 0
112
+ and all(re.match(r"^:?-{2,}:?$", c) for c in raw_cells if c)
113
+ )
114
+
115
+ if is_separator:
116
+ if pending_header:
117
+ file_idx = -1
118
+ for i, col in enumerate(pending_header):
119
+ col_lower = col.lower()
120
+ if any(w in col_lower for w in ["file", "path", "source", "location"]):
121
+ file_idx = i
122
+ break
123
+
124
+ sym_idx = -1
125
+ for i, col in enumerate(pending_header):
126
+ if i == file_idx:
127
+ continue
128
+ col_lower = col.lower()
129
+ if any(w in col_lower for w in ["class", "function", "symbol", "component", "name", "element", "method", "tool"]):
130
+ sym_idx = i
131
+ break
132
+
133
+ if sym_idx == -1 and file_idx != -1:
134
+ sym_idx = 0 if file_idx != 0 else 1
135
+
136
+ current_table_active = (file_idx != -1 and sym_idx != -1)
137
+ pending_header = None
138
+ continue
139
+
140
+ if pending_header is None and not current_table_active:
141
+ # Possible header row
142
+ pending_header = raw_cells
143
+ continue
144
+
145
+ if current_table_active:
146
+ if len(raw_cells) > max(sym_idx, file_idx):
147
+ sym_val = raw_cells[sym_idx].strip(" `").strip()
148
+ file_val = raw_cells[file_idx].strip(" `").strip()
149
+ header_keywords = {"component", "file", "path", "purpose", "class", "function", "description", "tool", "access"}
150
+ if (
151
+ sym_val
152
+ and file_val
153
+ and not sym_val.startswith("—")
154
+ and not file_val.startswith("—")
155
+ and not file_val.startswith("-")
156
+ and sym_val.lower() not in header_keywords
157
+ and file_val.lower() not in header_keywords
158
+ ):
159
+ table_symbols.append({"symbol": sym_val, "file": file_val})
104
160
 
105
161
  # Extract wiki-links [[KI_*.md]]
106
162
  wiki_links = []
@@ -9,34 +9,26 @@ from typing import List
9
9
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
10
10
  import ki_utils
11
11
 
12
- def get_unmapped_files(target_path: str = ".") -> List[str]:
13
- """Returns a sorted list of unmapped files relative to project root."""
14
- project_root = ki_utils.get_project_root()
15
- doc_config = ki_utils.get_doc_config()
16
-
17
- if not project_root or not doc_config:
18
- return []
19
-
20
- # 1. Collect all files that are already mapped in doc_config.json
21
- mapped_paths = set()
22
- knowledge_items = doc_config.get("knowledge_items", {})
23
12
 
24
- for ki in knowledge_items.values():
25
- depends_on = ki.get("depends_on", [])
26
- for p in depends_on:
27
- abs_p = os.path.abspath(os.path.join(project_root, p))
28
- mapped_paths.add(abs_p)
29
-
30
- # 2. Scan the target directory
13
+ def _scan_path(target_path: str, project_root: str, mapped_paths: set, exclude_patterns: set, ignored_exts: set) -> List[str]:
31
14
  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 []
34
- if not os.path.exists(abs_target):
15
+ if not ki_utils.is_subpath(abs_target, project_root) or not os.path.exists(abs_target):
35
16
  return []
36
17
 
37
18
  unmapped = []
38
- ignored_exts = {'.pyc', '.pyo', '.pyd', '.obj', '.dll', '.exe', '.bin'}
39
- exclude_patterns = ki_utils.get_exclude_patterns()
19
+ if os.path.isfile(abs_target):
20
+ rel_to_project = os.path.relpath(abs_target, project_root).replace("\\", "/")
21
+ fname = os.path.basename(abs_target)
22
+ if fname.startswith(".") or any(fname.endswith(ext) for ext in ignored_exts):
23
+ return []
24
+ if ki_utils.should_exclude(fname, rel_to_project, exclude_patterns):
25
+ return []
26
+ if abs_target in mapped_paths:
27
+ return []
28
+ for mapped in mapped_paths:
29
+ if os.path.isdir(mapped) and abs_target.startswith(os.path.join(mapped, "")):
30
+ return []
31
+ return [rel_to_project]
40
32
 
41
33
  for root, dirs, files in os.walk(abs_target):
42
34
  dirs[:] = [
@@ -67,10 +59,57 @@ def get_unmapped_files(target_path: str = ".") -> List[str]:
67
59
  if not is_parent_mapped:
68
60
  unmapped.append(rel_to_project)
69
61
 
70
- return sorted(unmapped)
62
+ return unmapped
71
63
 
72
64
 
73
- def find_unmapped_files(target_path: str):
65
+ def get_unmapped_files(target_path: str = ".") -> List[str]:
66
+ """Returns a sorted list of unmapped files relative to project root."""
67
+ project_root = ki_utils.get_project_root()
68
+ doc_config = ki_utils.get_doc_config()
69
+
70
+ if not project_root or not doc_config:
71
+ return []
72
+
73
+ # 1. Collect all files that are already mapped in doc_config.json
74
+ mapped_paths = set()
75
+ knowledge_items = doc_config.get("knowledge_items", {})
76
+
77
+ for ki in knowledge_items.values():
78
+ depends_on = ki.get("depends_on", [])
79
+ for p in depends_on:
80
+ abs_p = os.path.abspath(os.path.join(project_root, p))
81
+ mapped_paths.add(abs_p)
82
+
83
+ ignored_exts = {'.pyc', '.pyo', '.pyd', '.obj', '.dll', '.exe', '.bin'}
84
+ exclude_patterns = ki_utils.get_exclude_patterns()
85
+
86
+ norm_target = (target_path or ".").strip()
87
+
88
+ # If target is root/empty, scope to tracked_modules if defined
89
+ if norm_target in (".", ""):
90
+ tracked_modules = doc_config.get("coverage_settings", {}).get("tracked_modules") or doc_config.get("tracked_modules", [])
91
+ target_dirs = set()
92
+ if isinstance(tracked_modules, list) and tracked_modules:
93
+ for m in tracked_modules:
94
+ mod_path = m[0] if isinstance(m, (list, tuple)) else m
95
+ if isinstance(mod_path, str) and mod_path.strip():
96
+ norm_p = mod_path.replace("\\", "/").strip("/")
97
+ top_segment = norm_p.split("/")[0] if "/" in norm_p else norm_p
98
+ if top_segment:
99
+ target_dirs.add(top_segment)
100
+
101
+ if target_dirs:
102
+ unmapped_set = set()
103
+ for td in sorted(target_dirs):
104
+ unmapped_set.update(_scan_path(td, project_root, mapped_paths, exclude_patterns, ignored_exts))
105
+ return sorted(unmapped_set)
106
+ else:
107
+ return sorted(_scan_path(".", project_root, mapped_paths, exclude_patterns, ignored_exts))
108
+
109
+ return sorted(_scan_path(norm_target, project_root, mapped_paths, exclude_patterns, ignored_exts))
110
+
111
+
112
+ def find_unmapped_files(target_path: str = "."):
74
113
  project_root = ki_utils.get_project_root()
75
114
  doc_config = ki_utils.get_doc_config()
76
115
 
@@ -78,24 +117,27 @@ def find_unmapped_files(target_path: str):
78
117
  print("Error: Could not resolve project root or doc_config.json")
79
118
  return
80
119
 
81
- abs_target = os.path.abspath(os.path.join(project_root, target_path))
82
- if not os.path.exists(abs_target):
83
- print(f"Error: Target path '{target_path}' does not exist.")
84
- return
120
+ display_target = (target_path or ".").strip()
121
+ if display_target != ".":
122
+ abs_target = os.path.abspath(os.path.join(project_root, display_target))
123
+ if not os.path.exists(abs_target):
124
+ print(f"Error: Target path '{display_target}' does not exist.")
125
+ return
85
126
 
86
- unmapped = get_unmapped_files(target_path)
127
+ unmapped = get_unmapped_files(display_target)
87
128
  if unmapped:
88
- print(f"### Unmapped Files in '{target_path}':")
129
+ print(f"### Unmapped Files in '{display_target}':")
89
130
  for f in unmapped:
90
131
  print(f"- {f}")
91
132
  else:
92
- print(f"All files in '{target_path}' are already mapped in doc_config.json.")
133
+ print(f"All files in '{display_target}' are already mapped in doc_config.json.")
134
+
93
135
 
94
136
  if __name__ == "__main__":
95
137
  if sys.platform == "win32":
96
138
  sys.stdout.reconfigure(encoding="utf-8")
97
139
  parser = argparse.ArgumentParser(description="Find files in a directory not covered by any KI in doc_config.json")
98
- parser.add_argument("path", help="Relative path from project root to scan")
140
+ parser.add_argument("path", nargs="?", default=".", help="Relative path from project root to scan (default: tracked_modules or .)")
99
141
  args = parser.parse_args()
100
-
142
+
101
143
  find_unmapped_files(args.path)
@@ -335,6 +335,34 @@ def get_local_config() -> dict:
335
335
  return {}
336
336
 
337
337
 
338
+ def load_gitignore_patterns(project_root: str = None) -> set:
339
+ """Parses .gitignore file in project_root if present, returning a set of pattern strings."""
340
+ if not project_root:
341
+ project_root = get_project_root()
342
+ if not project_root or not os.path.isdir(project_root):
343
+ return set()
344
+
345
+ gitignore_path = os.path.join(project_root, ".gitignore")
346
+ if not os.path.isfile(gitignore_path):
347
+ return set()
348
+
349
+ patterns = set()
350
+ try:
351
+ with open(gitignore_path, "r", encoding="utf-8", errors="replace") as f:
352
+ for line in f:
353
+ line = line.strip()
354
+ if not line or line.startswith("#") or line.startswith("!"):
355
+ continue
356
+ norm = line.replace("\\", "/").rstrip("/")
357
+ if norm.startswith("/"):
358
+ norm = norm.lstrip("/")
359
+ if norm:
360
+ patterns.add(norm)
361
+ except Exception:
362
+ pass
363
+ return patterns
364
+
365
+
338
366
  DEFAULT_EXCLUDED_DIRS = {
339
367
  ".git", "__pycache__", "node_modules", ".venv", "venv", "dist", "build",
340
368
  ".mypy_cache", ".pytest_cache", ".ruff_cache",
@@ -344,33 +372,45 @@ DEFAULT_EXCLUDED_DIRS = {
344
372
  def get_exclude_patterns() -> set:
345
373
  """
346
374
  Returns the set of directory/file exclude patterns.
347
- Combines default excluded dirs with exclude_patterns from:
375
+ Combines default excluded dirs with exclude_patterns and exclude from:
348
376
  1. config.json (machine-specific)
349
- 2. doc_config.json (coverage_settings.exclude_patterns or top-level exclude_patterns)
377
+ 2. doc_config.json (coverage_settings.exclude_patterns, coverage_settings.exclude,
378
+ top-level exclude_patterns, top-level exclude)
350
379
  3. ki_config.json
380
+ 4. .gitignore in project root
351
381
  """
352
382
  patterns = set(DEFAULT_EXCLUDED_DIRS)
353
383
 
384
+ def _add_from_cfg(cfg_dict, key):
385
+ val = cfg_dict.get(key, [])
386
+ if isinstance(val, list):
387
+ for p in val:
388
+ if p:
389
+ patterns.add(str(p))
390
+ elif isinstance(val, str) and val:
391
+ patterns.add(val)
392
+
354
393
  # 1. config.json
355
394
  local_cfg = get_local_config()
356
- for p in local_cfg.get("exclude_patterns", []):
357
- if p:
358
- patterns.add(str(p))
395
+ _add_from_cfg(local_cfg, "exclude_patterns")
396
+ _add_from_cfg(local_cfg, "exclude")
359
397
 
360
398
  # 2. doc_config.json
361
399
  doc_cfg = get_doc_config()
362
- for p in doc_cfg.get("exclude_patterns", []):
363
- if p:
364
- patterns.add(str(p))
365
- for p in doc_cfg.get("coverage_settings", {}).get("exclude_patterns", []):
366
- if p:
367
- patterns.add(str(p))
400
+ _add_from_cfg(doc_cfg, "exclude_patterns")
401
+ _add_from_cfg(doc_cfg, "exclude")
402
+ cov_cfg = doc_cfg.get("coverage_settings", {})
403
+ if isinstance(cov_cfg, dict):
404
+ _add_from_cfg(cov_cfg, "exclude_patterns")
405
+ _add_from_cfg(cov_cfg, "exclude")
368
406
 
369
407
  # 3. ki_config.json
370
408
  ki_cfg = get_ki_cfg()
371
- for p in ki_cfg.get("exclude_patterns", []):
372
- if p:
373
- patterns.add(str(p))
409
+ _add_from_cfg(ki_cfg, "exclude_patterns")
410
+ _add_from_cfg(ki_cfg, "exclude")
411
+
412
+ # 4. .gitignore
413
+ patterns.update(load_gitignore_patterns())
374
414
 
375
415
  return patterns
376
416
 
@@ -389,15 +429,17 @@ def should_exclude(name: str, rel_path: str = "", patterns: set = None) -> bool:
389
429
  norm_rel = rel_path.replace("\\", "/").strip("/") if rel_path else ""
390
430
 
391
431
  for pat in patterns:
392
- if fnmatch.fnmatch(name, pat):
432
+ norm_pat = pat.replace("\\", "/").strip("/")
433
+ if not norm_pat:
434
+ continue
435
+ if fnmatch.fnmatch(name, norm_pat):
393
436
  return True
394
437
  if norm_rel:
395
- norm_pat = pat.replace("\\", "/").strip("/")
396
438
  if norm_rel == norm_pat or norm_rel.startswith(norm_pat + "/"):
397
439
  return True
398
440
  if fnmatch.fnmatch(norm_rel, norm_pat):
399
441
  return True
400
- if any(fnmatch.fnmatch(comp, pat) for comp in norm_rel.split("/")):
442
+ if any(fnmatch.fnmatch(comp, norm_pat) for comp in norm_rel.split("/")):
401
443
  return True
402
444
 
403
445
  return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ki-manager
3
- Version: 2.3.1.dev1
3
+ Version: 2.3.2.dev1
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
@@ -1,19 +1,19 @@
1
- ki_manager/__init__.py,sha256=MAN4L7RnYO2ztV2ZzLJdo7eI_mXUiEYRT60LTrll54g,104
2
- ki_manager/cli.py,sha256=sQZQDYh4Z8GOqQlT4Y2X60PZK27jSnhHhv5tW-RR7f0,4752
1
+ ki_manager/__init__.py,sha256=enfAcDoPGKB3xfHgl54qRK3vJ557ZWc7PaeC2q5BqxQ,104
2
+ ki_manager/cli.py,sha256=LQzThkf2PBhZTu4sTAmG6Zzj9XHCBl-PJ8ag-jrcKms,4854
3
3
  ki_manager/server.py,sha256=tq90-lKx3mBHuM5MAOhbeGp8lAHqyuBVAh_8sYrrRLk,105625
4
4
  ki_manager/scripts/add_ki_to_config.py,sha256=v7YhEVRpbD4U2x6FuGKuX3ZDp6CbXp8VnsoqWX9SJh8,2527
5
5
  ki_manager/scripts/analyze_module.py,sha256=UX2M4rrilLkztcMkDliOPeBDcRiUDUCo6f3SOqCEbd0,3945
6
6
  ki_manager/scripts/audit_coverage.py,sha256=O15QCSmBByB-yMo8C5debq_QGUr5NCA8EMDlzke1Y5s,16585
7
- ki_manager/scripts/check_ki_drift.py,sha256=3BP5XVWyHH-Np96CTQD_lLI4QRL_ba48RJ3-WhRpB04,14036
7
+ ki_manager/scripts/check_ki_drift.py,sha256=OTPJeYdm3pkkZ8GpracEl_ZZwRIbAHNgmTcEPXMAMzE,15994
8
8
  ki_manager/scripts/finalize_ki_scaffolds.py,sha256=0BSr6J0WtMKpywvCzWKfesqkDMDbpHfFN5FYqAo_vDM,5784
9
- ki_manager/scripts/find_unmapped_files.py,sha256=sNuUiNamQL5lYHXNBogfEjXe-CFJ3j98xURnIIkCKbs,3481
9
+ ki_manager/scripts/find_unmapped_files.py,sha256=t0IS8Zriey61xf_kdKVmqFz9imqiEcG5268ZWaSQEao,5597
10
10
  ki_manager/scripts/generate_dir_index.py,sha256=ODIgA7QxhdBMLViRTk5CK4vv5XrZcqSoCz1KrChTWPo,5381
11
11
  ki_manager/scripts/generate_ki_scaffolds.py,sha256=LzvTMu13QySVdxvnOEdGXlVp6xDP0Nxb-VU9ShFdb88,27097
12
12
  ki_manager/scripts/git_diff_secured.py,sha256=zTTmqGshPRALwYgU8TIeCU7W4FbhcHz9MlCB-f7cCnk,2662
13
13
  ki_manager/scripts/ki_dependency_analyzer.py,sha256=378pSZZX_nAAoh9QTFb_A5yqdKFfp8G7iqis0dJu6eg,9794
14
14
  ki_manager/scripts/ki_graph_visualize.py,sha256=3Xg6WrWliREeCUxKjmeivBYT56lFBbC3Jot7uD2e81g,9102
15
15
  ki_manager/scripts/ki_search.py,sha256=YAHkPKWx3AVh6n1ivsaHu_0-QowoqpyAwUtZ-LcBrnM,11670
16
- ki_manager/scripts/ki_utils.py,sha256=L056zbYD88MI789Zj-s8kIUZKGTeF5dbKEy9mbjUAUs,20909
16
+ ki_manager/scripts/ki_utils.py,sha256=3z8wwMxB7fV0CdvM4nJVCIO1_D4qvFZ9hjgau7L1kzk,22394
17
17
  ki_manager/scripts/knowledge_engine.py,sha256=H81WiwSnQh-fXVEakzAUXNcxs8JhYamBFUxAqdnOTSM,9862
18
18
  ki_manager/scripts/sync_agents_md.py,sha256=0Lbx8MidQXmysw4HHrnighXMemsmIp4CAqvnbVY7_WE,5982
19
19
  ki_manager/templates/KI_template.md,sha256=s7GKEqt5PlmOZ7V_IytU6h0viTedW_ZKeJ8w_t3wJFA,875
@@ -24,8 +24,8 @@ ki_manager/workflows/create-adr.md,sha256=dx5uX5UFnxuztXXEf13JQU6FEZfMcKWQf2_g1y
24
24
  ki_manager/workflows/expand-knowledge.md,sha256=cfa4WOj2rUNlSTVmLmyhij_OHxRp1HUSLvBUeBjjNjs,6448
25
25
  ki_manager/workflows/scaffold-knowledge.md,sha256=xhnNQeKu0yzJrPIrnLXzFbSSwk1v8H2CzAymiwqKUNc,8284
26
26
  ki_manager/workflows/sync-knowledge.md,sha256=wyYbVeYWy0autJCK34eOlyyPeoW0Ks1hAEkHjvmrTU4,4255
27
- ki_manager-2.3.1.dev1.dist-info/METADATA,sha256=oa1BgfRjEHnVWOSE4ehi4KbMDPfXBXRtHC7yS9apnZs,11217
28
- ki_manager-2.3.1.dev1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
29
- ki_manager-2.3.1.dev1.dist-info/entry_points.txt,sha256=V-NMpOuZiRNc4hDwZ0AcWnEltRkc_T4WD_sUxOQf7TE,94
30
- ki_manager-2.3.1.dev1.dist-info/licenses/LICENSE,sha256=KBvPtUXGgjjiHBWYvaNazfiZ_U-VYaqisz_yVCUfOBs,1072
31
- ki_manager-2.3.1.dev1.dist-info/RECORD,,
27
+ ki_manager-2.3.2.dev1.dist-info/METADATA,sha256=kftq-bPgBdkGhnx4GVl2jKZAbaHckhprULGTvJQ4eYY,11217
28
+ ki_manager-2.3.2.dev1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
29
+ ki_manager-2.3.2.dev1.dist-info/entry_points.txt,sha256=V-NMpOuZiRNc4hDwZ0AcWnEltRkc_T4WD_sUxOQf7TE,94
30
+ ki_manager-2.3.2.dev1.dist-info/licenses/LICENSE,sha256=KBvPtUXGgjjiHBWYvaNazfiZ_U-VYaqisz_yVCUfOBs,1072
31
+ ki_manager-2.3.2.dev1.dist-info/RECORD,,